父进程与子进程的进程地址空间彼此独立,要从父进程中用子进程的东西,需要在父进程和子进程之间架一座桥梁
IPC(InterProcess Communication) 进程间通信
进程间通信是借助内核空间的缓冲区(一般是4096大小)
进程间通信的常用方式,特征:
管道:简单,但只能应用到有血缘关系之间的进程(父子进程、兄弟进程、叔侄进程)
信号:开销小、速度快
mmap映射(共享内存映射):非血缘关系进程间
socket(本地套接字):稳定但实现复杂度较高
浅析进程间通信的几种方式(含实例源码) - 知乎 (zhihu.com)
管道
原理与特点
作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道
也可使用mkfifo 创建管道
普通文件、目录、软连接这三种是真正在用磁盘空间的,字符设备、块设备、管道、套接字称为伪文件(不占用磁盘空间),只占用内存(缓冲区)
实现原理: 内核借助环形队列机制,使用内核缓冲区实现。
特质;
1. 伪文件(内核缓冲区)
2、由两个文件描述符引用,一个表示读端,一个表示写端,规定数据从管道的写端流入,从读端流出
3.管道中的数据只能一次读取。
局限性:
1. 自己写,不能自己读。
2.数据不可以反复读。
3.半双工通信。(数据不能双向流动,还有单工通信(固定发送方与接收方)、全双工通信(打电话(都能相互说话)))
4.血缘关系进程间可用。
函数
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;
}
管道读写行为
管道的读写行为:
读管道:
1. 管道有数据,read返回实际读到的字节数。
2. 管道无数据: 1)无写端,read返回0 (类似读到文件尾)
2)有写端,read阻塞等待。
写管道:
1. 无读端, 异常终止。 (SIGPIPE导致的)
2. 有读端: 1) 管道已满, 阻塞等待(基本不会出现,因为就算4096的缓冲区满了,也会扩容)
2) 管道未满, 返回写出的字节个数
父子进程间使用管道通信实现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;
}
fifo命名管道
fifo管道:可以用于无血缘关系的进程间通信。
命名管道: mkfifo
无血缘关系进程间通信:
读端,open fifo O_RDONLY
写端,open fifo O_WRONLY
fifo操作起来像文件
mmap(共享内存映射)
传统读写文件的过程:将文件内容通过read函数读入到用户空间,用户空间再通过write函数写到文件中。
在使用read/write函数读写文件时,需要借助页缓存(page cache),其在内核空间。如下图:
使用read/write函数读写文件时实际操作的是页缓存。
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 (4096)的整数倍。
返回值:
成功:映射区的首地址。
失败:MAP_FAILED (void*(-1)), errno
flags里面的shared意思是修改会反映到磁盘上
private表示修改不反映到磁盘上
nt fd = open(filepath, O_RDWR, 0644); // 打开文件
void *addr = mmap(NULL, 8192, PROT_WRITE, MAP_SHARED, fd, 4096); // 对文件进行映射
addr 参数设置为 NULL,表示让操作系统自动选择合适的虚拟内存地址进行映射。
length 参数设置为 8192 表示映射的区域为 2 个内存页的大小(一个内存页的大小为 4 KB)。
prot 参数设置为 PROT_WRITE 表示映射的内存区为可读写。
flags 参数设置为 MAP_SHARED 表示共享映射区。
fd 参数设置打开的文件句柄。
offset 参数设置为 4096 表示从文件的 4096 处开始映射。
int munmap(void *addr, size_t length); 释放映射区。
addr:mmap 的返回值
length:大小
使用注意事项
1. 用于创建映射区的文件大小为 0,实际指定非0大小创建映射区,出 “总线错误”。
2. 用于创建映射区的文件大小为 0,实际指定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通信
父子进程使用 mmap 进程间通信:
父进程先 创建映射区。 open( O_RDWR) mmap( MAP_SHARED );
指定 MAP_SHARED 权限
fork()创建子进程。
一个进程读,另外一个进程写。
#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); //扩展文件大小(新创建的文件大小为0,必须扩展fd文件大小)
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;
}
无血缘关系进程间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;
}
信号
信号共性:
简单、不能携带大量信息、满足条件才发送。
信号的特质:
信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。
所有信号的产生及处理全部都是由【内核】完成的。
产生方式
按键产生:ctrl+c、ctrl+z、ctrl+\
系统调用产生:kill、raise、abort
软件条件产生:定时器alarm
硬件异常产生:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
命令产生:kill命令
概念
未决:产生与递达之间状态。 (阻塞或者屏蔽)
递达:产生并且送达到进程。直接被内核处理掉。
当信号成功产生、并且递达,信号处理方式:执行默认处理动作、忽略、捕捉(自定义)
阻塞信号集(信号屏蔽字):本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。
未决信号集:本质:位图。用来记录信号的处理状态。该信号集中的信号,表示已经产生但尚未被处理。
未决信号集和信号屏蔽字都是位图,未决信号集和信号屏蔽字里面默认为0,例如当按了ctrl-c(2号信号),2号翻转为1,假如未对2号信号设置信号屏蔽字,信号产生后立马递达,内核会马上对这个信号进行处理,在处理后会再将此信号翻转为0;;假如对2号信号设置信号屏蔽字(此时2号信号的未决信号集和信号屏蔽字都是1),此时信号被阻塞,不能递达,一直处于未决态,一直等到信号屏蔽字改为0,从而再立即递达,再内核处理。
常规信号
kill -l 查看当前系统中常规信号
信号4要素:
信号使用之前,应先确定其4要素,而后再用!!!
编号、名称、对应事件、默认处理动作。
1-31是常规信号;34以后都是编写底层(航空、航天等领域)才能用到
Man 7 signal 会列出所用信号
前20:1、2、3、7、8、9、10、11、12、13、14、15、17、19重点
kill函数、alarm函数、setitimer函数
int kill(pid_t pid, int signum)
参数:
pid: > 0:发送信号给指定进程
= 0:发送信号给与调用kill函数的那个进程处于同一进程组的进程。
< -1: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员。
= -1:发送信号给,有权限发送的所有进程。
signum:待发送的信号
返回值:
成功: 0
失败: -1 errno
小例子,子进程发送信号kill父进程:
kill -9 -groupname 杀一个进程组
alarm 函数:使用自然计时法。
定时发送SIGALRM给当前进程。
unsigned int alarm(unsigned int seconds);
seconds:定时秒数
返回值:上次定时剩余时间。
每个进程都有且仅有一个定时器,在3秒后,alarm(4)返回是2,之后5秒由于alarm(4)返回0,最后alarm(0)返回5
alarm(0); 取消闹钟。
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秒间隔打印一次。可以理解为第一次是有个定时器,什么时候触发打印,之后就是间隔时间。
信号集操作函数
未决信号集不提供操作方法,通过操作阻塞信号集来影响未决信号集,最开始信号都是未决的(0);阻塞信号下默认情况下也是非阻塞(0);ctrl+c后2号未决信号变为1,但若让ctrl+c后2号未决信号不变为1就是让其变为阻塞(1)
那如何将阻塞信号设置为1?
使用相应函数
使用信号集操作函数思想:
因为信号屏蔽字是在pcb里,linux内核不想让你直接对其进行操作
所以用户自定义一个集合,学着阻塞信号集弄成二进制位,修改相应的信号就是使用自定义集合与阻塞信号集做与,或者直接替换的操作,用到的一系列函数就是下面的信号集操作函数
信号集操作函数:
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: 传出的 未决信号集。
阻塞信号集基本都是0,因为一般不会无缘无故阻塞某一个信号
Sigset_t set; 定义一个信号集
Sigemptyset(&set); 清空定义的信号集
Sigaddset(&set, SIGINT); 将自定义的信号集中的信号变为1(直接将信号作为输入))
Sigprocmask(SIG_BLOCK, &set,); 将自定义信号集与原有信号集做或操作,改变原有阻塞信号集
按ctrl+c后,未决信号集变为了1,正常是内核发现变为未决信号要马上处理,但处理之前发现mask设置为阻塞,内核不进行处理
Sigpending(&myset),传出未决信号集
利用自定义集合设置信号阻塞
利用自定义集合,来设置信号阻塞,我们输入被设置阻塞的信号,可以看到未决信号集发生变化:
#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.
信号捕捉
1、进程正常运行时,默认PCB中有个信号屏蔽字mask,决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号后,要调用该函数。而该函数可能执行很长时间,在这期间所屏蔽的信号不能由mask来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为mask。
2、xx信号函数执行期间,xx信号自动被屏蔽
3、阻塞的常规信号不支持排队,产生多次只记录一次。
signal和sigaction注册一个信号捕捉函数,然后利用内核来进行信号捕捉,信号捕捉后就去做别的事了
signal不常用,正规使用sigaction
signal函数
其中第二行是一个函数指针,其是用于捕捉信号后的操纵函数
函数指针及其定义和用法,C语言函数指针详解 (biancheng.net)
参数:
signum :待捕捉信号
handler:捕捉信号后的操纵函数(回调函数)
sigaction函数
Sigaction中
signum:待捕捉的信号编号
oldact:保存旧有的对此信号的处理状态
act: 对此信号的处理状态
结构体中:(*sa_sigaction)(int, siginfo_t *, void *),(*sa_restorer)(void)不用管,(*sa_handler)(int)是回调函数
mask一旦有程序了都有, sa_mask只作用于信号捕捉函数执行期间,当信号捕捉函数执行期间,突然来了其他信号,需要将突然来的信号加入到sa_mask这个阻塞屏蔽字中,然后将突然来的信号先进行屏蔽,而sa_flgs就是直接对突然来的信号进行屏蔽(使用sa_flgs,不加入屏蔽阻塞字中也能进行屏蔽)
sa_mask和sa_flgs绝大多数都设置为0。
使用sigaction捕捉两个信号:
函数指针的例子:用于做回调函数, 通俗理解“回调函数”_昂刺鱼人工智能的博客-CSDN博客
#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; // 设置捕捉函数名字 设置回调函数
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;
}
编译运行,如下:
如图,两个信号都捕捉到了,并且输出了对应字符串。
内核实现信号捕捉原理
信号捕捉回收子进程
SIGCHLD的产生条件:
子进程终止时
子进程接收到SIGSTOP
子进程处于停止态,接收到SIGCONT后唤醒时
创建子进程,并使用SIGCHILD信号回收:
#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; //pid_t类型在Linux环境编程中用于定义进程ID
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;
}
编译运行
所有子进程都回收