Linux进程控制 (上)

        之前博主写了关于进程概念的博客,今天呢,来和大家一起学习一下关于进程控制的知识。虽然临近端午节了,但是作为学习人,我们眼里是没有放假的~

目录

进程创建

fork函数初识

fork函数返回值

fork调用失败的原因

进程终止

进程退出的场景

退出码

 查看退出码

进程常见的退出方法

exit函数

_exit函数

return退出

进程退出,OS层面做了什么?

进程等待

为什么要有进程等待?

进程等待的方法

wait方法

waitpid方法

​编辑 waitpid的三个参数&返回值

 返回值

pid

 status

 options

进一步理解status 


进程创建

fork函数初识

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

进程调用fork,当控制转移到内核中的fork代码后(状态身份发生改变,这个以后会在信号博客讲解),内核做:

1、分配新的内存块和内核数据结构给子进程

2、将父进程部分数据结构内容拷贝至子进程
3、添加子进程到系统进程列表当中(本质上是OS多了一个进程,OS就要去管理)
4、fork返回,开始调度器调度

注意:

1、       父进程fork之后创建子进程, 子进程这时候继承父进程的代码和数据(不发生写时拷贝的话是共享的),但是子进程会执行父进程fork之后的代码(After的代码)。因为子进程继承了父进程的程序计数器,pc指针是指向fork的下一行代码。此时子进程就会执行After后面的代码了。

2、fork之后,父子进程谁先执行,完全由调度器决定。

fork函数返回值

给父进程返回子进程的pid。

给子进程返回0。

fork失败的话,返回-1。

fork调用失败的原因

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

关于进程创建,实际上在进程概念的博客中已经写过了,在这里博主就不废话了~

进程终止

进程退出的场景

实际上我们运行代码只有下面三种结果:

1、代码运行完毕,结果正确

结果正确,我们通常用0来表示success(成功)。

2、代码运行完毕,结果不正确

结果不正确我们通常用!0来表示failed(失败)。
3、代码异常终止

异常终止,就是程序崩溃,这时候退出码也变没有意义了!这时候要关注的是退出信号(这部分知识在后面的博客再讲~)

退出码

我们曾经见到的main函数的return值实际就是进程的退出码,我们是否想过我们为什么在main函数内总是reutrn 0呢?

我们首先介绍一个C语言函数:strerror

 这个函数参数是一个int类型。这个函数意思是,我们传入一个整型,它会返回对应的错误信息。也就是每个数字(可以理解为一个函数的返回值)就对应了一个特定的错误信息。

我们再来通过一段代码来看一下Linux中C语言的退出码:

int main()
{
    for(int i = 0; i < 140; i++)
    {
        printf("%d: %s\n", i, strerror(i));
    }
    return 0;
}

部分截图:

       我们发现每一个数字对应一个退出信息,其中0对应的信息是:Success,也就是成功。其余的数字对应的退出信息,我们可以发现都是一些错误信息,也就是代码运行完毕,结果不对的情况。

       这时候我们同时也就可以解释了,为什么要用0代表成功,非0代表结果不正确,因为不正确的情况是有多种可能的。 

 查看退出码

[cyq@VM-0-7-centos 进程控制]$ echo $?

echo $?:输出最近一次进程退出时的退出码。

我们写一个程序来使用它查看退出码:

int main()
{
    return 123;
}

运行完之后,我们来查看退出码:

这时候我们发现退差码是123,和我们预期的一样,这时候如果我们再来echo $? 一下我们看看结果是什么?

我们发现竟然是0!

       因为echo $?指令本身就是一个可执行程序,它本身正常退出码就是0,所以我们再次查看退出码的时候就查看到的是上一个指令echo $?的退出码。 

注意:

我们输入的正确的指令如果正常退出的话结果都为0,要是输入错误的指令(代码跑完,结果不对),我们的退出码就不是0了:

进程常见的退出方法

exit函数

#include <unistd.h>
void exit(int status);

exit在任何地方调用,都代表进程终止,参数就是退出码!

我们举个栗子:

int func()
{
    exit(123);
    printf("hello cyq\n");

    return 1;
}
int main()
{
    printf("hello wmm\n");
    func();
    return 0;
}

我们打印一下看看结果:

       我们可以看到,main函数中调用func函数,func函数里面运行到exit(123),这时候我们发现exit(123)后面的内容就不再执行了,程序直接退出了!我们再来查看进程退出码时,发现是123。

这时候我们就证明了exit在任何地方调用,都代表进程终止,参数就是退出码!

_exit函数

_exit函数和exit函数用法一样,这里我们就简单演示一下:

int func()
{
    _exit(123);
    printf("hello cyq\n");

    return 1;
}
int main()
{
    printf("hello wmm\n");
    func();
    return 0;
}

实验效果:

我们发现_exit在任何地方被调用都代表进程终止,参数就是退出码。 

_exit和exit函数的区别:

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

实际上_exit是exit的一个子函数。

我们用个图来形象展示一下:

_exit是直接执行终止进程的代码,而exit在调用_exit前先执行其它部分工作。

       换句话说,_exit终止进程,是强制终止进程,不执行进程的后续的收尾工作,比如刷新缓冲区(这里所说的缓冲区不是用户层的缓冲区,关于这块知识在文件IO会有讲解~)。

return退出

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数。

注意:

main函数return,代表进程退出!!非mian函数return,代表函数返回(它不是退出码)!

进程退出,OS层面做了什么?

     系统层面,少了一个进程:free PCB,free mm_struct,free 页表和各种映射关系,代码+数据申请的空间也要释放掉!

由于这个函数大家还是很熟悉的,在这里就不介绍了~

进程等待

为什么要有进程等待?

        我们知道父进程fork创建子进程,子进程帮父进程完成完成某种任务,子进程完成任务需要反馈给父进程信息,比如是否完成、完成失败原因等。这时候就需要保证父进程必须比子进程"活"得更久,不能让父进程比子进程先退出。

所以,父进程fork之后需要通过wait/waitpid等待子进程退出。

为什么父进程要等待子进程?

总结:

1、通过获取子进程的退出信息,能够得知子进程的执行结果。

2、可以保证:时序问题,子进程先退出,父进程后退出。

3、进程退出的时候会先进入僵尸状态,(长时间的话)会造成内存泄漏的问题,需要通过父进程wait,释放该子进程占用的资源!!

我们之前讲过:进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。

进程等待的方法

wait方法

wait的参数类型是int*,参数是status,在这里我们先不介绍,待会介绍waitpid后大家就会明白了,在这里我们不关心的话就先传过去NULL先代替。 

返回值:如果返回值大于0(实际是等待的子进程的pid)就表示返回成功,如果小于0表示等待失败。

我们写一段代码来感受一下wait及等待的过程:

int main()
{
    pid_t fd = fork();
    if(fd == 0)
    {
        //child
        int cnt = 3;
        while(cnt--)
        {
            printf("child[%d] is running: %d\n", getpid(), cnt);
            sleep(1);
        }
        exit(1);
    }
    sleep(7);
    pid_t ret = wait(NULL);
    if(ret > 0)
    {
        //parent
        printf("parent wait: %d, success\n", ret);
    }
    else
    {
        printf("parent wait fail\n");
    }
    return 0;
}

代码的意思是,子进程被创建后3s就退出,而父进程休眠7s,在子进程3s后退出的4s时间里,父进程没有检测到子进程的退出信息,因为父进程还在休眠,这时候子进程是僵尸状态,4s后父进程wait子进程pid成功后,就打印退出了。

我们来借助一下监控指令:

[cyq@VM-0-7-centos 进程控制]$ while :; do ps axj | head -1 && ps axj | grep test; sleep 1; echo "####################"; done

运行结果:

我们发现父进程等待的子进程和脚本内容对应的子进程id一致,可以说明等待成功了。 

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。 

对于上面的介绍如果看不懂,先不着急,待会博主会详细介绍~

我们先来看现象,先大概了解waitpid的用法:

还是上面wait的代码,在这里我们只修改一个地方来看看:

pid_t ret = wait(NULL);我们改成pid_t ret = waitpid(-1,NULL, 0);实际上这两种写法在这里是等效的,wait是等待任意子进程,我们给waitpid传-1,意思就也是等待任意进程 。

其次wait等待方式是阻塞等待(待会讲),我们给waitpid第三个参数传0,意思就是阻塞等待,剩下的参数传的都一样,这样的话,wait和waitpid这里就是一样的功能了~

运行结果:

进程状态截图:

 waitpid的三个参数&返回值

 返回值

如果父进程等待失败,就返回-1。

如果父进程等待成功。

情况一,如果是阻塞等待,就返回子进程的pid。

情况二,如果是非阻塞等待,检测的时候发现子进程没有退出,就返回0。

在这里我们先不用关心阻塞等待和非阻塞等待是什么,我们先只考虑阻塞等待的情况,待会讲完后,再来回顾这里就明白了了~

pid

pid=-1,等待任一个子进程。与wait等效。
pid>0,等待的进程ID与pid相等的子进程,如果不相等就等待失败。

举个栗子:

等待指定进程的pid:

int main()
{
    pid_t fd = fork();
    if(fd == 0)
    {
        //child
        printf("i am child: %d\n", getpid());
        sleep(2);
        exit(1); //子进程退出
    }
    //parent,这里的fd表示子进程的pid
    pid_t ret = waitpid(fd, NULL, 0);
    if(ret > 0)
    {
        printf("parent waitpid: %d success\n",ret);
    }
    else
    {
        printf("parent wait fail\n");   
    }
    return 0;
}

运行结果:

 等待任意子进程时:

 还是上面的代码,我们把fd改成-1,表示等待任意子进程。

运行结果:

等待失败情况:

我们知道子进程时fd,但是fd+1就没有对应的这个子进程了,这时候父进程就没有等待成功了。

运行结果:

 status

status其实是一个输出型参数,我们在外部定义一个in status = 0;然后传地址过去,waitpid用int* 指针接受,函数内部会给*status赋一个值,这样在外部,就可以拿到值了。

wait和waitpid,都有一个status参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

       由此我们就可以知道,父进程通过waitpid拿到的status结果,一定和子进程如何退出强相关!!至于子进程退出的话题,就是我们刚刚讲过的进程的退出。 

代码异常退出在这里我们就知道了,本质是这个进程因为异常问题,导致自己收到了某种信号(具体内容在后面的博客将~)。

我们在这里来通过status获取退出码和退出信号:

int main()
{
    pid_t fd = fork();
    if(fd == 0)
    {
        //child
        printf("i am child: %d\n", getpid());
        exit(1); //子进程退出
    }
    //parent
    int status = 0;
    pid_t ret = waitpid(-1, &status, 0);
    if(ret > 0)
    {
        printf("parent waitpid: %d success ",ret);
        printf("status exit code: %d, status exit signal: %d\n",(status>>8)&0xff, status&0x7f);
    }
    else
    {
        printf("parent wait fail\n");   
    }
    return 0;
}

运行结果:

根据刚才status的位图结构:

(status>>8)&0xff:次低8位是退出码,所以我们让它右移8位,再和1111 1111按位与就可以拿到了对应的值。

status&0x7f :第7位对应了退出信号,和0111 111按位与,和上面的计算方法类似。

是不是觉得上面的位运算比较麻烦?当然系统中给我们提供了宏:

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

测试代码:

int main()
{
    pid_t fd = fork();
    if(fd == 0)
    {
        //child
        printf("i am child: %d\n", getpid());
        exit(123); //子进程退出
    }
    //parent
    int status = 0;
    pid_t ret = waitpid(-1, &status, 0);
    if(WIFEXITED(status))
    {
        printf("parent waitpid: %d success ",ret);
        printf("status exit code: %d\n",WEXITSTATUS(status));//(查看进程退出码)
    }
    else
    {
        printf("parent wait fail\n");   
    }
    return 0;
}

测试结果:

我们发现使用WIFEXITED和WEXITSTATUS配合使用也可以拿到退出码。

此外关于退出信号的宏:

当然还有很多宏,可以通过man手册来查询:

[cyq@VM-0-7-centos 进程控制]$ man 2 waitpid

 options

WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

如果第三个参数我们给0的话,就认为是阻塞等待。

什么是阻塞等待和非阻塞等待?

假设一个场景:比如快期末考试了,你的好朋友张三学习很好,你想找他帮你复习功课。于是你到张三的宿舍楼下并打电话给张三:"张三,快出来,帮帮我复习一下我的物理"。但是张三说"等一下,我现在正在复习英语,你得等我1小时"。于是你就说"可以,不过电话你先别挂,就放到你旁边,什么时候好了就立马通知我"。,接着你就在他的宿舍楼下不断等待,目不转睛的等待,其他事情什么也干不了,这种等待方式就相当于阻塞等待。

       如果张三给你说等他1小时,但是你觉得太长了,不想一直在宿舍楼下等,然后就忙自己的事情了,并且每隔3分钟就给他打电话问他好了没,就这样,可能需要多次检测,是基于非阻塞等待的轮询方案,这种等待方式相当于非阻塞等待。

进一步理解阻塞等待:

阻塞了是不是意味着父进程不被调度执行了呢?

阻塞的本质:其实就是进程的PCB被放入了等待队列,并将状态改为S状态。

返回的本质:进程的PCB从等待队列拿到R队列,从而被CPU调度。

所以,当我们看到某些引用或者OS本身,卡住长时间不动,其实就是应用或者程序hang住了。

WNOHANG:非阻塞等待方式。会有下面的结果:

1、等待成功,但是子进程根本没有退出。

2、子进程退出了,waitpid等待成功(返回子进程pid)or失败(失败就返回-1)。

WNOHANG就好比不断向张三打电话,问他是否准备好了的轮询检测过程。

代码测试:

int main()
{
    pid_t fd = fork();
    if (fd == 0)
    {
        // child
        printf("i am child: %d\n", getpid());
        sleep(1);
        _exit(123); //子进程退出
    }
    // parent
    while (1)
    {
        sleep(3);
        int status = 0;
        pid_t ret = waitpid(-1, &status, WNOHANG);
        if (ret == 0)
        {
            //子进程没有退出,但是这时候waitpid等待是成功的,需要父进程重复进行等待
            printf("do parent things...\n");
        }
        else if(ret > 0)
        {
            //子进程退出了,waitpid也成功了,获取到了对应的结果
            printf("father wait: %d success \n", ret);
            printf("status exit code: %d, status exit signal: %d \n",(status>>8)&0xff, status&0x7f);
            break;
        }
        else 
        {
            printf("parent wait fail\n");
        }
    }
    return 0;
}

上面的代码既然是轮询检测(非阻塞等待),我们就要使用while循环,如果waitpid返回值为0,那么就还需要继续检测。

运行结果:

进一步理解status 

     OS系统内部工作大概就是这样,子进程退出要把退出码、退出信息反馈给父进程,通过 |= 方式实现,父进程就有了一个status参数,在用户层就可以通过wait/waitpid的输出型参数获取,然后可以得到退出码、退出信号等(方法刚才讲过了~)。

看到这里,支持博主一下吧~

猜你喜欢

转载自blog.csdn.net/qq_58724706/article/details/125109711