• 【Linux】终端、作业控制与守护进程(学习兼顾复习)


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

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

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

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

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

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

    🧸 人的心态决定姿态!

    🚀 本文章CSDN首发!

    目录

    0.前言

    1. 任务管理

    2. 守护进程


    0.前言

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

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

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

    1. 任务管理

    我先来写一个死循环:

              当我们直接运行的时候,这个进程为前台进程,也可以看到R后面有一个 + 符号。

            当后面加上& ,这个进程就是后台进程了,R后面就没有 + 符号了。

            而且当我们把一个进程放在后台之后,这个进程对应的作业号为 [1] 。

     

            我们可以启动 4 个进程,而且都是处于运行状态。

            我们可以用 jobs 去查看:

            当我们用 fg * 的时候可以将后台进程变为前台进程:

            所以在 bash 中只允许存在一个前台进程,但是 bash 也是一个进程。所以:

             我们会发现我们使用 pwd ls top 就没有用了。这是因为 bash 只有在前台的时候才会接收命令,现在 bash 是后台了,所以就接收不了了,当然也就没有用了。

            我们用 CTRL+Z 也可以将进程放到后台,但是现在他是处于 Stopped 。我们可以通过 bg * 使他运行起来:


    ps -o pid,ppid,session,tpgid,comm

            每个进程在启动时以前端作业的形式启动,这个作业是处于 24440 这个会话(SESS),很明显就是它的父进程,它的父进程是 bash 。

            那么我们创建另一个 SSH 渠道,在那个会话中运行一个任务:

            我们可以看到两个 bash 不是同一个 bash ,而且看到同样的 mytest ,一个会话属于 24440 、一个属于 24984 。

            也就是这两个进程在不同的终端下都属于各自 bash 所对应的会话。

            每一次用 xshell 或 终端登录,本质都是先创建一个 bash 进程,这个 bash 进程整体称之为一个会话,所有的命令行启动的任务都是在对应的会话内运行的。

            当然立马还有一个 TPGID ,也就是组 ID,就是属于同一组这样一个概念。

             我们可以看到让另一个终端的进程。

    那么接下来我们当左边的终端退出呢?

            我们可以看到,这个终端退出后,这个进程的 ppid 从 24440 变成了 1,也就是孤儿进程。因为我用的是服务器,所以终端退出后会出现这个情况,如果是虚拟机可能终端退出后,这个进程也就被回收了。

            不过这样不是一个好的方案,我们为了保证终端退出的时候这个服务还在终端后端跑所以就有了守护进程!

    2. 守护进程

    a. 调用 umask 将文件模式创建屏蔽字设置为 0 。

    b. 调用 fork,父进程退出(exit),原因:

    1. 如果该守护进程是作为一条简单的 shell 命令启动的,那么父进程终止使得 shell 认为该命令已经执行完毕。
    2. 保证子进程不是一个进程组的组长进程。

    c. 调用 setsid 创建一个新会话,setsid 会导致:

    1. 调用进程成为新会话的首进程。
    2. 调用进程成为一个进程组的组长进程。
    3. 调用进程没有控制终端。(再次 fork 一次,保证 daemon 进程,之后不会打开 tty 设备)

    d. 将当前工作目录更改为根目录。

    e. 关闭不再需要的文件描述符。

    f. 其他:忽略 SIGCHLD 信号。


            我们创建守护进程的本质是:让这个会话结束之后,这个进程在服务器上一直进行。

    ps -o pid,ppid,pgrp,session,tpgid,comm

            我们可以看到多显示了 PGRP 也就是进程组,我们可以看到 mytest 是自成一个进程组的,如果 PGRP 和 PID 一样的话就说明这个是这个进程的组长。

            比如说我们在进程中 fork 创建了很多进程,这么多进程是父子关系,那么它们在系统中是属于同一组进程,而这个进程组的 ID 是以父进程的 PID 来命名的。

    1. 1 #include
    2. 2 #include
    3. 3
    4. 4 int main()
    5. 5 {
    6. 6 fork();
    7. 7 while(1);
    8. 8 return 0;
    9. 9 }

            我们可以看到 mytest 有两个进程了,它们同属于 11502 这个进程组,11502 同是上面 mytest 的 PID。

            我们用 setsid() 设置新会话的时候必须要保证当前进程不是进程组的组长!所以要先 fork() 。

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 int main()
    5. 5 {
    6. 6 if(fork() > 0){
    7. 7 exit(1);
    8. 8 }
    9. 9 setsid();
    10. 10
    11. 11 while(1);
    12. 12 return 0;
    13. 13 }

             我们可以看到运行之后 ps axj | head -1 && ps axj | grep mytest 是看不到 ./mytest 的,这是因为它已经是守护进程了,所以在当前会话中的看不到这个进程的。

            然后我们通过 ps axj | head -1 && ps axj | grep mytest 可以看到当前 mytest 已经是守护进程,而且她的 PGID 和它的 PID 已经是一个了,也就是它自己成了一个进程组。然后我们可以看到 SID (也就是SESS ID),它的会话也是 29810 。

            在上一次我们看到的进程的 SID 是 bash 的会话组。而现在它是自己的会话组,换句话说,它已经是一个独立的会话了!


            因为如果是一个守护进程,一般就不会在显示器上打印了,也不会将信息从键盘上获取了,所以一般默认打开的那三个:标准输入、标准输出、标准错误。一般重定向到 /dev/null 这个目录下。

            也就是说只要它是会话首进程就有权限打开终端,但是又不用它访问终端了,为了防止它打开终端,我们可以再进行 fork ,让父进程再进行退出,那么此时也就不再是话首了,但还是独立的会话。

            而且我们可以通过用 umask(0) ,当我们设置权限的时候就可以或者想要设置的权限,用 signal(SIGCHLD, SIG_IGN) ,忽略子进程退出给父进程发的信号。还可以用 chdir("/") 将当前工作目录(cwd) 设置为根目录,这样查找的时候就是从根目录开始查找,就相当于只能用绝对路径了,所以以后运行的时候就不会出错。

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7
    8. 8 int main()
    9. 9 {
    10. 10 umask(0);
    11. 11
    12. 12 if(fork() > 0){
    13. 13 exit(0);
    14. 14 }
    15. 15 signal(SIGCHLD, SIG_IGN);//忽略子进程退出给父进程发的信号
    16. 16 setsid();
    17. 17
    18. 18 //不是必须的,防御性的编程
    19. 19 if(fork() > 0){
    20. 20 exit(0);
    21. 21 }
    22. 22
    23. 23 chdir("/");//可选的选项
    24. 24 //孙子进程
    25. 25
    26. 26 while(1);
    27. 27 return 0;
    28. 28 }

            然后就是像上面所说,我已经是独立会话了,不应该再与键盘显示器有任何关系了,但是:

            我们可以看到还是关联着的,但其实是不应该这样的。

            所以我们就应该将文件描述符 1、2、3 关掉,或者重定向到 /dev/null 这个路径下。

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7 #include
    8. 8
    9. 9 int main()
    10. 10 {
    11. 11 umask(0);
    12. 12
    13. 13 if(fork() > 0){
    14. 14 exit(0);
    15. 15 }
    16. 16 signal(SIGCHLD, SIG_IGN);//忽略子进程退出给父进程发的信号
    17. 17 setsid();
    18. 18
    19. 19 //不是必须的,防御性的编程
    20. 20 if(fork() > 0){
    21. 21 exit(0);
    22. 22 }
    23. 23
    24. 24 chdir("/");//可选的选项
    25. 25 //孙子进程
    26. 26
    27. 27 close(0);//在0 open
    28. 28 int fd = open("/dev/null", O_RDWR);
    29. 29 if(fd < 0){
    30. 30 return 0;
    31. 31 }
    32. 32 dup2(fd, 1);
    33. 33 dup2(fd, 2);
    34. 34 while(1);
    35. 35 return 0;
    36. 36 }

            我们可以看到此时已经将文件描述符 1、2、3 重定向到 /dev/null 下了。

            还要说的是这里的 TTY 是 ? 表示的是已经和终端去关联了。

            当然用 close(0)、close(1)、close(2) 也不是不行,但是不推荐,最后看到的是这个情况:

             但是我们实际上一般都是这样用的:

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7 #include
    8. 8
    9. 9 void my_daemon(int nochdir, int noclose)
    10. 10 {
    11. 11 umask(0);
    12. 12
    13. 13 if(fork() > 0){
    14. 14 exit(0);
    15. 15 }
    16. 16 signal(SIGCHLD, SIG_IGN);//忽略子进程退出给父进程发的信号
    17. 17 setsid();
    18. 18
    19. 19 //不是必须的,防御性的编程
    20. 20 if(fork() > 0){
    21. 21 exit(0);
    22. 22 }
    23. 23 if(nochdir == 0){
    24. 24 chdir("/");//可选的选项
    25. 25 }
    26. 26 //孙子进程
    27. 27 if(noclose == 0){
    28. 28 close(0);//在0 open
    29. 29 //close(1);
    30. 30 //close(2);
    31. 31 int fd = open("/dev/null", O_RDWR);
    32. 32 if(fd < 0){
    33. 33 return ;
    34. 34 }
    35. 35 dup2(fd, 1);
    36. 36 dup2(fd, 2);
    37. 37 }
    38. 38 }
    39. 39
    40. 40 int main()
    41. 41 {
    42. 42 my_daemon(0, 0);
    43. 43 while(1){
    44. 44 //todo 某种周期性任务
    45. 45 sleep(1);
    46. 46 }
    47. 47 return 0;
    48. 48 }

            我们如果想更改目录或者想关闭 1、2、3 这三个文件,只要在传参的时候注意就好。

            所以,我们上面做的工作完成了一点,就是模拟实现了 daemon !

     

             我们可以看到,和我们自己实现的基本是一样的!

            但是有不同的是 PID 和 PGID、SID 这里是一样的,也就是说系统默认实现的没有考虑到后期用户可能会再打开这个文件。

            换句话说,其实我们完成的,要比系统默认的还要好一点~!

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

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

  • 相关阅读:
    eMagin:当月产百万片时,4K MicroOLED成本将不是问题
    Docker——docker容器时间同步
    我朋友软件测试月薪5w,跟他聊过之后,才知道差距在哪里!
    TensorFlow开发者认证通过心得
    pikachu之暴力破解
    使用python操作文件和文件夹
    YUVToRGB(CUDA Conversion)库的学习
    【网络安全】学过编程就是黑客?
    leetcode——最长回文子串——百日算法成就第五天5%
    tcp协议讲解
  • 原文地址:https://blog.csdn.net/weixin_69725192/article/details/126131407