1. 进程组
每个进程除了有一个进程 ID之外,还属于一个进程组。进程组是一个或多个进程的集合。
通常,它们与同一作业相关联,可以接收来自同一终端的各种信号。 每个进程组有一个唯
一的进程组ID。每个进程组都可以有一个组长进程。组长进程的标识是,其进程组 ID等于
其进程ID。
组长进程可以创建一个进程组,创建该组中的进程,然后终止。 只要在某个进程组中一个
进程存在,则该进程组就存在,这与其组长进程是否终止无关。
2.作业
Shell分前后台来控制的不是进程而是 作业(Job)或者进程组( Process Group)。一个
前台作业可以由多个进程组成,一个后台也可以由多个进程组成, Shell可以运行一个前台
作业和任意多个后台作业,这称为作业控制。
作业与进程组的区别:如果作业中的某个进程又创建了子进程,则子进程不属于作业。
一旦作业运行结束, Shell就把自己提到前台,如果原来的前台进程还存在(如果这个子进
程还没终止),它自动变为后台进程组。
例子:
#include<stdio.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t id ; id = fork(); if (id == 0){ while (1) { printf("child\n"); sleep(4); } }else { printf("father exit\n"); exit(0); } return 0; }
运行:
[bozi@localhost test_20160731]$ gcc -o work work.c [bozi@localhost test_20160731]$ ./work father exit [bozi@localhost test_20160731]$ child // 父进程结束 bash调到前台 又变成后台 因为子进程还在运行 child child child
3.会话
会话(Session)是一个或多个进程组的集合。
一个会话可以有一个控制终端。 这通常是登陆到其上的终端设备(在终端登陆情况下)或
伪终端设备(在网络登陆情况下)。 建立与控制终端连接的会话首进程被称为控制进程。
一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个
会话中,应该包括控制进程(会话首进程),一个前台进程组和任意后台进程组。
1 $ proc1 | proc2 &
2 $ proc3 | proc4 | proc5
其中proc1 与proc2 属于同一个后台进程组, proc3, proc4和 proc5属于同一个前台进程组,
Shell本身属于一个单独的进程组。这些进程组的控制终端相同,它们同属于一个会话,当
用户在控制终端输入特殊的控制键(如 Ctrl+C,产生 SIGINT,Ctrk+\,产生 SIGQUIT,Ctrl+Z,
产生SIGTSTP),内核发送相应的信号给前台进程组中的所有进程。
一般在进程组中 第一个进程就是组长进程
组长进程的组ID与组长的PID是一样的
其他进程的组ID与组长进程的组ID一样
进程组中只要还有一个进程存在,组就存在,与组长存在不存在没有关系。
在命令行上运行一个二进制代码,就运行起来一个作业,一个进程完成一个任务,而作业是多个进程合作完成的复杂的任务
Ctrl+c 只能终止 前台进程组
[root@localhost test_20160731]# sleep 200|more|sleep 300
用管道 创建 进程组
[bozi@localhost ~]$ ps axj|grep -ER 'sleep|more'
3828 7640 7640 3440 pts/0 7640 S+ 0 0:00 sleep 200
3828 7641 7640 3440 pts/0 7640 S+ 0 0:00 more
3828 7642 7640 3440 pts/0 7640 S+ 0 0:00 sleep 300
7646 7684 7683 7646 pts/2 7683 S+ 500 0:00 grep -ER sleep|more
[bozi@localhost ~]$ ps axj|head
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
[bozi@localhost ~]$ ps axj|grep -ER 'sleep|more|bash'
3438 3440 3440 3440 pts/0 7728 Ss 500 0:00 bash 最开始的用户组 进程ID 组ID SID(会话ID) 相同
3820 3828 3828 3440 pts/0 7728 S 0 0:00 bash 【特殊 su产生的】
3438 7118 7118 7118 pts/1 7118 Ss+ 500 0:00 bash
3438 7646 7646 7646 pts/2 7731 Ss 500 0:00 bash
3438 7668 7668 7668 pts/3 7668 Ss+ 500 0:00 bash
3828 7728 7728 3440 pts/0 7728 S+ 0 0:00 sleep 200
3828 7729 7728 3440 pts/0 7728 S+ 0 0:00 more
3828 7730 7728 3440 pts/0 7728 S+ 0 0:00 sleep 300
7646 7732 7731 7646 pts/2 7731 S+ 500 0:00 grep -ER sleep|more|bash
只是普通用户 没有su的情况
8194 9587 9587 9587 pts/1 9685 Ss 500 0:00 bash
9587 9685 9685 9587 pts/1 9685 S+ 500 0:00 sleep 200
9587 9686 9685 9587 pts/1 9685 S+ 500 0:00 more
9587 9687 9685 9587 pts/1 9685 S+ 500 0:00 sleep 300
+表示是前台进程
bash 创建 sleep more sleep 后 自己转为后台 因为只允许一个前台作业(子进程不属于作业)
终端就是文件
查看终端对应的设备
代码:
#include<stdio.h> #include <unistd.h> int main() { printf("fd:%d -> %s \n", 0 , ttyname(0)); printf("fd:%d -> %s \n", 1 , ttyname(1)); printf("fd:%d -> %s \n", 2 , ttyname(2)); return 0; }
运行:
[bozi@localhost test_20160731]$ ./printf_ttyname fd:0 -> /dev/pts/0 fd:1 -> /dev/pts/0 fd:2 -> /dev/pts/0
重新打开一个终端
[bozi@localhost test_20160731]$ ./printf_ttyname fd:0 -> /dev/pts/1 fd:1 -> /dev/pts/1 fd:2 -> /dev/pts/1
[bozi@localhost ~]$ ls -al /dev/pts/
总用量 0
drwxr-xr-x. 2 root root 0 7月 31 10:09 .
drwxr-xr-x. 19 root root 3840 7月 31 02:10 ..
crw--w----. 1 bozi tty 136, 0 7月 31 10:16 0
crw--w----. 1 bozi tty 136, 1 7月 31 08:53 1
crw--w----. 1 bozi tty 136, 2 7月 31 09:57 2
c---------. 1 root root 5, 2 7月 31 10:09 ptmx
0 1 2 就是终端文件 再新建一个终端则 文件 多一个 如3 4 5 。。。
向别的终端显示 就是先别的终端文件 写信息
[bozi@localhost ce]$ ./a.out&
[1] 7984
[1]是 作业号 7984是作业的最后一个进程 的进程ID
如 进程组
[bozi@localhost test_20160731]$ sleep 100 | more&
[1] 9544
[bozi@localhost test_20160731]$
[1]+ Stopped sleep 100 | more
[bozi@localhost test_20160731]$ ps axj|grep -ER 'sleep |more'
8196 9543 9543 8196 pts/0 9547 T 500 0:00 sleep 100
8196 9544 9543 8196 pts/0 9547 T 500 0:00 more
9870就是进程组的最后一个进程的进程ID
fg 1(1是作业号) 后台变成前台
Ctrl+z 前台变成后台(但是stop状态)
bg 1 让后台的作业运行起来
jobs 查看有多少作业
jobs
[1]+ Done ./a.out
不允许后台程序 从标准输入读数据 但可以允许向标准输出 写数据
如 cat 从标准输入读入 然后输出
[bozi@localhost test_20160731]$ cat sd sd sd sd aa aa cc cc aa aa
将cat转到后台
[bozi@localhost test_20160731]$ cat& [1] 8234 [bozi@localhost test_20160731]$ [1]+ Stopped cat
不允许读数据的 一直是Stop状态
[bozi@localhost test_20160731]$ jobs [1]+ Stopped cat [bozi@localhost test_20160731]$ bg 1 [1]+ cat & [bozi@localhost test_20160731]$ jobs [1]+ Stopped cat
解释进程接受信号 是在适合的时候
[bozi@localhost test_20160731]$ ps PID TTY TIME CMD 8196 pts/0 00:00:00 bash 8301 pts/0 00:00:00 ps [bozi@localhost test_20160731]$ cat& [1] 8308 [bozi@localhost test_20160731]$ [1]+ Stopped cat [bozi@localhost test_20160731]$ kill -15 8308 [bozi@localhost test_20160731]$ ps PID TTY TIME CMD 8196 pts/0 00:00:00 bash 8308 pts/0 00:00:00 cat 8310 pts/0 00:00:00 ps [bozi@localhost test_20160731]$ fg 1 cat 已终止
现象:后台stop 发送kill -15 不终止 但转到前台时 就马上终止了
原因:cat转到后台后 变成stop状态 不能接受信号 只有前台进程可以接受标准输入(如kill -15)
所以在cat从后台转到前台时 他就可以接受标准输入 他的状态由后台转到前台状态 但是这个变化是由在内核中修改进程PID完成的 这样就有了从内核到用户态接受信号的时机 这样就可以接受信号了
特殊的是 9号状态 直接在内核区就终止了
[bozi@localhost test_20160731]$ cat& [1] 8336 [bozi@localhost test_20160731]$ [1]+ Stopped cat [bozi@localhost test_20160731]$ kill -9 8336 [bozi@localhost test_20160731]$ [1]+ 已杀死 cat
原因:9号信号特殊 操作系统在内核检测到 就马上终止进程 不用转前台
守护进程
进程名多以d结尾 如httpd
以【】包裹的进程名为内核进程
TTY 为? 表示守护进程 是没有终端的 与控制终端脱离关联
[bozi@localhost test_20160731]$ ps aux | grep -ER 'd]$' USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 2 0.0 0.0 0 0 ? S 07:16 0:00 [kthreadd] root 17 0.0 0.0 0 0 ? S 07:16 0:00 [kacpid] root 22 0.0 0.0 0 0 ? S 07:16 0:00 [ksuspend_usbd] root 23 0.0 0.0 0 0 ? S 07:16 0:00 [khubd] root 24 0.0 0.0 0 0 ? S 07:16 0:00 [kseriod] root 28 0.0 0.0 0 0 ? S 07:16 0:00 [khungtaskd] root 30 0.0 0.0 0 0 ? SN 07:16 0:00 [ksmd] root 38 0.0 0.0 0 0 ? S 07:16 0:00 [pciehpd] root 40 0.0 0.0 0 0 ? S 07:16 0:00 [kpsmoused] root 72 0.0 0.0 0 0 ? S 07:16 0:00 [kstriped] root 1038 0.0 0.0 0 0 ? S 07:17 0:00 [kauditd]
守护进程 是 后台进程的一种 只是与终端无关的 自成用户组 自成会话 与终端脱离关联 父进程为1
创建守护进程:
方法多,但核心是利用setsid函数
函数:
pid_t setsid(void);
DESCRIPTION
setsid() creates a new session
练习代码【自己的一个精灵程序】:
#include<stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include<signal.h> #include <fcntl.h> void my_daemon() { umask(0);// 设置文件掩码为0 pid_t id = fork(); if (id == 0) { // child setsid();// 设置 新会话 chdir("/");// 更换 目录 close(0); close(1); close(2); signal(SIGCHLD,SIG_IGN);// 注册子进程退出忽略信号 } else { sleep(14); exit(0);// 终止父进程 } close(0);// 关闭标准输入 int fd0 = open("dev/null", O_RDWR);// 重定向所有标准输出、 错误到/dev/null dup2(fd0, 1); dup2(fd0, 2); } int main() { my_daemon(); while(1); }
运行:
[bozi@localhost test_20160731]$ ./my_daemon [bozi@localhost ~]$ ps axj|grep -ER 'my_' 8196 10168 10168 8196 pts/0 10168 S+ 500 0:00 ./my_daemon // 父进程 10168 10169 10169 10169 ? -1 Rs 500 0:04 ./my_daemon // 子进程 10132 10173 10172 10132 pts/1 10172 S+ 500 0:00 grep -ER my_ [bozi@localhost ~]$ ps axj|grep -ER 'my_' 8196 10168 10168 8196 pts/0 10168 S+ 500 0:00 ./my_daemon 10168 10169 10169 10169 ? -1 Rs 500 0:05 ./my_daemon 10132 10175 10174 10132 pts/1 10174 S+ 500 0:00 grep -ER my_ [bozi@localhost ~]$ ps axj|grep -ER 'my_' 8196 10168 10168 8196 pts/0 10168 S+ 500 0:00 ./my_daemon 10168 10169 10169 10169 ? -1 Rs 500 0:11 ./my_daemon 10132 10178 10177 10132 pts/1 10177 S+ 500 0:00 grep -ER my_ [bozi@localhost ~]$ ps axj|grep -ER 'my_' 1 10169 10169 10169 ? -1 Rs 500 0:15 ./my_daemon 10132 10180 10179 10132 pts/1 10179 S+ 500 0:00 grep -ER my_ [bozi@localhost ~]$ ps axj|head -n1 PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
改进:{创建两次子进程 让孙子进程当守护进程}
#include<stdio.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <sys/stat.h> void creat_daemon() { int i; int fd0; pid_t pid; struct sigaction sa; umask(0);// 设置文件 掩码为0 if ((pid = fork()) < 0) { } else if (pid != 0) { exit(0); // 终止父进程 } setsid(); // 设置新会话 sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGCHLD, &sa, NULL) < 0)//注册子进程退出忽略信号 { return; } if ((pid = fork()) < 0) { printf("fork error\n"); return ; } else if (pid != 0) // 终止父进程 保证子进程 不是话首 进程 从而保证此后不会再和其他终端关联 { exit(0); } if (chdir("/") < 0) { // 更改工作目录到根 printf("child dir error\n"); return ; } close(0); // 关闭标准输入 fd0 = open("/dev/null", O_RDWR);// 重定向标准输出 标准错误到/dev/null dup2(fd0, 1); dup2(fd0, 2); } int main() { creat_daemon(); while (1) { sleep(1); } }
运行监视:
[bozi@localhost dev]$ ps -axj | grep -ER 'my_'
Warning: bad syntax, perhaps a bogus '-'? See /usr/share/doc/procps-3.2.8/FAQ
1 10942 10941 10941 ? -1 S 500 0:00 ./my_daemon_op
10132 10945 10944 10132 pts/1 10944 S+ 500 0:00 grep -ER my_
[bozi@localhost dev]$ ps -axj | head -n1
Warning: bad syntax, perhaps a bogus '-'? See /usr/share/doc/procps-3.2.8/FAQ
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
【fork两次的原因】
fork第二次主要目的是。防止进程再次打开一个控制终端。因为打开一个控制终端的前台条件是该进程必须是会话组长。再fork一次,孙子进程ID != sid(sid是进程子进程的sid)。所以也无法打开新的控制终端。
终止子进程 保证孙子进程 不是话首 进程 从而保证此后不会再和其他终端关联
直接调用系统函数:
int daemon(int nochdir, int noclose);
代码:
#include<stdio.h> #include <unistd.h> int main() { daemon(0,0); while (1); }
运行:
[bozi@localhost dev]$ ps -axj | grep -ER 'my_' Warning: bad syntax, perhaps a bogus '-'? See /usr/share/doc/procps-3.2.8/FAQ 1 11147 11147 11147 ? -1 Rs 500 0:03 ./my_daemon_sys
一些缩写
PID = 进程ID (由内核根据延迟重用算法生成)
PPID = 父进程ID(只能由内核修改)
PGID = 进程组ID(子进程、父进程都能修改)
SID = 会话ID(进程自身可以修改,但有限制,详见下文)
TPGID= 控制终端进程组ID(由控制终端修改,用于指示当前前台进程组)