linux篇【6】:进程等待

目录

一.fork函数初识

1.概念

2.父子进程共享fork之前和fork之后的所有代码,只不过子进程只能执行fork之后的

3.fork之后,操作系统做了什么?

进程具有独立性,代码和数据必须独立的

写时拷贝

fork常规用法

fork调用失败的原因

4.Fork后子进程保留了父进程的什么?

5.fork和exec系统调用

扫描二维码关注公众号,回复: 16870243 查看本文章

二.进程终止

1.常见进程退出

2.关于进程终止的正确认识

(1)进程退出码

 echo $? 查看进程退出码

(2)关于终止的常见做法——exit()

(3)exit和_exit

(4)关于终止,内核做了什么?

三.进程等待

1.为什么要进行进程等待

①为了解决僵尸进程内存泄漏问题

②为了获取子进程的退出状态

2.wait与waitpid

waitpid:

第二个参数status:

(1)低16个比特位的次低8比特位(次低8比特位) 是退出码

(2)低8个比特位是终止信号

3.父进程非阻塞等待(WNOHANG)

(1)父进程基于非阻塞的轮询等待的 例子:

(2)父进程基于非阻塞的轮询等待,父进程也有任务的例子


一.fork函数初识

1.概念

在linux fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回 0 ,父进程返回子进程 id ,出错返回 -1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
        ①分配新的内存块和内核数据结构给子进程
        ②将父进程部分数据结构内容拷贝至子进程
        ③添加子进程到系统进程列表当中(链进运行队列)
        ④fork返回,开始调度器调度
当一个进程调用 fork 之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序

2.父子进程共享fork之前和fork之后的所有代码,只不过子进程只能执行fork之后的

fork之前父进程独立执行,fork之后, 父子两个执行流分别执行。

那么fork之后,是否只有fork之后的代码是被父子进程共享的? ?
fork之后,父子共享所有的代码,但fork之前的代码也是父子共享的,只不过子进程只能执行fork之后的
子进程执行的后续代码! =共享的所有代码,只不过子进程只能从这里开始执行! !

为什么呢:

CPU中有一个程序计数器叫eip,用途是eip叫做,保存当前正在执行指令的下一条指令!
eip程序计数器会拷贝给子进程,子进程便从该eip所指向的代码处开始执行啦! !

如果我想让子进程拿到fork之前的代码,可以让子进程把CPU中的eip改成main函数入口就可以执行fork之前的代码。

3.fork之后,操作系统做了什么?

进程=内核的进程数据结构+进程的代码和数据
创建子进程的内核数据结构(struct task_ struct + struct mm_ struct +页表) +代码继承父进程,数据以写时拷贝的方式,来进行共享或者独立!
 

进程具有独立性,代码和数据必须独立的

数据通过写时拷贝保证独立性
代码因为是只读的,不可修改

写时拷贝

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

fork常规用法

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

fork调用失败的原因

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

4.Fork后子进程保留了父进程的什么?

A.环境变量

B.父进程的文件锁,pending alarms和pending signals

C.当前工作目录

D.进程号

fork函数功能是通过复制父进程,创建一个新的子进程。

  • A选项正确:环境变量默认会继承于父进程,与父进程相同
  • B选项错误:信号相关信息各自独立,并不会复制
  • C选项正确:工作路径也相同
  • D选项错误:每个进程都有自己独立的标识符

根据理解分析,正确选项为A和C选项

5.fork和exec系统调用

  • fork生成的进程是当前进程的一个相同副本(fork调用通过复制父进程创建子进程,子进程与父进程运行的代码和数据完全一样)
  • fork系统调用与clone系统调用的工作原理基本相同(fork创建子进程就是在内核中通过调用clone实现)
  • exec是程序替换函数,本身并不创建进程
  • clone函数的功能是创建一个pcb,fork创建进程以及后边的创建线程本质内部调用的clone函数实现,而exec函数中本身并不创建进程,而是程序替换,因此工作机理并不相同

二.进程终止

1.常见进程退出

________________________________
常见进程退出:                                         |         
1.代码跑完,结果正确                            | 
2.代码跑完,结果不正确                           | 
3.代码没跑完,程序异常了                     | 
——————————————————

2.关于进程终止的正确认识

C/C++的时候,main函数为什么 return 0; ?
a.return 0,给谁return
b.为何是0?其他值可以吗?


return 0表示进程代码跑完,结果正确
return 非零:结果不正确

在main函数中return代表进程结束。其他非main 函数return 代表函数调用结束

(1)进程退出码

代码跑完,结果正确就没什么好说的就exit(0)/return 0返回码是0;如果代码跑完,结果不正确,那我们最想知道的是失败的原因!
所以:非零标识不同的原因! 比如exit(13)
return X的X叫做进程退出码,表征进程退出的信息,是让父进程读取的! !

 echo $? 查看进程退出码

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

解释这里:第一次 echo $? 打印了进程退出码 123 ,第二次 echo $? 打印的是上一次 echo $?的进程退出码,因为上一次 echo $? 执行成功了,所以进程退出码是0,。

一般而言,失败的非零值我该如何设置呢? ?以及默认表达的含义?
可以自定义,也可以用系统定义的sterror。
错误码退出码可以对应不同的错误原因,方便定位问题!

(2)关于终止的常见做法——exit()

1. 在main函数中return代表进程结束。其他非main 函数return 代表函数调用结束
2.在自己的代码任意地点中main函数/非main函数,调用exit()都叫进程退出,exit(X)中的X是退出码

(3)exit和_exit

exit终止进程刷新缓冲区
_exit,是系统调用,直接中止进程,不会有任何刷新操作

终止进程推荐exit或main函数中的return。

会打印: hello bit,即刷新缓冲区

如果是_exit(0),就不会打印任何东西,即不刷新缓冲区

(4)关于终止,内核做了什么?

进程 = 内核结构 + 进程代码 和 数据
进程代码 和 数据一定会释放,但是 task/struct && mm_ struct:操作系统可能并不会释放该进程的内核数据结构
因为再次创建对象:1.开辟空间 2.初始化 都要花时间。
linux会维护一张废弃的数据结构链表叫 obj,若释放进程,对应的进程的数据结构会被维护到这个链表中,这个链表没有释放空间,只是被设成无效,需要时就拿,节省开辟时间(这样的链表也称 内核的数据结构缓冲池,操作系统叫:slab分派器)

三.进程等待

1.为什么要进行进程等待

①为了解决僵尸进程内存泄漏问题

子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力。

②为了获取子进程的退出状态

父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,
或者是否正常退出。

2.wait与waitpid

wait()的方案可以解决回收子进程z状态,让子进程进入X十,wait是等待任意一个退出的子进程

  wait演示:

waitpid:

pid_ t (int) 的返回值 :
>0:等待子进程成功,返回值就是子进程的pid
<0:等待失败  
=0:等待成功,但子进程没有退出     

第一个参数 pid:
>0:是几,就代表等待哪一个子进程退出,比如pid=1234, 指定等待
-1:等待任意进程退出(通常是最后退出的那个进程)

第三个参数 option:

0:标识阻塞等待(就是父进程等待子进程死,子进程死后就回收它)

当options被设置为WNOHANG则函数非阻塞(Wait No Hang 夯住了),且当没有子进程退出时,waitpid返回0

第二个参数status:

int* status:是一个整数,是输出型参数:父进程调用waitpid,可以通过status拿到子进程的退出码(具体过程:子进程退出后变为Z状态—子进程代码释放,只是维护着子进程的进程控制块 task_struct;此时子进程的进程控制块 task_struct中 有两个整形退出码int exit_ code和退出信号 exit_ signal 会被填充,然后父进程会拿到这两个值放入status中,所以输出型参数要先定义然后传参时取地址 waitpid(id, &status, 0),或者传nullptr 是不需要退出码 )

只需要关心改整数的低16个比特位!

(1)低16个比特位的次低8比特位(次低8比特位) 是退出码

证明:让子进程先睡眠5秒,然后退出,退出码设为0。子进程睡眠5秒期间父进程用 wait/waitpid 设成阻塞态,已知返回值:

pid_ t (int) 的返回值 :
>0:等待子进程成功,返回值就是子进程的pid
<0:等待失败        
=0:等待成功,但子进程没有退出 

利用ret接收返回值,当接收成功时,打印ret(这里的ret就是子进程的pid),

并打印(status>>8) &0xFF ,status>>8是 次低8比特位开始,与上0xFF(8个1),就是退出码。我们会发现status中的退出码确实记录了子进程的退出码

(2)低8个比特位是终止信号

(kill -l 可查退出信号,异常就是因为收到信号,status&0x7F 可打印终止信号)

 status&0x7F:status的低8位与上111 1111 ,就可得到终止信号

没有0号信号:

另一个接收

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

 

 waitpid():
阻塞等待和非阻塞等待

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

3.父进程非阻塞等待(WNOHANG)

waitpid(-1,&status, WNOHANG);
WNOHANG 父进程为非阻塞等待 (Wait No Hang 夯住了)

返回值:=0,等待成功,但子进程没有退出 ;等待成功返回子进程pid,等待失败返回-1。

(1)父进程基于非阻塞的轮询等待的 例子:

waitpid(-1,&status, WNOHANG); 在while循环内每次 waitpid执行一次,就检测一次子进程,如果子进程退出,就等待成功返回子进程pid;如果子进程没有退出,因为是非阻塞等待就等待成功返回0;

 

(2)父进程基于非阻塞的轮询等待,父进程也有任务的例子

#include <iostream>
#include <vector>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

typedef void (*handler_t)();

//方法集
std::vector<handler_t> handlers;

void fun1()
{
    printf("hello, 我是方法1\n");
}
void fun2()
{
    printf("hello, 我是方法2\n");
}

void Load()
{
    //加载方法
    handlers.push_back(fun1);
    handlers.push_back(fun2);
}

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        while(1)
        {
            printf("我是子进程, 我的PID: %d, 我的PPID:%d\n", getpid(), getppid());
            sleep(3);
        }

        exit(104);
    }
    else if(id >0)
    {
        //父进程
        // 基于非阻塞的轮询等待方案
        int status = 0;
        while(1)
        {
            pid_t ret = waitpid(-1, &status, WNOHANG);
            if(ret > 0)
            {
                printf("等待成功, %d, exit sig: %d, exit code: %d\n", ret, status&0x7F, (status>>8)&0xFF);
                break;
            }
            else if(ret == 0)
            {
                //等待成功了,但是子进程没有退出
                printf("子进程好了没,奥, 还没,那么我父进程就做其他事情啦...\n");
                if(handlers.empty())    
                    Load();                添加任务
                for(auto f : handlers)
                {
                    f(); //回调处理对应的任务,即执行任务
                }
                sleep(1);
            }
            else{
                //出错了,暂时不处理
            }
        }
}

猜你喜欢

转载自blog.csdn.net/zhang_si_hang/article/details/126589679