【Linux】Linux进程控制
🥕个人主页:开敲🍉
🔥所属专栏:Linux🍊
🌼文章目录🌼
1. 进程创建
1.1 初始 fork 函数
1.2 写时拷贝
2. 进程终止
2.1 进程退出场景
2.2 进程常见退出方法
3. 进程等待
3.1 进程等待的必要性
3.2 进程等待的方法
3.3 获取子进程 status
3.4 进程阻塞等待/非阻塞等待
4. 进程程序替换
4.1 替换原理
4.2 替换函数
4.3 函数解释
4.3.1 函数参数解释
4.3.2 函数替换原理解释
4.4 命名理解
1. 进程创建
1.1 初始 fork 函数
在 Linux 中 fork 是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:子进程返回0,父进程返回子进程的 pid,出错则返回-1
进程调用 fork,当控制转移到内核中的 fork 代码后,内核做:
① 分配新的内存块和数据空间给子进程。
② 将父进程的部分数据拷贝给子进程。
③ 添加子进程到系统进程列表中。
④ fork返回,开始调度器调用。
当一个进程调用 fork 后,就有两个代码相同的进程。它们都运行到相同的地方,但每个进程都开始独立地运行自己的代码,看如下程序:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{pid_t id;printf("Before fork begin:%d\n",getpid());id = fork();if(id<0) perror("fork()\n");printf("After fork() begin:%d\n",getpid());sleep(3);return 0;
}
运行结果
显然,在调用 fork 函数创建子进程之前,只有父进程调用了 printf 函数打印 pid:23187;在调用 fork 函数创建子进程之后,父进程和子进程同时调用了后面的 printf(After),打印了两个pid。这也就说明了子进程在创建后是运行 fork 以下的代码。
fork 函数返回值
子进程返回0
父进程返回的是子进程的 pid
1.2 写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
用如下程序证明:
1 #include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 5 int g_val = 0;6 7 int main()8 {9 printf("父进程:%d\n",g_val);10 pid_t id = fork();11 if(id==0)12 {13 g_val = 100;14 printf("子进程:%d\n",g_val);15 }16 else17 {18 sleep(1);19 printf("父进程:%d\n",g_val);20 }21 return 0;22 }
这里第一次 printf 打印g_val的初始值,随后创建子进程,子进程修改 g_val后打印,同时后面继续执行父进程,因为 else 语句中父进程 sleep 1秒,因此子进程一定比父进程先执行完,则 g_val的值一定被修改了,我们来看结果:
可以看到,子进程确实修改了 g_val = 100,但是父进程随后打印出来的值还是0。由此便可证明发生了写时拷贝。
fork常规用法
① 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
② 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的情况
① 系统中有太多的进程。
② 用户实际的进程数超过了限制
2. 进程终止
2.1 进程退出场景
① 代码运行完毕,结果正确。
② 代码运行完毕,结果错误。
③ 代码异常终止。
2.2 进程常见退出方法
正常结束(包括结果错误)
代码正常运行结束,我们可以使用 echo $? 指令来查看程序的退出码:
int main()7 {8 printf("正常终止\n");9 return 0;10 }
可以看到,程序正常终止时退出码为0。
异常终止
通过如下程序获取异常终止退出码:
1 #include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 5 6 int main()7 {8 while(1)9 {10 sleep(1);11 printf("死循环:%d\n",getpid());12 }13 return 0;14 }
这里我们写一个死循环程序,随后通过 kill 命令强行终止程序:
此时程序的退出码不再是0。具体为什么是这个数字我们后面再将。
_exit函数
#inlcude <sys/wait.h>
void _exit(int status)
参数:status定义了进程的终止状态,父进程通过wait来获取该值。见如下程序:
1 #include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 #include <sys/wait.h>5 6 int main()7 {8 printf("Test\n");9 _exit(-1);10 }
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。
exit函数
#include <stdlib.h>
void exit(int status);
exit 本质还是调用 _exit 函数,但在调用 _exit前,还做了其他工作:
① 执行用户通过 atexit 或 on_exit 定义的清理函数。
② 关闭所有打开的流,所有的缓存数据均被写入。
③ 调用 _exit。
return 退出
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
3. 进程等待
3.1 进程等待的必要性
① 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
② 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
③ 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
④ 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
3.2 进程等待的方法
wait
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status)
返回值:
成功返回被等待进程的 pid,失败返回-1
参数status:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
waitpid
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1:等待任一个子进程。与wait等效。
Pid>0:等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
① 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
② 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。如果不存在该子进程,则立即出错返回。
③ 如果不存在该子进程,则立即出错返回。
3.3 获取子进程 status
① wait 和 waitpid 都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。
② 如果传递 NULL,表示不关心子进程的退出状态信息;否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
③ status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
测试代码1:正常退出
1 #include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 #include <sys/wait.h>5 #include <stdlib.h>6 7 int main()8 {9 gid_t pid = fork();10 if(pid<0)11 {12 perror("fork()\n");13 return 1;14 }15 else if(pid==0)16 {17 sleep(5);18 exit(10);19 }20 else21 {22 int st;23 int ret = wait(&st);24 if(ret>0&&(st&0x7f)==0)25 printf("正常退出:%d\n",st&0x7f);26 else if(ret>0)27 printf("异常退出:%d\n",st&0x7f);28 }29 return 0;30 }
测试结果
测试代码2:异常退出
1 #include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 #include <sys/wait.h>5 #include <stdlib.h>6 7 8 int main()9 {10 gid_t pid = fork();11 if(pid<0)12 {13 perror("fork()\n");14 return 1;15 }16 else if(pid==0)17 {18 while(1)19 {20 printf("子进程死循环:%d\n",getpid());21 sleep(1);22 }23 }24 else25 {26 int st;27 int ret = wait(&st);28 if(ret>0&&(st&0x7f)==0)29 printf("正常退出:%d\n",st&0x7f);30 else if(ret>0)31 printf("异常退出:%d\n",st&0x7f);32 }33 return 0;34 }
这里我们让子进程死循环,通过 kill 执行终止子进程:
此时父进程获取到子进程的退出码为9,也就是我们 kill 指令的 -9 选项。
3.4 进程阻塞等待/非阻塞等待
进程的阻塞等待
测试代码:
1 #include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 #include <sys/wait.h>5 #include <stdlib.h>6 7 8 int main()9 {10 gid_t id = fork();11 if(id<0)12 {13 perror("fork()\n");14 return 1;15 }16 else if(id==0)17 {18 printf("child is run,pid is:%d\n",getpid());19 sleep(5);20 exit(1);21 }22 else23 {24 int status = 0;25 gid_t ret = 0;26 ret = waitpid(-1,&status,0);//阻塞等待,子进程结束前父进程什么也不干27 if( WIFEXITED(status) && ret == id)28 {29 printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));30 }31 else32 {33 printf("wait child failed, return.\n");34 return 1;35 }36 }37 return 0;38 }
运行结果
进程非阻塞等待
测试代码:
1 #include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 #include <sys/wait.h>5 #include <stdlib.h>8 int main()9 {10 gid_t id = fork();11 if(id<0)12 {13 perror("fork()\n");14 return 1;15 }16 else if(id==0)17 {18 printf("child is run,pid is:%d\n",getpid());19 sleep(5);20 exit(1);21 }22 else23 {24 int status = 0;25 gid_t ret = 0;26 do//非阻塞等待,等待子进程结束期间父进程执行自己的代码27 {28 ret = waitpid(-1,&status,WNOHANG);29 if(ret==0)30 printf("parent is run,pid is :%d\n",getpid());31 sleep(1);32 }while(ret==0);33 34 if( WIFEXITED(status) && ret == id)35 {36 printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));37 }38 else39 {40 printf("wait child failed, return.\n");41 return 1;42 }43 }44 return 0;45 }
运行结果
4. 进程程序替换
4.1 替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 系列的函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
4.2 替换函数
有六种以 exec 为开头的函数,统称为 exec 函数:
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);int execle(const char *path, const char *arg, ...,char *const envp[]);
int execvp(const char *file, char *const argv[]);
这时候就有疑问了:不是说有六种吗,怎么上面只列举了五种呢?这是因为,上面五个函数都不是系统层面的函数,而是封装好的语言函数,上面五个函数实际上真正调用的是下面的这个系统函数:
int execve(const char *path, char *const argv[], char *const envp[]);
这点我们从 man 就可以证明:
4.3 函数解释
4.3.1 函数参数解释
我们首先以第一个函数为例进行解析:int execl(const char *path, const char *arg, ...);
① 第一个参数 "path",意为 "路径",这个参数需要我们传递:想要替换的程序的路径。比如想要替换为 "ls" 命令程序,则需要传递 "usr/bin/ls"。
② 第二个参数传递的是 程序名,这是可选的,可传可不传。
③ 第三个 ... 就是可变参数,传递我们执行命令的选项。比如:我们在Linux下执行 ls 命令时,后面通常会跟选项 ls -a -l ,这里我们如果想要带选项,则将选项传给后面的参数。
用法:
④ 可以看到,最后我们还传了一个NULL参数,这个不可省略,这是 参数传递完毕 的标志。
执行结果:
(这里有个奇怪的现象相信细心的人已经发现了:最后一个 printf没有打印。这就是下面 原理解释 部分讲解的)
有了第一个函数的基础,理解第二个就非常简单了:int execlp(const char *file, const char *arg, ...);
第二个和第一个的区别就在于第一个参数发生了变化——file。
这个也非常简单,第一个函数我们在替换时需要传递想要替换的程序的路径,而这个我们不需要传递路径,直接传程序名即可:
其它的和 execl 完全一样,执行结果我们就不看了。
但是这个函数有个局限:我们之所以能够直接传程序名,是因为这个函数默认会去 PATH 环境变量中查找,系统的命令都在 PATH 中的路径里面,因此可以查到。意味着我们使用这个函数想要执行自己的程序时如果直接传程序名是没法调用成功的:
上面我们创建一个 tmp.c 文件,用于判断我们能否直接传递程序名进行替换:
执行结果:
可以看到,最后一个 printf 打印了出来,这意味着我们 execlp函数调用失败,也就意味着我们如果想要执行自己的程序不能够直接传程序名,而要在程序名前面加上 "./" 用来告诉操作系统传递的这个程序在当前路径下,不要去 PATH 中查找:
下面再来第三个:int execle(const char *path, const char *arg, ...,char *const envp[]);
第三个乍一看和第一个 execl 十分相似,但是多了最后一个 const envp[] 的参数。在我们之前学习进程概念时,提到了 环境变量 的概念(进程概念),在那部分,我们还解释了 main 函数中的三个参数。其中,main函数中的第三个参数名为 env[],传递的是系统的环境变量。
我们再回到这,envp[],就能够知道这应该就是传递环境变量的参数,我们写个程序来验证一下:code2.c
我们先执行一下 code2:
环境变量获取成功,接着我们在 code.c 中替换 code2 程序:
执行结果:
这里我们还可以传递自己定义的环境变量:
执行结果:
此时这里就又出现了一个现象:当我们传递自己定义的环境变量时,最终打印出来的就是自己的环境变量,而不再打印原本系统的环境变量,这就说明:myenv 替换了系统的 env。
那如果我们不想直接让自己的环境变量替换掉系统的,而是在系统的环境变量中加入自己的环境变量最终打印出来,该如何操作呢?看如下程序:
我们使用 putenv 函数,将我们自己定义的环境变量放入当前进程的环境变量,随后使用环境变量表指针 environ 作为参数传递:这里直接传递 env 的话可能没法显示自己定义的环境变量(当时调这个bug调到我怀疑人生)。
执行结果:
可以看到,这时候我们再打印环境变量就可以打印出自己的和系统的。
接下来我们继续理解第四个:int execv(const char *path, char *const argv[]);
这个函数和前面的区别就比较大了,这个函数只有两个参数:第一个参数还是程序的路径;第二个参数是什么呢?
可以看到第二个参数是一个数组,命名为 argv,唉?这不还是之前学习的 main 函数三个参数中的第二个参数吗?
仔细回忆一下,char* argv[]不就是命令的选项吗?那么结合这个参数是个数组的情况我们就可以作出如下判断:这还是传递命令选项的参数,只不过与前面的区别在于,前面是一个一个地传,而这里则是将要传递地选项全都放入一个数组中,随后直接传递整个数组。
判断有了,下面我们来验证:
执行结果:
可以看到,和我们判断的差不多,只不过少了一个:程序名也要放入 argv数组中,随后跟着的就是各种选项。
有了上面的基础,理解最后一个就非常简单了:int execvp(const char *file, char *const argv[]);
无非就是不需要再传递程序的路径,直接传递程序名即可。同样的,这个函数和 execlp有着同样的局限性:执行自己的程序时不能只传程序名。
最后还有个特殊的系统层级的函数:int execve(const char *path, char *const argv[], char *const envp[]);
这个就是上面 5 个封装好的 exec 函数最终调用的函数,有了上面 5 个函数的理解,理解这个也是信手拈来。
下面这张图也可以帮我们很好理解上面五个函数之间的以及上面五个函数和系统函数的关系:
4.3.2 函数替换原理解释
① 这些函数如果调用成功则加载新的程序,从启动代码开始执行,不再返回。
② 如果调用出错则返回 -1。
由上我们可以知道,exec 系列函数调用后,只要有返回值,就一定是调用出错了;否则,就一定调用成功。
见如下程序:
这里我们用系统命令 "ls" 来替换当前进程,这里一定能够替换成功,因为 "ls" 是系统提供的。执行结果:
可以看到,最后并没有打印返回值,因为:进程替换成功后,原进程后面的代码就被新进程完全覆盖了,因此可以说原进程的代码消失了。
再来看下面的例子:
这里我们想要用命令程序 "ll"来替换当前程序,但是 "ll"并不是系统提供的命令,因此这里一定会调用出错。执行结果:
此时,函数调用失败,进程没有替换,因此还是会继续执行原进程后面的代码。
下面还有个例子证明进程替换的原理:
执行结果:
可以看到,最开始的 printf 是正常执行的,随后调用 进程替换函数 后最后的 printf 就没有执行了。这也就可以证明 4.1 替换原理 中所说的:
我们还可以用 pid 来证明 exec 并不创建新进程,见如下程序:
我们再创建一个文件,用于观察进程替换后的进程pid:
准备工作做好后,我们用 code1 程序替换 code:
4.4 命名理解
这些函数原型看起来容易混淆,但只要掌握了规律就非常好记忆:
l(list):表示参数参用列表的形式一个一个传 execl
v(vector):表示参数用数组形式传递 execv
p(path):表示不需要传递路径,直接传递程序名,自动在 PATH 环境变量中检索
e(env):传递环境变量
创作不易,点个赞呗,蟹蟹啦~