• <Linux进程控制>——《Linux》


    目录

    1.进程创建

    1.1 fork函数初识

    1.2 写时拷贝

    1.3 fork常规用法

    1.4 fork调用失败的原因

    2.进程终止

    2.1进程退出场景

    2.2进程常见退出方法

    3. 进程等待

    3.1进程等待必要性

    3.2进程等待的方法

    3.2.1wait方法

    3.2.1waitpid方法

     3.3 获取子进程status

    3.4 具体代码实现

    3.4.1进程的阻塞等待方式

    3.4.2进程的非阻塞等待方式:

    后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

                                                                           ——By 作者:新晓·故知


    1.进程创建

    1.1 fork函数初识

    linux fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
    #include
    pid_t fork(void);
    返回值:自进程中返回0,父进程返回子进程id,出错返回-1
    进程调用fork,当控制转移到内核中的fork代码后,内核做:
    • 分配新的内存块和内核数据结构给子进程
    • 将父进程部分数据结构内容拷贝至子进程
    • 添加子进程到系统进程列表当中
    • fork返回,开始调度器调度

    当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序:
    1. int main( void )
    2. {
    3. pid_t pid;
    4. printf("Before: pid is %d\n", getpid());
    5. if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
    6. printf("After:pid is %d, fork return %d\n", getpid(), pid);
    7. sleep(1);
    8. return 0;
    9. }
    10. 运行结果:
    11. [root@localhost linux]# ./a.out
    12. Before: pid is 43676
    13. After:pid is 43676, fork return 43677
    14. After:pid is 43677, fork return 0
    这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after 消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示:
    所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。写一段程序用于演示:

    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. printf("我是一个进程:pid: %d, ppid: %d\n",getpid(),getppid());
    7. fork();
    8. printf("已调用fork()函数,我依旧是一个进程:pid: %d, ppid: %d\n",getpid(),getppid());
    9. return 0;
    10. }
    fork之后,有两个进程,父子进程共享所有的代码!但子进程执行的后续代码!=共享的所有代码,只不过子进程只能从这里开始执行!
    可以通过vfork+return验证!
    进程=内核的进程数据结构+进程的代码和数据。
    fork之后,创建子进程的内核数据结构(struct task_struct+struct mm_struct + 页表)+代码继承父进程,数据以写时拷贝的方式,来进行共享!
    fork函数返回值
    • 子进程返回0,
    • 父进程返回的是子进程的pid。

    1.2 写时拷贝

    通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副 本。具体见下图:
    写时拷贝本身就是由OS的内存管理模块实现的!
    为什么要写时拷贝?
    创建子进程的时候,就把数据分开不行吗?这种方案是可以的,但是不选择这样做:因为父进程的数据子进程不一定全用,避免浪费空间!即便使用,也不一定全部写入!且最理想的情况:只有会被父子进程修改的数据进行分离拷贝,不需要修改的数据共享即可,但这从技术角度实现复杂!如果fork的时候,就直接拷贝数据给子进程,这会增加fork的成本(内存和时间)因此采用写时拷贝!
    写时拷贝的好处:只会拷贝父子进程修改的(拷贝数据的最小成本)。只读的数据共享!
    但拷贝的成本依旧存在!而写时拷贝本质上是延迟拷贝策略,只有真正使用的时候才传递(你想要资源,但是不立马使用,那就先不给你,那也就意味着可以先给别人,这样变相的提高资源利用率)。

    1.3 fork常规用法

    • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
    • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

    1.4 fork调用失败的原因

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

     

    统计失败前,已创建的子进程个数: 

    1. #include
    2. #include
    3. #include
    4. int cnt=0;
    5. int main()
    6. {
    7. for(; ;)
    8. {
    9. pid_t id=fork();
    10. if(id<0)
    11. {
    12. printf("创建子进程失败!已创建的子进程个数 cnt:%d\n",cnt);
    13. break;
    14. }
    15. if(id==0)
    16. {
    17. printf("我是一个子进程,我得pid:%d\n",getpid());
    18. sleep(200);
    19. exit(0);
    20. }
    21. cnt++; //统计 父进程创建的子进程个数
    22. }
    23. return 0;
    24. }

     

    2.进程终止

    2.1进程退出场景

    • 代码运行完毕,结果正确
    • 代码运行完毕,结果不正确
    • 代码异常终止

    2.2进程常见退出方法

    正常终止(可以通过 echo $? 查看进程退出码):
    1. 从main返回
    2. 调用exit
    3. _exit
    异常退出:
    ctrl + c,信号终止
    _exit函数:
    1. #include
    2. void _exit(int status);

    参数:status 定义了进程的终止状态,父进程通过wait来获取该值
    说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。
    exit 函数:
    1. #include
    2. void exit(int status);

    exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
    1. 执行用户通过 atexit或on_exit定义的清理函数。
    2. 关闭所有打开的流,所有的缓存数据均被写入
    3. 调用_exit

     

    实例:

    1. int main()
    2. {
    3. printf("hello");
    4. exit(0);
    5. }
    6. 运行结果:
    7. [root@localhost linux]# ./a.out
    8. hello[root@localhost linux]#
    9. int main()
    10. {
    11. printf("hello");
    12. _exit(0);
    13. }
    14. 运行结果:
    15. [root@localhost linux]# ./a.out
    16. [root@localhost linux]#

    return 退出:
    return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
    C/C++中main函数是程序的入口,那return 0是给谁return?为何是0?其他值可以吗?
    return是进程代码执行完,把结果是否做正确返回!(0:sucess,!0:fail)非零标识不同失败的原因!因此将return X称为进程退出码,表征进程退出信息!这个退出信息让父进程读取!(父进程创建子进程是要让子进程帮父进程做一些事情),因此return给了父进程!

    $?:在bash中,最近一次执行完毕时,对应进程的退出码! 

     

    一般而言,失败的非零值该如何设置?以及默认表达的含义?

    可以随便设置。但OS是如何处理呢?

    写一段C语言程序查看C语言处理: 

     错误退出码可以对应不同的错误原因,方便定位问题!

    关于进程终止的常见做法:

    1.在main函数中return,(非main函数中return不代表进程退出,只代表函数结束)

    2.在自己的代码任意地点中,调用exit()函数。(或_exit()函数)

     

    exit终止进程,会刷新缓冲区,_exit直接终止进程,不会刷新缓冲区! 

    关于进程终止,内核做了什么?

    进程=内核结构+进程代码和数据。进程终止,内核会释放进程代码和数据,但可能并不会释放进程的内核数据结构!我们知道,创建对象:1.开辟空间2.初始化。而进程终止时,内核可能不会释放内核的数据结构,只是将其设置为无效,进行维护!当再次创建时,节省时间成本。这里维护与内核的数据结构缓冲池有关,(slab分派器)(多次高频度创建结构,就会考虑使用数据结构池) 

    exit与_exit举例:

    3. 进程等待

    3.1进程等待必要性

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

    3.2进程等待的方法

    3.2.1wait方法

    1. #include
    2. #include
    3. pid_t wait(int*status);
    4. 返回值:
    5. 成功返回被等待进程pid,失败返回-1。
    6. 参数:
    7. 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

    3.2.1waitpid方法

    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,子进程存在且正常运行,则进程可能阻塞。
    • 如果不存在该子进程,则立即出错返回。

    模拟进程等待:
    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. pid_t id=fork();
    9. if(id==0)
    10. {
    11. //id==0:子进程
    12. while(1)
    13. {
    14. printf("我是子进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid());
    15. sleep(1);
    16. }
    17. }
    18. else
    19. {
    20. //id>0:父进程
    21. printf("我是父进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid());
    22. printf("我准备等待子进程\n");
    23. sleep(40);
    24. pid_t ret=wait(NULL);
    25. if(ret<0)
    26. {
    27. printf("等待失败!\n");
    28. }
    29. else
    30. {
    31. printf("等待成功! result: %d\n",ret);
    32. }
    33. sleep(20);
    34. }
    35. return 0;
    36. }

     

    先终止子进程,让父进程等待  

     使用waitpid()函数接口:

    pid_t wait(int *status)

    wait等待任意一个退出的子进程!

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

    返回值pid_t >0:等待子进程成功,返回值就是子进程的pid

    返回值pid_t <0:等待失败

    pid>0:pid是几,就代表等待哪一个子进程

    pid = -1:等待任意进程!

    options =0:阻塞等待

    status:指针参数,是一个输出型参数:通过调用该函数,从函数内部拿出来特定的数据!

    其实就是从操作系统拿出来!

    当父进程等待子进程时:

    1.子进程执行完正常退出后,子进程会将自己的退出信息写入自己的task_struct结构,父进程调用wait/waitpid()会从操作系统找到子进程的task_struct获取退出信息(从子进程的进程控制块中获取)。

    2.子进程没有正常执行完(没有退出),那么父进程就必须阻塞等待,等子进程退出后,才能正式拿出来。

    注:

    wait/waitpid()是系统调用!

     

    关于status:只需要关心该整数的低16bit位!(32bit /整数) 这16个bit位被分为3部分。

     9~16bit位:(退出状态)

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. pid_t id=fork();
    9. if(id==0)
    10. {
    11. //id==0:子进程
    12. int cnt=5;
    13. while(1)
    14. {
    15. printf("我是子进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid());
    16. sleep(1);
    17. cnt--;
    18. if(!cnt)
    19. {
    20. break;
    21. }
    22. }
    23. exit(23);
    24. }
    25. else
    26. {
    27. //id>0:父进程
    28. printf("我是父进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid());
    29. printf("我准备等待子进程\n");
    30. int status=0;
    31. pid_t ret=waitpid(id,&status,0);
    32. if(ret>0)
    33. {
    34. printf("等待成功!ret: %d,我所等待的子进程的退出码:%d \n",ret,(status>>8)&0xFF); //取出status的低16位
    35. }
    36. // sleep(40);
    37. // pid_t ret=wait(NULL);
    38. // if(ret<0)
    39. // {
    40. // printf("等待失败!\n");
    41. // }
    42. // else
    43. // {
    44. // printf("等待成功! result: %d\n",ret);
    45. // }
    46. // sleep(20);
    47. }
    48. return 0;
    49. }

     

     

    status的低8位:(1~8bit位)

    1~7bit:(终止信号)

    第8bit位:code dump 

     

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. pid_t id=fork();
    9. if(id==0)
    10. {
    11. //id==0:子进程
    12. //int cnt=5;
    13. while(1)
    14. {
    15. printf("我是子进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid());
    16. sleep(1);
    17. // cnt--;
    18. // if(!cnt)
    19. // {
    20. // break;
    21. // }
    22. }
    23. exit(0);
    24. }
    25. else
    26. {
    27. //id>0:父进程
    28. printf("我是父进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid());
    29. printf("我准备等待子进程\n");
    30. int status=0;
    31. pid_t ret=waitpid(id,&status,0);
    32. if(ret>0)
    33. {
    34. printf("等待成功!ret: %d,我所等待的子进程的退出码:%d ,退出信号:%d\n",
    35. ret,(status>>8)&0xFF,status&0x7F); //取出status的低16位,以及低16位的前7位(终止信号)
    36. }
    37. // sleep(40);
    38. // pid_t ret=wait(NULL);
    39. // if(ret<0)
    40. // {
    41. // printf("等待失败!\n");
    42. // }
    43. // else
    44. // {
    45. // printf("等待成功! result: %d\n",ret);
    46. // }
    47. // sleep(20);
    48. }
    49. return 0;
    50. }

     结论:

    代码运行完毕:退出码:exit()=0

    反馈结果:看退出信号

    异常终止:看退出信号

    一旦进程出现异常,退出码就没有意义,只看退出信号!

    可以使用Linux提供的宏查看:

     

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. pid_t id=fork();
    9. if(id==0)
    10. {
    11. //id==0:子进程
    12. int cnt=5;
    13. while(1)
    14. {
    15. printf("我是子进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid());
    16. sleep(1);
    17. cnt--;
    18. if(!cnt)
    19. {
    20. break;
    21. }
    22. }
    23. exit(0);
    24. }
    25. else
    26. {
    27. //id>0:父进程
    28. printf("我是父进程,我的pid:%d, 我的父进程ppid:%d\n",getpid(),getppid());
    29. printf("我准备等待子进程\n");
    30. int status=0;
    31. pid_t ret=waitpid(id,&status,0);
    32. if(ret>0)
    33. {
    34. if(WIFEXITED(status))
    35. {
    36. printf("子进程是正常退出的,退出码:%d\n",WEXITSTATUS(status));
    37. printf("等待成功!ret: %d,我所等待的子进程的退出码:%d ,退出信号:%d\n",
    38. ret,(status>>8)&0xFF,status&0x7F); //取出status的低16位,以及低16位的前7位(终止信号)
    39. }
    40. }
    41. // sleep(40);
    42. // pid_t ret=wait(NULL);
    43. // if(ret<0)
    44. // {
    45. // printf("等待失败!\n");
    46. // }
    47. // else
    48. // {
    49. // printf("等待成功! result: %d\n",ret);
    50. // }
    51. // sleep(20);
    52. }
    53. return 0;
    54. }

     3.3 获取子进程status

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

    1. 测试代码:
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main( void )
    8. {
    9. pid_t pid;
    10. if ( (pid=fork()) == -1 )
    11. perror("fork"),exit(1);
    12. if ( pid == 0 ){
    13. sleep(20);
    14. exit(10);
    15. } else {
    16. int st;
    17. int ret = wait(&st);
    18. if ( ret > 0 && ( st & 0X7F ) == 0 ){ // 正常退出
    19. printf("child exit code:%d\n", (st>>8)&0XFF);
    20. } else if( ret > 0 ) { // 异常退出
    21. printf("sig code : %d\n", st&0X7F );
    22. }
    23. }
    24. }
    25. 测试结果:
    26. [root@localhost linux]# ./a.out #等20秒退出
    27. child exit code:10
    28. [root@localhost linux]# ./a.out #在其他终端kill掉
    29. sig code : 9

    3.4 具体代码实现

    当我们调用某些函数的时候,因为条件不就绪,需要我们阻塞等待。本质:就是当前进程自己变成阻塞状态,等条件(任意的软硬件)就绪的时候,再被唤醒!

    3.4.1进程的阻塞等待方式

    1. int main()
    2. {
    3. pid_t pid;
    4. pid = fork();
    5. if(pid < 0){
    6. printf("%s fork error\n",__FUNCTION__);
    7. return 1;
    8. } else if( pid == 0 ){ //child
    9. printf("child is run, pid is : %d\n",getpid());
    10. sleep(5);
    11. exit(257);
    12. } else{
    13. int status = 0;
    14. pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
    15. printf("this is test for wait\n");
    16. if( WIFEXITED(status) && ret == pid ){
    17. printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
    18. }else{
    19. printf("wait child failed, return.\n");
    20. return 1;
    21. }
    22. }
    23. return 0;
    24. }
    25. 运行结果:
    26. [root@localhost linux]# ./a.out
    27. child is run, pid is : 45110
    28. this is test for wait
    29. wait child 5s success, child return code is :1.

    3.4.2进程的非阻塞等待方式:

    1. #include
    2. #include
    3. #include
    4. #include
    5. int main()
    6. {
    7. pid_t pid;
    8. pid = fork();
    9. if(pid < 0){
    10. printf("%s fork error\n",__FUNCTION__);
    11. return 1;
    12. }else if( pid == 0 ){ //child
    13. printf("child is run, pid is : %d\n",getpid());
    14. sleep(5);
    15. exit(1);
    16. } else{
    17. int status = 0;
    18. pid_t ret = 0;
    19. do
    20. {
    21. ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
    22. if( ret == 0 ){
    23. printf("child is running\n");
    24. }
    25. sleep(1);
    26. }while(ret == 0);
    27. if( WIFEXITED(status) && ret == pid ){
    28. printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
    29. }else{
    30. printf("wait child failed, return.\n");
    31. return 1;
    32. }
    33. }
    34. return 0;
    35. }

    1. #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6
    7. 7 int main()
    8. 8 {
    9. 9 pid_t id = fork();
    10. 10 if(id==0)
    11. 11 {
    12. 12 //子进程
    13. 13 while(1)
    14. 14 {
    15. 15 printf("我是子进程,我的pid:%d,我的ppid:%d\n",getpid(),getppid());
    16. 16 sleep(5);
    17. 17 int *p=NULL;
    18. 18 *p=100; //野指针
    19. 19 // break;
    20. 20 }
    21. 21 exit(100);
    22. 22 }
    23. 23 else if(id>0)
    24. 24 {
    25. 25 //父进程
    26. 26 printf("我是父进程,我的pid:%d,我的ppid:%d\n",getpid(),getppid());
    27. 27 int status=0;
    28. 28 pid_t ret=waitpid(-1,&status,0);
    29. 29 if(ret>0)
    30. 30 {
    31. 31 printf("等待成功!我等待的返回值:%d,exit signal:%d,exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
    32. 32 }
    33. 33 sleep(3);
    34. 34 }
    35. 35 }

     非阻塞:

    多次调用非阻塞接口,轮询检测!

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. pid_t id = fork();
    9. if(id==0)
    10. {
    11. //子进程
    12. while(1)
    13. {
    14. printf("我是子进程,我的pid:%d,我的ppid:%d\n",getpid(),getppid());
    15. sleep(5);
    16. // int *p=NULL;
    17. // *p=100; //野指针
    18. // break;
    19. }
    20. exit(100);
    21. }
    22. else if(id>0)
    23. {
    24. //父进程
    25. //基于非阻塞的轮巡检测方案
    26. int status=0;
    27. while(1)
    28. {
    29. pid_t ret=waitpid(-1,&status,WNOHANG);
    30. if(ret>0)
    31. {
    32. printf("等待成功,返回值为:%d,exit signal:%d,exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
    33. break;
    34. }
    35. else if(ret==0)
    36. {
    37. //等待成功了,但是子进程没有退出
    38. printf("我是父进程,等待子进程,奥,还没,那么我父进程做其他事情...\n");
    39. sleep(1);
    40. }
    41. }
    42. // printf("我是父进程,我的pid:%d,我的ppid:%d\n",getpid(),getppid());
    43. // int status=0;
    44. // pid_t ret=waitpid(-1,&status,0);
    45. // if(ret>0)
    46. // {
    47. // printf("等待成功!我等待的返回值:%d,exit signal:%d,exit code:%d\n",ret,status&0x7F,(status>>8)&0xFF);
    48. // }
    49. // sleep(3);
    50. }
    51. }

     使用C++演示,父进程回调处理对应的任务:

    后记:
    ●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

                                                                           ——By 作者:新晓·故知

  • 相关阅读:
    数据的距离度量 三、Jaccard距离,卡方相似度,相关系数,Dice系数
    雪花算法改造: 兼容JS截短位数的53bit分布式ID生成器
    【Axure视频教程】取整函数
    报价33万的极星电动车,一组电池就高达40万?
    全日制和非全日制之争,看完六年前的这个文件心里就有数了
    2021年12月电子学会图形化一级编程题解析含答案:放学
    XCTF1-web unseping
    编程都用什么电脑:深入解析编程者的电脑选择之道
    信息化发展47
    net-java-php-python-阿克苏水果销售管理计算机毕业设计程序
  • 原文地址:https://blog.csdn.net/m0_57859086/article/details/126454856