• linux操作系统进程控制详解


    目录

    前言

    一、进程创建

    1、fork函数

     2、写时拷贝

    二、进程终止

    1、退出码

     2、进程常见退出方法

    2.1  正常退出

     2.2 异常退出

    三、进程等待 

    1、进程等待的必要性

    2、进程等待的方法

    2.1 wait方法

    2.2 waitpid方法

    3、阻塞等待和非阻塞等待

    四、进程程序替换 

    1、替换原理

    2、替换函数 

    2.1 函数解释

    2.2 命名理解

    总结


    前言

    哈喽,小伙伴们大家好,今天我来带大家了解一下进程控制的相关知识。我将主要从四个方面进行讲解,分别是进程创建、进程终止、进程等待和进程替换。希望小伙伴们看完本文后能对进程有更加深刻的认识。


    一、进程创建

    1、fork函数

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

    • fork函数有两个返回值:子进程中返回0,父进程返回子进程pid,出错返回-1。
    • fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

    fork的基本使用在上一篇文章中介绍过,这里不做赘述,下面我们重点思考两个问题。

    (1)为何要给子进程返回0,给父进程返回子进程的pid?

    计算机中的父子关系和现实中的父子关系一样,一个父亲可以有多个孩子,但一个孩子只能有一个父亲。假设老王有三个儿子,三个儿子统一管老王叫爸爸即可。但当三个儿子同时在场时,老王想要找他们中的一个时不能简单的称呼为儿子,因为这样会造成混淆,必须叫他们对应的姓名才能加以区分。

    在操作系统中也是如此,父进程不需要标识,子进程需要标识,子进程是要执行任务的,父进程需要对它们进行区分。

    (2)如何理解fork有两个返回值的问题?

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

    • 分配新的内存块和内核数据结构给子进程
    • 将父进程部分数据结构内容拷贝至子进程
    • 添加子进程到系统进程列表当中

    在返回之前,这一系列操作已经完成,子进程已经创建成功,返回这条代码由父子进程一起执行,所以有两个返回值。

    fork调用失败的原因:

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

     2、写时拷贝

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

    为何需要写时拷贝?

    因为进程具有独立性,要保证两个进程的数据不互相干扰。

    那么为何不在一开始就把数据分开呢?

    因为子进程不一定会用到或写入父进程的所有数据,如果把所有数据都拷贝一份的话会造成内存浪费。写时拷贝可以保存按需分配,同时也满足了延时分配,就好比银行一样,有人取钱时它才会给,其余时间就可以自由支配这些资源,达到资源利用的最大化。

    二、进程终止

    1、退出码

    进程运行的目的是为了完成某种工作,那么这种工作有没有成功完成呢?进程需要给出一个反馈,来供用户查看工作是否返程,这种反馈就叫做退出码。

    指令: echo $?    打印最近一次的退出码

    进程退出场景: 

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

    前两种退出场景:当代码运行结果正确时,退出码为0,当代码运行结果不正确时,退出码为非0。因为成功了就是成功了,不需要加以解释,而失败了一般都是有原因的,需要将失败的原因反馈出来。不同的非0值代表着不同的失败原因。由于计算机是很擅长处理数字的,所以退出码在计算机中是以数字的形式传递。但人是非常不擅长处理数字的,需要将数字进行翻译才能够识别它代表的含义。

    sterror()函数:把错误码转化为对应含义的字符串

    我们可以通过sterror翻译错误码来确认任务失败的原因。

    注意:如果是第三种退出场景代码异常终止(比如指针越界等情况),则退出码毫无意义。就好比如果一个人考试作弊被逮到,大家只会取关心他作弊这件事本身,没有人会去关心他考了多少分。 

     2、进程常见退出方法

    2.1  正常退出

    • return退出
    • 调用exit
    • _exit

    注意: 只有main函数中的return代表进程退出,而在任何地方调用exit或_exit都代表进程退出。

    exit和_exit的区别:exit最后也会调用_exit,只不过在调用之前还做了其它工作。

    • 执行用户定义的清理函数
    • 关闭所有打开的流,写入缓冲区中的数据
    • 调用_exit

     2.2 异常退出

    ctrl+c,信号终止。进程异常退出会反馈信号,信号的详细内容之后再聊。我们只要先知道kill -l 可以打印信号列表即可。

    进程终止后,操作系统会释放曾经申请的内存,释放曾经申请的数据结构,从队列等数据结构中移除。

    三、进程等待 

    1、进程等待的必要性

    子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程的问题,进而造成内存泄漏。进程等待一般由父进程来完成。

    进程等待的作用:回收子进程资源,获取子进程的退出信息。

    2、进程等待的方法

    2.1 wait方法

    1. #include
    2. #include
    3. pid_t wait(int* status);

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

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

    status不能简单的当成整形来看待,应该当成位图来看待。我们只关心后十六位。后十六位中的低七位代表进程退出时的退出信号,高八位代表进程退出时的退出码。

    但在实际写代码中,我们一般不会用位操作来控制status,因为这样很复杂。库里给我提供了对应的宏。

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

    wait的时候,父进程在干什么?

    父进程什么也不干,只是单纯的在“等”。父子谁先运行不知道,但是wait之后,一般都是子进程先退出,父进程拿到子进程退出消息后才会退出。

    2.2 waitpid方法

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

    参数:

    • pid: 如果pid=-1,则等待任意一个子进程,与wait等价。若pid>0,等待进程ID与pid相等的进程。
    • status:与wait相同
    • options:暂时不关心 ,传0即可。
    1. #include
    2. #include
    3. #include
    4. #include
    5. int main()
    6. {
    7. pid_t id=fork();
    8. if(id==0)
    9. {
    10. //child
    11. int count=10;
    12. while(count--)
    13. {
    14. printf("I am child\n");
    15. sleep(1);
    16. }
    17. }
    18. else
    19. {
    20. int status=0;
    21. //father
    22. pid_t ret=waitpid(id,&status,0);
    23. if(ret>=0)
    24. {
    25. printf("wait success\n");
    26. if(WIFEXITED(status))
    27. {
    28. printf("%d\n",WEXITSTATUS(status));
    29. }
    30. else
    31. {
    32. printf("child not exit normal\n");
    33. }
    34. }
    35. }
    36. return 0;
    37. }

    3、阻塞等待和非阻塞等待

    阻塞等待:父进程什么也不干,一直卡在wait函数专门等子进程结束。

    非阻塞等待:父进程调用wait函数时如果子进程还未结束,父进程先往下走,每隔一段时间调用一次wait函数查看子进程是否完成,在时间间隔时可以干其它事情。

    在看一下waitpid:

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

     第三个参数options如果传0的话默认是阻塞等待,如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0,然后执行下面的代码。若等待成功,则和之前一样,返回该子进程的ID。

    1. int main()
    2. {
    3. pid_t id=fork();
    4. if(id==0)
    5. {
    6. //child
    7. int count=5;
    8. while(count--)
    9. {
    10. printf("I am child\n");
    11. sleep(3);
    12. }
    13. }
    14. else
    15. {
    16. //father
    17. while(1)
    18. {
    19. int status=0;
    20. pid_t ret=waitpid(id,&status,WNOHANG);
    21. if(ret>0)
    22. {
    23. printf("wait success\n");
    24. break;
    25. }
    26. else if(ret==0)
    27. {
    28. //子进程未完成
    29. printf("father do other things\n");
    30. sleep(1);
    31. }
    32. else
    33. {
    34. printf("waitpid error\n");
    35. break;
    36. }
    37. }
    38. }
    39. return 0;
    40. }

    四、进程程序替换 

    1、替换原理

    用fork创建子进程后,子进程和父进程共用的是一个代码(虽然可能是不同的代码路径)。但实际情况下,子进程一般不会和父进程用一套代码,而是会使用exec函数,使子进程的代码和数据被新程序替换,执行另一套程序。

    注意:

    • exec函数替换的仅仅是程序,不会创建新进程,所以子进程的pid不会改变。
    • 进程程序替换,替换成功后将不会返回,exec后面的代码不会被执行。
    • 如果exec调用失败,依旧执行原程序,后续代码不会受到影响。

    2、替换函数 

    2.1 函数解释

    其实有六种以exec开头的函数,统称exec函数:

    #include `
    int execl(const char *path, const char *arg, ...);
    int execlp(const char *file, const char *arg, ...);
    int execle(const char *path, const char *arg, ...,char *const envp[]);
    int execv(const char *path, char *const argv[]);
    int execvp(const char *file, char *const argv[]);

    int execve(const char *path, char *const argv[], char *const envp[]); 

    系统实际调用的是最后一个函数,前五个函数是对最后一个函数进行的封装 。

    注意: 

    • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
    • 如果调用出错则返回-1,exec函数只有出错的返回值而没有成功的返回值。我们一般情况下不会关注exec函数的返回值,因为只要返回了就说明调用失败了。
    • exec函数可以用系统的程序进行替换,可以用我们自己写的程序进行替换,甚至可以在c语言中调用phython,java等其它语言的程序进行替换。

    2.2 命名理解

    • l(list):表示参数采用列表传,一个一个传
    • v(vector):表示参数用数组传,一起传过去
    • p(path):有p自动搜索环境变量PATH下的路径
    • e(env):表示自己维护环境变量,不要系统默认的
    1. char *const argv[] = {"ps", "-ef", NULL};
    2. char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};//自己定义的环境变量
    3. //带l的,传参一个一个传
    4. execl("/bin/ps", "ps", "-ef", NULL);
    5. // 带p的,可以使用环境变量PATH,无需写全路径
    6. execlp("ps", "ps", "-ef", NULL);
    7. // 带e的,传入自己定义的环境变量
    8. execle("ps", "ps", "-ef", NULL, envp);
    9. //带v的,直接传一个数组
    10. execv("/bin/ps", argv);
    11. // 带p的,可以使用环境变量PATH,无需写全路径
    12. execvp("ps", argv);
    13. // 带e的,传入自己定义的环境变量
    14. execve("/bin/ps", argv, envp);


    总结

    以上就是今天要将的内容。本文主要从四个方面来讲解了进程控制的相关知识,不知道小伙伴们看完后有没有收获呢。感谢大家的阅读,来日方长,我们下次见~

  • 相关阅读:
    MATALAB绘制色图变换和Voronoi图
    CUDA图像处理加速demo
    ICCV2023
    性能学习笔记--k8s下mysql的连接数分析和调优
    第六章redux的使用(餐饮版)
    【每日训练】组队竞赛&&删除公共字符
    【GUI视频教程】GUI综合实战视频教程第3期:GUIX Studio一条龙设计主界面,底栏和窗口切换控制(2022-11-21)
    Spring 源码阅读 13:执行 BeanFactoryPostProcessor 中的处理方法
    Android 系统一级休眠命令
    C++每日面经
  • 原文地址:https://blog.csdn.net/weixin_59371851/article/details/126078206