• 【Linux】Linux进程控制(学习复习兼顾)


    🏠 大家好,我是 兔7 ,一位努力学习C++的博主~💬

    🍑 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀

    🚀 如有不懂,可以随时向我提问,我会全力讲解~

    🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!

    🔥 你们的支持是我创作的动力!

    🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证!

    🧸 人的心态决定姿态!

    🚀 本文章CSDN首发!

    目录

    0.前言

    1. 进程创建

    1.1 fork函数初识

    1.2 fork函数返回g值

    问题:

    1.3 写时拷贝

    问题:

    1.4 fork常规用法

    1.5 fork调用失败的原因

    2. 进程终止

    2.1 进程退出场景

    2.2 进程常见退出方法

    2.3 _exit函数

    2.4 exit函数

    2.5 return退出

    2.6 引入

    问题:

    3. 进程等待

    3.1 进程等待必要性

    3.2 进程等待的方法

    3.2.1 wait方法

    问题:

    3.2.2 waitpid方法

    3.3 获取子进程status

    3.4 阻塞等待 VS 非阻塞等待

    4. 进程程序替换

    4.1 替换原理

    4.2 替换函数

    问题:

    结论:

    4.3 函数解释

    4.4 命名理解

    Makefile生成两个可执行的方法

    小知识:

    5. 综合前面的知识,做一个简易的shell

    6. 我们来思考函数和进程之间的相似性


    0.前言

            此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。

            大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~

            感谢大家对我的支持,感谢大家的喜欢, 兔7 祝大家在学习的路上一路顺利,生活的路上顺心顺意~!

    1. 进程创建

    1.1 fork函数初识

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

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

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

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

            当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以 开始它们自己的旅程,看如下程序:

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 int main()
    5. 5 {
    6. 6 pid_t pid;
    7. 7 printf("Before: pid is %d\n", getpid());
    8. 8 if ( (pid=fork()) == -1 )
    9. 9 {
    10. 10 perror("fork()");
    11. 11 exit(1);
    12. 12 }
    13. 13 printf("After:pid is %d,fork return %d\n", getpid(), pid);
    14. 14 sleep(1);
    15. 15 return 0;
    16. 16 }

            这里看到了三行输出,一行before,两行after。进程9045先打印before消息,然后它又打印after。另一个after 消息有9046打印的。注意到进程9046没有打印before,为什么呢?如下图所示:

            所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

    1.2 fork函数返回g值

    • 子进程返回0。
    • 父进程返回的是子进程的pid。

    问题:

    1. 为何要给子进程返回0,给父进程返回子进程的pid?
    2. 如何理解fork有两个返回值的问题?

    1. 答:首先父子进程的 1 : n 的关系,所以在父子进程的立场中,父进程不需要标识,子进程需要标识。其次子进程是要执行任务的,父进程需要区分子进程,所以给父进程返回子进程的pid,因为父进程可以通过这个pid来区分是哪个子进程。给子进程返回0,本质上是因为子进程不需要访问父进程pid,因为子进程也不需要知道父进程pid,子进程不需要管理父进程,任务是给子进程的,它只需要知道自己调用成功了就可以。

    2. 答:(看图)

    1.3 写时拷贝

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

    问题:

    对这里的"共享"怎么理解?

            答:父子进程对应的页表指向的是同一块物理内存。当任何一方写入的时候,以便使用写时拷贝的方式生成一份副本。

    为何要写时拷贝?

            答:进程具有独立性!

    为何不在创建的时候就分开了?

            答:子进程不一定会使用父进程的所有数据,写入,本质是需要的时候!也就是按需分配,这种方式还做到了一点:延时分配,因为当被创建的时候,不一定被立马调度,如果不立马被调度,那就不需要先给它分配空间。因为要是先给它分配空间了,那也就是在它被调度之前的时间段中,系统可用的内存是变少的,所以延时分配永远可以保证系统可用资源是最大化的!所以延时分配的本质是:可以高效使用任何内存空间!

    代码会不会写时拷贝呢?

            答:90%的情况不会(但是不代表不能),因为我们学语言到现在,我们要改的永远是数据,我们没有在让程序运行的时候,改程序运行的逻辑。

    1.4 fork常规用法

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

    1.5 fork调用失败的原因

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

    2. 进程终止

    2.1 进程退出场景

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

    2.2 进程常见退出方法

    正常终止(可以通过 echo $? 查看进程退出码):

    1. 从main返回
    2. 调用exit
    3. _exit

    异常退出:

    • ctrl + c,信号终止

            接下来就说明一下 echo $? ,echo $? 输出上次运行的错误码,运行成功的时候,输出0,运行错误的时候输出非0。

     

            而错误码是人为定义的,接下来我带大家看看Linux用的错误码:

    1. 1 #include
    2. 2 #include
    3. 3
    4. 4 int main()
    5. 5 {
    6. 6 for(int i = 0 ; i < 150 ; i++)
    7. 7 {
    8. 8 printf("%d: %s\n",i , strerror(i));
    9. 9 }
    10. 10
    11. 11 return 0;
    12. 12 }

            接下来做两个实现证明上面的错误码:

    2.3 _exit函数

            _exit 的功能和 exit 的功能基本相同,但是有一点不同,等我讲下面 exit 的时候再引出来。

    2.4 exit函数

    1. 1 #include
    2. 2 #include
    3. 3 int show()
    4. 4 {
    5. 5 printf("hello show()!\n");
    6. 6 return 7;
    7. 7 }
    8. 8
    9. 9 int main()
    10. 10 {
    11. 11 show();
    12. 12 printf("process is not done!\n");
    13. 13 exit(11);
    14. 14 return 0;
    15. 15 }

            由此可见,exit 和 return 在这里的功能是一样的,都是使进程退出返回退出码,那么到底哪里不一样呢?

    1. 1 #include
    2. 2 #include
    3. 3 int show()
    4. 4 {
    5. 5 printf("hello show()!\n");
    6. 6 exit(7);
    7. 7 //return 7;
    8. 8 }
    9. 9
    10. 10 int main()
    11. 11 {
    12. 12 show();
    13. 13 printf("process is not done!\n");
    14. 14 //exit(11);
    15. 15 return 0;
    16. 16 }

            我们知道在show函数的return是返回给main函数,仅仅代表该函数结束,但是要是用exit就是可以在任何地方调用都可以使进程终止。

            我最开始就讲过 \n 刷新缓冲区的概念,这里不加 \n 就不会刷新缓冲区,如果和 return 一样返回就不会打印 "hello world!" 但是这里打印了,就说明 exit 有刷新缓冲区的作用,这也是和 return 的不同之处。

    exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

    1. 执行用户通过 atexit或on_exit定义的清理函数。
    2. 关闭所有打开的流,所有的缓存数据均被写入
    3. 调用_exit

    2.5 return退出

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

    2.6 引入

    问题:

    进程异常退出了,退出码还有意义么?

            没有意义!说简单点,到异常的地方就已经被终止了,根本没有执行 return 。

    进程终止了,操作系统做了什么?

            释放曾经申请的数据结构,释放曾经申请的内存,从各种队列等数据中移除。

    为何要有进程等待?

            1. 回收子进程资源。2. 获取子进程退出信息。(这个在下面)

    3. 进程等待

    3.1 进程等待必要性

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

    3.2 进程等待的方法

    3.2.1 wait方法

    1. #include
    2. #include
    3. pid_t wait(int*status);
    4. 返回值:
    5. 成功返回被等待进程pid,失败返回-1
    6. 参数:
    7. 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
    1. 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 int count = 0;
    12. 12 while(count < 5)
    13. 13 {
    14. 14 printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
    15. 15 sleep(1);
    16. 16 count++;
    17. 17 }
    18. 18 exit(0);
    19. 19 }
    20. 20 else{
    21. 21 printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
    22. 22 pid_t ret = wait(NULL);
    23. 23 if(ret >= 0)
    24. 24 {
    25. 25 printf("wait child success!, %d\n",ret);
    26. 26 }
    27. 27 printf("Father running...");
    28. 28 sleep(5);
    29. 29 }
    30. 30 return 0;
    31. 31 }

            由此可见,在父进程等待的时候,子进程在运行,重要的是,在子进程运行结束后没有看到Z状态进程,这是因为父进程在等待子进程结束,然后回收子进程。

    问题:

    在子进程运行期间,父进程wait的时候,父进程在做什么?

            就是在 "等" 什么也没干,就是在等子进程退出,这种等子进程退出的过程叫做阻塞等待

    还有一点是:

            因为父子谁先运行不确定,但是wait之后,大部分情况都是子进程先退出,父进程读取子进程退出信息,父进程才退出。建议大家以后一定要让父进程等待子进程退出,如果不等的话,一定会导致僵尸进程的问题。

    3.2.2 waitpid方法

    1. pid_ t waitpid(pid_t pid, int *status, int options);
    2. 返回值:
    3. 当正常返回的时候waitpid返回收集到的子进程的进程ID;
    4. 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
    5. 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
    6. 参数:
    7. pid:
    8. Pid=-1,等待任一个子进程。与wait等效。
    9. Pid>0.等待其进程ID与pid相等的子进程。
    10. status:
    11. WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    12. WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
    13. options:
    14. WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
    15. 程的ID。
    • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
    • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
    • 如果不存在该子进程,则立即出错返回。

    1. 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 int count = 0;
    12. 12 while(count < 5)
    13. 13 {
    14. 14 printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
    15. 15 sleep(1);
    16. 16 count++;
    17. 17 }
    18. 18 exit(0);
    19. 19 }
    20. 20 else{
    21. 21 printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
    22. 22 //pid_t ret = wait(NULL);
    23. 23 int status = 0;
    24. 24 pid_t ret = waitpid(id, &status, 0);
    25. 25 if(ret >= 0)
    26. 26 {
    27. 27 printf("wait child success!, %d\n",ret);
    28. 28 }
    29. 29 printf("Father running...");
    30. 30 sleep(5);
    31. 31 }
    32. 32 return 0;
    33. 33 }

            由此可见,waitpid和wait没有什么区别。

    进程等待成功是否意味着子进程运行成功?

            绝对不是,进程等待成功只意味着子进程退出了。


            我们接下来看看status表示的是什么:

    1. 1: test.c ? ? ?? buffers
    2. 7 int main()
    3. 8 {
    4. 9 pid_t id = fork();
    5. 10 if(id == 0){
    6. 11 int count = 0;
    7. 12 while(count < 5)
    8. 13 {
    9. 14 printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
    10. 15 sleep(1);
    11. 16 count++;
    12. 17 }
    13. 18 exit(77);
    14. 19 }
    15. 20 else{
    16. 21 printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
    17. 22 //pid_t ret = wait(NULL);
    18. 23 int status = 0;
    19. 24 pid_t ret = waitpid(id, &status, 0);
    20. 25 if(ret >= 0)
    21. 26 {
    22. 27 printf("wait child success!, %d\n",ret);
    23. 28 printf("status: %d\n",status);
    24. 29 }
    25. 30 printf("Father running...\n");
    26. 31 sleep(2);
    27. 32 }
    28. 33 return 0;
    29. 34 }

            由此可见,status表示的不是退出码,这个数字很奇怪,那它到底表示的是什么呢?

    3.3 获取子进程status

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

    1. 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 int count = 0;
    12. 12 while(count < 5)
    13. 13 {
    14. 14 printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
    15. 15 sleep(1);
    16. 16 count++;
    17. 17 }
    18. 18 exit(77);
    19. 19 }
    20. 20 else{
    21. 21 printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
    22. 22 //pid_t ret = wait(NULL);
    23. 23 int status = 0;
    24. 24 pid_t ret = waitpid(id, &status, 0);
    25. 25 if(ret >= 0)
    26. 26 {
    27. 27 printf("wait child success!, %d\n",ret);
    28. 28 printf("status: %d\n",status);
    29. 29 printf("child exit code: %d\n",(status>>8)&0xFF);
    30. 30 }
    31. 31 printf("Father running...\n");
    32. 32 sleep(2);
    33. 33 }
    34. 34 return 0;
    35. 35 }

            这也就获取了子进程退出时的退出码。

            我先说一下结论(信号以后我会讲~):进程异常的时候,本质是进程运行的时候出现了某种错误,导致进程收到信号!

            那么我们怎么知道我们收到信号了呢?我们接着看。

            由此可见,我们没有收到任何信号。

            由上图,我在进程运行的时候用2号信号把子进程给kill了,子进程立马终止然后过去了2号信号。且退出码为0(我刚才说过,如果进程出现异常,退出码没有任何意义!)

            上面都是提到的单进程,接下来我就写一个多进程执行的,前面没有说waitpid的第一个参数,其实第一个参数在单进程的时候就标识的那个参数,多参数的时候就可以指定一个参数,也就是说一个waitpid只能等一个子进程

    1. 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 ids[3];
    10. 10 for(int i = 0; i < 3; i++)
    11. 11 {
    12. 12 pid_t id = fork();
    13. 13 if(id == 0)
    14. 14 {
    15. 15 int count = 3;
    16. 16 while(count > 0)
    17. 17 {
    18. 18 printf("child do something!: %d, %d\n",getpid(),getppid());
    19. 19 sleep(1);
    20. 20 count--;
    21. 21 }
    22. 22 exit(i);
    23. 23 }
    24. 24 //father
    25. 25 ids[i] = id;
    26. 26 }
    27. 27
    28. 28 int count = 0;
    29. 29 while(count < 3)
    30. 30 {
    31. 31 int status = 0;
    32. 32 pid_t ret = waitpid(ids[count], &status, 0);
    33. 33 if(ret >= 0)
    34. 34 {
    35. 35 printf("wait child success!, %d\n",ret);
    36. 36 printf("status: %d\n",status);
    37. 37 printf("child exit code: %d\n",(status>>8)&0xFF);
    38. 38 printf("child get signal: %d\n",status&0x7F);
    39. 39 }
    40. 40 count++;
    41. 41 }
    42. 42 return 0;
    43. 43 }

            但其实我们很少用到创建多进程的场景。

            还有就是,在最开始我们说waitpid的时候给了两个宏,也就是说,我们可以不适用位操作,直接使用宏即可。

    1. 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 ids[3];
    10. 10 for(int i = 0; i < 3; i++)
    11. 11 {
    12. 12 pid_t id = fork();
    13. 13 if(id == 0)
    14. 14 {
    15. 15 int count = 3;
    16. 16 while(count > 0)
    17. 17 {
    18. 18 printf("child do something!: %d, %d\n",getpid(),getppid());
    19. 19 sleep(1);
    20. 20 count--;
    21. 21 }
    22. 22 exit(i);
    23. 23 }
    24. 24 //father
    25. 25 ids[i] = id;
    26. 26 }
    27. 27
    28. 28 int count = 0;
    29. 29 while(count < 3)
    30. 30 {
    31. 31 int status = 0;
    32. 32 pid_t ret = waitpid(ids[count], &status, 0);
    33. 33 if(ret >= 0)
    34. 34 {
    35. 35 printf("wait child success!, %d\n",ret);
    36. 36 if(WIFEXITED(status))//正常退出
    37. 37 {
    38. 38 printf("child exit code: %d\n",WEXITSTATUS(status));
    39. 39 }
    40. 40 else{//不正常退出
    41. 41 printf("child not exit normal!\n");
    42. 42 }
    43. 43 // printf("status: %d\n",status);
    44. 44 // printf("child exit code: %d\n",(status>>8)&0xFF);
    45. 45 // printf("child get signal: %d\n",status&0x7F);
    46. 46 }//
    47. 47 count++;
    48. 48 }
    49. 49 return 0;
    50. 50 }

            为了让小伙伴看清失败的情况,我在进程运行的时候kill掉了一个子进程。

    3.4 阻塞等待 VS 非阻塞等待

            以上我做的都是阻塞等待,接下来我简单说明一下阻塞等待非阻塞等待的区别,当然大家能理解就理解,等我以后讲网络的时候还会再继续讲的~

    阻塞等待:一直等,什么都不干,就是等!

    非阻塞等待:也是等,不过并不会因为条件不满足,而 "卡住" ,而是在条件不满足的时候做自己的事情,直到条件满足了。


             这里我解释一下waitpid()的返回值:如果成功的话,返回等待子进程的退出码,如果WNOHANG被指定的、并且指定的子进程是存在的、并且这个子进程的状态没有改变,就返回0,否则的话就返回-1。

            这里先说一个细节:waitpid的返回值要么大于0要么小于0这两个状态,要是设置成非阻塞就很有可能出现第三个状态,就是调用waitpid调用成功了,但是子进程并没有退出,没有退出的话调用waitpid检车的时候,就相当于,我等它的时候,他没有退出,但是我waitpid调用成功了,因为状态没有变,所以我直接返回了,就相当于我就进行了一次检测。所以如果waitpid的返回值是0的话就证明waitpid调用是成功的,只不过被等的那个子进程没有退出罢了。

    1. 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 int count = 0;
    13. 13 while(count < 5)
    14. 14 {
    15. 15 printf("I am child, pid: %d, ppid: %d\n",getpid(), getppid());
    16. 16 sleep(1);
    17. 17 count++;
    18. 18 }
    19. 19 exit(1);
    20. 20 }
    21. 21
    22. 22 int status = 0;
    23. 23 pid_t ret = waitpid(id, &status, WNOHANG);
    24. 24 if(ret > 0)
    25. 25 {
    26. 26 printf("wait success!\n");
    27. 27 printf("exit code: %d\n",WEXITSTATUS(status));
    28. 28 }
    29. 29
    30. 30 printf("ret: %d\n", ret);
    31. 31 return 0;
    32. 32 }

            由此可见,此时waitpid的返回值立马就是0,但是子进程还在,而父进程立马就退出了,此时子进程的ppid立马改为了1,那么子进程也就变成了孤儿进程。

            所以我们可以得到一个结论,如果我们以非阻塞的方式进行等待的时候,此时我们就不应该只等待一次,而是让父进程不断的轮询式的等待。

    1. 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 int count = 0;
    13. 13 while(count < 5)
    14. 14 {
    15. 15 printf("I am child, pid: %d, ppid: %d\n",getpid(), getppid());
    16. 16 sleep(1);
    17. 17 count++;
    18. 18 }
    19. 19 exit(1);
    20. 20 }
    21. 21 while(1)
    22. 22 {
    23. 23 int status = 0;
    24. 24 pid_t ret = waitpid(id, &status, WNOHANG);
    25. 25 if(ret > 0)
    26. 26 {
    27. 27 printf("wait success!\n");
    28. 28 printf("exit code: %d\n",WEXITSTATUS(status));
    29. 29 break;
    30. 30 }
    31. 31 else if(ret == 0)
    32. 32 {
    33. 33 printf("father do other things!\n");
    34. 34 sleep(1);
    35. 35 }
    36. 36 else{
    37. 37 printf("waitpid error!\n");
    38. 38 break;
    39. 39 }
    40. 40 }
    41. 41 return 0;
    42. 42 }

            由此可见,在子进程在做自己的事情的时候,父进程并不是刻意的去等待,而是父进程也在做自己的事情,它们两个之间并不会相互影响,只不过每隔1秒进行一次检测,当子进程运行结束之后,父进程获取子进程相关的退出信息。

            这种方案叫做基于非阻塞接口的轮询检测方案

    4. 进程程序替换

    4.1 替换原理

            用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

    4.2 替换函数

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

    1. #include
    2. int execl(const char *path, const char *arg, ...);
    3. int execlp(const char *file, const char *arg, ...);
    4. int execle(const char *path, const char *arg, ...,char *const envp[]);
    5. int execv(const char *path, char *const argv[]);
    6. int execvp(const char *file, char *const argv[]);
    7. int execve(const char *path, char *const argv[], char *const envp[]);

            接下来我先来用几个,其实剩下的都是非常相似的。

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 int main()
    5. 5 {
    6. 6 printf("I am a process!\n");
    7. 7 sleep(2);
    8. 8 execl("/usr/bin/ls","ls", "-a", "-i", "-l", NULL);
    9. 9
    10. 10 return 0;
    11. 11 }

            我们可以看到,我们利用execl成功的调用起了ls命令。

            我们得到一个结论,我的进程可以把别人的程序调用起来

            我们学语言的时候肯定都听说过一句话,任何程序要被运行之前,必须要先从磁盘中加载到内存当中(因为冯诺依曼体系是这么决定的,因为磁盘属于外设)。

    问题:

    1. 那么如何加载呢?

            我们刚才用的execl就可以称之为叫做Linux下的加载器所用的底层技术。

    2. 当前进程在进行程序替换的时候,有没有创建新的进程?

            没有!也就是说,我们在进行程序替换的时候,没有进行任何的程序创建。有的小伙伴可能认为不对啊!这里执行的代码和数据都已经被替换掉了,那怎么能是没创建新进程呢?

            其实衡量一个进程是进程,并不是根据这个进程执行什么代码、访问什么数据决定的,衡量一个进程是进程是由它在内核中的相关数据结构决定的,而其中我们在进行程序替换时PCB、虚拟地址空间、页表这三种结构是没有发生质的变化的。我们只是把老的代码用新的磁盘上的文件的代码和数据进行了替换,仅此而已。

            所以这也就印证了我之前说的话,进程不等价于程序,进程要比程序大的多。

    3. 进程替换之后如果还有代码会执行么?

            不会!因为已经被替换了,进程程序替换,一经替换,绝不返回,后续代码不会执行。

    4. 如果程序替换失败呢?

            程序替换失败后,程序后续并不会受到影响!也就是说,一旦替换失败,后面的代码正常运行。

    结论:

            execl系列的函数,根本就需不要判断返回值,因为只要是返回了就是失败!所以我们一般在程序替换后加上exit(1),也就是说只要你成功了,那你就被替换掉了,只要失败了就不往后走了,就终止进程。


            刚才的案例有一个缺点,就是:就有一个进程,让这一个进程执行命令的时候,它将命令执行完了后父进程也就不存在了,但理论上最好的是我让它执行新程序,让一个新进程去执行,不要影响我父进程。所以exec系列的函数经常配合创建子进程去使用。

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 int main()
    7. 7 {
    8. 8 pid_t id = fork();
    9. 9 if(id == 0)
    10. 10 {
    11. 11 printf("I am a process!\n");
    12. 12 sleep(2);
    13. 13 execl("/usr/bin/ls","ls", "-a", "-i", "-l", NULL);
    14. 14 exit(1);
    15. 15 }
    16. 16
    17. 17 int status = 0;
    18. 18 pid_t ret = waitpid(id, &status, 0);
    19. 19 if(ret > 0)
    20. 20 {
    21. 21 printf("signal: %d\n",status&0x7F);
    22. 22 printf("exit code: %d\n",(status>>8)&0xFF);
    23. 23 //printf("exit code: %d\n",WEXITSTATUS(status));
    24. 24 }
    25. 25
    26. 26 return 0;
    27. 27 }

            由此可见,这样编写代码就实现了让子进程去执行新的程序,而不影响父进程。这也是我们以后经常用到的结构。

    4.3 函数解释

    • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
    • 如果调用出错则返回-1
    • 所以exec函数只有出错的返回值而没有成功的返回值。

    4.4 命名理解

    这些函数原型看起来很容易混,但只要掌握了规律就很好记。

    • l(list) : 表示参数采用列表
    • v(vector) : 参数用数组
    • p(path) : 有p自动搜索环境变量PATH
    • e(env) : 表示自己维护环境变量

            带p的是会自动的在环境变量PATH中查找可以执行程序,不需要用户主动的填写文件所在的路径。

            接下来我演示带v的:

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 int main()
    7. 7 {
    8. 8 pid_t id = fork();
    9. 9 if(id == 0)
    10. 10 {
    11. 11 printf("I am a process!\n");
    12. 12 sleep(2);
    13. 13 //execl("/usr/bin/ls","ls", "-a", "-i", "-l", NULL);
    14. 14 char* myargv[] = {"ls", "-a", "-i", "-l", NULL};
    15. 15 execv("/usr/bin/ls", myargv);
    16. 16 exit(1);
    17. 17 }
    18. 18
    19. 19 int status = 0;
    20. 20 pid_t ret = waitpid(id, &status, 0);
    21. 21 if(ret > 0)
    22. 22 {
    23. 23 printf("signal: %d\n",status&0x7F);
    24. 24 printf("exit code: %d\n",(status>>8)&0xFF);
    25. 25 //printf("exit code: %d\n",WEXITSTATUS(status));
    26. 26 }
    27. 27
    28. 28 return 0;
    29. 29 }

            由此可见也是可以正常运行的。

            所以通过这里我们就可以感受到exec的强大了,其实通过exec系列的函数可以用C程序调用任何程序,比如:Shell脚本、Python、Java等。

    Makefile生成两个可执行的方法

            .PHONY是伪目标,我之前是讲过的,相当于在Makefile头部添加了一个新的依赖关系all,all是依赖于exec和cmd的,没有依赖方法,也就意味着后续我们默认生成的可执行程序在Makefile看来认为是all,但是因为all没有依赖方法,所以不会真的生成all,而是根据all的依赖关系,先生成exec再生成cmd。

            接下来来演示带e的:

            我们可以看到,MYENV是不存在的,是我自己随便写的,所以打印出来是null,但是PATH这个环境变量是操作系统的,所以可以打印出来。那么这时我们可以通过execle调用的时候传这个环境变量。

            我们可以看到,my env这个环境变量就被替换的程序获取到了,但是系统的环境变量却不在了。因为这种导环境变量的方式是覆盖式的。

            到现在我将各个exec系类的函数都讲完了,没有讲的那些跟讲了的都有区别,只是传参形式上有差别,所以其他的大家可以自己去试试。


            事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。

            下图exec函数族 一个完整的例子:

    小知识:

            因为main函数也是函数,所以main函数也是要被人调的,我之前讲过,在VS下是被CRT_mainstartup这样一个函数,那么这个函数可以理解成被系统调用的。那么现在就可以理解成是被加载器调的,就是被刚才讲到的exel系列函数调用的,当exel系列函数将代码和数据加载到内存的时候,最后一步再调用startup这样的函数最终再从main函数开始调用新的函数。

    5. 综合前面的知识,做一个简易的shell

            shell叫做命令行解释器,它的作用是将你输入的命令交给bash去执行,而bash本身不会亲自给你去实现命令,而是会创建一个子进程去帮你执行,因为进程和进程之间是由独立性的,你运行的命令如果没有BUG还有,有BUG的话,而且这个命令由BUG亲自执行,可能会将bash搞挂了,bash挂了,就没有办法给用户提供新的命令行解释服务了,所以这种事一般由子进程去做,子进程挂掉也不影响,首先不影响父进程bash,而且运行后的结果不管对还是不对,父进程都可以拿到结。

            所以我们咋子实现简单的shell,它的根本原理其中一定要由fork()这样的调用,这是其一。

            其二就是我们创建出来的子进程,我们不是为了让子进程帮我们去执行解释器部分的代码,它的任务只是执行命令,所以也就是创建子进程,让子进程去执行一个全新的程序(程序替换)。


            用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

            然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。

            所以要写一个shell,需要循环以下过程:

    1. 获取命令行
    2. 解析命令行
    3. 建立一个子进程(fork)
    4. 替换子进程(execvp)
    5. 父进程等待子进程退出(wait)

            根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了。

    先看效果:

    实现代码:

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7
    8. 8 #define LEN 1024
    9. 9 #define NUM 32
    10. 10
    11. 11 int main()
    12. 12 {
    13. 13 char cmd[LEN];
    14. 14 char* myarg[NUM];
    15. 15 while(1)
    16. 16 {
    17. 17 printf("[兔7@my-centos_mc dir]$ ");
    18. 18 fgets(cmd, LEN, stdin);
    19. 19 // 我们创建出来的子进程要执行命令(命令再cmd中)
    20. 20 // 要执行命令就要将一个个命令拆开才可以调用
    21. 21 // 所以要解析字符串
    22. 22 //
    23. 23 // 将最后一个命令的\n去掉(换成\0就行了)
    24. 24 cmd[strlen(cmd) - 1] = '\0';
    25. 25 myarg[0] = strtok(cmd, " ");
    26. 26 int i = 1;
    27. 27 while(myarg[i] = strtok(NULL, " "))
    28. 28 {
    29. 29 i++;
    30. 30 }
    31. 31
    32. 32 pid_t id = fork();
    33. 33 if(id == 0)//child
    34. 34 {
    35. 35 execvp(myarg[0], myarg);
    36. 36 exit(-1);//随便写的
    37. 37 }
    38. 38 int status = 0;
    39. 39 pid_t ret = waitpid(id, &status, 0);
    40. 40 if(ret > 0)
    41. 41 {
    42. 42 printf("exit code: %d\n", WEXITSTATUS(status));
    43. 43 }
    44. 44 }
    45. 45
    46. 46 return 0;
    47. 47 }

            所以我们说shell就是一个进程,现在就可以理解了,当我们 ./myshell 的时候 myshell 是不是变成进程了,是不是一直在运行,所以shell是在系统启动的时候就由某些任务将其进行启动。

            所以也就是:系统启动以及用户登录的到时候,某些登陆软件会自动调用bash程序,将其运行起来变成进程

    6. 我们来思考函数和进程之间的相似性

    exec/exit就像call/return 一个C程序有很多函数组成。

            一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。 这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间。如下图:

            一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait&ret)来获取exit的返回值。

            如上就是 Linux进程控制 的所有知识,如果大家喜欢看此文章并且有收获,可以支持下 兔7 ,给 兔7 三连加关注,你的关注是对我最大的鼓励,也是我的创作动力~!

            再次感谢大家观看,感谢大家支持!

  • 相关阅读:
    作为一名开发者,对你影响最深的书籍是哪一本?
    【AI】用iOS的ML(机器学习)创建自己的AI App
    C语言--输出1-100以内同时能被3和5整除的数
    windows CMD命令的一些使用方法及注意事项
    Spring WebFlux—Reactive 核心
    苍穹外卖-day11
    【MIT6.824】lab2C-persistence, lab2D-log compaction 实现笔记
    Minecraft 1.16.5模组开发(五十一) 方块实体 (Tile Entity)
    HBase查询一张表的数据条数的方法
    大型语言模型在AMD GPU上的推理优化
  • 原文地址:https://blog.csdn.net/weixin_69725192/article/details/125840715