一、进程组/作业/会话
1、进程组
1)每个进程除了有一个进程ID之外,还属于一个进程组,进程组是一个或多个进程的集合,通常,它们与同一作业相关联,可以接收来自同一终端的各种信号
2)每个进程有一个唯一的进程组ID。每个进程组可以有一个组长进程,组长进程的标识是,进程组ID等于其进程的ID。
3)组长进程可以创建一个进程组,创建该组中的进程。然后终止,只要在某个进程组中有一个进程存在,则该进程组就存在,这与组长进程是否终止无关,实例如下:
2、作业
1)shell分前后 台来控制的不是进程而是作业,或者进程组;
2)一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成。shell可以运行一个前台程序和任意多个后台程序,这称为作业控制。
3)作业和进程组的区别在于:如果作业中的某个进程又创建了子进程,子进程不属于作业。而是属于进程组的。
4)一旦作业运行结束,shell就把自己提到了前台(子进程还在,可是子进程不属于作业),如果原来的前台进程还存在(如果这个子进程还没终止),它自动变为后台进程组。
5)我们重新理解一下,在前台新起作业,shell无法运行的,因为它被提到了后台。但是他被提到了后台,如果原来的前台进程退出,shell又被提到了后台,所以可以继续的接受用户的输入。
我们来看一个例子:
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main(){
5 pid_t id=fork();
6 if(id<0){
7 perror("fork");
8 return 1;
9 }
10 else if(id==0){//child
11 while(1){
12 printf("child(%d)# I am running !\n",getpid());
13 sleep(1);
14 }
15 }
16 else{//parent
17 int i=5;
18 while(i){
19 printf("parent(%d)# I am going to dead...%d\n",getpid(),i--);
20 sleep(1);
21 }
22 }
23 return 0;
24 }
25
结果展示:
我们在来接着上面的代码看下
我们看到的是子进程被提到前台执行shell命令,当我们执行kill -9 命令可以看到被打散了,但是换是能在前台进行识别,所以执行kill -9可以直接杀死子进程。
作业控制:
Semission和进程组一样“shell同时运行一个前台进程和任意多个后台进程”其实是不全面的,现在我们来研究更复杂的结构,事实上,shell分前后台来控制的不是进程而是作业或者进程组(Process Group),一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,shell可以同时运行一个前台作业和任意多个后台作业,,这称为作业控制。
作业控制有关的的信号
我们通过实验来理解与作业控制有关的信号
将cat放到后台运行,由于cat需要读标准输入(也就是终端输入),而后台进程是不能读终端输入的,因此内核发SIGTTIN信号给进程,该信号的默认处理动作是使进程停止。
1)如果该作业的进程组正在后台运行则提至前台运行,如果该作业处于停止状态,则给进程组的每个进程发SINCONT信号,可以使它继续运行。
2)参数4表示的是将作业4提到了前台运行,cat提到前台运行后,挂起等待终端输cat打印出哦同样的一行,然后继续挂起等待输入。
3)如果输入Ctrl + Z则向所有的前台进程发SIGTSTP信号,该信号的默认处理动作是使进程停止,cat继续以后台作业的形式存在。
4)bg命令可以让某个停止的动作在后台继续运行,也需要给该作业的进程组的每个进程进行发送SIGTTIN信号,cat进程继续运行,又要读终端输入,然而它在后台不能读终端输入,所以又收到SIGTTIN信号而停止。
用kill命令给一个停止的进程发SIGTERM(15)信号,这个信号并不会立即被处理,而是要等进程准备继续运行之前处理,默认动作是终止进程。但是如果给一个停止的进程发SIGKILL信号就不同了。
后台进程不能从终端下读取数据,但是能写吗?
答案: 可以。
3、会话(session)
1)会话是一个或多个进程组的集合,一个会话可以有一个控制终端。这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下),建立与控制终端连接的会话首进程被称为控制进程。
2)一个会话中的几个进程组可被分为一个前台进程组(通常是bash)以及一个或者多个后台进程组。
3)一个会话中应该包括控制进程(会话首进程),一个前台进程组和任意后台进程组。删除会话相当于注销。
注意:一般我们的在本地终端登陆的是bash,而网络登陆是-bash
4、了解终端
1.终端的基本概念
(1)Shell进程的控制终端 (Controlling Terminal):在UNIX系统中,用户通过终端登录系统后得到一个Shell进程。控制终端是保存在PCB中的信息,而我们知道fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下(没有重定向)。
每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。另外,在控制终端输入一些特殊的控制键可以给前台进程发信号(例如Ctrl C表示SIGINT,Ctrl \表示SIGQUIT)。
2.每个进程都可以通过一个特殊的设备文件/dev/tty来访问它的控制终端。
ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不 能是任意文件。
查看终端对应的设备:
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 printf("fd:%d -> %s\n",0,ttyname(0));
7 printf("fd:%d -> %s\n",1,ttyname(1));
8 printf("fd:%d -> %s\n",2,ttyname(2));
9 return 0;
10 }
11
二、守护进程(Daemon)
认识守护进程
守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。Linux的大多数服务器就是用守护进程实现的,比如ftp服务器,ssh服务器,Web服务器httpd等。同时,守护进程完成许多系统的任务。比如,作业规划进程crond等。
linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,,不能直接和用户交互,其他进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程(守护进程)不受用户登录注销的影响,(因为它们自成会话,不在会话的范围内)它们一直运行着,这种进程叫做守护进程(Daemon)。
下面我们可以用ps axj命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制控端的进程,参数j表示的是列出与作业控制相关的信息。
ps axj | more
其中more与cat类似,在读取的时候是以按页读取,按空格键进行下一页
- TPGID前台进程组ID,写着-1的都是没有控制终端的进程,也就是守护进程。
- 在COMMAND一列用[ ]括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用以k开头的名字,表示Kernel。
- udevd负责维护dev目录下的设备文件,acpid负责电源管理,syslogd负责维护/var/log下的日志文件。
- 守护进程通常采用d结尾的名字,表示Daemon。
创建守护进程
创建守护进程最关键的一步是调用setsid函数创建一个新的Semssion,并成为Semssion leader。
setsid函数
#include <unistd.h>
pid_t setsid(void);
该函数调用时返回新创建的Semssion的id(其实也就是当前进程的id)出错返回-1
注意:调用这个函数之前,当前进程不允许是进行组的leader’,否则会返回-1。要保证当前进程不是进程组的leader也很容易,只要先fork再调用setsid就行了。
fork创建的子进程和父进程同在一个进程组中,进程组的leader必然是第一个进程,所以子进程不可能是该组的第一个进程,在子进程中调用setsid就不会有问题了。
成功调用该函数的结果是:
- 创建一个新的Session,当前进程成为Session Leader,当前进程的id就是Session的id
- 创建一个新的进程组,当前进程成为进程组的Leader,当前进程的id就是进程组的id。
- 如果当前进程原本有一个控制终端,则它失去这个控制终端,成为一个没有控制终端的进程,所谓失去控制终端是指原来的控制终端仍然是打开的,仍然可以读写,但只是一个普通的打开的文件而不是控制终端了。
编写守护进程代码:
1)首先调用umask函数,将文件创建模式屏蔽字设置为已知值(通常为0)。
2)调用fork函数,使父进程exit,可以实现(a):父进程可以让shell认为该命令是执行完毕了。(b)守护进程调用库函数创建文件,可以将屏蔽字设置为一个更强的限制性的值,(如007)因为库函数可能不允许调用者通过一个显式的函数参数来设置权限。
3)调用seisid创建一个新会话。
4)将当前的工作目录改为根目录,从父进程处继承过来的当前工作目录可能在一个挂载文件系统中。因为守护进程通常在系统再引导之前是一直存在的。
5)关闭不再需要的文件描述符,这使守护进程不再持有从父进程继承而来的任何文件描述符。可以用open_max函数或getrlimit函数来判定最高文件描述符值,并关闭直到该值的所有的文件描述符。
6)某些守护进程打开/dev/null使具有文件描述符0、1和2,这样任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果。因为守护进程并不与终端设备相关联所以输出无处显示,,也无从从交互式用户那里接受输入。
daemon函数
nochdir:当此参数为0时,会更改创建出的danmon的执行目录为根目录,否则(非0)时保持当前执行目录不变。
noclose:当次函数为0时,会将标准输入(0),标准输出(1),标准错误(2)重定向到/dev/null,否则保持原有标准输入(0),标准输出(1),标准错误(2)不变。
代码实现:
#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(void)
9 {
10 int fd0;
11 pid_t pid;
12 struct sigaction sa;
13 umask(0);//1、调用umask将文件模式创建屏蔽字设置为0
14 //2、调用fork,父进程退出(exit),如果该守护进程是作为一条简单的shell命令进行启动的
15 //那么父进程终止使得shell认为该命令已经执行完毕
16 //保证子进程不是一个进程组的组长进程
17 if((pid=fork()<0)){
18 perror("fork");
19 }else if(pid>0){
20 exit(0);//终止父进程
21 }
22 setsid();//3、创建一个新的会话
23 sa.sa_handler=SIG_IGN;//4、忽略SIGCHID信号
24 sigemptyset(&sa.sa_mask);
25 sa.sa_flags=0;
26
27 if(sigaction(SIGCHLD,&sa,NULL)<0){//注册子进程退出忽略信号,不要等待
28 return ;
29 }
30 if(chdir("/")<0){//将当前目录更改为根目录
31 printf("child dir error\n");
32 return ;
33 }
34 //关闭文件描述符,或重定向到/dev/null
35 close(0);
36 fd0=open("/dev/null",O_RDWR);
37 dup2(fd0,1);
38 dup2(fd0,2);
39 }
40
41
42 int main()
43 {
44 mydaemon();
45 while(1){
46 sleep(1);
47 }
48 }
49
~
~
这样我们就成功的创建出来了守护进程。