一. 进程组/作业/会话
1.进程组
每一个进程除了有一个进程ID之外, 还属于一个进程组. 进程是一个或多个进程的集合. 通常, 它们与同一个作业向关联, 可以接收来自同一个终端下的各种命令,信号. 每一个进程组都有唯一的进程组 ID. 每一个进程组都可以有一个组长进程. 组长进程的标识是, 其进程组 ID 等于组长进程 ID. 组长进程可以创建一个进程组, 也可以创建该进程组中的进程, 然后终止.只要该组中有一个进程存在, 则该组就存在, 与该组中的组长进程是否存在没有任何关系
2. 相关接口函数
其中getpgrp 函数用来返回调用进程的进程组 ID, setpid 用来将 pid 进程的进程组 ID 设置为 pgid, 如果两个参数值相等, 则由 pid 指定的进程将会变成进程组组长. 如果 pid 为 0, 则使用调用者的进程 id. 另外, 如果 pgid 为 0, 则由进程 pid 指定的进程 id将会变成进程组 id.一个进程只能为自己的子进程或者为自己设定进程组ID, 但是当这个函数的子进程已经调用了 exec 之后, 父进程就不能再改变子进程的组ID了
在大多数shell下通常是调用 fork 后让父进程调用 setpgid , 同时也让子进程调用 setpgid.
其中 ps 命令常用来显示进程相关信息, 其中选项 a 表示不仅列出当前用户的进程, 也列出其他所有用户进程, x 表示不仅列出有控制终端的进程, 也列出没有控制终端的进程, j 表示列出与作业相关的信息, 同时在上图中可以看出当我们杀死这个进程组中的组长时, 此时该组还是任然存在的(该组中的成员还依旧在). 利用 jobs 命令可以查看后台相关进程
3. 作业
shell分前台和后台运行的不是进程, 而是作业或者进程组. 一个前台作业可以由多个进程构成, 一个后台作业也可以由多个进程组成, shell可以一次运行一个前台作业和任意多个后台作业, 这就叫做作业控制.
作业和进程组的区别: 在作业中某个进程创建了一个子进程, 该子进程属于该进程组, 但是该子进程不属于这个作业.一旦作业结束, shell就把自己提到前台, (子进程还在, 但是子进程不属于改作业), 如果原来的前台进程还在, (如果原来的子进程还没有终止), 它将自动成为一个后台进程. 此时我们就可以理解当我们在前台起一个新作业时, 此时shell无法执行, 那是因为shell被放到了后台, 而当改作业退出的时候, shell就被提到了前台.
#include<stdio.h>
#include<unistd.h>int main()
{pid_t id = fork();if(id == -1){return 1;}else if(id == 0){while(1){printf("child(%d)# I am running\n", getpid());}sleep(2);}else{int i = 5;while(i){printf("parent(%d)# I am going to dead\n", getpid());i--;}sleep(2);}return 0;
}
3. 会话
会话是一个或多个进程组的集合, 一个会话可以有一个控制终端. 这通常是登录到其上的终端设备或者是伪终端设备. 建立与控制终端连接的会话首进程是控制进程. 一个终端下的几个进程组可以被分为一个前台作业以及一个或多个后台作业.所以一个会话中应该包括一个控制进程, 一个前台进程组和多个后台进程组
通过上面的一些解释, 我们可以得知, 一个作业或者一个进程组由多个进程组成, 而一个会话则由一个前台进程组和多个后台进程组构成.
(1)会话相关接口
该函数用于建立一个新的会话, 如果调用函数的进程不是一个进程的组长, 那么就建立新的会话. 此时该进程会变成新的会话首进程, 而此时该进程就会成为新会话中的唯一的一个进程. 而该进程会成为一个新进程组长, 该进程 ID 等于调用该进程的进程 ID. 该进程没有控制端, 如果在调用 setsid 前该进程有一个控制端, 那么原有的这个控制端将会变成一个普通文件不再是控制终端
查看进程的会话编号SID, 当 pid = 0 时, 函数返回调用进程会话首进程进程组 ID
一个作业有多个进程构成, 一个会话由多个作业构成, 其中包括一个前台作业和多个后台作业, 建立一个会话, 就是会建立一个话首进程, 而删除话首进程, 该会话将会退出.
(2)相关命令
1)将一个作业放到后台去执行时, 只需在可执行程序后面加一个 & 即可. 例如 sleep 100 | sleep 200 &
2)查看后台作业: jobs
3) 将一个后台作业由前台提到后台 Ctrl + Z, bg 作业号即可
4. 作业控制相关信号
后台作业是不能读取终端输入的, jobs 命令可以查看后台相关作业. fg 可以将某个作业从后台提到前台, 但是此时如果该作业是停止状态, 则给作业发送一个 SIGCONT 信号使得该作业能够继续运行. bg 将某个停止的后台作业在后台运行, 也需要给作业发送一个 SIGCONT 信号. 此时该作业就可以在后台运行了.后台作业不能从终端读取数据, 但是后台作业可以往终端写数据
二.守护进程
1. 相关概念
守护进程也叫做精灵进程, 是运行在后台的一个特殊进程. 它独立于控制终端, 自成进程组, 自成会话, 并且周期性地执行某些任务或者周期性地处理某些发生的事件. Linux 下大多数的服务器就是用守护进程来实现的.
Linux下有一些进程没有控制终端, 不能直接和用户交互. 其他进程都需要用户登录或运行时创建, 但是守护进程不受用户登录或注销的影响, 它会一直运行.
通过 ps 命令查看系统中的进程, 其中 PPID 是父进程编号, PID 是当前进程编号, PGID 是进程组ID, SID 是会话 ID, TTY 表示终端名称, TPGID 表示会话组ID, STAT 表示进程状态, UID表示用户ID, COMMAND 表示命令字符串可以看到 凡是 TPGID 为 -1 的都是守护进程, COMMAND 用 [] 括起来的都是内核线程, 这些线程都是在内核中创建, 没有用户代码, 没有程序文件以及命令行, 通常以 k 开头. 守护进程一般以 d 结尾的名字
2. 守护进程的创建
#include<unitd.h>
pid_t setsid();
调用成功时返回创建的会话ID, 失败时返回 -1
该函数在调用的时候必须保证调用的进程不能是进程组组长, 为了保证该点, 就先 fork 然后再让子进程去执行 setsid 即可. 其中在创建会话时, 子进程就会自成组长, 即子进程的 id 就会成为其组长的id, 并且该进程也会自成会话, 即会话 ID就是该进程的 ID, 并且如果当前进程有一个会话终端的时候, 它将会失去原有的会话终端, 即原来的会话终端还是打开的, 仍然可以读写, 但是将会变成一个普通文件, 不再是控制终端了.
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<stdlib.h>void mydaemon()
{int fd0;pid_t id;struct sigaction sa;//1. 屏蔽umaskumask(0);id = fork();if(id < 0){perror("fork");exit(1);}//2. fork 父进程退出, 子进程运行if(id > 0){exit(0);}if(setsid() < 0){perror("setsid");exit(1);}//3. 初始化sasigemptyset(&sa.sa_mask);sa.sa_handler = SIG_IGN;sa.sa_flags = 0;//4. 对SIGCHLD信号忽略, 防止产生僵尸进程if(sigaction(SIGCHLD, &sa, NULL) < 0){perror("sigaction");exit(1);}fd0 = open("/dev/null", O_RDWR);close(0);dup2(fd0, 1);dup2(fd0, 2);
}int main()
{mydaemon();while(1){sleep(1);}return 0;
}
关闭当前终端, 打开另外一个终端时, 继续查看, 会发现原来的会话还存在, 于是我们可以得出结论, 守护进程单独成组, 单独成回话, 不受控制终端控制