简介
进程(Process)是正在运行的程序,是操作系统进行资源分配和调度的基本单位。程序是存储在硬盘或内存的一段二进制序列,是静态的,而进程是动态的。进程包括代码、数据以及分配给它的其他系统资源(如文件描述符、网络连接等)。例如在windos系统下打开一个浏览器或者其余任何一个应用程序,都属于一个进程,而程序本身并不是进程,只有当他在运行时才被称为进程。
system创建子进程
system是C标准库函数 int system(const char *command) ;
作用是把 command 指定的命令名称或程序名称传给要被命令处理器执行的主机环境,并在命令完成后返回。也就是根据传入的命令启动一个进程来执行这个命令。他可以自适应对应的操作系统,在windos和linux系统下都可以直接调用。
参数是传入的shell命令,以字符串形式传入,执行成功返回0,失败返回非0
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define handle_error(cmd,result) \
if(result < 0) \
{ \
perror(cmd); \
exit(EXIT_FAILURE); \
} \
int main(int argc, char const *argv[])
{
int result = 0;
result = system("ping -c 10 www.baidu.com");
handle_error("system",result);
return 0;
}
本段代码使用system创建一个子进程来执行ping -c 10 www.baidu.com这个命令。在程序运行后在终端中使用ps -ef 来查看所有进程。
如图第一个进程是执行makefile,第二个是运行system这个可执行文件(因为我把上述代码的.c文件命名为了system),他启动了子进程3723,子进程3723又启动了子进程3724来执行ping -c 10 www.baidu.com
进程处理
进程处理主要涉及创建子进程,跳转到其他进程,等待进程结束等,包括fork,execve,waitpid函数。
在进程处理中,程序的main函数主要采取有参数格式,int main(int argc, char *argv[]);
argc是传递给程序的命令行参数的数量。
argv指向字符串数组的指针,存储了命令行参数,是在命令行运行此程序时传入的参数。
argv[0]通常是程序的名称。
argv[1]到argv[argc-1]是实际的命令行参数。
每个进程都有一个独立的编号,称为pid(process id),即进程ID,用于区分不同的进程,pid的数据格式为__pid_t或者pid_t,层层定义下来最终也就是int类型。
fork函数
fork()函数的作用是创建一个子进程,并继承父进程的资源,即将父进程的资源复制了一遍,但是子进程也是一个进程,拥有独立的进程ID,创建这个子进程的进程叫做父进程,在调用fork之前,程序内只有父进程在运行,fork之后,程序就被复制了一遍,fork之后的程序在父子进程内都会执行一遍。
fork函数在成功创建子进程之后会返回一个值,在父进程中会返回子进程的PID,在子进程中就会返回0,这就可以帮助我们区分父子进程,用于设置父子进程分别执行不同的功能。
pid_t getpid(void);
getpid函数会返回调用该函数的进程的进程ID。
pid_t getppid(void);
getppid函数会返回调用该函数的进程的父进程的进程ID
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#define handle_error(cmd,result) \
if(result < 0) \
{ \
perror(cmd); \
exit(EXIT_FAILURE); \
} \
int main(int argc, char const *argv[])
{
pid_t pid = 0;
printf("现在是父进程\n");
pid = fork();
handle_error("fork",pid);
if(pid == 0)//子进程
{
printf("现在是fork之后创建的子进程,进程ID为%d,我的父进程的进程ID为%d\n",getpid(),getppid());
}
else if(pid > 0)//父进程
{
printf("现在是fork之后的父进程,进程ID为%d\n",getpid());
}
printf("此段代码,父子进程都会执行\n");
return 0;
}
程序运行结果如下
可以看到在fork之前的程序只执行了一次,在fork之后如果不区分pid的值,程序就会在父子进程中都执行,且子进程可以正确返回父进程的ID。
文件描述符的引用计数
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define handle_error(cmd,result) \
if(result < 0) \
{ \
perror(cmd); \
exit(EXIT_FAILURE); \
} \
int main(int argc, char const *argv[])
{
int fd = 0;
fd = open("io.txt",O_RDWR | O_CREAT,0666);
handle_error("open",fd);
pid_t pid = 0;
char write_buf[60] = "父进程在fork之前写入的\n";
write(fd,write_buf,strlen(write_buf));
pid = fork();
handle_error("fork",pid);
if(pid == 0)//子进程
{
strcpy(write_buf,"子进程在fork之后写入的\n");
write(fd,write_buf,strlen(write_buf));
}
else if(pid > 0)
{
strcpy(write_buf,"父进程在fork之后写入的\n");
write(fd,write_buf,strlen(write_buf));
}
close(fd);
return 0;
}
程序运行结果如下,查看io.txt文件
在这个程序中,经过fork创建子进程之后,子进程并没有再单独打开io.txt文件,而是可以直接使用父进程打开的文件描述符fd从而向文件中写入数据。这是因为子进程复制了父进程的文件描述符fd,二者指向的应是同一个底层文件描述(struct file结构体)。
如果子进程通过close()释放文件描述符之后,父进程对于相同的文件描述符执行write()操作仍然可以成功。这是因为struct file结构体中有一个属性为引用计数,记录的是与当前struct file绑定的文件描述符数量。close()系统调用的作用是将当前进程中的文件描述符和对应的struct file结构体解绑,使得引用计数减一。如果close()执行之后,引用计数变为0,则会释放struct file相关的所有资源。通过fork创建子进程,子进程复制父进程的文件描述符,在这一过程中,会使对应文件的引用计数+1,所以在关闭时,父子进程都需要调用close关闭文件描述符,才能使其引用计数降为0。否则struct file相关的所有资源将不会释放。
execve
exec是一个系列函数,可以在一个进程中跳转到另外一个函数并开始执行,如果跳转成功,那么该函数之后的内容都不会再执行,而是跳转到新程序开始执行新程序的内容,跳转之后的进程ID保持不变。本文以execve为例。
函数原型为int execve (const char *__path, char *const __argv[], char *const __envp[]);
char *__path: 需要执行程序的完整路径名
char *const __argv[]: 指向字符串数组的指针 需要传入多个参数
(1) 需要执行的程序命令(和*__path相同)
(2) 执行程序需要传入的参数
(3) 最后一个参数必须是NULL
char *const __envp[]: 指向字符串数组的指针 需要传入多个环境变量参数,环境变量也可以不传,直接赋值为NULL
(1) 环境变量参数 固定格式 key=value
(2) 最后一个参数必须是NULL
return: 成功就回不来了 下面的代码都没有意义 失败返回-1
先写一个跳转到的目标程序,编译为可执行文件,命名为execve_dst
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if(argc < 2)
{
printf("参数小于两个\n");
}
else if(argc >= 2)
{
printf("参数正常,跳转到的进程ID为%d\n",getpid());
}
return 0;
}
再写一个跳转的程序,跳转到execve_dst,该程序命名为execve_src
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
char *argvs[] = {"./execve_dst","跳转的源头",NULL};
char *envp[] = {NULL};
printf("我是跳转源头,我的进程ID是%d\n",getpid());
execve("./execve_dst",argvs,envp);
printf("我已经跳转了\n");
return 0;
}
编译运行execve_src,结果如下
可以看到两次调用getpid查询到的进程ID相同,且execve之后的printf没有执行。
waitpid
Linux中父进程除了可以启动子进程,还要负责回收子进程的状态。如果子进程结束后父进程没有正常回收,那么子进程就会变成一个僵尸进程——即程序执行完成,但是进程没有完全结束,其内核中的相关资源没有释放。如果父进程在子进程结束前就结束了,那么其子进程的回收工作就交给了父进程的父进程的父进程。本次使用waitpid使父进程等待子进程结束后回收资源后再结束,防止子进程变为僵尸进程。即时子进程使用了exit()结束进程,也需要使用waitpid回收子进程。
子进程调用exit会主动结束进程并返回相关状态,释放文件描述符、内存等用户空间的资源,但是内核资源(如进程描述符PCB)仍未回收,此时子进程变为了僵尸进程。所以仍需waitpid对子进程进行回收,waitpid可以释放子进程残留的内核相关资源(PCB等),彻底终止子进程。
wait函数
原型为pid_t wait(int *wstatus);
作用是等待子进程停止并获取退出状态。
waitpid函数
原型为pid_t waitpid(pid_t pid, int *wstatus, int options);
pid: 等待的模式
* (1) 小于-1 例如 -1 * pgid,则等待进程组ID等于pgid的所有进程终止
* (2) 等于-1 会等待任何子进程终止,并返回最先终止的那个子进程的进程ID -> 儿孙都算
* (3) 等于0 等待同一进程组中任何子进程终止(但不包括组领导进程) -> 只算儿子
* (4) 大于0 仅等待指定进程ID的子进程终止
wstatus: 整数指针,子进程返回的状态码会保存到该int
options: 选项的值是以下常量之一或多个的按位或(OR)运算的结果;二进制对应选项,可多选:
* (1) WNOHANG 如果没有子进程终止,也立即返回;用于查看子进程状态而非等待
* (2) WUNTRACED 收到子进程处于收到信号停止的状态,也返回。
* (3) WCONTINUED(自Linux 2.6.10起)如果通过发送SIGCONT信号恢复了一个已停止的子进程,则也返回。
若填0则为waitpid最初的功能,等待子进程关闭。
return: (1) 成功等到子进程停止 返回pid
* (2) 没等到并且没有设置WNOHANG 一直等
* (3) 没等到设置WNOHANG 返回0
* (4) 出错返回-1
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#define handle_error(cmd,result) \
if(result < 0) \
{ \
perror(cmd); \
exit(EXIT_FAILURE); \
} \
int main(int argc, char const *argv[])
{
pid_t pid = 0;
pid = fork();
handle_error("fork",pid);
if(pid == 0)
{
sleep(10);
printf("子进程马上结束\n");
}
else
{
printf("父进程正在等待子进程结束\n");
waitpid(pid,NULL,0);
printf("父进程等待子进程结束,即将结束\n");
}
return 0;
}
程序的运行结果应该是先打印出父进程正在等待子进程结束,等待十秒打印出子进程马上结束,之后子进程结束,父进程等待子进程结束后打印父进程等待子进程结束,即将结束。
进程树
Linux的进程是通过父子关系组织起来的,所有进程之间的父子关系共同构成了进程树(Process Tree)。进程树中每个节点都是其上级节点的子进程,同时又是子结点的父进程。一个进程的父进程只能有一个,而一个进程的子进程可以不止一个。
ps -ef可以查看当前系统中的所有进程,我们在程序中调用命令行输入来阻塞程序内容,然后在终端中使用ps -ef命令查看进程的ID号和父进程的ID号,最终会查到ID为1的进程,实质上,1号进程就是systemd,它由内核创建,是第一个进程,负责初始化系统,启动其他所有用户空间的服务和进程。它是所有进程的祖先。
查看进程树的命令是pstree,会以树状图展示所有用户线程的依赖关系,就像一张族谱图
调用pstree -p可以显示每个进程的ID号
孤儿进程
孤儿进程(Orphan Process)是指父进程已结束或终止,而它仍在运行的进程。
当父进程结束之前没有等待子进程结束,且父进程先于子进程结束时,那么子进程就会变成孤儿进程。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#define handle_error(cmd,result) \
if(result < 0) \
{ \
perror(cmd); \
exit(EXIT_FAILURE); \
} \
int main(int argc, char const *argv[])
{
pid_t pid = 0;
pid = fork();
handle_error("fork",pid);
if(pid == 0)
{
printf("子进程刚刚创建,进程ID为%d,我现在的父进程ID为%d\n",getpid(),getppid());
sleep(5);
printf("子进程已经休眠了5s,我现在的父进程ID为%d\n",getppid());
}
else if(pid > 0)
{
printf("父进程的ID号为%d\n",getpid());
sleep(2);
printf("父进程已经结束\n");
}
return 0;
}
根据结果可以看到,父进程结束之后,程序已经是退出状态,运行结束,子进程仍未结束,又重新占用了终端输出,而且其父进程变为了祖先的ID,所以孤儿进程会被其祖先自动领养。此时的子进程因为和终端切断了联系,所以很难再进行标准输入使其停止了,所以写代码的时候一定要注意避免出现孤儿进程。