本片博客会粘贴部分代码,想要了解更多代码信息,可访问 小编的GitHub关于本篇的代码
进程组/作业/会话的关系
进程组
- 进程组是一个或多个进程的集合,进程组也有进程组唯一标识(进程组ID就是进程组长的进程ID)。同一个进程组中的进程通常和同一个作业相关联,接受同一终端的各种信号。
- 进程组长可以创建和销毁一个进程,但是只要进程组中有一个进程存在,那么这个进程就存在,与进程组长是否存在无关。
为了看看这个组长ID我写了下面的代码,这个代码唯一的功能是创建了一个子进程,然后一直等。
#include<stdio.h>
int main()
{
int pid=fork();
while(1)
{
sleep(1);
}
return 0;
}
运行上面段程序,我们打开另外一个终端。查看这个a.out
进程信息ps axj|grep a.out
a | x | j |
---|---|---|
all | — | job |
所有进程 | 有无终端控制的进程都要列出 | 与作业相关的进程 |
PPID | PID | PGID | SID | TTY | TPGID | STAT | UID | COMMAND |
---|---|---|---|---|---|---|---|---|
父进程ID | 进程ID | 进程组ID | 会话ID | 终端名称 | 会话首进程ID | 进程状态 | 用户ID | 命令 |
- 进程ID=进程的主线程ID
- 进程组ID=进程组长ID
当杀掉进程组长,进程组依然存在,子进程依然存在。但是如果ctrl+c发送中断信号,前台进程组就会退出。
作业
-
Shell分前后台控制的是作业(或者进程组),Shell可以运行一个前台作业(或者前台进程组),可以有多个后台作业(或者进程组)。
-
一般情况下bash(shell)是终端的前台作业,当运行了一个命令,那么这个命令将成为前台作业,bash将成为后台作业,当命令执行完毕之后,bash将成为前台作业。
-
作业是一个或多个进程的集合。 作业和进程组的区别:前台进程创建的子进程不属于作业,但是属于进程组。
同样是刚刚的程序,在终端1当杀死父进程之后,整个前台作业就挂了,但是这个进程组还存在,因为进程组中的子进程还在,而子进程不属于会话。当前台会话挂了之后,子进程就成了后台进程,可以看到终端0的Shell进程被提到前台。
前后台进程组区分及切换方式
-
在终端输入的所有命令和数据都会给前台进程组。
-
前台进程组在进程状态之后会有
+
,后台进程组则没有。在可执行程序后加&
,就可以让这个进程组在后台运行,如果是已经运行起来的前台进程组,ctrl+z就可以让这个在前台运行的进程组去后台。
-
jobs查看作业,
fg
+作业号就可以将后台进程组放到前台。bg+作业号,就可以将前台进程组放到后台运行。
会话
- 成功登陆到终端,就开始了一个会话,其实这个会话可以有(也可以没有)一个会话首进程,这个首进程是shell(bash),这个会话首进程也称为控制进程。 会话ID=会话首进程ID。
- 一个会话=一个终端控制进程(会话首进程,即shell)+一个前台进程组+任意个后台进程组
守护进程(精灵进程)
守护进程概念和创建方式
特殊的孤儿进程----->建立新的会话----->关闭描述符------->重新设置当前工作路径------>(设置文件的默认创建权限掩码------->)成功逆袭成为守护进程
1. 后台运行
2. 脱离与终端的关系,脱离原会话,不受原会话和终端的影响
- daemon函数
int daemon(int nochdir, int noclose);
The daemon() function is for programs wishing to detach themselves from the
controlling terminal and run in the background as system daemons.
nochdir:若其为0时,即可将工作目录修改为根目录。
noclose:若其为空时,输入、输出、以及错误输出重定向到/dev/null.
子进程脱离原会话,称为守护进程
- setsid函数
功能:使用其创建一个新的Session,并且成为Session Leader。
setsid() creates a new session if the calling process is not a process group leader.
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include <fcntl.h>
void daemon_t()
{
int pid=-1;
pid=fork();
if(pid<0)exit(-1);
else if(pid>0)exit(1);
//孤儿子进程,非进程组长,setsid创建新会话
if(setsid()<0)exit(-1);
//脱离了原会话,成为守护进程,关闭描述符
chdir("/");//改变当前工作路径,自己成家
umask(0);
int fd=open("/dev/null",O_RDWR);//将文件描述符重定向到黑洞文件
if(fd<0)exit(-1);
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
// close(0);
// close(1);
// close(2);
}
int main()
{
daemon_t();
while(1)
{
sleep(1);
}
return 0;
}
控制进程TPGID=-1,没有控制终端进程,这个4759进程就成了守护进程了,它进程ID、进程组ID、和会话ID都是4759。
为什么守护进程应该使用孙子进程?
-
这里有一个假定,父进程生成守护进程后,还有自己的事要做,它的人生意义并不只是为了生成守护进程。这样,如果父进程fork一次创建了一个守护进程,然后继续做其它事时阻塞了,这时守护进程一直在运行,父进程却没有正常退出。如果守护进程因为正常或非正常原因退出了,就会变成ZOMBIE进程。
-
如果fork两次呢?父进程先fork出一个儿子进程,儿子进程再fork出孙子进程做为守护进程,然后儿子进程立刻退出,守护进程被init进程接管,这样无论父进程做什么事,无论怎么被阻塞,都与守护进程无关了。所以,fork两次的守护进程很安全,避免了僵尸进程出现的可能性。
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>
void mydaemon(void)
{
umask(0);//调用umask将文件模式创建屏蔽字设置为0.
pid_t id=fork();//调用fork,创建子进程
if(id<0){
perror("fork()");
}else if(id>0){
//father
exit(0);
}
setsid();//set new session//调用setsid函数创建一个会话
signal(SIGCHLD,SIG_IGN);//忽略SIGCHLD信号,子进程退出时不再给父进程发信号。
pid_t id1=fork();
if(id1<0){
perror("fork()");
}else if(id1>0){
//father
exit(0);
}
if(chdir("/")<0){
//将当前工作目录更改为根目录:
printf("child dir error\n");
return;
}
//关闭不需要的文件描述符,或者重定向到/dev/null中
close(0);
int fd0;
fd0=open("dev/null",O_RDWR);
dup2(fd0,1);
dup2(fd0,2);
}
int main()
{
mydaemon();
while(1){
sleep(1);
}
return 0;
}
-
第一次fork:是为了调用setsid函数,将子进程自成一个独立会话,成为一个进程组,且不受控制终端的控制。
-
第二次fork:由于当前的子进程为独立会话且为会话组长,独立的进程组,不受控制终端的控制且可打开控制终端,则需再进行一次fork,之后会话sid与id不同,其便不可以打开控制终端。