进程通信
100P-进程间通信常见方式
IPC(InterProcess Communication)进程间通信
进程间通信的常用方式,特征:
管道:简单
信号:开销小
mmap映射:非血缘关系进程间
socket(本地套接字):稳定
101P-管道的特质
管道:
实现原理: 内核借助环形队列机制,使用内核缓冲区实现。
特质; 1. 伪文件
2. 管道中的数据只能一次读取。
3. 数据在管道中,只能单向流动。
局限性:1. 自己写,不能自己读。
2. 数据不可以反复读。
3. 半双工通信。
4. 血缘关系进程间可用。
102P-管道的基本用法
pipe函数: 创建,并打开管道。
int pipe(int fd[2]);
参数: fd[0]: 读端。
fd[1]: 写端。
返回值: 成功: 0
失败: -1 errno
管道通信原理:
一个管道通信的示例,父进程往管道里写,子进程从管道读,然后打印读取的内容:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int ret;
int fd[2];
pid_t pid;
char *str = "hello pipe\n";
char buf[1024];
ret = pipe(fd);
if (ret == -1)
sys_err("pipe error");
pid = fork();
if (pid > 0) {
close(fd[0]); // 关闭读段
//sleep(3);
write(fd[1], str, strlen(str));
close(fd[1]);
} else if (pid == 0) {
close(fd[1]); // 子进程关闭写段
ret = read(fd[0], buf, sizeof(buf));
printf("child read ret = %d\n", ret);
write(STDOUT_FILENO, buf, ret);
close(fd[0]);
}
return 0;
}
编译运行,结果如下:
要是不想让终端提示和输出混杂在一起,就在父进程写入内容之后sleep一秒钟。
103P-管道读写行为
管道的读写行为:
读管道:
1. 管道有数据,read返回实际读到的字节数。
2. 管道无数据: 1)无写端,read返回0 (类似读到文件尾)
2)有写端,read阻塞等待。
写管道:
1. 无读端, 异常终止。 (SIGPIPE导致的)
2. 有读端: 1) 管道已满, 阻塞等待
2) 管道未满, 返回写出的字节个数。
104P-父子进程通信练习分析
练习:使用管道实现父子进程间通信,完成:ls | wc -l 假定父进程实现ls,子进程实现wc
ls命令正常会将结果集写到stdout,但现在会写入管道写端
wc -l命令正常应该从stdin读取数据,但此时会从管道的读端读。
要用到 pipe dup2 exec
105P-总结
gdb调试:
设置父进程调试路径:set follow-fork-mode parent (默认)
设置子进程调试路径:set follow-fork-mode child
exec函数族:
使进程执行某一程序。成功无返回值,失败返回 -1
int execlp(const char *file, const char *arg, ...); 借助 PATH 环境变量找寻待执行程序
参1: 程序名
参2: argv0
参3: argv1
...: argvN
哨兵:NULL
int execl(const char *path, const char *arg, ...); 自己指定待执行程序路径。
int execvp();
ps ajx --> pid ppid gid sid
孤儿进程:
父进程先于子进终止,子进程沦为“孤儿进程”,会被 init 进程领养。
僵尸进程:
子进程终止,父进程尚未对子进程进行回收,在此期间,子进程为“僵尸进程”。 kill 对其无效。
wait函数: 回收子进程退出资源, 阻塞回收任意一个。
pid_t wait(int *status)
参数:(传出) 回收进程的状态。
返回值:成功: 回收进程的pid
失败: -1, errno
函数作用1: 阻塞等待子进程退出
函数作用2: 清理子进程残留在内核的 pcb 资源
函数作用3: 通过传出参数,得到子进程结束状态
获取子进程正常终止值:
WIFEXITED(status) --》 为真 --》调用 WEXITSTATUS(status) --》 得到 子进程 退出值。
获取导致子进程异常终止信号:
WIFSIGNALED(status) --》 为真 --》调用 WTERMSIG(status) --》 得到 导致子进程异常终止的信号编号。
waitpid函数: 指定某一个进程进行回收。可以设置非阻塞。 waitpid(-1, &status, 0) == wait(&status);
pid_t waitpid(pid_t pid, int *status, int options)
参数:
pid:指定回收某一个子进程pid
> 0: 待回收的子进程pid
-1:任意子进程
0:同组的子进程。
status:(传出) 回收进程的状态。
options:WNOHANG 指定回收方式为,非阻塞。
返回值:
> 0 : 表成功回收的子进程 pid
0 : 函数调用时, 参3 指定了WNOHANG, 并且,没有子进程结束。
-1: 失败。errno
总结:
wait、waitpid 一次调用,回收一个子进程。
想回收多个。while
===========================
进程间通信的常用方式,特征:
管道:简单
信号:开销小
mmap映射:非血缘关系进程间
socket(本地套接字):稳定
管道:
实现原理: 内核借助环形队列机制,使用内核缓冲区实现。
特质; 1. 伪文件
2. 管道中的数据只能一次读取。
3. 数据在管道中,只能单向流动。
局限性:1. 自己写,不能自己读。
2. 数据不可以反复读。
3. 半双工通信。
4. 血缘关系进程间可用。
pipe函数: 创建,并打开管道。
int pipe(int fd[2]);
参数: fd[0]: 读端。
fd[1]: 写端。
返回值: 成功: 0
失败: -1 errno
管道的读写行为:
读管道:
1. 管道有数据,read返回实际读到的字节数。
2. 管道无数据: 1)无写端,read返回0 (类似读到文件尾)
2)有写端,read阻塞等待。
写管道:
1. 无读端, 异常终止。 (SIGPIPE导致的)
2. 有读端: 1) 管道已满, 阻塞等待
2) 管道未满, 返回写出的字节个数。
106P-复习
普通文件,目录,软链接,这三个要占磁盘空间
管道,套接字,字符设备,块设备,不占磁盘空间,伪文件
107P-父子进程lswc-l
练习:使用管道实现父子进程间通信,完成:ls | wc -l 假定父进程实现ls,子进程实现wc
ls命令正常会将结果集写到stdout,但现在会写入管道写端
wc -l命令正常应该从stdin读取数据,但此时会从管道的读端读。
要用到 pipe dup2 exec
代码如下,还是蛮简单的:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void sys_err(const char *str)
- {
- perror(str);
- exit(1);
- }
- int main(int argc, char *argv[])
- {
- int fd[2];
- int ret;
- pid_t pid;
- ret = pipe(fd); // 父进程先创建一个管道,持有管道的读端和写端
- if (ret == -1) {
- sys_err("pipe error");
- }
- pid = fork(); // 子进程同样持有管道的读和写端
- if (pid == -1) {
- sys_err("fork error");
- }
- else if (pid > 0) { // 父进程 读, 关闭写端
- close(fd[1]);
- dup2(fd[0], STDIN_FILENO); // 重定向 stdin 到 管道的 读端
- execlp("wc", "wc", "-l", NULL); // 执行 wc -l 程序
- sys_err("exclp wc error");
- }
- else if (pid == 0) {
- close(fd[0]);
- dup2(fd[1], STDOUT_FILENO); // 重定向 stdout 到 管道写端
- execlp("ls", "ls", NULL); // 子进程执行 ls 命令
- sys_err("exclp ls error");
- }
- return 0;
- }
编译运行,结果如下:
直接执行命令,如下:
其实代码和题目要求有出入,但是,问题不大,就这样吧。
108P-兄弟进程间通信
练习题:兄弟进程间通信
兄:ls
弟:wc -l
父:等待回收子进程
要求,使用循环创建N个子进程模型创建兄弟进程,使用循环因子i标识,注意管道读写行为
测试:
是否允许,一个pipe有一个写端多个读端 可
是否允许,一个pipe有多个写端一个读端 可
课后作业,统计当前系统中进程ID大于10000的进程个数
代码如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <sys/wait.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void sys_err(const char *str)
- {
- perror(str);
- exit(1);
- }
- int main(int argc, char *argv[])
- {
- int fd[2];
- int ret, i;
- pid_t pid;
- ret = pipe(fd);
- if (ret == -1) {
- sys_err("pipe error");
- }
- for(i = 0; i < 2; i++) { // 表达式2 出口,仅限父进程使用
- pid = fork();
- if (pid == -1) {
- sys_err("fork error");
- }
- if (pid == 0) // 子进程,出口
- break;
- }
- if (i == 2) { // 父进程 . 不参与管道使用.
- close(fd[0]); // 关闭管道的 读端/写端.
- close(fd[1]);
- wait(NULL); // 回收子进程
- wait(NULL);
- } else if (i == 0) { // xiong
- close(fd[0]);
- dup2(fd[1], STDOUT_FILENO); // 重定向stdout
- execlp("ls", "ls", NULL);
- sys_err("exclp ls error");
- } else if (i == 1) { //弟弟
- close(fd[1]);
- dup2(fd[0], STDIN_FILENO); // 重定向 stdin
- execlp("wc", "wc", "-l", NULL);
- sys_err("exclp wc error");
- }
- return 0;
- }
编译运行,结果如下:
这个代码需要注意一点,父进程不使用管道,所以一定要关闭父进程的管道,保证数据单向流动。
109P-多个读写端操作管道和管道缓冲区大小
下面是一个父进程读,俩子进程写的例子,也就是一个读端多个写端。需要调控写入顺序才行。
- #include <stdio.h>
- #include <unistd.h>
- #include <sys/wait.h>
- #include <string.h>
- #include <stdlib.h>
- int main(void)
- {
- pid_t pid;
- int fd[2], i, n;
- char buf[1024];
- int ret = pipe(fd);
- if(ret == -1){
- perror("pipe error");
- exit(1);
- }
- for(i = 0; i < 2; i++){
- if((pid = fork()) == 0)
- break;
- else if(pid == -1){
- perror("pipe error");
- exit(1);
- }
- }
- if (i == 0) {
- close(fd[0]);
- write(fd[1], "1.hello\n", strlen("1.hello\n"));
- } else if(i == 1) {
- close(fd[0]);
- write(fd[1], "2.world\n", strlen("2.world\n"));
- } else {
- close(fd[1]); //父进程关闭写端,留读端读取数据
- sleep(1);
- n = read(fd[0], buf, 1024); //从管道中读数据
- write(STDOUT_FILENO, buf, n);
- for(i = 0; i < 2; i++) //两个儿子wait两次
- wait(NULL);
- }
- return 0;
- }
编译运行,结果如下:
这个例子需要注意,父进程必须等一下,不然可能俩子进程只写了一个,父进程就读完跑路了。
管道大小,默认4096
110P-命名管道fifo的创建和原理图
管道的优劣:
优点:简单,相比信号,套接字实现进程通信,简单很多
缺点:1.只能单向通信,双向通信需建立两个管道
2.只能用于有血缘关系的进程间通信。该问题后来使用fifo命名管道解决。
fifo管道:可以用于无血缘关系的进程间通信。
命名管道: mkfifo
无血缘关系进程间通信:
读端,open fifo O_RDONLY
写端,open fifo O_WRONLY
fifo操作起来像文件
下面的代码创建一个fifo:
编译运行,结果如下:
如图,管道就通过程序创建出来了。
111P-fifo实现非血缘关系进程间通信
下面这个例子,一个写fifo,一个读fifo,操作起来就像文件一样的:
编译执行,如图:
下面测试多个写管道,一个读管道,就是多开两个fifo.w,就一个fifo.r,这是可以的,懒得做了,就这样吧。
测试一个写端多个读端的时候,由于数据一旦被读走就没了,所以多个读端的并集才是写端的写入数据。
112P-文件用于进程间通信
文件实现进程间通信:
打开的文件是内核中的一块缓冲区。多个无血缘关系的进程,可以同时访问该文件。
文件通信这个,有没有血缘关系都行,只是有血缘关系的进程对于同一个文件,使用的同一个文件描述符,没有血缘关系的进程,对同一个文件使用的文件描述符可能不同。这些都不是问题,打开的是同一个文件就行。
113P-mmap函数原型
存储映射I/O(Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是从缓冲区中取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不使用read和write函数的情况下,使地址指针完成I/O操作。
使用这种方法,首先应该通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 创建共享内存映射
参数:
addr: 指定映射区的首地址。通常传NULL,表示让系统自动分配
length:共享内存映射区的大小。(<= 文件的实际大小)
prot: 共享内存映射区的读写属性。PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags: 标注共享内存的共享属性。MAP_SHARED、MAP_PRIVATE
fd: 用于创建共享内存映射区的那个文件的 文件描述符。
offset:默认0,表示映射文件全部。偏移位置。需是 4k 的整数倍。
返回值:
成功:映射区的首地址。
失败:MAP_FAILED (void*(-1)), errno
flags里面的shared意思是修改会反映到磁盘上
private表示修改不反映到磁盘上
int munmap(void *addr, size_t length); 释放映射区。
addr:mmap 的返回值
length:大小
114P-复习
115P-mmap建立映射区
下面这个示例代码,使用mmap创建一个映射区(共享内存),并往映射区里写入内容:
编译运行,如下所示:
116P-mmap使用注意事项1
使用注意事项:
1. 用于创建映射区的文件大小为 0,实际指定非0大小创建映射区,出 “总线错误”。
2. 用于创建映射区的文件大小为 0,实际制定0大小创建映射区, 出 “无效参数”。
3. 用于创建映射区的文件读写属性为,只读。映射区属性为 读、写。 出 “无效参数”。
4. 创建映射区,需要read权限。当访问权限指定为 “共享”MAP_SHARED时, mmap的读写权限,应该 <=文件的open权限。 只写不行。
5. 文件描述符fd,在mmap创建映射区完成即可关闭。后续访问文件,用 地址访问。
6. offset 必须是 4096的整数倍。(MMU 映射的最小单位 4k )
7. 对申请的映射区内存,不能越界访问。
8. munmap用于释放的 地址,必须是mmap申请返回的地址。
9. 映射区访问权限为 “私有”MAP_PRIVATE, 对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上。
10. 映射区访问权限为 “私有”MAP_PRIVATE, 只需要open文件时,有读权限,用于创建映射区即可。
117P-mmap使用注意事项2
都写在116P了,看上面就行。
mmap函数的保险调用方式:
1. fd = open("文件名", O_RDWR);
- mmap(NULL, 有效文件大小, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
118P-mmap总结
- 创建映射区的过程中,隐含着一次对映射文件的读操作
- 当MAP_SHARED时,要求:映射区的权限应该<=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制
- 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭
- 特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!!mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。如,400字节大小的文件,在简历映射区时,offset4096字节,则会报出总线错误
- munmap传入的地址一定是mmap返回的地址。坚决杜绝指针++操作
- 文件偏移量必须为4K的整数倍
- mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
119P-父子进程间mmap通信
父子进程使用 mmap 进程间通信:
父进程 先 创建映射区。 open( O_RDWR) mmap( MAP_SHARED );
指定 MAP_SHARED 权限
fork() 创建子进程。
一个进程读, 另外一个进程写。
下面这段代码,父子进程mmap通信,共享内存是一个int变量:
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <fcntl.h>
- #include <sys/mman.h>
- #include <sys/wait.h>
- int var = 100;
- int main(void)
- {
- int *p;
- pid_t pid;
- int fd;
- fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
- if(fd < 0){
- perror("open error");
- exit(1);
- }
- ftruncate(fd, 4);
- p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
- //p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
- if(p == MAP_FAILED){ //注意:不是p == NULL
- perror("mmap error");
- exit(1);
- }
- close(fd); //映射区建立完毕,即可关闭文件
- pid = fork(); //创建子进程
- if(pid == 0){
- *p = 7000; // 写共享内存
- var = 1000;
- printf("child, *p = %d, var = %d\n", *p, var);
- } else {
- sleep(1);
- printf("parent, *p = %d, var = %d\n", *p, var); // 读共享内存
- wait(NULL);
- int ret = munmap(p, 4); //释放映射区
- if (ret == -1) {
- perror("munmap error");
- exit(1);
- }
- }
- return 0;
- }
编译运行,如下所示:
如图,子进程修改p的值,也反映到了父进程上,这是因为共享内存定义为shared的。
如果将共享内存定义为private,运行结果如下:
120P-无血缘关系进程间mmap通信
无血缘关系进程间 mmap 通信: 【会写】
两个进程 打开同一个文件,创建映射区。
指定flags 为 MAP_SHARED。
一个进程写入,另外一个进程读出。
【注意】:无血缘关系进程间通信。mmap:数据可以重复读取。
fifo:数据只能一次读取。
下面是两个无血缘关系的通信代码,先是写进程:
- #include <stdio.h>
- #include <sys/stat.h>
- #include <sys/types.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <sys/mman.h>
- #include <string.h>
- struct STU {
- int id;
- char name[20];
- char sex;
- };
- void sys_err(char *str)
- {
- perror(str);
- exit(1);
- }
- int main(int argc, char *argv[])
- {
- int fd;
- struct STU student = {10, "xiaoming", 'm'};
- char *mm;
- if (argc < 2) {
- printf("./a.out file_shared\n");
- exit(-1);
- }
- fd = open(argv[1], O_RDWR | O_CREAT, 0664);
- ftruncate(fd, sizeof(student));
- mm = mmap(NULL, sizeof(student), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
- if (mm == MAP_FAILED)
- sys_err("mmap");
- close(fd);
- while (1) {
- memcpy(mm, &student, sizeof(student));
- student.id++;
- sleep(1);
- }
- munmap(mm, sizeof(student));
- return 0;
- }
然后是读进程:
- #include <stdio.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <sys/mman.h>
- #include <string.h>
- struct STU {
- int id;
- char name[20];
- char sex;
- };
- void sys_err(char *str)
- {
- perror(str);
- exit(-1);
- }
- int main(int argc, char *argv[])
- {
- int fd;
- struct STU student;
- struct STU *mm;
- if (argc < 2) {
- printf("./a.out file_shared\n");
- exit(-1);
- }
- fd = open(argv[1], O_RDONLY);
- if (fd == -1)
- sys_err("open error");
- mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
- if (mm == MAP_FAILED)
- sys_err("mmap error");
- close(fd);
- while (1) {
- printf("id=%d\tname=%s\t%c\n", mm->id, mm->name, mm->sex);
- sleep(2);
- }
- munmap(mm, sizeof(student));
- return 0;
- }
编译并运行,结果如下:
如图,一读一写,问题不大。
多个写端一个读端也没问题,打开多个写进程即可,完事儿读进程会读到所有写进程写入的内容。
这里要注意一个,内容被读走之后不会消失,所以如果读进程的读取时间间隔短,它会读到很多重复内容,就是因为写进程没来得及写入新内容。
121P-mmap总结
无血缘关系进程间 mmap 通信: 【会写】
两个进程 打开同一个文件,创建映射区。
指定flags 为 MAP_SHARED。
一个进程写入,另外一个进程读出。
【注意】:无血缘关系进程间通信。mmap:数据可以重复读取。
fifo:数据只能一次读取。
122P-mmap匿名映射区
匿名映射:只能用于 血缘关系进程间通信。
p = (int *)mmap(NULL, 40, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
123P-总结
pipe管道: 用于有血缘关系的进程间通信。 ps aux | grep ls | wc -l
父子进程间通信:
兄弟进程间通信:
fifo管道:可以用于无血缘关系的进程间通信。
命名管道: mkfifo
无血缘关系进程间通信:
读端,open fifo O_RDONLY
写端,open fifo O_WRONLY
文件实现进程间通信:
打开的文件是内核中的一块缓冲区。多个无血缘关系的进程,可以同时访问该文件。
共享内存映射:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 创建共享内存映射
参数:
addr: 指定映射区的首地址。通常传NULL,表示让系统自动分配
length:共享内存映射区的大小。(<= 文件的实际大小)
prot: 共享内存映射区的读写属性。PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags: 标注共享内存的共享属性。MAP_SHARED、MAP_PRIVATE
fd: 用于创建共享内存映射区的那个文件的 文件描述符。
offset:默认0,表示映射文件全部。偏移位置。需是 4k 的整数倍。
返回值:
成功:映射区的首地址。
失败:MAP_FAILED (void*(-1)), errno
int munmap(void *addr, size_t length); 释放映射区。
addr:mmap 的返回值
length:大小
使用注意事项:
1. 用于创建映射区的文件大小为 0,实际指定非0大小创建映射区,出 “总线错误”。
2. 用于创建映射区的文件大小为 0,实际制定0大小创建映射区, 出 “无效参数”。
3. 用于创建映射区的文件读写属性为,只读。映射区属性为 读、写。 出 “无效参数”。
4. 创建映射区,需要read权限。当访问权限指定为 “共享”MAP_SHARED是, mmap的读写权限,应该 <=文件的open权限。 只写不行。
5. 文件描述符fd,在mmap创建映射区完成即可关闭。后续访问文件,用 地址访问。
6. offset 必须是 4096的整数倍。(MMU 映射的最小单位 4k )
7. 对申请的映射区内存,不能越界访问。
8. munmap用于释放的 地址,必须是mmap申请返回的地址。
9. 映射区访问权限为 “私有”MAP_PRIVATE, 对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上。
10. 映射区访问权限为 “私有”MAP_PRIVATE, 只需要open文件时,有读权限,用于创建映射区即可。
mmap函数的保险调用方式:
1. fd = open("文件名", O_RDWR);
2. mmap(NULL, 有效文件大小, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
父子进程使用 mmap 进程间通信:
父进程 先 创建映射区。 open( O_RDWR) mmap( MAP_SHARED );
指定 MAP_SHARED 权限
fork() 创建子进程。
一个进程读, 另外一个进程写。
无血缘关系进程间 mmap 通信: 【会写】
两个进程 打开同一个文件,创建映射区。
指定flags 为 MAP_SHARED。
一个进程写入,另外一个进程读出。
【注意】:无血缘关系进程间通信。mmap:数据可以重复读取。
fifo:数据只能一次读取。
匿名映射:只能用于 血缘关系进程间通信。
p = (int *)mmap(NULL, 40, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
124P-复习
125P-信号的概念和机制
信号共性:
简单、不能携带大量信息、满足条件才发送。
信号的特质:
信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。
所有信号的产生及处理全部都是由【内核】完成的。
126P-与信号相关的概念
信号相关的概念:
产生信号:
1. 按键产生
2. 系统调用产生
3. 软件条件产生
4. 硬件异常产生
5. 命令产生
概念:
未决:产生与递达之间状态。
递达:产生并且送达到进程。直接被内核处理掉。
信号处理方式: 执行默认处理动作、忽略、捕捉(自定义)
阻塞信号集(信号屏蔽字): 本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。
未决信号集:本质:位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生,但尚未被处理。
127P-信号屏蔽字和未决信号集
概念:
未决:产生与递达之间状态。
递达:产生并且送达到进程。直接被内核处理掉。
信号处理方式: 执行默认处理动作、忽略、捕捉(自定义)
阻塞信号集(信号屏蔽字): 本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。
未决信号集:本质:位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生,但尚未被处理。
128P-信号四要素和常规信号一览
kill -l 查看当前系统中常规信号
信号4要素:
信号使用之前,应先确定其4要素,而后再用!!!
编号、名称、对应事件、默认处理动作。
129P-kill函数和kill命令
kill命令 和 kill函数:
int kill(pid_t pid, int signum)
参数:
pid: > 0:发送信号给指定进程
= 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。
< -1: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员。
= -1:发送信号给,有权限发送的所有进程。
signum:待发送的信号
返回值:
成功: 0
失败: -1 errno
小例子,子进程发送信号kill父进程:
编译运行,结果如下:
这里子进程不发送kill信号,发其他信号也行,比如段错误什么的。
kill -9 -groupname 杀一个进程组
130P-alarm函数
alarm 函数:使用自然计时法。
定时发送SIGALRM给当前进程。
unsigned int alarm(unsigned int seconds);
seconds:定时秒数
返回值:上次定时剩余时间。
无错误现象。
alarm(0); 取消闹钟。
time 命令 : 查看程序执行时间。 实际时间 = 用户时间 + 内核时间 + 等待时间。 --》 优化瓶颈 IO
小例子就不写了,使用alarm函数计时,打印变量i的值。
131P-setitimer函数
setitimer函数:
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
参数:
which: ITIMER_REAL: 采用自然计时。 ——> SIGALRM
ITIMER_VIRTUAL: 采用用户空间计时 ---> SIGVTALRM
ITIMER_PROF: 采用内核+用户空间计时 ---> SIGPROF
new_value:定时秒数
类型:struct itimerval {
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
}it_interval;---> 周期定时秒数
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
}it_value; ---> 第一次定时秒数
};
old_value:传出参数,上次定时剩余时间。
e.g.
struct itimerval new_t;
struct itimerval old_t;
new_t.it_interval.tv_sec = 0;
new_t.it_interval.tv_usec = 0;
new_t.it_value.tv_sec = 1;
new_t.it_value.tv_usec = 0;
int ret = setitimer(&new_t, &old_t); 定时1秒
返回值:
成功: 0
失败: -1 errno
小例子,使用setitimer定时,向屏幕打印信息:
编译运行,结果如下:
第一次信息打印是两秒间隔,之后都是5秒间隔打印一次。可以理解为第一次是有个定时器,什么时候触发打印,之后就是间隔时间。
132P-午后回顾
133P-信号集操作函数
信号集操作函数:
sigset_t set; 自定义信号集。
sigemptyset(sigset_t *set); 清空信号集
sigfillset(sigset_t *set); 全部置1
sigaddset(sigset_t *set, int signum); 将一个信号添加到集合中
sigdelset(sigset_t *set, int signum); 将一个信号从集合中移除
sigismember(const sigset_t *set,int signum); 判断一个信号是否在集合中。 在--》1, 不在--》0
设置信号屏蔽字和解除屏蔽:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how: SIG_BLOCK: 设置阻塞
SIG_UNBLOCK: 取消阻塞
SIG_SETMASK: 用自定义set替换mask。
set: 自定义set
oldset:旧有的 mask。
查看未决信号集:
int sigpending(sigset_t *set);
set: 传出的 未决信号集。
134P-信号操作函数使用原理分析
135P-信号集操作函数练习
信号列表:
其中9号和19号信号比较特殊,只能执行默认动作,不能忽略捕捉,不能设置阻塞。
下面这个小例子,利用自定义集合,来设置信号阻塞,我们输入被设置阻塞的信号,可以看到未决信号集发生变化:
- #include <stdio.h>
- #include <signal.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void sys_err(const char *str)
- {
- perror(str);
- exit(1);
- }
- void print_set(sigset_t *set)
- {
- int i;
- for (i = 1; i<32; i++) {
- if (sigismember(set, i))
- putchar('1');
- else
- putchar('0');
- }
- printf("\n");
- }
- int main(int argc, char *argv[])
- {
- sigset_t set, oldset, pedset;
- int ret = 0;
- sigemptyset(&set);
- sigaddset(&set, SIGINT);
- sigaddset(&set, SIGQUIT);
- sigaddset(&set, SIGBUS);
- sigaddset(&set, SIGKILL);
- ret = sigprocmask(SIG_BLOCK, &set, &oldset);
- if (ret == -1)
- sys_err("sigprocmask error");
- while (1) {
- ret = sigpending(&pedset);
- print_set(&pedset);
- sleep(1);
- }
- return 0;
- }
编译运行,如下图所示:
可以看到,在输入Ctrl+C之后,进程捕捉到信号,但由于设置阻塞,没有处理,未决信号集对应位置变为1.
136P-signal实现信号捕捉
参数:
signum :待捕捉信号
handler:捕捉信号后的操纵函数
返回值:
信号捕捉特性:
1. 捕捉函数执行期间,信号屏蔽字 由 mask --> sa_mask , 捕捉函数执行结束。 恢复回mask
2. 捕捉函数执行期间,本信号自动被屏蔽(sa_flgs = 0).
3. 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次!
一个信号捕捉的小例子:
编译运行,如下:
137P-sigaction实现信号捕捉
sigaction也是注册一个信号捕捉函数
下面的小例子,使用sigaction捕捉两个信号:
- #include <stdio.h>
- #include <signal.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void sys_err(const char *str)
- {
- perror(str);
- exit(1);
- }
- void sig_catch(int signo) // 回调函数
- {
- if (signo == SIGINT) {
- printf("catch you!! %d\n", signo);
- sleep(10);
- }
- else if (signo == SIGQUIT)
- printf("-----------catch you!! %d\n", signo);
- return ;
- }
- int main(int argc, char *argv[])
- {
- struct sigaction act, oldact;
- act.sa_handler = sig_catch; // set callback function name 设置回调函数
- sigemptyset(&(act.sa_mask)); // set mask when sig_catch working. 清空sa_mask屏蔽字, 只在sig_catch工作时有效
- //sigaddset(&act.sa_mask, SIGQUIT);
- act.sa_flags = 0; // usually use. 默认值
- int ret = sigaction(SIGINT, &act, &oldact); //注册信号捕捉函数
- if (ret == -1)
- sys_err("sigaction error");
- ret = sigaction(SIGQUIT, &act, &oldact); //注册信号捕捉函数
- while (1);
- return 0;
- }
编译运行,如下:
如图,两个信号都捕捉到了,并且输出了对应字符串。
138P-信号捕捉的特性
信号捕捉特性:
1. 捕捉函数执行期间,信号屏蔽字 由 mask --> sa_mask , 捕捉函数执行结束。 恢复回mask
2. 捕捉函数执行期间,本信号自动被屏蔽(sa_flgs = 0).
- 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次!
139P-内核实现信号捕捉简析
140P-借助信号捕捉回收子进程
SIGCHLD的产生条件:
子进程终止时
子进程接收到SIGSTOP
子进程处于停止态,接收到SIGCONT后唤醒时
下面是一个例子,创建子进程,并使用信号回收:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <signal.h>
- #include <sys/wait.h>
- #include <errno.h>
- #include <pthread.h>
- void sys_err(const char *str)
- {
- perror(str);
- exit(1);
- }
- void catch_child(int signo)
- {
- pid_t wpid;
- wpid = wait(NULL);
- printf("-----------catch child id %d\n", wpid);
- return ;
- }
- int main(int argc, char *argv[])
- {
- pid_t pid;
- //阻塞
- int i;
- for (i = 0; i < 5; i++)
- if ((pid = fork()) == 0) // 创建多个子进程
- break;
- if (5 == i) {
- struct sigaction act;
- act.sa_handler = catch_child; // 设置回调函数
- sigemptyset(&act.sa_mask); // 设置捕捉函数执行期间屏蔽字
- act.sa_flags = 0; // 设置默认属性, 本信号自动屏蔽
- sigaction(SIGCHLD, &act, NULL); // 注册信号捕捉函数
- //解除阻塞
- printf("I'm parent, pid = %d\n", getpid());
- while (1);
- } else {
- printf("I'm child pid = %d\n", getpid());
- return i;
- }
- return 0;
- }
编译运行,如下图所示:
如图,只回收到1个子进程,多次执行,会发现回收到的子进程数量不是固定的。
原因分析:
问题出在,一次回调只回收一个子进程这里。同时出现多个子进程死亡时,其中一个子进程死亡信号被捕捉,父进程去处理这个信号,此时其他子进程死亡信号发送过来,由于相同信号的不排队原则,就只会回收累积信号中的一个子进程。
修改代码,回调函数中添加循环,一次回调可以回收多个子进程:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <signal.h>
- #include <sys/wait.h>
- #include <errno.h>
- #include <pthread.h>
- void sys_err(const char *str)
- {
- perror(str);
- exit(1);
- }
- void catch_child(int signo)
- {
- pid_t wpid;
- while((wpid = wait(NULL)) != -1){
- printf("-----------catch child id %d\n", wpid);
- }
- return ;
- }
- int main(int argc, char *argv[])
- {
- pid_t pid;
- //阻塞
- int i;
- for (i = 0; i < 5; i++)
- if ((pid = fork()) == 0) // 创建多个子进程
- break;
- if (5 == i) {
- struct sigaction act;
- act.sa_handler = catch_child; // 设置回调函数
- sigemptyset(&act.sa_mask); // 设置捕捉函数执行期间屏蔽字
- act.sa_flags = 0; // 设置默认属性, 本信号自动屏蔽
- sigaction(SIGCHLD, &act, NULL); // 注册信号捕捉函数
- //解除阻塞
- printf("I'm parent, pid = %d\n", getpid());
- while (1);
- } else {
- printf("I'm child pid = %d\n", getpid());
- return i;
- }
- return 0;
- }
编译运行,结果如下:
这下就是回收了所有子进程,很强势。
还有一个问题需要注意,这里有可能父进程还没注册完捕捉函数,子进程就死亡了,解决这个问题的方法,首先是让子进程sleep,但这个不太科学。在fork之前注册也行,但这个也不是很科学。
最科学的方法是在int i之前设置屏蔽,等父进程注册完捕捉函数再解除屏蔽。这样即使子进程先死亡了,信号也因为被屏蔽而无法到达父进程。解除屏蔽过后,父进程就能处理累积起来的信号了。
141P-慢速系统调用中断
慢速系统调用:
可能会使进程永久阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期),也可以设定系统调用是否重启。如read, write, pause…
142P-总结
信号共性:
简单、不能携带大量信息、满足条件才发送。
信号的特质:
信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。
所有信号的产生及处理全部都是由【内核】完成的。
信号相关的概念:
产生信号:
1. 按键产生
2. 系统调用产生
3. 软件条件产生
4. 硬件异常产生
5. 命令产生
概念:
未决:产生与递达之间状态。
递达:产生并且送达到进程。直接被内核处理掉。
信号处理方式: 执行默认处理动作、忽略、捕捉(自定义)
阻塞信号集(信号屏蔽字): 本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。
未决信号集:本质:位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生,但尚未被处理。
信号4要素:
信号使用之前,应先确定其4要素,而后再用!!!
编号、名称、对应事件、默认处理动作。
kill命令 和 kill函数:
int kill(pid_t pid, int signum)
参数:
pid: > 0:发送信号给指定进程
= 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。
< -1: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员。
= -1:发送信号给,有权限发送的所有进程。
signum:待发送的信号
返回值:
成功: 0
失败: -1 errno
alarm 函数:使用自然计时法。
定时发送SIGALRM给当前进程。
unsigned int alarm(unsigned int seconds);
seconds:定时秒数
返回值:上次定时剩余时间。
无错误现象。
alarm(0); 取消闹钟。
time 命令 : 查看程序执行时间。 实际时间 = 用户时间 + 内核时间 + 等待时间。 --》 优化瓶颈 IO
setitimer函数:
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
参数:
which: ITIMER_REAL: 采用自然计时。 ——> SIGALRM
ITIMER_VIRTUAL: 采用用户空间计时 ---> SIGVTALRM
ITIMER_PROF: 采用内核+用户空间计时 ---> SIGPROF
new_value:定时秒数
类型:struct itimerval {
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
}it_interval;---> 周期定时秒数
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
}it_value; ---> 第一次定时秒数
};
old_value:传出参数,上次定时剩余时间。
e.g.
struct itimerval new_t;
struct itimerval old_t;
new_t.it_interval.tv_sec = 0;
new_t.it_interval.tv_usec = 0;
new_t.it_value.tv_sec = 1;
new_t.it_value.tv_usec = 0;
int ret = setitimer(&new_t, &old_t); 定时1秒
返回值:
成功: 0
失败: -1 errno
其他几个发信号函数:
int raise(int sig);
void abort(void);
信号集操作函数:
sigset_t set; 自定义信号集。
sigemptyset(sigset_t *set); 清空信号集
sigfillset(sigset_t *set); 全部置1
sigaddset(sigset_t *set, int signum); 将一个信号添加到集合中
sigdelset(sigset_t *set, int signum); 将一个信号从集合中移除
sigismember(const sigset_t *set,int signum); 判断一个信号是否在集合中。 在--》1, 不在--》0
设置信号屏蔽字和解除屏蔽:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how: SIG_BLOCK: 设置阻塞
SIG_UNBLOCK: 取消阻塞
SIG_SETMASK: 用自定义set替换mask。
set: 自定义set
oldset:旧有的 mask。
查看未决信号集:
int sigpending(sigset_t *set);
set: 传出的 未决信号集。
【信号捕捉】:
signal();
【sigaction();】 重点!!!
信号捕捉特性:
1. 捕捉函数执行期间,信号屏蔽字 由 mask --> sa_mask , 捕捉函数执行结束。 恢复回mask
2. 捕捉函数执行期间,本信号自动被屏蔽(sa_flgs = 0).
3. 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次!
借助信号完成 子进程回收。
143P-复习子进程借助信号回收
信号回收子进程的完整代码,如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <signal.h>
- #include <sys/wait.h>
- #include <errno.h>
- #include <pthread.h>
- void sys_err(const char *str)
- {
- perror(str);
- exit(1);
- }
- void catch_child(int signo) // 有子进程终止,发送SGCHLD信号时,该函数会被内核回调
- {
- pid_t wpid;
- int status;
- //while((wpid = wait(NULL)) != -1) {
- while((wpid = waitpid(-1, &status, 0)) != -1) { // 循环回收,防止僵尸进程出现.
- if (WIFEXITED(status))
- printf("---------------catch child id %d, ret=%d\n", wpid, WEXITSTATUS(status));
- }
- return ;
- }
- int main(int argc, char *argv[])
- {
- pid_t pid;
- //阻塞
- sigset_t set;
- sigemptyset(&set);
- sigaddset(&set, SIGCHLD);
- sigprocmask(SIG_BLOCK, &set, NULL);
- int i;
- for (i = 0; i < 15; i++)
- if ((pid = fork()) == 0) // 创建多个子进程
- break;
- if (15 == i) {
- struct sigaction act;
- act.sa_handler = catch_child; // 设置回调函数
- sigemptyset(&act.sa_mask); // 设置捕捉函数执行期间屏蔽字
- act.sa_flags = 0; // 设置默认属性, 本信号自动屏蔽
- sigaction(SIGCHLD, &act, NULL); // 注册信号捕捉函数
- //解除阻塞
- sigprocmask(SIG_UNBLOCK, &set, NULL);
- printf("I'm parent, pid = %d\n", getpid());
- while (1);
- } else {
- printf("I'm child pid = %d\n", getpid());
- return i;
- }
- return 0;
- }
编译运行,结果如下:
所有子进程都回收了,就很强势。
144P-会话
会话:多个进程组的集合
创建会话的6点注意事项:
- 调用进程不能是进程组组长,该进程变成新会话首进程
- 该进程成为一个新进程组的组长进程
- 需要root权限(ubuntu不需要)
- 新会话丢弃原有的控制终端,该会话没有控制终端
- 该调用进程是组长进程,则出错返回
- 建立新会话时,先调用fork,父进程终止,子进程调用setsid
getsid函数:
pid_t getsid(pid_t pid) 获取当前进程的会话id
成功返回调用进程会话ID,失败返回-1,设置error
setsid函数:
pid_t setsid(void) 创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID
成功返回调用进程的会话ID,失败返回-1,设置error
145P-守护进程创建步骤分析
守护进程:
daemon进程。通常运行于操作系统后台,脱离控制终端。一般不与用户直接交互。周期性的等待某个事件发生或周期性执行某一动作。
不受用户登录注销影响。通常采用以d结尾的命名方式。
创建守护进程,最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader
守护进程创建步骤:
1. fork子进程,让父进程终止。
2. 子进程调用 setsid() 创建新会话
3. 通常根据需要,改变工作目录位置 chdir(), 防止目录被卸载。
4. 通常根据需要,重设umask文件权限掩码,影响新文件的创建权限。 022 -- 755 0345 --- 432 r---wx-w- 422
5. 通常根据需要,关闭/重定向 文件描述符
6. 守护进程 业务逻辑。while()
146P-守护进程创建
下面这个例子,创建一个守护进程:
- #include <stdio.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void sys_err(const char *str)
- {
- perror(str);
- exit(1);
- }
- int main(int argc, char *argv[])
- {
- pid_t pid;
- int ret, fd;
- pid = fork();
- if (pid > 0) // 父进程终止
- exit(0);
- pid = setsid(); //创建新会话
- if (pid == -1)
- sys_err("setsid error");
- ret = chdir("/home/zhcode/Code/code146"); // 改变工作目录位置
- if (ret == -1)
- sys_err("chdir error");
- umask(0022); // 改变文件访问权限掩码
- close(STDIN_FILENO); // 关闭文件描述符 0
- fd = open("/dev/null", O_RDWR); // fd --> 0
- if (fd == -1)
- sys_err("open error");
- dup2(fd, STDOUT_FILENO); // 重定向 stdout和stderr
- dup2(fd, STDERR_FILENO);
- while (1); // 模拟 守护进程业务.
- return 0;
- }
编译运行,结果如下:
查看进程列表,如下:
这个daemon进程就不会受到用户登录注销影响。
要想终止,就必须用kill命令