【Linux】进程控制

秦jh_ 2024-10-03 12:37:03 阅读 56

 🌈个人主页:秦jh__https://blog.csdn.net/qinjh_?spm=1010.2135.3001.5343

🔥 系列专栏:https://blog.csdn.net/qinjh_/category_12625432.html

9efbcbc3d25747719da38c01b3fa9b4f.gif

目录

fork函数初识

fork 函数返回值 

写时拷贝

 fork调用失败的原因

进程终止 

 进程退出场景

 进程常见退出方法

 进程等待

进程等待必要性

进程等待的方法

wait方法

 waitpid方法

 获取子进程status

非阻塞等待


前言

💬 hello! 各位铁子们大家好哇。

             今日更新了Linux的进程控制的内容

🎉 欢迎大家关注🔍点赞👍收藏⭐️留言📝

fork函数初识

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

<code>#include <unistd.h>

pid_t fork(void);

//返回值:子进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

分配新的内存块和内核数据结构给子进程将父进程部分数据结构内容拷贝至子进程添加子进程到系统进程列表当中 fork返回,开始调度器调度

4222d2554a3e4c3482974e91c4c48a30.png

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以 开始它们自己的旅程。 

fork 函数返回值 

子进程返回0,父进程返回的是子进程的pid。 

为什么父进程返回的是子进程的pid?

为了让父进程方便对子进程进行标识,进而进行管理。 

写时拷贝

通常,父子代码共享,父子不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。 (进程的独立性)

 fork调用失败的原因

系统中有太多的进程实际用户的进程数超过了限制 

进程终止 

进程终止做的事:

释放曾经的代码和数据所占据的空间释放内核数据结构

内核数据结构中,PCB会被延期处理,因为有一种状态是僵尸状态。

<code> #include<stdio.h>

2 #include<unistd.h>

3

4 int main()

5 {

6 printf("I am process, pid:%d, ppid:%d\n",getpid(),getppid());

7 sleep(2);

8 return 100;

9 }

319ed017acbb4221a7e20a2b62f493c0.png

任何命令行启动的进程,它的父进程都是bash,所以ppid都一样。 

echo是内建命令,打印的都是bash内部的变量数据。是一个变量名。

echo $?表示的是父进程获取到的,最近一个子进程退出的退出码。

main函数的返回值叫做进程的退出码。

退出码:

为0,标识成功不为0,表示失败

a681cdc819a542419cecb86022a7e7d7.png

第一个echo $?返回./myprocess 的退出码,第二个echo $?返回上一个echo $?的退出码

虽然echo $?没有创建子进程,但它是由父进程执行的,所以他也会影响?的值。 

024160f61dac4cfc85cbc3b89226a2bf.png

<code> #include<stdio.h>

2 #include<unistd.h>

3 #include<string.h>

4

5 int main()

6 {

7 for(int errcode=0;errcode<=255;errcode++)

8 {

9 printf("%d:%s\n",errcode,strerror(errcode));

10 }

11 printf("I am process, pid:%d, ppid:%d\n",getpid(),getppid());

12 sleep(2);

13 return 0;

14 }

退出码不为0表示失败。不同的非0值,一方面表示失败,另一方面表示失败的原因。

strerror函数会将错误码转成对应的错误描述,如下图;

2f7eae6278334a21bd3c64ad768dd2ca.png

进程为什么要得到子进程的退出码呢?

因为要知道子进程的退出情况。(成功还是失败,失败的原因是什么),然后展现给用户看。

退出码可以使用系统默认的,也可以自定义。 

 进程退出场景

 进程终止的3中情况:

代码跑完,结果正确代码跑完,结果不正确代码异常终止

代码跑完,结果不正确的原因可以通过退出码确定。

一旦出现异常,退出码就没有意义了。

进程出异常,本质是因为进程收到了OS发给进程的信号。

<code> int main()

6 {

7 int *p=NULL;

8 while(1)

9 {

10 printf("I am a process,pid:%d\n",getpid());

11 sleep(1);

12 *p=100;//野指针

13 }

19 sleep(2);

20 return 0;

21 }

c79e444ac1c14d4691619137518b0917.png

当外面运行上面代码后,会报段错误。 OS就会提前终止进程

f5bd024b28a74553aa72b45b4af936c3.png

 我们把代码里的野指针注释掉,此时代码正常运行,一直循环。此时我们给该进程发11号信号,该进程即使没有错误,收到信号后,也会进行对应的报错。 所以说进程出异常,本质是因为进程收到了OS发给进程的信号。

所以如果进程异常了,我们可以通过退出信号,就可以判断进程为什么异常了,此时的退出码是无意义的。

在用户层面上,要确定进程是什么情况:

先确认是否异常如果不是异常,就一定是代码跑完了,看退出码即可。

衡量一个进程退出,只需要两个数字:退出码和退出信号。

58d7e42eb536436d8d30361cd37dee9b.png

 进程的PCB里面有退出信号和退出码,当进程退出时,会释放代码和数据,但是PCB会保存一段时间,该进程变成Z(僵尸)状态。父进程就可以从子进程的PCB中拿到退出信息。

 进程常见退出方法

正常终止:

main函数return,表示进程终止(非main函数的return,都只是表示函数结束)调用exit函数  注意:在代码的任意位置调用exit,都表示进程退出_exit (系统调用)

下面是exit的使用举例: 

46d0faa80bd24f759e5c9572f12af158.png

9c106357391140a581b5d83ed16b10b3.png


_exit和exit在使用上没什么区别,只有一个细微的差别,如下例子: 

65bd46c902a64a94b00829c6cf6698bc.png

c51ee9f6ed84403d8b4dee798ddc2d84.png

 上图是带\n的。结果打印并且换行了。

 

ca30f4665c2a40039a65bfc4fd127b33.png

0a40b2280e9741efb41f326a66f0d1eb.png

 上面是不带\n的。结果打印了但没换行。

6742125addad46eaa4ea95fbf9ae6d67.png

 

8a5cbfd368f143749dce3ce8a544cd7e.png

上面是不带\n的_exit的使用。结果什么也没打印。 

 结论:exit会在进程退出的时候,冲刷缓冲区,_exit不会。

73846c51dafc4242aa9ddbf97785c5f6.png

exit在底层是调用_exit的。 由上面的结论可得,缓冲区在_exit之上,不然_exit也会冲刷缓冲区。

 进程等待

进程等待必要性

子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的方法

wait方法

<code> 1 #include <stdio.h>

2 #include <unistd.h>

3 #include <string.h>

4 #include <stdlib.h>

5 #include <sys/types.h>

6 #include <sys/wait.h>

7

8 void ChildRun()

9 {

10 int cnt = 5;

11 while(cnt)

12 {

13 printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), get ppid(), cnt);

14 sleep(1);

15 cnt--;

16 }

17 }

18

19 int main()

20 {

21 printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());

22

23 pid_t id = fork();

24 if(id == 0)

25 {

26 // child

27 ChildRun();

28 printf("child quit ...\n");

29 exit(0);

30 }

31 // fahter

32 pid_t rid = wait(NULL);

33 if(rid > 0)

34 {

35 printf("wait success, rid: %d\n", rid);

36 }

37 }

1bac1ec205b14d62a97797e7dfcbe407.png

作用:等待任何一个子进程退出

返回值:等待成功时,返回子进程的pid。失败返回-1。 

参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

运行上面的代码,结果如下图:

4f55d1b963d740bf9c21cd334c853893.png

07a8e754be6149718b8baafab6e08c0c.png

 上面代码if后面不需要else就表示是父进程的代码了。因为if里面即子进程里面用exit退出了,所以后面的都是父进程的。

 下面做出修改:

2040a5225160487bae54789443ffac91.png

运行结果:

f3ef0f84845e444faad7a920a0897341.png

395d944bbd75468b9936c06fb78454ed.png

 修改后的代码先让父进程休眠十秒。子进程运行五秒后退出,此时由于父进程还在休眠无法回收,所以子进程就变成Z状态,再过五秒后,子进程就被父进程回收了。

21b2b53882e04cebbcdcea678b9f4761.png

 如果我们把sleep(10)注释掉,此时父进程开始就马上进入等待,等待期间父进程不会被调度。如果子进程没有退出,父进程其实一直在阻塞等待。

 waitpid方法

44b52cc9c6234dfe8b37ffe4fc075db1.png

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。 

b539af90b6fc4f2d930fb6f54774eef9.png

waitpid有三个参数,当pid,即第一个参数为-1时,等待任意一个子进程,与wait等效。

当第一个参数pid>0时,就会等待其进程ID与pid相等的子进程 。

如下图,此时等待上方父进程的子进程。

 

e1b3369a4d09403a83fc39f93abea3ed.png

等待失败例子:

267ce05c9408456eac8ccb2e3ba03255.png

当我们把pid给一个错误的,此时进程就是等待失败。 

 获取子进程status

9561a3f9229b4f85bd2f60442eb20512.png

267e939dbf8e482abbf940c6c85eb65b.png

 第二个参数status代表的是子进程的退出信息。

退出信息=退出码+退出信号

fdeb1eae4f2d4a638ec006d69b6901db.png

wait和waitpid,都有一个status参数,该参数是一个输出型参数。如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。status不能简单的当作整形来看待,可以当作位图来看待(只研究status低16比特 位):

 最低7位表示终止信号,9到16位表示退出码。所以退出码的范围是0~255。退出信号的范围是0~125。这两个范围足以表示退出码的退出信号的情况了。

<code> 19 int main()

20 {

21 printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());

22

23 pid_t id = fork();

24 if(id == 0)

25 {

26 // child

27 ChildRun();

28 printf("child quit ...\n");

29 exit(1);

30 }

31 sleep(7);

32 // fahter

33 // pid_t rid = wait(NULL);

34 int status=0;

35 pid_t rid=waitpid(id,&status,0);

36 if(rid > 0)

37 {

38 printf("wait success, rid: %d\n", rid);

39 }

40 else

41 {

42 printf("wait failed !\n");

43 }

44 sleep(3);

45 printf("father quit, status:%d, child quit code : %d,child quit signal: % d\n",status,(status>>8)&0xFF,status&0x7F);

46 }

b968fef8bab94d0ea2eb1cf604e4da1d.png

上面是通过status退出信息来获取退出码和退出信号的代码。

status右移8位,然后与0xFF即二进制的8个1进行按位与,获取退出码。

status按位与0x7F即二进制的7个1来获取退出信号。

结果表明,前面所说都是正确的。

 实际上我们不使用位操作符处理status,而是使用两个宏,WIFEXITED和WEXITSTATUS。

0c3b4634e6fb4ebdb32d5fb1ccc41d80.png

0886a08b7f2740959fed79bad71261af.png

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

非阻塞等待

 我们用的大部分接口都是阻塞等待接口,在阻塞等待时,父进程干不了别的事,一直在等待子进程退出。下面介绍非阻塞等待。

这里也需要用到一个宏:WNOHANG

<code>1 #include <stdio.h>

2 #include <unistd.h>

3 #include <string.h>

4 #include <stdlib.h>

5 #include <sys/types.h>

6 #include <sys/wait.h>

7

8 void ChildRun()

9 {

10 int cnt = 5;

11 while(cnt)

12 {

13 printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid (), cnt);

14 sleep(1);

15 cnt--;

16 }

17 }

18

19 int main()

20 {

21 printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());

22

23 pid_t id = fork();

24 if(id == 0)

25 {

26 // child

27 ChildRun();

28 printf("child quit ...\n");

29 exit(123);

30 }

31

32 //father

33 while(1)

34 {

35 int status=0;

36 pid_t rid=waitpid(id,&status,WNOHANG); //非阻塞等待

37 if(rid==0)

38 {

39 sleep(1);

40 printf("child is running, father check next time!\n");

41 //DoOtherThing();

42 }

43 else if(rid>0)

44 {

45 if(WIFEXITED(status))

46 {

47 printf("child quit success, child exit code:%d\n",WEXITSTATUS(status));

48 }

49 else

50 {

51 printf("child quit unnormal!\n");

52 }

53 break;

54 }

55 else

56 {

57 printf("waitpid failed!\n");

58 break;

59 }

60 }

使用WNOHANG的时候,需要使用循环结构。因为WNOHANG只会查看一次子进程是否结束,使用循环结构就可以到最后判断子进程是什么情况了。即非阻塞等待的时候+循环=非阻塞轮询。

在非阻塞等待时,父进程可以在每次查看子进程的间隙做其他事情。

pid_ t waitpid(pid_t pid, int *status, int options);

pid_t >0 :等待成功,子进程退出了,并且父进程回收成功

pid_t <0 :等待失败了

pid_t =0 :检测是成功的,只不过子进程还没退出,需要下一次进行重复等待。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。