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

1, 进程组(Process Group)

每个进程除了有一个进程ID之外,还属于一个进程组。
进程组是一个或多个进程的集合。
通常,它们与同一 作业 相关联,可以接收来自同一终端的各种信号。
每个进程组有一个唯一的进程组ID。每个进程组都可以有一个组长进程。
组长进程的标识是,其进程组ID等于其进程ID。
组长进程可以创建一个进程组,创建该组中的进程,然后终止。
只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。
实例如下:

ps axj | head -n1 
ps axj | grep sleep | grep -v grep

这里写图片描述
& 表示将进程组放在后台执行
进程 2859
进程 2860
进程 2861
进程组长是进程 2859, 所以进程组 ID (PGID)就是2859
kill -9 杀掉组长
这里写图片描述
进程组还在

ps选项:

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

2, 作业(Job)

实际上,Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。
一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell 可以运行一个前台作业和任意多个后台作业,这称为作业控制 。

作业与进程组的区别:
如果作业中的某个进程又创建了子进程,则子进程不属于作业。
一旦作业运行结束,Shell就把自己提到前台(子进程还在,可是子进程不属于作业)
如果原来的前台进程还存在(如果这个子进程还没终止),它自动变为后台进程组。 在前台新起作业,Shell是无法运行,因为他被提到了后台。 但是如果前台进程退出, Shell就又被提到了前台,所以可以继续接受用户输入。

示例

/*================================================================
  Copyright (C) 2018 RJM. All rights reserved.
# File Name: test.c
# Author: rjm
# mail: [email protected]
# Created Time: 2018年04月25日 星期三 12时06分08秒

================================================================*/



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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        while(1)
        {
            printf("i am child: %d, i am runing\n", getpid());
            sleep(1);
        }
    }
    else
    {
        //father
        int i = 5;
        while(i)
        {
            printf("i am father: %d\n", getpid());
            i--;
        }
        sleep(1);
    }
    return 0;
}

这里写图片描述
程序运行,在前台新起了一个作业,这时shell无法接收命令,说明shell被切到了后台
5s 之后, 父进程退出, shell可以接收命令,说明此时shell是前台作业,但是此时子进程仍存在,并且还在一直打印消息(可以用 kiil -9 杀掉)
虽然他在还打印消息,但是他是后台进程,无法接受键盘输入,只能往屏幕上打印,也就是说只能写不能读。此时输入命令虽然会被子进程打印的消息冲乱,但其实他们仍是连续的命令,可以执行。
这里写图片描述
可以看到虽然父进程退出了, 但进程组还在, PGID是3088,此时子进程变孤儿了,可以看到他被 1号进程领养了。

3, 会话(Session)

会话(Session)是一个或多个进程组的集合。
一个会话可以有一个控制终端。这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下)。
建立与控制终端连接的会话首进程被称为 控制进程
一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。
所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意后台进程组
这里写图片描述
SID(会话 ID)是2610, 三个进程都属于同一个进程组,同一个会话 ,他们的父进程也都是2610
那么2610 是谁呢?
可以看到2610就是 bash,也就是我们的解释器,会话首进程。
多打开几个终端,对比你就会发现,每打开一个终端,就新建了一个会话

这里写图片描述

可以看到我们打开了 4 个会话,前3个是用xhell网络登陆的,后面是 - bash
最后一个是直接终端登录的,后面是 bash

4, 作业控制

Shell可以同时运行一个前台进程和任意多个后台进程”其实是不全面的
事实上,Shell分前后台来控制的不是进程而是作业 (Job)或者进程组(Process Group)
一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以同时运行一个前台作业和任意多个后台作业,这称为作业控制(Job Control)
这里写图片描述

jobs     //查看后台运行的程序及运行状态
fg 1     //将后台的程序调到前台
ctrl + z //是暂停当前正在前台运行的程序
bg 1     //将刚才暂停的程序放到后台,并让其运行

这里写图片描述
ctrl + c 杀掉的不是进程,而是整个作业

作业控制有关的信号

这里写图片描述
将cat放到后台运行,由于cat需要读标准输入(也就是终端输入),而后台进程是不能读终端输入的,因此内核发 21号信号 SIGTTIN 给该进程,该信号的默认处理动作是使进程停止(而不是终止退出),将cat 提到前台依然可以运行
这里写图片描述

jobs命令可以查看当前有哪些作业
fg命令可以将某个作业提至前台运行
如果该作业的进程组正在后台运行 则提至前台运行
如果该作业处于停止状态,则给进程组的每个进程发SIGCONT信号使它继续运行
参数1表示将第1个作业提至前台运行。
cat提到前台运行后,挂起等待终端输入,当输入hello并回车后,cat打印出同样的一行,然后继续挂起等待输入。
如果输入Ctrl-Z则向所有前台进程发SIGTSTP信号,该信号的默认动作是使进程停止,cat继续以后台作业的形式存在。
bg命令可以让某个停止的作业在后台继续运行,也需要给该作业的进程组的每个进程发SIGCONT信号。cat进程继续运行,又要读终端输入,然而它在后台不能读终端输入,所以又收到SIGTTIN信号而停止
这里写图片描述
kill 命令给一个停止的进程发 SIGTERM(15) 信号,这个信号并不会立刻处理,而要等进程准备继续运行之前处理,默认动作是终止进程。但如果给一个停止的进程发SIGKILL信号就不同了

5, 守护进程

守护进程也称精灵进程(Daemon)
是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些事件。
守护进程是一种很有用的进程。Linux的大多数服务器就是用守护进程实现的。
比如,ftp服务器,ssh服务器,Web服务器httpd等。
同时,守护进程完成许多系统任务。
比如,作业规划进程crond等。
Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其它进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止。
但系统服务进程(守护进程)不受用户登录注销的影响 ,它们一直在运行着。

下面我们用ps axj命令查看系统中的进程

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

这里写图片描述
凡是TPGID一栏写着 -1 的都是没有控制终端的进程,也就是守护进程。
在COMMAND一列用 [ ] 括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码
因此没有程序文件名和命令行, 通常采用以k开头的名字,表示Kernel
init进程我们已经很熟悉了
udevd 负责维护/dev目录下的设备文件
acpid负责电源管理
syslogd负责维护/var/log下的日志文件
可以看出,守护进程通常采用以d结尾的名字,表示Daemon

创建守护进程

创建守护进程最关键的一步是调用setsid函数创建一个新的Session
并成为Session Leader

#include <unistd.h> 
pid_t setsid(void); 
//该函数调用成功时返回新创建的Session的id(其实也就是当前进程的id)
//出错返回-1

注意, 调用这个函数之前 , 当前进程不允许是进程组的 Leader, 否则该函数返回 -1

要保证当前进程不是进程组的Leader也很容易,只要先fork再调用setsid就行了fork创建的子进程和父进程在同一个进程组中,进程组的 Leader必然是该组的第一个进程,也就是父进程,所以子进程不可能是该组Leader,在子进程中调用setsid就不会有问题了
成功调用该函数的结果是
创建一个新的Session,当前进程成为Session Leader,当前进程的id就是Session的id
创建一个新的进程组,当前进程成为进程组的Leader,当前进程的id就是进程组的id
如果当前进程原本有一个控制终端,则它失去这个控制终端,成为一个没有控制终端的进程。
所谓失去控制终端是指,原来的控制终端仍然是打开的,仍然可以读写,但只是一个普通的打开文件而不是控制终端了。

创建代码

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

void mydaemon(void)
{
    int i;
    int fd0;
    pid_t pid;
    struct sigaction sa;
    //1. 调用umask将文件模式创建屏蔽字设置为0.
    umask(0); 

    //2. 调用fork, 父进程退出(exit), 保证子进程不是一个进程组的组长进程。
    //   如果该守护进程是作为一条简单的shell命令启动的
    //   那么父进程终止使得shell认为该命令已经执行完毕
    if( (pid = fork()) < 0 )
    {
        perror("fork");
    }
    else if (pid > 0)
    {
        exit(0);
        //终止父进程
    }
    //3. 调用setsid创建一个新会话
    setsid(); 
    //4. 忽略SIGCHLD信号
    sa.sa_ handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if( sigaction(SIGCHLD, &sa, NULL ) < 0 )  // 注册子进程退出忽略信号
    {
        return;
    }

    //5.将当前工作目录更改为根目录。
    if( chdir("/") < 0 )
        printf("child dir error\n");
    return;

    //关闭不需要的文件描述符,或者重定向到 /dev/null
    close(0);
    fd0 = open("/dev/null", O_RDWR);
    dup2(fd0, 1);
    dup2(fd0, 2);
}

int main()
{
    mydaemon();
    while(1)
    {
        sleep(1);
    }
}

这里写图片描述
可以看到我们创建了自己的守护进程

实际上,系统已经提供了专门创建守护进程的函数

daemon函数

int daemon(int nochdir,int noclose);

创建守护进程的时候,往往要做以下两件事情

  • 1.将进程的工作目录修改为”/”根目录
    daemon的参数nochdir为0时,即可将工作目录修改为根目录;
  • 2.将标准输入,输出和错误输出重定向到/dev/null
    daemon的参数noclose为0时,输入,输出以及错误输出重定向到/dev/null
#include <stdio.h> 
#include <unistd.h> 
int main() 
{    
    daemon(0,0);    
    while(1); 
}

猜你喜欢

转载自blog.csdn.net/sinat_36629696/article/details/80078421