¶一、进程通信概述
¶1. 进程通信、线程通信
进程通信:在用户空间实现进程通信**不可能**,需要通过 Linux 内核通信
线程通信:可以在用户空间实现,通过全局变量即可
1 |
|
无法通过用户空间全局变量实现进程间通信,上段程序只能运行父进程。
¶2. 进程通信方式
单机模式下:(只有一个 Linux 内核)
- 管道通信:无名管道、有名管道(文件系统中有名)
- 信号通信:信号的发送、信号的接收和信号的处理
- IPC(Inter-Process Communication):共享内存、消息队列和信号灯(量)
多机模式下:(两个 Linux 内核)
- Socket 通信:存在于一个网络中两个进程之间的通信。
¶3. 进程通信的学习思路
进程通信的学习思路:每一种通信方式都是基于文件IO的思想
open
:创建或者打开进程通信对象。函数形式可能不同,可能有多个函数完成write
:向进程通信对象中写入内容。函数形式可能不同。read
:从进程通信对象中读取内容。函数形式可能不同。close
:关闭或删除进程通信对象。函数形式可能不同。
¶二、无名管道
通信原理:
管道文件是一个特殊文件,是有队列来实现的。
¶2.1 无名管道的创建
在文件IO中创建一个文件或打开一个文件是由 open
函数实现的,它不能创建管道文件。只能用 pipe
函数来创建管道。
函数形式: int pipe(int fd[2])
功能:创建管道,为系统调用, unistd.h
参数:就是得到的文件描述符。可见有两个文件描述符,fd[0]
、fd[1]
,管道有一个读端 fd[0]
用来读, fd[1]
用来写。
返回值:成功 0,失败 -1
¶2.2 pipe
函数使用
1 |
|
注意:
- 管道是创建在内存中的,进程结束,空间释放,管道就不再了;
- 管道中的东西,读完了就删除了;
- 如果管道没有东西可读,就会阻塞。
¶2.3 验证读阻塞
1 |
|
¶2.4 验证写阻塞
1 |
|
¶2.5 实现进程通信。
1 |
|
可以实现进程间通信
¶三、有名管道
无名管道的**缺点**:不能实现不是父子进程(亲缘关系)之间的通信。
正由于这无名管道的缺点,对于无名管道进行改进,有名管道。
所谓的有名,即文件系统中存在这个一样文件节点,每一个文件节点都有一个 inode
号,而且这是一个特殊的文件类型: p
管道类型。
创建这个文件节点,不可以通过
open
函数,open
函数只能创建普通文件,不能创建特殊文件(管道-mkfifo
,套接字-socket
,字符设备文件-mknod
,块设备文件-mknod
,符号链接文件-ln -s
,目录文件-mkdir
)管道文件只有
inode
号,不占磁盘块空间,和套接字、字符设备文件、块设备文件一样。普通文件和符号链接文件及目录文件,不仅有inode
号,还占有磁盘块空间mkfifo
: 用来创建管道文件的节点,没有在内核中创建管道。只有通过open
函数打开这个文件时才会在内核空间创建管道。
¶3.1 有名管道的创建
mkfifo
的用法
函数形式: int mkfifo(const char* filename, mode_t mode);
创建管道文件。
参数:管道文件名,权限,创建的文件权限任然和 umask
有关系
返回值:创建成功返回 0,失败返回 -1。
1 |
|
¶3.2 通过管道实现无亲缘关系进程间通信
first.c
1 |
|
second.c
1 |
|
先运行 first
open 管道文件:myfifo
运行 second
,此时两端开始通信,first
先运行,second
后运行
¶四、信号通信
信号通信,其实就是内核向用户空间进程**发送信号,只有内核才能发信号**,用户空间进程不能发信号。
内核可以发送多少种信号?kill -l
: 64 种,常见命令:kill 9 pid
(杀死进程)
信号通信的框架
- 信号的发送(发送信号进程):
kill
raise
alarm
- 信号的接收(接收信号进程):
pause()
sleep
while(1)
- 信号的处理(接收信号进程):
signal
¶4.1 信号的发送(发送信号进程)
¶4.1.1 kill
例子:编写自己的kill函数
创建一个简单的进程 test.c
1 |
|
运行后系统死循环
mykill.c
1 |
|
使用 9 号信号量 SIGKILL
来 kill 掉 ./test
进程
¶4.1.2 raise
raise
: 发信号给自己,等价于 kill(getpid(), sig)
例子:raise 函数
raise.c
1 |
|
raise_2.c
1 |
|
程序运行后,首先子进程输出语句
之后调用 raise
函数进入暂停状态,此时父进程睡眠状态 S
,子进程暂停状态 T
父进程睡眠 8 秒后,父进程进入运行状态 R
,此时子进程仍然是暂停状态 T
稍微修改一下 raise_2.c
1 |
|
程序运行后,首先子进程输出语句
之后调用 raise
函数进入暂停状态,此时父进程睡眠状态 S
,子进程暂停状态 T
父进程睡眠 8 秒后,父进程进入运行状态 R
,此时子进程进入僵尸状态 Z
加上 wait()
子进程被 kill
后不会变成僵尸进程
¶4.1.3 alarm
alarm
与 raise
的比较:
相同点:让内核发送信号给当前进程
不同点:
alarm
只会发送SIGALARM
信号 (14 号信号)alarm
会让内核定时一段时间之后发送信号,raise
会让内核立刻发信号
例子:alarm
alarm.c
1 |
|
运行结果如下所示,alarm
在 9 秒后终止操作
¶4.2 信号的接收(接收信号进程)
接收信号的进程的条件:要想使接收的进程能接收信号,这个进程不能结束
sleep
while(1)
pause
进程状态为S
pause.c
1 |
|
运行程序后,进程处于睡眠状态 S
¶4.3 信号的处理(接收信号进程)
收到信号的进程,应该怎样处理?处理的方式:
(1)进程的默认处理方式(内核为用户进程设置的默认处理方式):
- 忽略
- 终止进程
- 暂停
(2)自定义的处理方式:自定义处理信号的方法告诉内核,这样线程收到了这个信号就会采用自定义的处理方式
signal
函数有两个参数,
第一个参数是一个整形变量(信号值),
第二个参数是一个函数指针,是自定义的处理函数,这个函数的返回值是一个函数指针。
¶4.3.1 自定义处理
signal.c
1 |
|
signal(14, myfun);
表示接收到 14 号信号,进行自定义的一些处理(14号信号 --> alarm)
程序运行,9秒后发送 alarm
信号,期间进程一直运行
接收到 alarm
信号后,处理 myfun
处理 myfun
结束后,重新回到 main
中继续进行剩下的操作
¶4.3.2 SIG_IGN
, SIG_DFL
1 |
|
SIG_IGN
, SIG_DFL
¶4.4 signal 处理进程间通信
1 |
|
父进程死循环打印输出,子进程睡眠 10 s 后发送 10 号 SIGUSER1
信号,父进程接收到信号就会跳入处理自定义函数 myfun
,处理结束后基于死循环打印输出。结果如下:
此时进程状态如下,父进程运行状态( R
),子进程僵尸状态 ( Z
) ,需要父进程进行资源回收处理。
因为 wait()
操作为阻塞操作,不能直接放在处理函数之上,这会导致处理函数不会进行下去。
1 | if (pid > 0) { |
由结果可以知道,父进程开始会处于 阻塞状态 ,只有接收 10 号信号后才会处理 myfun
,之后**等子进程退出**,之后才会处理 printf
操作
因为 exit(0)
发送了 kill SIGCHLD
信号,17 号信号,所以父进程可以接收 17 信号后处理 wait
即可
1 | void myfun1(int signum) { |
这样的话,在子进程结束之后,就不会产生僵尸进程
¶五、IPC 通信
常用的 IPC 相关的 shell
命令
查看 IPC 对象 : ipcs
-m
(共享内存) -q
(消息队列) -s
(信号灯)
删除 IPC 对象:ipcrm
-m
(共享内存) \ -q
(消息队列) \ -s
(信号灯) id
返回值:共享内存,消息队列,信号灯标识符,IPC 的 ID 号
每次用 IPC_PRIVATE
操作时, key
都是一样的,都是 0
¶六、共享内存
¶6.1 创建和打开共享内存
shmget
函数
shmget.c
1 |
|
每次运行一次,创建一个共享内存,下图运行三次后。
使用 ipcrm -m id
删除后两个共享内存
¶6.2 ftok 函数,创建 key 值
char ftok(const char* path, char key)
参数:第一个参数:文件路径和文件名;第二个参数:一个字符
返回值:正确返回一个 key 值,出错返回 -1
IPC_PRIVATE
操作时,共享内存的 key 值都是一样,都是 0, 所以对于非亲缘关系进程使用 ftok
来创建 key 值。只要 key 值是一样的,用户空间的进程通过这个函数打开,则会对内核的同一个 IPC 对象操作。
shmget.c
1 |
|
结果如下:之前用 IPC_PRIVATE
创建的 key = 0,现在用自定义的 key
¶6.3 将共享内存映射到用户空间中
为了方便用户空间对共享内存的操作,使用地址映射的方式。利用 shmat
函数。
void *shmat (int shmid, const void * shmaddr, int shmflg);
参数:
第一个参数:ID号;
第二个参数:映射到的地址,NULL 为系统自动完成的映射;
第三个参数:
shmflg
- **
SHM_RDONLY
**共享内存只读 - 默认 0,表示共享内存可读写
- **
返回值: 成功:映射后的地址;失败:NULL
shmat.c
1 |
|
程序运行后,键盘输入: test 进共享内存,后读数据输出
与管道不同,共享内存中的数据在读取后并没有被删除,两条输出语句的结果如下:
共享内存特点:
- 共享内存创建后,一直存在于内核中,知道被删除或系统关闭;
- 共享内存和管道不一样,读取后,内容仍在其共享内存中。
¶6.4 删除进程里的地址映射
使用 shmdt
函数,删除用户空间的地址映射
int shmdt(const void* shmaddr);
参数:shmaddr
:共享内存映射后的地址(用户空间)
返回值:成功:0; 失败: -1
shmdt.c
1 |
|
共享内存释放后,不能再次使用它,所以 memcpy
函数操作会出现段错误。
¶6.5 shmctl:删除共享内存对象
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
参数:
shmid
:要操作的共享内存标识符cmd
:IPC_STAT
(获取对象属性) – 实现了ipcs - m
IPC_SET
(设置对象属性)IPC_RMID
(删除对象) – 实现了ipcrm -m id
buf
: 指定IPC_STAT/IPC_SET
时可以保存 / 设置属性。
返回值:成功:0; 失败:-1;
shmctl.c
1 |
|
运行程序,键盘输入 test ,结果打印两遍后删除共享内存对象,发现 shmid = 3
的共享内存被删除。
myipcrm.c
1 |
|
自己创建的 ipcrm -m id
操作,成功的删除了 shmid = 0
的共享内存
¶6.6 有亲缘关系的进程(IPC_PRIVATE)
shmctl_2.c
1 |
|
程序运行,父进程写,子进程读取,两个进程状态都是睡眠状态 (S
)
¶6.7 无亲缘关系的进程(key)
server.c
1 |
|
client.c
1 |
|
该程序的关键是:要先通过共享内存交互双方的 pid
;
运行服务器,客户端程序,可以发现双方都可以操作 shmid = 8
的共享内存,服务器端发送多次发送数据,客户端多次接受数据;服务器每次发送完,等待下一次输入;客户端每次接受,等待下一次服务器的消息发送。
结果如下:
¶七、消息队列
链式队列
msqid_ds
内核维护消息队列的结构体,队列的第一个消息指针 msg_first
,最后一个消息指针 msg_last
,消息中有一个成员指针 next
每一个消息中包含有哪些内容:
- Data :数据
- Length:数据的长度
- Type:数据的类型
¶7.1 消息队列的创建和打开
msgget.c
1 |
|
三次运行程序创建了三个消息队列
¶7.2 消息队列控制
msgctl.c
1 |
|
程序运行,先创建一个 msgid = 3
消息队列,之后删除它
¶7.3 消息发送
msgsnd.c
1 |
|
¶7.4 消息接收
msgrcv.c
1 |
|
程序运行,键盘写入消息,然后在读取消息打印输出
如果两次读取则会发生错误,程序或阻塞,因为读取后消息被删除,读函数阻塞等待
¶7.5 单向读写操作
write.c
1 |
|
read.c
1 |
|
程序运行后,write.c
写消息,read.c
读消息 (该例子与上一章最后一个例子相似)
¶7.6 双向通信
server.c
1 |
|
client.c
1 |
|
运行程序:server
,client
可以实现双方同时通信,皆可以收发
测试如下,双方交替通信三次。
¶八、信号灯
所有函数是对一个集合的操作:
¶8.1 创建和打开信号灯
semget.c
1 |
|
由内核给我们创建拥有三个信号量,权限是 777 的信号灯,因为使用 IPC_PRIVATE
key,所以键值是 0
¶8.2 信号灯的控制
semctl.c
1 |
|
可以发现被创建的 semid = 4
的信号灯被删除
¶8.3 线程同步
¶8.3.1 posix 实现方法
1 |
|
编译:gcc -o posix posix.c -lpthread
因为没有加 PV
操作,所以每次程序进行的顺序都是乱的。
使用 posix 信号量来实现两个线程之间的同步
1 |
|
通过使用信号量,可以实现线程之间的同步,主线程完成后子线程操作
¶8.3.2 信号灯实现方法
**semop
**函数
sem.c
1 |
|
实现结果与 posix 实现方法相同
¶8.4 进程同步
server.c
1 |
|
client.c
1 |
|
两个进程之间的逻辑如下:
首先打开 client
进程,创建信号灯,初始化信号灯,之后 P 操作,处于阻塞状态
打开 server
进程,打开信号灯,执行程序代码,打印输出,睡眠 3 秒后,执行 V 操作
client
执行 P 操作之后的代码,打印输出