我们已经了解了进程,再来了解一下进程间关系。
我们先了解一下以下概念:
一、进程组、作业和会话
1、进程组
每个进程除了有一个进程ID之外,还属于一个进程组。进程组是一个或多个进程的集合。通常,它们与同一作业相关联,可以接收来自同一终端的各种信号。每个进程组有一个唯一的进程组ID。每个进程组都可以有 一个组长进程。组长进程的标识是,其进程组ID等于其进程ID。 组长进程可以创建一个进程组,创建该组中的进程,然后终止。
举例如下:
看上图:
我们在后台运行了三个进程 。
‘&’: 表示将进程组放在后台执行。
看一下我们使用的命令:ps
ps选项:
a: 不仅列当前用户的进程,也列出所有其他用户的进程
x: 表示不仅列有控制终端的进程,也列出所有无控制终端的进程
j:表示列出与作业控制相关的信息
观察上图:
进程:2483,2484,2485
组长:2483, 进程组当中的第一个进程(和进程组ID相同ID的进程被称为组长进程)
注意:只要在某个进程组中一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。
那么?我们杀掉这个组长,其他进程还会在吗?如下:
可以看出:杀死组长这个进程组还是存在的。所以,只要在某个进程组中一个进程存在则该进程组就存在,这与其组长进程是否终止无关。
2、作业
我们必须知道,Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。
我们刚才了解了进程组,那什么是作业呢?
shell控制的并不是单个的进程,而是进程组或者是作业,且这个控制是分前台后台的。
了解一下作业控制:
一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,Shell 可以运行一个前台作业和任意多个后台作业,这称为作业控制 。
通过以上的学习,我们知道,作业和进程组都是由多个进程组成,那他们的区别是什么呢?
作业与进程组的区别:
如果作业中的某个进程又创建了子进程,则子进程不属于作业,但属于进程组。
一旦作业运行结束,Shell就把自己提到前台(子进程还在,可是子进程不属于作业),如果原来的前台进程还存在(如果这个子进程还没终止),它自动变为后台进程组。
我们在重新理解一下,在前台新起作业,shell是无法运行,因为他被提到了后台。 但是如果前台进程退出, shell就又被提到了前台,所以可以继续接受用户输入。
一个例子:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
pid_t id=fork();
if(id<0){
perror("fork");
return 1;
}
else if(id==0){//child
while(1){
printf("child(%d)# I am running!\n",getpid());
sleep(1);
}
}
else{//parent
sleep(5);
exit(1);
}
return 0;
}
运行结果很有意思,
我们可以发现:
5s之内,shell无法接受任何命令,因为此时的前台作业不是shell。
但是父进程退出之后,子进程还在运行,但此时输入的命令,shell可以处理,说明此时shell变成了前台作业。
换句话说,我们刚新起的作业退出了,但子进程还在,就自动被提到后台。
你可以再看看,子进程所属的进程组还在。组长是父进程(已经退出) 我们发现他还在一致打消息,杀掉即可。
可以证明在作业中创建子进程,则子进程不属于作业。
3、会话
我们再看上面的图
这里的SID就指的是会话ID。
认识一下会话:
会话(Session)是一个或多个进程组的集合。 一个会话可以有一个控制终端。这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下)。
建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。
所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意后台进程组。
新打开一个终端:
如下图:
图中第一部分,会话id2402,三个进程都属于同一个进程组。
图中下半部分,查看2402,看出来2402是bash。也就是我们的解释器,前面提到的会话首进程,而且三个进程的父进程都是bash。
多打开几个终端,对比你就会发现,每打开一个终端,就新建了一个会话 。
二、作业控制
前面了解作业时已经接触了作业控制的概念,事实上:
Shell分前后台来控制的不是进程而是作业 (Job)或者进程组(Process Group)。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以同时运行一个前台作业和任意多个后台作业,这称为作业控制(Job Control)。
接下来我们主要看作业控制的有关信号:
先看一个实验,
将cat放到后台运行,由于cat需要读标准输入(也就是终端输入),而后台进程是不能读终端输入的,因此内核发 SIGTTIN信号给进程,该信号的默认处理动作是使进程停止。
1、jobs命令:查看当前有哪些作业
2、fg命令可以将某个作业提至前台运行,如果该作业的进程组正在后台运行 ,则提至前台运行,如果该作业处于停止状态,则给进程组的每个进程发SIGCONT信号使它继续运行。
3、Ctrl+Z:给当前进程发送SIGTSTP信号,该信号的默认动作是停止该进程。
cat继续以后台进程的形式存在。
4、bg命令可以让某个停止的作业在后台继续运行,也需要给该作业的进程组的每个进程发SIGCONT信号。
cat进程继续运行,又要读终端输入,然而它在后台不能读终端输入,所以⼜又收到SIGTTIN信号而停止。
5、kill命令:给一个停止的进程发SIGTERM(15)信号,这个信号并不会立刻处理,而要等进程准备继续运行之前处理,默认动作是终止进程。
当我们再次fg +进程号将它提到前台时,就会发现该进程终止了。
总结:后台进程不能从终端下读取数据,但是根据上面fork的那个例子,我们知道后台是可以从终端写数据的。
三、守护进程
1、认识守护进程
守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程。
它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其它进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程(守护进程)不受用户登录 注销的影响 ,它们一直在运行着。这种进程有一个名称叫守护进程(Daemon)。
2、内核守护进程
下面我们用ps axj命令查看系统中的进程。
参数:
a:表示不仅列当前用户的进程,也列出所有其他用户的进程
x:表示不仅列有控制终端的进程,也列出所有无控制终端的进程
j:表示列出与作业控制相关的信息
注意:
凡是TPGID一栏写着-1的都是没有控制终端的进程,也就是守护进程。
在COMMAND一列用[]括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行, 通常采用以k开头的名字,表示Kernel。
我们看到的第一行的进程就是我们常说的init进程,也是回收孤儿进程的1号进程,它也是一个守护进程。而且init进程还是所有用户级进程的父进程。
守护进程通常采用以d结尾的名字,表示Daemon
3、创建守护进程
创建守护进程最重要的就是创建一个会话并成为会话首进程,来看看创建一个会话要用的函数:
调用成功时返回新创建的Session的id(其实也就是当前进程的id)
出错返回-1
使用此函数时需注意:
当前进程不能是进程组的组长,否则该函数就会出错返回 。
所以我们可以利用fork()创建出一个子进程,然后让父进程直接终止,然后在子进程中调用setsid函数。子进程的进程ID是新分配的,所以两者不可能相等。这样就保证了子进程不是一个进程组的组长。
看一段实现守护进程的代码:
#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);
}
int main(){
mydaemon();
while(1)
{}
return 0;
}
解释一下:
1)调用umask函数将当前文件模式创建屏蔽字为一个值(通常为0)
2)fork子进程,并且结束父进程
3)调用setsid函数,创建一个新会话
4)调用函数chdir将当前进程的工作目录更改为根目录或者某个指定位置
5)调用fclose函数关闭不在需要的文件描述符(0,1,2等)
6)忽略SIGCHLD信号
还有一个函数:
直接调用即可。