进程间关系和守护进程

  • 进程组

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


‘&’表示将进程组放在后台执行;

进程:40553、40554、40555;

组长:40553,进程组中的第一个进程;

kill  -9  40553,即杀掉组长,此时进程还在。即可验证,只要在某个进程组中一个进程存在,则该进程组就存在,与其组长进程是否终止无关这个结论。

注意:父进程创建子进程的顺序是从左往右执行的。

ps选项:

    a:不仅列当前用户的进程,也列出所有其他用户的进程。

    x:不仅列有控制终端的进程,也列出所有无控制终端的进程。

    j:列出与作业控制相关的信息。


此时我们可以启动一个在前台运行的进程:


然后打开另外一个终端,输入命令查看进程信息:


“Ctrl+\”,“Ctrl+z”,“Ctrl+c”一次可以终止一个进程组。


  • 作业

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

作业与进程组的区别是:如果作业中的某个进程又创建了子进程,则子进程不属于作业,而属于进程组。

一旦作业运行结束,Shell就把自己提到前台(此时子进程仍旧存在,但是子进程不属于作业),如果原来的前台进程还存在(若该子进程还未终止),它自动变为后台进程组。

当在前台新起一个作业时,Shell就被提到了后台,此时Shell是无法运行的。但如果前台进程退出,Shell就又被提到了前台,此时Shell就可以继续接受用户输入。


代码实例:

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

int main(){
    printf("I am running\n");
    sleep(4);
    pid_t id = fork();
    if(id == 0){//child
        while(1){
            printf("I am child:%d , I am running\n",getpid());
            sleep(1);
        }
    }
    else{//father
        int i = 3;
        for(;i > 0;i--){
            printf("I am father:%d , I am going to dead\n",getpid());
            sleep(1);
        }
    }
    return 0;
}

结果演示:


我们可以发现,只要程序一跑起来,就相当于在前台新起了一个作业,此时Shell就被提到了后台。

最开始的四秒钟,只有一个进程。四秒钟结束后,进程fork()创建子进程,就变成了父子两个进程。此时子进程不属于作业,而属于进程组。

三秒钟后,父进程退出,子进程仍旧在运行,在此时输入的命令Shell是可以处理的:


这就说明,此时Shell重新被提到了前台,变成了前台作业。换句话说,我们新起的前台作业退出了,但子进程还在,自动被提到了后台。

此时子进程所属的进程组还在,组长为父进程(已退出)。

此时子进程还在一直打消息,我们重新开一个终端,用kill   -9杀掉即可。

  • 会话

会话是一个或多个进程组的集合。一个会话可以有一个控制终端。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分为一个前台进程组和任意多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意多个后台进程组。其中控制进程通常由一个特定的bash承担。

登录是建立会话的过程;注销是删除会话的过程。

我们可以打开一个终端:


图中的SID为会话ID。三个进程同属于一个会话,编号为42697。三个进程同属于一个进程组,组长进程的编号为42808。

那么,42697又是谁呢?


由此我们即可验证,控制进程(会话首进程)通常由bash承担。而且三个进程的父进程都为bash。

  • 作业控制

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


此时我们在后台新起了两个作业,通过jobs命令即可查看后台作业。


我们通过fg 1命令将作业1提到了前台,然后按下“Ctrl+c”。此时我们利用jobs命令再次查看后台作业:


可以发现,作业1已经不存在了。只剩下作业2在后台执行。由此我们可以再得出一个结论:“Ctrl+c”杀掉的不是进程,而是整个作业。

我们再利用同样的方法结束作业2:


此时后台就没有正在执行的作业了。

然后我们可以利用同样的方法新起两个作业:


然后将作业1提到前台:


此时按下“Ctrl+z”:


然后通过jobs命令查看后台作业:


我们发现,此时作业1仍旧存在,只不过状态为Stopped。由此我们可以得出一个结论:“Ctrl+z”是将前台的作业提到了后台。

那么怎样使作业1重新恢复Running状态呢?我们可以使用bg  1命令使作业1从Stopped状态重新恢复为Running状态


这里我们还需要讨论一个问题:


我们新起了两个后台作业,然后在另外一个终端下输入如下命令:


此时再通过jobs查看后台作业:


发现此时作业1和作业2都已经收到信号core  dump了。

那么我们换一种情境,又会出现什么样的现象呢?


和之前的步骤一样,我们先在后台新起两个作业,将作业1提到前台,按下“Ctrl+z”使其提到后台并处于Stopped状态:


在另外一个终端下,我们给1号作业发送7号信号:


然后再用jobs查看后台作业,发现和之前一模一样:


然后我们使用bg  1命令使作业1重新恢复Running状态,再次查看后台作业:


此时作业1就core  dump了。由此我们可以得出一个结论:当进程处于stopped状态时,给该进程发送除9号信号以外的其他普通信号,该信号并不会被立即捕捉。当该进程被重新恢复至Running状态时,才会捕捉信号并执行信号内容。

  • 作业控制有关的信号

当我们在前台输入cat命令新起一个前台作业时,cat会将我们输入的内容打印到标准输出设备上。


我们将cat放至后台运行,发现cat作业的状态变成了stop。这是由于cat需要读标准输入(即终端输入),而后台进程是不能读终端输入的。因此内核发送SIGTTIN信号给进程,该信号的默认处理动作是使进程停止。

这里我们需要注意两点问题:

    (1)后台作业禁止从终端读取数据;

    (2)9号信号不能被捕捉。


jobs命令可以查看当前有哪些作业。fg命令可以将某个作业提至前台运行,如果该作业的进程组正在后台运行,则提至前台运行;如果该作业处于停止状态,则给进程组的每个进程发SIGCONT信号使它继续运行。

cat提到前台运行后,挂起等待终端输入,当输入hello并回车时,cat会打印出同样的一行,然后继续挂起等待输入。


如果输入“Ctrl+z“,则向所有前台进程发SIGTSTP信号,该信号的默认动作是使得进程停止,cat会继续以后台作业的形式存在。


bg命令可以让某个停止的作业在后台继续运行,也需要给该作业的进程组中的每个进程发送SIGCONT信号。cat进程继续运行,又要读终端输入,然而它在后台不能读终端输入,所以又收到SIGTTIN信号而停止。

总结:后台进程不能从终端下读取数据,但能写数据。

  • 守护进程

守护进程也称精灵进程,是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程,Linux的大多数服务器就是用守护进程实现的,比如:FTP服务器,ssh服务器,Web服务器httpd等。同时,守护进程完成许多系统任务,比如:作业规划进程crond等。

Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其他进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程(守护进程)不受用户登录注销的影响,它们一直在运行着,这种进程被称作守护进程(Deamon)。


(1)凡是TPGID一栏写着-1的都是没有控制终端的进程,即守护进程。

(2)在COMMAND一列用[]括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用以k开头的名字,表示Kernel。

(3)守护进程通常采用以d结尾的名字,表示Daemon。

  • 创建守护进程

创建守护进程最关键的一步是调用setsid函数创建一个新的会话,并成为控制进程(会话首进程)。

函数原型:pid_t   setsid(void);

头文件:#include<unistd.h>

返回值:该函数调用成功时返回新创建的Session的id(即当前进程的id),出错返回-1。

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

要保证当前进程不是进程组的组长进程,只要先fork再调用setsid即可。fork创建的子进程和父进程在同一个进程组中,进程组的组长进程必然是该组的第一个进程,所以子进程不可能是该组的第一个进程,在子进程中调用setsid就不会有问题了。

成功调用该函数的结果是:

(1)创建一个新的会话,当前进程成为会话首进程,当前进程的id就是会话的id。

(2)创建一个新的进程组,当前进程成为进程组的组长进程,当前进程的id就是进程组的id。

(3)如果当前进程原本有一个控制终端,则它失去这个控制终端,成为一个没有控制终端的进程。所谓失去控制终端是指,原来的控制终端仍然是打开的,仍然可以读写,但只是一个普通的打开文件而不是控制终端了。

代码实例:

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

void mydaemon(){
    pid_t pid;
    umask(0);//1.调用umask将文件模式创建屏蔽字设为0

    //2.调用fork,父进程退出(exit)
    //如果该守护进程是作为一条简单的Shell命令启动的,那么父进程终止使得Shell认为该命令已执行完毕
    //保证子进程不是一个进程组的组长进程。
    if((pid = fork()) < 0){
        perror("fork");
    }
    else if(pid > 0){//终止父进程
        exit(0);
    }
    setsid();//3.调用setsid创建一个新会话
    signal(SIGCHLD,SIG_IGN);//4.忽略SIGCHLD信号
    chdir("/");//5.将当前工作目录更改为根目录
    close(0);//关闭不再需要的文件描述符
    return;
}

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

结果演示:


此时,我们就创建出了一个守护进程。

  • daemon函数
#include <stdio.h>
#include<unistd.h>
int main(){
    daemon(0,0);
    while(1);
}

结果演示:


此时,利用daemon函数,我们就可以直接创建出一个守护进程。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值