1. 进程组
每一个进程除了有一个进程ID之外,还属于一个进程组。进程组是一个或多个进程的集合。
每个进程组都有一个进程组ID。每个进程组都可以有一个组长进程。组长ID一般是进程组中的第一个进程的ID。
组长进程可以创建一个进程组,创建该组中的进程,然后终止。
只要在某个进程组中一个进程存在,则该进程组就存在,不管其组长进程是否终止无关。
我们用sleep创建如下任务并使用命令查看:
我们可以看到:(1)利用 ps axj | head -n1获取每一项的名称,其中PPID是进程的父ID,PID是进程的ID,PGID是进程组的ID,SID是会话ID,TTY是终端号,TPGID是前台进程组ID,STAT是进程的状态。
(2)利用 ps axj | grep sleep命令可以获得包括当前进程(grep sleep命令)的信息。
(3)利用ps axj | grep sleep |grep -v grep命令可以获得除了"grep sleep"命令的其他进程的信息,其中-v选项表示去除“grep"的信息。
(4)&符号是将当前任务放置后台运行,前台依然是bash运行。
(5)这里有三个进程,三个进程同属于一个进程组,该进程组的PGID(进程组ID)是5931,也是第一个进程的进程ID。
在三个进程同时存在的情况下,我们利用kill -9 进程组ID 杀死进程组组长,但是其他两个进程依然在,并且属于同一个进程组。
我们利用fg命令将进程组提到前台,利用Ctrl+C 杀死整个进程组。
2. 作业
Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。
一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成。
Shell 可以运行一个前台作业和多个后台作业,这称为作业控制。
我们先来看一个例子:
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 pid_t id=fork();
7 if(id<0){
8 perror("fork");
9 return 1;
10 }
11 else if(id==0)
12 {
13 while(1)
14 {
15 printf("child[%d]:i am child runing!\n",getpid());
16 sleep(1);
17 }
18 }
19 else
20 {
21 int i=5;
22 while(i)
23 {
24 printf("parent[%d] i am father going to dead! %d\n",getpid(),i-- );
25 sleep(1);
26 }
27 }
28 return 0;
29 }
运行结果:
我们可以看到:
(1)在父进程和子进程各打了5次之后,父进程退出,子进程仍在运行。
(2)在父进程退出后,我们可以在终端上输入命令,说明bash被提到前台,而子进程被放置后台,并不断向前台打印消息。
(3)换句话说,我们刚在前台起的任务已经退出,但是子进程还在,就自动被提到后台,并且子进程所属的进程组还在,组长是父进程(已经退出)。
那么,作业与进程组的区别是什么呢?
答案是:如果作业中的父进程创建了子进程,则子进程不属于运行的作业 ,而属于进程组。一旦作业运行结束,shell就将自己提到前台,若子进程还存在,就将子进程放置后台,自动变为后台进程组。
3. 会话
会话是一个或多个进程组的集合。一个会话有一个控制终端。
会话首进程(一般是第一个进程)被称为控制进程。
一个会话中的几个进程组可被分为一个前台进程组和一个或者多个后台进程组。
一个会话包括控制进程,一个前台进程组和任意多个后台进程组。
我们用sleep创建如下任务并使用命令查看:
我们可以看到:(1)SID会话ID,会话ID为2400,三个进程属于同一个进程组,进程组ID为2517,同属于一个会话,会话ID为2400. 会话首进程为2400.
那么,2400会是谁呢?我们利用ps aux | grep -E 2400 | grep -v grep命令查看如下:
我们可以看到,2400是bash,也就是我们的解释器!
总结:
(1)打开一个终端,也就打开了一个会话,一个会话中包括多个进程组,默认情况下会话首进程(控制进程)为bash。
(2)当利用kill -9 杀死终端(bash) 时,会话关闭,但是进程组仍然存在,并被放置在后台。
(3)进程,进程组,作业及会话的联系:
4. 作业控制
前面我们说过,shell分前后台控制的不是进程而是进程组(Process Group)或者 作业 (Job)。一个前台作业由任意多个进程组成,一个后台作业也可以由一个或者多个进程组成。
作业控制(Job control )就是shell可以运行一个前台作业和任意多个后台作业。
我们可以看到:
(1)将两个作业同时放置后台,我们利用jobs 命令查看所有的后台作业,发现有两个运行(Running)的作业。[1],[2]表示作业的编号,可以利用作业编号执行一些命令。
(2)fg 1表示将作业编号为[1]的作业提到前台,Ctrl+z 表示将前台作业提到后台,但是不能运行,即我们利用jobs查看后台作业时,发现作业1被停止运行。
(3)bg 1表示将作业编号为[1]的作业后置后台,此时我们再利用jobs命令查看后台作业时,发现两个作业都可以运行了。
我们将两个作业都提到前台运行,并且利用Ctrl+c 杀死它们。可以看到,Ctrl+c 杀掉的不是进程,而是杀掉整个作业。
注:后台作业不能从终端下读取数据,可以在终端下写数据。
5. 守护进程
守护进程也称为精灵进程(Daemon),是运行在后台的一种特殊进程。
守护进程独立于控制终端,并且周期性的执行某种任务或者等待处理的事件。
守护进程不受用户登录或者注销的影响,他们一直运行着。
守护进程本质是孤儿进程,其父进程ID为1,自成会话,自成进程组,与终端控制无关。
我们利用ps axj | more命令来查看控制终端的信息:
注:(1)凡是TPGID栏为-1的都是没有控制终端的进程,也就是守护进程。
(2)在COMMAND一栏用[]括起来的名字表示内核线程,这写线程在内核中创建,没有用户空间代码,也没有程序行和命令行。
(3)可以看出,守护进程通常采用以d 结尾。
创建守护进程
创建守护进程最关键的一步就是调用setsid函数,但是在调用这个函数之前,必须创建子进程,在子进程中调用setsid函数。
要保证当前进程不是进程组Leaders,只要先fork在调用setsid函数,否则该函数就会调用失败,返回-1.
成功调用该函数的结果是:
(1)创建一个新的session,当前进程成为Session Leader,当前进程的id就是Session的id。
(2) 创建一个新的进程组,当前进程成为进程组的Leader,当前进程的id就是进程组的id。
(3) 如果当前进程原本有一个控制终端,则它失去这个控制终端,成为一个没有控制终端的进程。所谓失去控制终端是指原来的终端仍然是打开的。
我们创建一个守护进程:
1 #include <stdio.h>
2 #include <signal.h>
3 #include <unistd.h>
4 #include <stdlib.h>
5 #include <fcntl.h>
6 #include <sys/stat.h>
7
8 void mydaemon()
9 {
10 int i;
11 int fd;
12 pid_t pid;
13 struct sigaction sa;
14
15 umask(0); //调用umask将文件模式创建屏蔽字设置为0
16
17 if(pid=fork()>0) { //令父进程退出
19 exit(0);
20 }
21
22 setsid(); //子进程调用setsid创建会话
23 sa.sa_handler=SIG_IGN; //忽略SIGCHLD信号
25 sigemptyset(&sa.sa_mask);
26 sa.sa_flags=0;
27
28 if(sigaction(SIGCHLD,&sa,NULL)<0) //注册子进程退出忽略信号
29 return;
30
31 if(chdir("/")<0) //将当前工作目录更改为根目录
32 {
33 printf("child dir error");
34 return;
35 }
36
37 close(0);
38 fd=open("/dev/null",O_RDWR); //关闭不再需要的文件描述符,或者重定向到 /dev/null;
39 dup2(fd,1);
40 dup2(fd,2);
41 }
42
43 int main()
44 {
45 mydaemon();
46 while(1){
47 sleep(1);
}