• Linux操作系统之进程控制


    进程创建

    我们创建一个进程的时候是使用系统调用接口:

    1. #include
    2. pid_t fork(void);
    3. // 返回值,子进程中返回0,父进程返回子进程的id,出错返回-1

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

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

    我们在之前就已经说过了,一开始父进程和子进程指向的其实都是同一份空间。

     此时,我们把页表项100这个部分改成非只读,这个时候我们就会发生缺页中断

    缺页中断:进程往一个不可写的内存写入了,操作系统就会把进程中断,例如上面的图片,我本来是只读的,我把它改成可读可写了,这个时候映射关系就会出现问题了,操作系统这个时候检测到了之后就会把进程停止,发生缺页中断,然后修改页表和对应关系,这个时候我的子进程被中断了,不知道发生了什么,这是分离现象。修改完之后写时拷贝,物理内存分开。

     因此,进程创建从时间和空间上的成本都非常高。

    进程终止

    进程退出的时候会发生三种情况:

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

    我们知道当程序结束的时候,我们可以使用return,但是有的同学可能知道,exit(EXIT_SUCCESS)也可以完成进程退出操作,那么这两者有什么不同呢?

    exit终止进程,强制终止进程,不需要进行进程的后序收尾工作,例如刷新缓冲区,而return可以进行后序的收尾工作。

     进程退出的时候,系统层面少了一个进程,free PCB,free mm_struct, free页表和各种映射关系,代码和数据申请的空间也要全部去掉。

    进程控制

    wait方法

    函数原型:pid_t wait(int* status);

    作用:等待任意子进程。

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

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

    waitpid方法

    函数原型:pid_t waitpid(pid_t pid, int* status, int options);

    作用:等待指定子进程或任意子进程。

    返回值:
    1、等待成功返回被等待进程的pid。
    2、如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
    3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。

    参数:
    1、pid:待等待子进程的pid,若设置为-1,则等待任意子进程。
    2、status:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
    3、options:当设置为WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid。

     我们的父进程fork之后可以得到子进程,子进程存在的目的是帮助父进程完成某种任务,于是我们的父进程在程序的结尾需要wait子进程,以保证子进程一定在父进程之后退出,为什么要这么做呢?

    理由其实很简单:

    1. 通过获取子进程退出的信息,能够得知子进程执行结果。假如说我和我爸去买酱油,我爸必须要等我买完了看我到底买完了没。
    2. 可以保证时序问题,子进程先退出,父进程后退出。
    3. 进程退出的时候会先进入僵尸状态,会造成内存泄漏的问题。需要通过父进程wait,释放该子进程占用的资源!!!

    获取子进程status

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

      这个core dump一般是0

    我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。

    1. exitCode = (status >> 8) & 0xFF; //退出码(1111 1111)
    2. exitSignal = status & 0x7F;      //退出信号(0111 1111)



    对于此,系统当中提供了两个宏来获取退出码和退出信号。

    • WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
    • WEXITSTATUS(status):用于获取进程的退出码。
    1. exitNormal = WIFEXITED(status);  //是否正常退出
    2. exitCode = WEXITSTATUS(status);  //获取退出码


    需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。

    阻塞等待和非阻塞等待

    举一个现实生活中的例子。假如说我要找张三借复习资料,但是张三说,我现在在看书,等我30分钟。那么我这个时候给张三打了一个电话,并且没有挂断电话,等张三下楼之前我都不会挂断电话,也就是说我电话功能全部被封死了,一直在通话中。这个叫阻塞等待

    但是假如我每过两分钟给张三打一个电话的话,每次打电话都确认他是否可以下来了,那么这个时候我的电话就有被闲置的时间让我完成其他的任务,这个叫基于非阻塞等待的轮询方案

    不管是阻塞还是非阻塞,都是等待的一种方式。谁等??等谁??等什么??

    等子进程。子进程退出是一个条件or事件。

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

    父进程如果在阻塞状态等子进程的话,那么它就是在纯等,什么都不会干。父进程本来是R(运行状态的)就会被成为S(等待状态),被加入到等待队列里面去。等子进程搞完之后发现父进程在等待,那么就再把等待队列里面的父进程放过来变成R。

    阻塞的本质就是把进程的PCB被放入了等待队列,并将进程的状态改为S状态。返回的本质,进程的PCB从等待队列拿到R队列,从而被CPU调度。

    非阻塞等待的代码:

    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. }

    我们可以看到有一个WNHOANG,加上这个,我们就可以实现非阻塞的等待了。

    进程替换

    用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数。

    当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。

    当进行进程程序替换时,有没有创建新的进程?

    进程程序替换之后,该进程对应的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的pid并没有改变。

    子进程进行进程程序替换后,会影响父进程的代码和数据吗?

    子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。

    也就是说进程替换就是用一个老的进程的壳子去执行一个新的代码,没用创建新的进程就执行了代码。

    它的本质是:把指定的进程代码 + 数据,加载进特定进程的上下文中去。
    exec函数实际上起到了加载器的作用,通过程序替换的方式来把磁盘上的东西加载到内存。

  • 相关阅读:
    从入门到精通:Go 实现基于 Token 的登录流程深度指南
    【云原生Kubernetes系列第六篇】Kubernetes的认证和授权
    电流互感器与电能仪表的施工安装指导
    TRC丨艾美捷 3-羟基己二酸说明书
    LeetCode 每日一题——623. 在二叉树中增加一行
    质数和约数
    【Android】-- Intent(显式和隐式Intent)
    Java语言基础
    Linux篇【3】:Linux环境基础开发工具使用(下)
    kibana监控
  • 原文地址:https://blog.csdn.net/qq_61039408/article/details/126507499