赞
踩
目录
在linux中 fork 函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
进程调用 fork ,当控制转移到内核中的 fork 代码后,内核会做如下工作:
当一个进程调用 fork 之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程。即 fork 之前父进程独立执行, fork 之后,父子两个执行流分别执行。注意, fork 之后,谁先执行完全由调度器决定。
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
进程退出共有以下三种场景:
当进程正常执行完后,会返回退出码。一般而言,当结果正确,退出码为 0 ,当结果不正确,退出码为 非 0 值,比如 1、2、3、4... ,分别对应不同的错误原因,供用户进行进程退出健康状态的判断。
使用如下代码进程举例说明:
- 1 #include <stdio.h>
- 2 #include <assert.h>
- 3 #include <unistd.h>
- 4
- 5
- 6 int add_to_top(int top)
- 7 {
- 8 int sum = 0;
- 9 for(int i = 0; i < top; ++i)
- 10 {
- 11 sum += i;
- 12 }
- 13 return sum;
- 14 }
- 15
- 16 int main()
- 17 {
- 18 int result = add_to_top(100);
- 19 if(result == 5050) return 0;//结果正确
- 20 else return 1;//结果不正确
- 21 }

计算从 1 到 100 的累加,如果结果等于 5050 ,则说明结果正确,正常返回 0,否则说明结果不正确,返回 1 。
查看进程返回结果的指令:
echo $?
根据我们自己所写的代码,返回值为 1 ,说明结果错误。
补充说明:
之后再输入 echo $? 后,显示的结果就都是 0 了:
这时因为 $? 只会保留最近一次执行的进程的退出码。
关于C语言提供的进程退出码所代表的含义我们可以通过函数 strerror 来获取:
其中 errnum 为退出码。
我们编写如下代码来查看这些退出码的含义:
- int main()
- {
- for(int i = 0; i < 140; ++i)
- {
- printf("%d : %s\n", i, strerror(i));
- }
-
- return 0;
- }
由于输出结果较长,这里就不再全部放出,我截取了其中一部分:
观察到:如果返回值为 0,说明进程成功,如果返回值为 2, 说明没有这个文件或目录。
但是并不是所有指令的退出码都是根据C语言提供的进程退出码为基准的,比如:
我们使用 kill -9 指令来杀死一个不存在的进程时所报的错误如果按照C语言的标准,退出码应该为3,但实际上退出码是 1 。
我们也可以自己来定义进程退出码的含义:
当一个进程退出时,OS中就少了一个进程,就要释放该进程对应的内核数据结构 + 代码和数据。
进程正常退出有三种方式:
众所周知,只有 main 函数 return 才标志进程退出,其他函数 return 仅仅代表函数返回,这说明进程执行的本质是 main 执行流执行。
前面的内容已经介绍过 return 退出的方式,接下来讲解 exit 函数退出的方式:
编写如下代码:
使用指令 echo $? 查看进程退出码:
看到退出码为我们自己写入的 123 。由此我们得知函数 exit(int code) 中的参数 code 代表的就是进程退出码。在代码的任意地方调用 exit 函数都表示进程退出。
函数 exit 为C标准库函数,除此之外还有一个 _exit 函数,该函数为系统调用。
其用法与 exit 相同。
exit 与 _exit 的区别在于,exit 中封装了 _exit, exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
进程等待就是通过系统调用,获取子进程退出码或者退出信号的方式,顺便释放内存。
等待进程有两种方式,分别为 wait 与 waitpid 。
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。
编写如下代码:
- 1 #include <stdio.h>
- 2 #include <string.h>
- 3 #include <stdlib.h>
- 4 #include <unistd.h>
- 5 #include <sys/wait.h>
- 6 #include <sys/types.h>
- 7
- 8 int main()
- 9 {
- 10 pid_t id = fork();
- 11 if(id == 0)
- 12 {
- 13 //子进程
- 14 int cnt = 5;
- 15 while(cnt)
- 16 {
- 17 printf("子进程,剩余%dS, pid: %d, ppid: %d\n", cnt--, getpid(), getppid());
- 18 sleep(1);
- 19 }
- 20 exit(0);
- 21 }
- 22
- 23 //父进程
- 24 sleep(10);
- 25 pid_t ret_id = wait(NULL);
- 26 printf("父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d\n", getpid(), getppid(), ret_id);
- 27 sleep(5);
- 28
- 29 return 0;
- 30 }

在命令行输入
while :; do ps ajx | head -1 && ps ajx | grep mytest | grep -v grep; sleep 1; echo "--------------"; done
运行观察结果:
可以观察到 mytest 进程的三个阶段,第一阶段,父子进程都在运行。第二阶段,子进程变为僵尸进程,父进程继续运行。第三阶段,经过等待,僵尸进程被回收,父进程继续运行。
函数 pid_ t waitpid(pid_t pid, int *status, int options);
1、返回值:
2、参数:
其中,waitpid函数的参数 status 是一个输出型参数,用于获取子进程的状态,即子进程的信号 + 退出码。我们可以把 status 看作位图:
整数 status 有 32 个比特位,我们只使用其中低 16 个比特位。
低16位中的次低 8 位代表退出状态,也成为退出码,低 7 位代表进程退出时收到的信号,如果为 0 ,就说明没有收到退出信号,为正常退出,如果信号不为 0 ,就说明进程是异常退出。只有在正常退出时,我们才会关注退出码。至于 core dump 以后再讲。
编写如下代码进行说明:
- 1 #include <stdio.h>
- 2 #include <string.h>
- 3 #include <stdlib.h>
- 4 #include <unistd.h>
- 5 #include <sys/wait.h>
- 6 #include <sys/types.h>
- 7
- 8 int main()
- 9 {
- 10 pid_t id = fork();
- 11 if(id == 0)
- 12 {
- 13 //子进程
- 14 int cnt = 5;
- 15 while(cnt)
- 16 {
- 17 printf("子进程,剩余%dS, pid: %d, ppid: %d\n", cnt--, getpid(), getppid());
- 18 sleep(1);
- 19 }
- 20 exit(123);
- 21 }
- 22
- 23 //父进程
- 24 int status = 0;
- 25 pid_t ret_id = waitpid(id, &status, 0);
- 26 printf("父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit code: %d, child exit signal: %d\n", getpid(), getppid(), ret_id, (status>>8)&0xFF, status & 0x7F);
- 28
- 29 return 0;
- 30 }

运行结果如下:
子进程的信号为 0 ,退出码为 123 。符合我们的预期。
对于status,除了我们自己按位操作以外,也可以使用库提供的宏来替换:
WIFEXITED(status) :若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status) :若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
我们知道子进程拥有自己的PCB结构 task_struct ,在task_struct中存在两个变量,分别为 int exit_code 与 int exit_signal 。当子进程退出时,OS会把退出码填写到 exit_code 中,把退出信号填写到 exit_signal 中,并维护子进程的 task_struct ,此时子进程的状态就是僵尸状态。通过 wait 或者 waitpid 系统调用可以访问到该内核数据结构,并把退出信息以上面所讲过的格式存放在 status 中,顺便释放该数据结构占用的内存空间。
了解了以上知识后,我们应该有一个疑问,父进程在等待子进程退出,并回收子进程。那么如果子进程一直都没有退出,父进程又在做什么呢?
默认情况下,在子进程没有退出的时候,父进程只能一直在调用 wait 或 waitpid 进行等待,我们称之为阻塞等待。关于阻塞的内容可以参考文章《进程概念》。
当子进程退出时,通过 parent 指针找到父进程,并把父进程放到运行队列中,继续执行 wait 或 waitpid 指令。
如果不想让父进程阻塞等待,则可以通过设置 waitpid 系统调用的参数 options 为 WNOHANG 来实现非阻塞轮询。
非阻塞轮询有三种结果:
- 1 #include <stdio.h>
- 2 #include <string.h>
- 3 #include <stdlib.h>
- 4 #include <unistd.h>
- 5 #include <sys/wait.h>
- 6 #include <sys/types.h>
- 7
- 8 int main()
- 9 {
- 10 pid_t id = fork();
- 11 if(id == 0)
- 12 {
- 13 //子进程
- 14 while(1)
- 15 {
- 16 printf("子进程, pid: %d, ppid: %d\n", getpid(), getppid());
- 17 sleep(1);
- 18 }
- 19 exit(0);
- 20 }
- 21 //父进程
- 22 while(1)
- 23 {
- 24
- 25 int status = 0;
- 26 pid_t ret_id = waitpid(id, &status, WNOHANG);
- 27 if(ret_id < 0)
- 28 {
- 29 printf("waitpid error!\n");
- 30 exit(1);
- 31 }
- 32 else if(ret_id == 0)
- 33 {
- 34 printf("子进程没退出,处理其他事情。。\n");
- 35 sleep(1);
- 36 continue;
- 37 }
- 38 else
- 39 {
- 40 printf("父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit code: %d, child exit signal: %d\n", \
- 41 getpid(), getppid(), ret_id, (status>>8)&0xFF, status & 0x7F);
- 42 break;
- 43 }
- 44
- 45 }
- 46 return 0;
- 47 }

运行观察结果:
这就叫做父进程的非阻塞状态。
下面我们来写一个完整的父进程非阻塞代码:
- 1 #include <stdio.h>
- 2 #include <string.h>
- 3 #include <stdlib.h>
- 4 #include <unistd.h>
- 5 #include <sys/wait.h>
- 6 #include <sys/types.h>
- 7
- 8 #define TASK_NUM 10
- 9
- 10 //预设任务
- 11 void sync_dick()
- 12 {
- 13 printf("刷新数据\n");
- 14 }
- 15
- 16 void sync_log()
- 17 {
- 18 printf("同步日志\n");
- 19 }
- 20
- 21 void net_send()
- 22 {
- 23 printf("网络发送\n");
- 24 }
- 25
- 26 //要保存的任务
- 27 typedef void (*func_t)();
- 28 func_t other_task[TASK_NUM] = {NULL};
- 29
- 30
- 31 int LoadTask(func_t func)
- 32 {
- 33 int i = 0;
- 34 for(; i < TASK_NUM; ++i)
- 35 {
- 36 if(other_task[i] == NULL) break;
- 37 }
- 38 if(i == TASK_NUM) return -1;
- 39 else other_task[i] = func;
- 40
- 41 return 0;
- 42 }
- 43
- 44
- 45 void InitTask()
- 46 {
- 47 for(int i = 0; i < TASK_NUM; ++i) other_task[i] = NULL;
- 48 LoadTask(sync_dick);
- 49 LoadTask(sync_log);
- 50 LoadTask(net_send);
- 51 }
- 52
- 53 void RunTask()
- 54 {
- 55 for(int i = 0; i <TASK_NUM; ++i)
- 56 {
- 57 if(other_task[i] == NULL) continue;
- 58 other_task[i]();
- 59 }
- 60 }
- 61
- 62 int main()
- 63 {
- 64 pid_t id = fork();
- 65 if(id == 0)
- 66 {
- 67 //子进程
- 68 int cnt = 5;
- 69 while(cnt--)
- 70 {
- 71 printf("子进程, pid: %d, ppid: %d\n", getpid(), getppid());
- 72 sleep(1);
- 73 }
- 74 exit(123);
- 75 }
- 76 //父进程
- 77 InitTask();
- 78 while(1)
- 79 {
- 80
- 81 int status = 0;
- 82 pid_t ret_id = waitpid(id, &status, WNOHANG);
- 83 if(ret_id < 0)
- 84 {
- 85 printf("waitpid error!\n");
- 86 exit(1);
- 87 }
- 88 else if(ret_id == 0)
- 89 {
- 90 RunTask();
- 91 sleep(1);
- 92 continue;
- 93 }
- 94 else
- 95 {
- 96 if(WIFEXITED(status))
- 97 {
- 98 printf("wait success, child exit code: %d\n", WEXITSTATUS(status));
- 99 }
- 100 else
- 101 {
- 102 printf("wait success, child exit code: %d\n", status & 0x7F);
- 103 }
- 104 // printf("父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit code: %d, child exit signal: %d\n", \
- 105 // getpid(), getppid(), ret_id, (status >> 8) & 0xFF , status & 0x7F);
- 106 break;
- 107 }
- 108
- 109 }
- 110 return 0;
- 111 }

创建子进程无非就两种目的:
为了让子进程执行全新的程序代码,就需要进行程序替换。
用 fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。
程序原本存放在磁盘中,当调用 exec 函数时,被加载到了内存中。所以程序替换就相当于程序加载器,我们平常说程序被加载到内存中,其实就是调用了 exec 。在创建进程的时候,是先创建的进程数据结构PCB,再把代码和数据加载到内存的。
程序替换的接口函数:
int execl(const char *path, const char *arg, ...);
函数参数列表中的 "..." 为可变参数。可以让我们给C函数传递任意个数个参数。 path 为程序路径, arg 为命令 + 命令参数,最后一定要以 NULL 为结尾。
编写如下代码进行解释说明:
- 1 #include <unistd.h>
- 2 #include <stdio.h>
- 3 #include <stdlib.h>
- 4
- 5 int main()
- 6 {
- 7 printf("hello world\n");
- 8 printf("hello world\n");
- 9 printf("hello world\n");
- 10 printf("hello world\n");
- 11
- 12 execl("/bin/ls", "ls", "-a", "-l", NULL);
- 13
- 14 printf("can you see me\n");
- 15 printf("can you see me\n");
- 16 printf("can you see me\n");
- 17 printf("can you see me\n");
- 18 return 0;
- 19 }

需要注意的是,命令参数一定要以 NULL 为结尾。
运行程序观察结果:
可以看到 execl 函数后,执行程序替换,新的代码和数据就被加载进内存了,后续的代码属于老代码,直接被替换掉,没机会再执行了。程序替换是整体替换,不能局部替换。
进程替换只会影响调用 execl 的进程,不会影响其他进程,包括父进程,因为进程具有独立性。换句话说,子进程加载新程序的时候,是需要进行程序替换的,发生代码的写时拷贝。
补充内容:
我们知道 execl 是一个函数,也有可能调用失败。如果程序替换失败,进程会继续执行老代码,并且 execl 一定会有返回值。反之,如果程序替换成功,则 execl 一定没有返回值。只要 execl 有返回值,则程序替换一定失败了。
如果程序替换成功,新程序的退出码会返回给子进程,同样可以被父进程拿到:
- 1 #include <unistd.h>
- 2 #include <stdio.h>
- 3 #include <stdlib.h>
- 4 #include <sys/wait.h>
- 5
- 6 int main()
- 7 {
- 8 pid_t id = fork();
- 9 if(id == 0)
- 10 {
- 11 printf("子进程, pid: %d\n", getpid());
- 12 execl("/bin/ls", "ls", "not found", NULL);
- 13 exit(1);
- 14 }
- 15
- 16 sleep(5);
- 17
- 18 int status = 0;
- 19 printf("父进程, pid: %d\n", getpid());
- 20 waitpid(id, &status, 0);
- 21 printf("child exit code: %d\n", WEXITSTATUS(status));
- 22
- 23 return 0;
- 24 }

因为 ls 指令没有 "not found" 命令选项,所以新程序一定会返回对应的退出码给子进程,并最终被父进程获取:
int execv(const char *path, char *const argv[]);
函数参数列表中, path 为程序路径, argv 数组内存放 命令 + 命令参数。 execl 与 execv 只在传参形式上有所不同。
观察结果:
int execlp(const char *file, const char *arg, ...);
函数参数列表中, file 为程序名, arg 为命令 + 命令参数, "..." 为可变参数。除了 file 外,其他用法与 execl 相同。
当我们执行指定程序的时候,只需要指定程序名即可,系统会自动在环境变量 PATH 中进行查找。
查看运行结果:
int execvp(const char *file, char *const argv[]);
函数参数列表中, file 为程序名, argv 数组内存放命令 + 命令参数。
- int execle(const char *path, const char *arg,
- ..., char * const envp[]);
execle 的函数参数列表中,比 execl 多了一个 envp , envp 为自定义环境变量。
我们在当前目录的子目录 exec 里再编写一个可执行文件 otherproc :
观察运行结果:
可以发现该子进程没有环境变量 MYENV 。
现在我们接着编写之前的 myproc 程序:
在 myproc 中使用 execle 函数调用 otherproc 程序,并给该程序传递环境变量 MYENV 。
运行并观察结果:
发现 otherproc 进程中已经有了环境变量 MYENV ,但是 PATH 却没有了。这是因为函数 execle 传递环境变量表是覆盖式传递的,老的环境变量表被全部清空了,只保留我们传递的自定义环境变量。
如果我们想在原有环境变量的基础上给进程添加环境变量,则可以使用函数 putenv :
运行观察结果:
此时就可以得到预期的结果了。
因为所有的进程都是 bash 的子进程,而 bash 执行所有的指令都可以直接通过 exec 来执行,如果需要把环境变量交给子进程,只需要调用 execle 就可以了。因此,我们成环境变量具有全局属性。
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
事实上,只有 execve 是真正的系统调用,其它函数都是 execve 的封装,最终都要调用 execve 。
关于进程控制的相关内容就讲到这里,希望同学们多多支持,如果有不对的地方欢迎大佬指正,谢谢!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。