Linux-----进程间关系和守护进程

在学习完进程之后,我们再来谈谈进程间关系
一个正在运行的程序(即进程)有着许多属性,经常用于进程控制的有:实际用户id,有效用户id,设置用户id、组id、进程id、进程组id、会话id。
在这里我们先介绍三个概念:


进程组 / 作业 / 会话

进程组

之前我们说过每个进程都有自己的进程ID,那么进程组是什么呢?其实每个进程除了有一个进程ID之外,还属于一个进程组。进程组是一个或多个进程的集合,属于同一个进程组的进程他们的组ID是一样的,来看看下面的例子:
这里写图片描述
可以看到我们在后台运行了三个进程
(注:利用管道连接了三个进程,再利用&符号将这些进程放在后台运行)
再来说一下我们使用的命令:ps
ps选项:

  • a: 不仅列当前用户的进程,也列出所有其他用户的进程
  • x: 表示不仅列有控制终端的进程,也列出所有无控制终端的进程
  • j:表示列出与作业控制相关的信息

大家对这里的有些名词可能不太理解,例如作业控制等,在后面会讲解。

继续来看这张图,在列出的信息中,我们很容易就会发现PID的信息,这个信息相信大家都不陌生,3个PID代表3个进程。再来看看PGID这一列的信息,我们会发现这三个进程的PGID都是3339,这个PGID就是进程组ID

细心的朋友会发现,这个进程组ID3339还是第一个给进程 sleep 100 的PID,那么我们就可以把 sleep 100 这个进程称为这个进程组的组长,来总体的解释一下:
每个进程除了有一个进程ID之外还属于一个进程组,每个进程组有一个唯一的进程组ID,和进程组ID相同ID的进程被称为组长进程,组长进程可以创建一个进程组。
那如果我们杀掉这个组长,这个进程组还会存在吗?来试一下:
这里写图片描述
我们发现,杀死组长这个进程组还是存在的额,所以:
只要在某个进程组中一个进程存在则该进程组就存在,这与其组长进程是否终止无关

作业

现在来解释刚刚提到的作业控制,先来看看什么叫做作业:
我们刚刚在前台运行过进程,也演示了在后台运行进程其实shell控制的并不是单个的进程,而且我们刚刚讲过的进程组或者是现在谈的作业,而前这个控制是分前后台的。
一个前台作业可以由许多个进程组成,同样的一个后台作业也可以由多个进程组成
重点来了,那作业控制是什么呢?
Shell 可以运行一个前台作业和任意多个后台作业,这称为作业控制

看到这里可能就有人想问了,进程组是由多个进程组成,作业也是由多个进程组成的,那么它们到底有什么区别呢?
如果作业中的某一个进程又创建了子进程,那么这个子进程不属于作业,但它属于进程组
来看个例子吧:
因为我们刚刚说过Shell只能运行一个前台作业,就像我们写一个死循环在Shell下运行的时候,在这个作业终止之前,我们在键盘上面进行一些普通输入时没有效果的(当然利用组合键发信号除外),因为这个事实前台已经运行了一个作业,所以我们的Shell就被放在了后台,一旦这个作业运行结束,Shell才会把自己提到前台,那么验证进程组和作业的区别就好验证了,来看看代码在解释:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    pid_t pid;
    pid=fork();
    if(pid<0)
    {
        perror("fork");
        return -1;
    }
    if(pid==0)
    {
        while(1)
        {
            printf("hello child id:%d\n",getpid());
            sleep(1);
        }
    }
    else
    {
        sleep(5);
        exit(1);
    }
    return 0;
}

我们利用fork创建一个子进程,然后子进程每隔一秒打印消息,父进程在5秒之后退出。
当我们运行这个程序的时候,在没执行到fork()函数时,这个进程一直是自成一个进程组,也就是这个前台作业只有一个进程,当子进程被创建出来的时候,如果子进程不属于作业,那么5秒钟之后,父进程退出就相当于这个前台作业已经运行结束了,可想而知Shell就应该被提到前台来,而刚刚剩下的子进程就会被放在后台变成一个后台进程了。反之不然,那来看看结果:
这里写图片描述

根据结果验证了我们说的,虽然我们输入的命令,被后台那个子进程打印的消息冲乱了,但并不影响我们的结果,可以证明在作业中创建子进程,则子进程不属于作业

会话

这里写图片描述
在刚刚谈进程组的时候我们说过PGID,我们还可以看到有一列信息是SID,这里的S就是Session会话,SID就是会话ID

我们可以看到sleep 1000、sleep 2000、sleep 3000都属于会话2705,那我们来找找看看2705到底是谁?
这里写图片描述

我们可以看到2705就是bash,也就是我们的解释器,细心的同学可以发现下面还有一个bash,在这里解释一下,因为最开始开启这个终端的时候,我是以普通用户的身份登录的,也就是2705代表的bash,然后切换成了超级用户也就是下面那个bash。

所以我们可以解释成会话是一个或多个进程组的集合,一个会话可以有一个控制终端,这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下,比如利用XShell)
在这里解释一下控制终端:在UNIX系统中,用户通过终端登录系统后会得到一个Shell进程,这个终端就是Shell进程的控制终端。控制终端是保存在PCB中的信息,而我们知道fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端,所以它们都属于一个会话。
多打开几个终端你会发现,每打开一个终端就会创建一个会话。那会话到底都有哪些组成部分呢?

  • 控制进程(会话首进程)
  • 一个前台进程组
  • 任意多个后台进程组

我们刚刚说到的bash就是刚刚新建会话的会话首进程


作业控制有关的信号

谈完三个概念,来补充一点知识。
这里写图片描述
我们将cat命令放在后台执行,cat命令会根据我们输入的消息输出相同的消息,但是我们发现,这个进程在后台的状态是Stopped,这是因为后台进程是不被允许从标准输入读数据的,因此内核发SIGTTIN信号给进程,该信号的默认处理动作是使进程停止。我们可以来验证一下

这里写图片描述

1、先利用 jobs 命令查看后台作业
2、然后我们利用命令 fg+作业号 将后台作业提到前台,如果该作业的进程组正在后台运行则提至前台运行,如果该作业处于停止状态,则给进程组的每个进程发SIGCONT信号使它继续运行,可以在图中看到结果。
3、然后我们在键盘上输入Ctrl+Z,给当前进程发送SIGTSTP信号,该信号的默认动作是停止该进程。cat继续以后台进程的形式存在。
4、由于Ctrl+Z之后进程都会处于停止状态,所以利用 bg+作业号 让停止的作业在后台继续运行,也需要给该作业的进程组的每个进程发SIGCONT信号。cat进程继续运行,又要从标准输入读数据,然而它在后台不被允许,所以又收到SIGTTIN信号而停止
5、接下来我们利用kill命令给cat进程发送SIGTERM(15)信号,这个信号并不会立刻处理,而要等进程准备继续运行之前处理,默认动作是终止进程。当我们再次fg +进程号将它提到前台时,就会发现该进程终止了。

在这里我们也证明了后台进程并不能从终端读数据,那么可以写数据吗?答案是肯定的,刚刚在谈作业的时候,那个被fork出子进程不就一直在写数据吗!


守护进程

概念

守护进程又被称为精灵进程,它也是一种后台进程,但是比较特殊的是守护进程独立于控制终端并且在周期性的执行某种任务或者等待处理某些发生的事件

作用:

回想我们在访问百度首页的时候,逛淘宝的时候不管在一天的什么时候去访问都是可以的,这就是有守护进程
再例如Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其它进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销的影响,它们一直在运行着。这种进程就是守护进程,可以认为守护进程的作用就是防止终端产生的一些信号让进程退出。

特点:

  • 所有的守护进程都没有控制终端,其终端名(TTY)设置为问号(?)
  • 守护进程通常采用以‘d’结尾的名字
  • 守护进程自成会话,自成进程组。不与其他会话或进程组相互关联,干扰。所以一般一个守护进程的进程ID,组ID,会话ID都相同。
  • 守护进程不受用户登录注销的影响,当你注销或者重登后,守护进程一直在运行。
  • 生存期长,在系统引导装入时启动,仅在系统关闭时终止。
  • 在后台运行(原因可归结于没有控制终端)

内核守护进程

这里写图片描述

内核守护进程以无终端的方式启动,凡是在TPGID一栏写着-1的进程都是没有控制终端的进程,也就是守护进程。在COMMAND列[ ]括起来的名字表示内核线程,这些线程在内核创建,没有用户空间代码,因此没有程序文件名和命令行, 通常采用以k开头的名字表示Kernel。对于需要在进程上下文执行工作但却不被用户层进程上下文调用的每一个内核组件,通常都有它自己的内核守护进程

我们可以看到第一行的进程就是我们常说的init进程也就是回收孤儿进程的1号进程,显然易见,它也是一个守护进程。而且init进程还是所有用户级进程的父进程

Linux通常使用一个叫kthreadd的特殊内核进程来创建其他内核进程,所以kthreadd表现为其他内核进程的父进程。来看看这个进程:
这里写图片描述
我们可以看到kthreadd进程的PID是2,也就是刚刚那些那些守护进程的父进程


创建守护进程

创建守护进程最重要的就是创建一个会话并成为会话首进程,来看看创建一个会话要用的函数

#include <unistd.h>
pid_t setsid(void)
//返回值:成功返回创建好的SID也就当前进程的id,失败返回-1

使用这个函数需要注意一个地方就是:当前进程不能是进程组的组长,否则该函数就会出错返回
所以我们可以利用fork()创建出一个子进程,然后让父进程直接终止,然后在子进程中调用setsid函数。子进程的进程ID是新分配的,所以两者不可能相等。这样就保证了子进程不是一个进程组的组长。

接下来看看创建守护进程的步骤:

  1. 调用umask函数将当前文件模式创建屏蔽字为一个已知值(通常为0)
  2. fork子进程,并且结束父进程
  3. 调用setsid函数,创建一个会话
  4. 调用函数chdir将当前进程的工作目录更改为根目录或者某个指定位置
  5. 调用fclose函数关闭不在需要的文件描述符(0,1,2等)
  6. 忽略SIGCHLD信号

解释一下步骤1、4、5、6

1、上面提到过,我们要操作的进程是一个子进程。而子进程从父进程的PCB继承过来的文件模式创建屏蔽字可能会屏蔽掉某些权限。如果我们创建的守护进程又正好需要这些被屏蔽的权限就会造成问题。另一方面,如果守护进程调用了库函数创建了文件,那么文件模式创建屏蔽字应该设置为更强的(如007)。因为库函数不允许调用者通过一个显式的函数参数来设置权限。
4、因为子进程从父进程继承来的工作目录可能是在一个挂载的文件系统中,而守护进程在系统再次引导前是一直存在的,如果不更改,那么挂载的文件系统就一直卸载不了。
5、使守护进程不再有从父进程继承来的任何文件描述符。或者还可以将文件描述符重定向到文件(/dev/null)。这样相当于当前进程的标准输入,标准输出,标准错误都失效。关闭文件描述符的是因为守护进程是与控制终端没有任何联系的,不能从终端下读数据和写数据。
6、常用于并发服务器的性能提升,因为并发服务器常常fork很多子进程,子进程终止之后需要服务器进程wait去清理资源,如果忽略SIGCHLD信号内核就会把僵尸子进程交给init进程处理,避免占用资源(非必须)
好了,来看代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <fcntl.h>

void mydaemon()
{
    umask(0);//设置权限屏蔽字
    pid_t pid=fork();//创建子进程
    if(pid>1)
    {
        exit(1);//父进程退出
    }

    setsid();//创建会话

    chdir("/");//更改当前工作目录为根目录

    close(0);//关闭文件描述符
    close(1);
    close(2);

    signal(SIGCHLD,SIG_IGN);//忽略SIGCHLD信号
}

int main()
{
    mydaemon();
    while(1)
    {}
    return 0;
}

这里写图片描述
查看这个进程,发现它的TTY是?,TPGID是-1表示没有终端,PPID是init进程,而且PID,PGID,SID都是一样的。

除了上面这种方式,Linux还提供了一个函数来创建守护进程

#include <unistd.h>

int daemon(int nochdir, int noclose);
//第一个参数nochdir如果设置为0的话表示将工作目录改为根目录。
//第二个参数noclose如果设置为0的话就将文件描述符重定向到/dev/null文件
//返回值:成功返回0,失败返回-1

很简单,直接调用即可

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


int main()
{
    daemon(0,0);
    while(1);
    return 0;
}

这里写图片描述

猜你喜欢

转载自blog.csdn.net/qq_34021920/article/details/79953789