【Linux】进程间关系及守护进程

进程组、作业、会话

1.进程组

  我们知道每个进程都有自己的pid,但是除此之外,还属于一个进程组进程组是一个或多个进程的集合。进程组与作业相关联,可以接受来自一个终端的各种信号。每一个进程组都有一个进程组ID,进程组中有一个进程组长,组长的进程ID与进程组的ID相同。这与Linux中线程组相似,多线程的进程称为线程组,线程组的ID与线程组组长的ID相同
这里写图片描述
  举一个例子:由上面的图,可以看到两个进程sleep 50和sleep 100,它们的PID分别是13589和13590,组长进程是sleep 50,进程组的PGID(process group ID)是13589。 组长进程可以创建一个进程组,创建该组中的进程,然后终止,只要进程组中的一个进程存在,进程组就存在,与组长进程是否终止无关。
这里写图片描述
上图中可以看到,我们创建两个进程,然后kill掉组长进程,可以看到进程组仍然存在。

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

2.作业

  我们知道在Linux中,Shell是分前台和后台的。在学习进程组之前,我们以为是按进程来分前台后台,但是此时应该澄清,Shell分前台后台应该是按照进程组或者作业。一个前台作业可以有多个进程,一个后台作业也可以有多个进程,Shell中可以同时有一个前台作业和多个后台作业。我们使用Ctrl+C杀掉的是整个作业,而不是一个进程
  首先,区分一下进程组和作业的概念吧。假如进程组的一个进程创建了子进程,那么这个子进程属于进程组,但是不属于作业。不过,我们以后在讨论进程组和作业时,不做明显区分。
 
  我们说一个前台作业开始运行,那么Shell就自动转到后台,一旦前台作业运行结束,Shell就将自己提到前台来。如果原来前台进程组的一个进程的子进程还存在,那么这个子进程就会被放到后台。
举一个例子:

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

int main()
{
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork");
        exit(1);
    }
    else if(pid == 0)
    {
        while(1)
        {
            printf("child(%d)# I am running\n", getpid());
            sleep(1);
        }
    }
    else
    {
        int i = 5;
        while(i)
        {
            printf("paretn(%d)# I am going to died\n", getpid());
            sleep(1);
            i--;
        }
    }
    return 0;
}

先看运行结果:
这里写图片描述
  上图中,我们发现当父进程运行结束后,我们可以继续输入命令,但是又一直在打印东西。这是因为程序一开始运行,即在前台新起一个作业,当运行到fork()时,创建了一个子进程,我们让子进程一直打印消息,而父进程只循环打印5次消息。此时,当父进程结束,也就意味着前台作业运行结束,Shell将到前台来,我们可以继续输入,而子进程将被提到后台,但是子进程是在屏幕上打印消息,这样将会造成显示屏混乱。

3.会话

  会话是一个或多个进程组的集合。一个会话可以控制一个终端,当我们在Linux中打开一个终端,也就是新建一个会话。建立与控制终端连在会话首进程被称为控制进程。一个会话中应该包括控制进程(也叫会话首进程),一个前台进程组和多个后台进程组
这里写图片描述
  上图中,我们创建三个进程,三个进程的PID分别是13901、13902和13903,进程组的ID是13901,后面的SID就是会话ID了,而13519也就是说bash了,也就是说,会话首进程是bash了,而三个进程的父进程也都是bash

作业控制

我们说Shell可以同时运行一个前台作业和多个后台作业,我们把这称之为作业控制

1.我们学习几个命令:

  • jobs 查看后台作业
  • fg 1将作业由后台提到前台
  • Ctrl+Z 将作业提到后台(变为Stopped状态)
  • bg 1 将Stopped状态的作业改为Running状态
    这里写图片描述

2.与作业控制有关的信号

这里写图片描述
  cat可以连接文件并打印到标准输出设备上,cat后面如果不跟文件名,可以把标准输入的内容打印出来。我们看一下将cat放到后台执行,在用bg命令让他跑起来,但是他仍然是Stopped状态,这是为什么?
  cat需要从标准输入上读取数据,但是后台进程没办法读取输入的,所以,内核会向进程发送SIGTTIN信号,该信号的默认处理动作是使进程停止,所以,cat就一直处于停止状态了。当我们使用bg命令的时候,cat进程继续运行,又要读终端输入,然而它在后台不能读终端输入,所以又收到SIGTTIN信号。
  
这里写图片描述

  • fg命令可以将后台作业提到前台来,如果作业在后台是运行的,那么直接提到前台运行;如果作业在后台是停止状态的,那么内核会发送SIFCONT信号使作业可以继续运行。
  • 我们知道Ctrl+Z是将作业由前台提到后台,其实是向所有前台进程发送SIGSTP信号,该信号的默认处理动作是使进程停止。
  • bg命令是让后台停止的进程继续运行,也是发送SIGCONT信号。

这里写图片描述
  假如我们使用kill命令向进程发送SIGTERM(15)信号,这个信号并不会立刻处理,而是在进程准备继续运行的时候处理,其默认的动作是终止进程。但如果是SIGKILL则立刻终止进程。

守护进程

1.什么是守护进程?

  守护进程又叫精灵进程,守护进程是运行在后台的一种特殊的进程,它独立于进程控制终端,并且周期的执行任务或等待处理某些发生的事情。Linux的大多数服务器就使用守护进程实现的,比如ssh服务器等。守护进程:

  1. 我们知道PPID为1的是孤儿进程,所以守护进程本质上也是孤儿进程
  2. 守护进程自成进程组,自成会话,所以不受用户登录注销的影响
  3. 与终端无关
    这里写图片描述
    由上图中可以观察看到:
    1. 守护进程的TPGID一栏是-1;COMMAND一列中用[]括起来的名字表示的是内核线程,这些线程没有用户空间代码,没有程序文件,通常以k开头的名字。
    2. 守护进程通常采用d结尾的名字。

2. 创建守护进程

  创建一个守护进程作重要的是:要创建一个会话,并且成为会话首进程

#include <unistd.h>
pid_t setsid(void);
//返回值:调用成功返回新建的会话的ID,也就是当前进程的ID,失败返回-1.

如果调用函数成功了,那么当前进程将自成会话,自成进程组,失去一个控制终端,成为一个没有控制终端的进程。

  需要注意:调用该函数之前,当前进程不允许时进程组的组长,否则,将调用失败。如果当前进程是进程组组长,它有可能是会话首进程,调用成功返回相同的会话ID,产生冲突。因此,我们可以先fork一个子进程,在子进程中调用setsid。

 在写代码之前,先整理一下思路:
1. 调用umask将文件模式创建屏蔽字设置为0;
  由继承提来的文件模式创建屏蔽字可能会拒绝设置某些权限。例如:若守护进程要创建一个可读,可写的文件,而继承的文件模式创建屏蔽字可能屏蔽了这两种权限,于是所要求的组可读、写就不能起作用。
2. 调用fork,父进程退出,保证子进程不是一个进程组的组长;
3. 调用setsid创建一个新会话;
  这样就可以:成为新会话的首进程,成为一个新进程的组长进程,没有终端控制(再次fork,保持子进程不是会话首进程,从而保证后续不会在和其他终端关联。但这并不是必须要做的)。
4. 忽略SIGCHLD信号;
5. 将当前工作目录更改为根目录。
  守护进程通常在系统在系统再引导之前是一直存在的,所以如果守护进程的当前工作目录在一个装配文件系统中,那么该文件系统就不能被umount,这与装配文件系统的原意不符。
6. 关闭不必要的文件描述符
  守护进程无输入输出,文件描述符继承自父进程,守护进程不需要。
下面用代码创建守护进程:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
void mydaemon()
{
    pid_t pid;
    int fd0;
    umask(0);//调用umask将文件模式创建屏蔽字设置为0
    if(pid = fork() < 0)
    {
        perror("fork");
        return;
    }
    else if(pid > 0)//父进程退出,保证子进程不是一个进程组的组长
        exit(0);
    setsid();//调用setsid创建一个新会话
    signal(SIGCHLD, SIG_IGN);//忽略SIGCHLD信号
    if(chdir("/") < 0)//将当前工作目录更改为根目录
    {
        perror("chdir");
        return;
    }
    close(0);//关掉不必要的文件描述符,或者重定义到“/dev/null”中,
             // “/dev/null”是一个无底洞,专门用来处理需要被丢弃的东西。
    fd0 = open("/dev/null", O_RDWR);
    dup2(fd0, 1);
    dup2(fd0, 2);
}
int main()
{
    mydaemon();
    while(1)
    {
        sleep(1);
    }
    return 0;
}

使用ps命令查看运行结果:
这里写图片描述
  可以看到三个进程自成进程组,自成会话,没有控制终端,TPGID都是-1,所以这三个进程都是守护进程。  

除此之外,系统给我们提供了一个函数,通过这个函数,我们可以直接创建守护进程。

#include <unistd.h>
int daemon(int nochdir, int noclose);

nochdir为0表示更改当前路径为根目录;noclose表示关闭不需要的文件描述符。

守护进程为什么要fork两次?
第一次fork:第一次fork的作用是让Shell认为这条命令已经终止,不用挂在终端输入上;在一个是为了后面的setsid服务,因为调用setsid函数的进程不能是进程组的组长。所以,到了这里子进程便成了一个新会话组的组长。
第二次fork:第二次不是必须的,主要目的是:放置进程再次打开一个控制终端,因为打开一个控制终端的前台条件是该进程必须是会话组长,再次fork,子进程ID!=sid(sid是进程父进程的sid)。所以也无法打开新的控制终端。

猜你喜欢

转载自blog.csdn.net/wei_cheng18/article/details/80012849