进程通信概念
进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放。那么释放的资源可能是其他进程需要的,然而进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。
进程通信应用场景
数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信的方式
管道(pipe)
管道实现细节
在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。如下图
匿名管道
仅仅适用于具有亲缘关系的进程间通信,因为匿名管道其它进程根本找不到,因此也就没有办法通信,所以只能通过子进程复制父进程的方法,让子进程能够访问相同的管道,来实现通信。
(管道的操作:io操作—文件描述符)
命名管道
有名字:体现在文件系统可见性,因为其它进程都能看见这个管道文件,因此都能打开可以用于任意(本机)进程间通信
(单向通信)所以一个管道使用的时候就必须确定数据流向,但是不能一创建就确 定,因为我们不确定谁读谁写。 因此,操作系统提供两个描述符来供
使用,一个读一个写,这样的确定方向就是将对应的一段关闭掉就可以,这样操作系统就把方向的控制权交给用户了
接口:pipe(int fd[2])fd[0]— 读 fd[1]—写
信号量(semophore)
信号量并不是用来数据传输的,而是用来进程控制,是解决进程间同步与互斥问题
是一个具有等待队列的计数器
释放资源+1,
获取资源-1,
当计数器的值不大于0,意味着没有资源,想要获取信号量资源(计数器-1)的就需要等待
同步:如果现在没有资源,等待,等待别人释放资源,别人释放资源后会通知等待的人
互斥:一元信号量实现互斥(计数器是0或 1)
消息队列(message queue)
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 它通常在内核中创建一个消息队列,其它的所有进程都可以通过相同的IPC_KEY
打开消息队列
这时候既可以向消息队列中放数据,也可以从中拿数据,但是这样的数据就有可能拿错了,拿到的不是自己的数据,因此消息队列中能够放的数据是有类型的数据块,并且读写的时候只能按消息块来发送/接收。
信号(sinal)
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
它是通过软中断的方式来进行的,信号产生之后第一时间也不是直接处理,而是先存储下来,处理信号
为了通知进程发生了某个事件,因为事件比较紧急,因此会打断当前进程正在执行的工作,然后去准备处理事件,事件处理完毕后进程回到原先运行的位置继续运行
信号的产生->信号的注册->注销信号->信号的处理
Linux下有62种信号,kill -l查看
信号分两类:普通信号(不可靠信号1~31)(非实时信号)
可靠信号(34~64)(实时信号)
信号的产生:
- 1.硬件中断产生 Ctrl+C
- 2.程序异常 SIGFPE SIGSEGV
- 3.软件条件产生
1
2
3
4
5
6
7int kill(pid_t pid,int sig);
kill(getpid(),SIGINT);
int raise();
raise(SIGTERM);
void abort(void);
sigqueue
alarm(3); // 在3秒后向进程发送SIGALRM信号,返回值会取消上一次的定时器,并且返回上一次定时器剩余时间
核心转储(core dump):
保存当前程序运行的数据以及调用栈信息,用于错误原因定位调试。如果程序运行出现错误,可以直接通过core文件来gdb调试(有些错误可能偶然发生) coredump默认关闭:隐私安全/资源占用
信号的注册:
pcb中有一个信号结构体,信号注册/进程发送信号,就是修改这个进程pcb中关于信号的pending位图,将相应的信号为置1;
1 | struct signal |
信号的阻塞:暂时不处理信号(阻止信号的递达),并不是不接收信号,
pcb中还有一个信号位图block,要阻止一个信号就是修改pcb中关于信号的block
位图,将相应的信号位置1,这个位置就像是一个备注说明如果接收到这
个信号暂时不处理
信号未决:这是一种状态,信号从注册成功到信号递达之间
信号的注销:
就是从pending
集合中将即将处理的信号相应位置0(从pcb的pending
集合中移除)
非可靠信号注册就是将相应pending位图置1,然后添加一个sigqueue
结构到链表中,之后如果有相应信号到来,一看位图已经 置1那么就不做任何操作,意味着后来的信号在前一个信号未处理之前不会重复注册,代表丢了!!!
可靠信号就是不管有没有注册都要置1,并且添加结点到链表中,所以不会丢信号。
非可靠注销就是删除链表结点,相应位图置0.
可靠信号删除结点,判断是否有相同信号结点,如果没有则位图置0,如果有则置1;
信号的递达(信号的处理):
- 默认操作——安装操作系统中对信号事件的既定处理方式
- 忽略操作——直接将信号丢掉
自定义处理—–用户自定义事件处理方式
1
2
3
4
5
6
7
8
9
10signal接口
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum:信号的编号
handler:处理方式
SIG_IGN 忽略
SIG_DFL 默认
信号的捕捉流程:
信号并不是立即处理,而是选择一个合适的时机处理,合适的时机就是当前程序从==内核态==切换到==用户态==的时候
程序如何从内核态切换到用户态:发起系统调用,程序异常,中断时
信号是当我们发起系统调用/程序异常/中断当前程序从用户态切换到内核态,去处理这些事情,处理完毕后,要从内核态返回用户态,但是在返回之前会看一下是否有信号需要被处理,如果有,就处理信号(切换到用户态执行信号的自定义处理方式),处理完毕后再次返回内核态,判断如果没有信号要处理了就调用sys_sigreturn
返回用户态(我们程序之前的运行位置)
1 | sigaddset |
struct sigqueue
操作系统如何通知父进程说子进程退出了?
信号:SIGCHLD -17号信号
用户自定义信号:SIGCHLD -17处理方式,相当于提前告诉进程,当接收到这个信号时使用waitpid,这样就不用一直等待
共享内存(shared memory)
是进程间通信速度最快的方式
其它的进程间通信方式,都会涉及到将用户空间的数据拷贝到内核空间(因为公共缓冲区都在内核空间),这是两步操作(拷入和拷出)
而共享内存的原理是多个进程将同一块物理内存映射到自己的虚拟地址空间,以这种方式实现数据共享,操作这个虚拟地址就是操作这个物理内存,相较于其它通信方式,少了两步用户空间和内核空间的拷贝过程,因此速度最快
共享内存操作步骤: 共享内存的生命周期随内核
1.创建/打开一块共享内存
2.将这块共享空间映射到自己的虚拟地址空间
3.各种内存操作
4.解除映射关系
5.删除共享内存
套接字(socket)
套解字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。因此被广泛应用于网络通信中。
进程间通信的实现
匿名管道通信
1 | /* 这是一个匿名管道的demo |
命名管道通信
这是一个命名管道的实现实例,实现两个进程聊天功能1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int main()
{
char *file = "./test.fifo";
umask(0);
if (mkfifo(file, 0664) < 0) {
if (errno == EEXIST) {
printf("fifo exist!!\n");
}else {
perror("mkfifo");
return -1;
}
}
int fd = open(file, O_RDONLY);
if (fd < 0) {
perror("open error");
return -1;
}
printf("open fifo success!!\n");
while(1) {
char buff[1024];
memset(buff, 0x00, 1024);
int ret = read(fd, buff, 1024);
if (ret > 0) {
printf("peer say:%s\n", buff);
}
}
close(fd);
return 0;
}
管道符实现命令
1 | /* 这是一个实现管道符的demo |
内存共享
1 | /* 这是一块共享内存的demo,共享数据 |
信号
1 | /* 这是一个演示signal接口修改信号处理方式的demo |
僵尸进程的避免
1 | /*僵尸进程的避免: |