进程间通信 (共享信息)
- 进程间通信的理解
通信是为了什么? 我知道, 是为了传递信息, 共享消息, 好日常的解释哈, perfect.
日常生活中的通信是为了在两个人之间共享消息. 进程间通信其实本质就是 两个进程之间共享一些资源,数据...
在前文我们介绍了一个进程的重点性质:独立性, 的确,在用户空间来看, 每一个进程之间都是完完全全的独立运行, 独立存在,互不干扰的. --- 为啥, 不让你乱来呀, 保持进程间的隔离独立
但是, 两个进程之间总会存在需要交流的情况, 如下
数据传输 eg: A进程需要传输 一串数据 到B进程
共享资源:eg : A B 进程之间需要共享一块内存资源memory, 使得A 跟 B 对于 这块memory都可见.
信号发送(通知): 好常见的好吧, 其实你已经用过很多了. 子进程挂了之后,父进程咋知道子进程躺尸了, 我要去为它接收退出码, 回收系统资源. --- 其实就是子进程终止会给父进程发送信号,通知
如何实现通信, 通信的场所在哪里? 内核
有必要搞得很清楚哈, 来张图: 啥意思? 各个进程之间在用户空间中确实是完全隔离的, 但是内核大家是共享的, 是操作系统来维护操作的. 所以内核好比一个交易场所, 进程间的通信都是在内核态中完成的
- 管道通信
- 图解管道
何为管道, 管道总是用来传输的, 下水管道是用来传输水的. 同样 进程间在内核态插入的一根管道是为了传输数据的.
管道存在空间, 是内核态的一个缓冲区, 缓存, 管道的实现还是基于Linux下一切皆为文件的思想来完成的, 所以在一定意义上面, 我们可以将pipe管道理解为一个内核文件的感觉 (仅作为个人理解, 可能存在错误, 但是将pipe理解为内核缓存是绝对OK的)
我们从用户进程A中将数据读取到内核pipe文件中, 再将数据从pipe文件写进进程B,也就实现了数据从A流向B, 传输到B的目的了
匿名管道
对于匿名管道, 第一印象应该是什么? 是 | ,你可能会说这不是竖划线嘛, 的确它是一根竖划线
但是,这是在你没有学习管道之前你可以这样说, 今天,请你记住它的另外一个称呼, 匿名管道. 其实很形象呀, 管道竖起来不正是很像一根竖划线吗? 对对对, 就是这样滴.
A | B 含义是什么? 将A的输出通过管道传入B 当作B的输入.
eg: 场景带如: fork出来一个子进程, 父进程关闭管道写端, 从管道中读取数据, 子进程关闭管道读端, 向管道中写入数据, 父读子写. --- 父子进程之间进行通信
#include <stdio.h> #include <sys/types.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #define EXIT_ERR(m)\ do { perror(m); exit(EXIT_FAILURE);} while(0) int main() { int fds[2]; if (pipe(fds) < 0) { //获取管道 EXIT_ERR("pipe"); } pid_t pid = fork(); //fork操作一定要在pipe的后面才OK if (pid < 0) { EXIT_ERR("fork"); } if (pid) { //fa close(fds[1]); //关闭写端 char buff[1024] = {0}; int n = read(fds[0], buff, 1024); close(fds[0]); if (n <= 0) { EXIT_ERR("read"); } printf("read %d bytes\n", n); write(1, buff, n); putchar(10); } else { //son close(fds[0]); //关闭读取端口 char buff[1024]; printf("请说:"); scanf("%s", buff); //sizeof(buff) 是容器大小 write(fds[1], buff, strlen(buff) + 1); //此处绝对不能使用sizeof() close(fds[1]); } return 0; }
匿名管道的弊端, 以及命名管道的引出, 匿名管道只能用于向父子进程这种存在血缘关系之间的进程间通信, 没有关系的进程间无法实现通信
- 命名管道
解决了匿名管道无法实现非血缘关系的进程之间的通信, 对于命名管道, 可以实现任意两个进程之间的通信.
mkfifo 命令可以创建一根命名管道, 或者使用系统调用也可以创建命名管道
管道模型的优缺点以及应用场景总结:
场景: 数据从一个进程传输到另外一个进程
优点: 数据传输比较及时
缺点1:不适合频繁的交换数据, 只要交换数据就必须要打通内核态中的管道通道, 但是我们都知道信息的传输, 数据的传输还存在一种场景就是,消息太多了, 我们可以不用立刻处理, 可以将消息, 信息放在哪里, 后面慢慢看, 就像发邮件一样. --- 提出问题, 如果需要频繁沟通, 我们不一定可以实时处理, 可以借助邮件思维, 先通过一个容器存储着, 慢慢看, 慢慢交流, 慢慢的完成频繁的数据交互。
缺点2:管道有同步,阻塞的问题限制. eg : 阻塞读, 当 读取的字节数 > 管道缓冲区中的已有字节数, 读的进程就会阻塞等待, 写进程写入数据 阻塞造成了低效
- ftok
ftok 仅仅只是一个简简单单的系统调用, 我却将其完全独立出来说明,是否小题大作了呀? NONONO, 因为后序的所有IPC 通信全部需要获取一个唯一key值, 获取这个key 值得最优方式就是通过一个file 的 inode 来获取这个全局唯一 key 值, 可以避免重复.
System V 消息队列
- 理解消息队列
消息队列: 生活化, 发邮件, 很日常呀, 别人发来的邮件都放在邮箱里面, 得空就可以对于邮件进行阅读和回寄邮件. 消息队列其实就蛮像一个邮箱的.
我们创建一个内核态的全局的邮箱 --- 消息队列.
通过系统调用,各个进程可以进入内核态, 像消息队列,邮箱中 send 邮件,和 recv 邮件. 这种工作模式就可以达到 频繁的数据交换
进程A : 我 send 一封邮件 (struct数据单元) 到 Message Queue 里面去
进程B : 我从 Message Queue 读取到一封邮件 (struct 数据单元) 然后按照约定回 进程A一封
注意:我特意的将每一封邮件说成是一个数据单元. 既然是一个单元, 就是存在大小和数据格式上面的限制的. --- 大小限制留疑, 其实答案在后文介绍的系统调用参数中
- msgget
Return Value : 规则同上 failure return -1 sucess return msgid;
eg :
key : 至此开始, 几乎在每一个 IPC 通信, 下面都可以看见, 他就是一个共享容器的一个代言, 标识, 名称.
msgflag : 9位权限标志位: 我们通常使用 0x666 | IPC_CREAT | IPC_EXCL
插一个权限介绍: r : 读权限 w : 可写权限 X : 可执行权限. 所谓的9位权限标识位, 就是针对: 文件拥有者, 文件拥有组人员, 其他人员的访问权限的限制
来吧: 虽然其他后面的还没有学习, 咱可以先创建一个msgqueue出来玩一玩呀:
code :
#include <unistd.h> #include <sys/ipc.h> #include <sys/types.h> #include <sys/msg.h> #include <stdio.h> int main(int argc, char* argv[]) { if (argc != 2) { fprintf(stderr, "usage: %s <filepath>\n", argv[0]); return -1; } key_t key = ftok("./a.txt", 'k'); if (-1 == key) { perror("ftok"); return -2; } printf("key: %d\n", key); //此刻已经获取到唯一key值了 //创建一个msg queue int msgid = msgget(key, 0666 | IPC_CREAT | IPC_EXCL); if (-1 == msgid) { perror("msgget"); return -3; } printf("msgid: %d\n", msgid); return 0; }
ans: 看吧 ipcs -q : 查看创建的 IPC 全局的 msg queue. ipcs -m 查看 ipc share memory
ystem V IPC 体系有一个统一的命令行工具:ipcmk,ipcs 和 ipcrm 用于创建、查看和删除 IPC 对象。
记忆大师 : ipcs : 就是简单的查看 ipcmk : ipc make 创建 ipcrm : ipc remove移除
msgsnd
功能: 像IPC queue消息队列发送一条消息
msgflg 设为 IPC_NOWAIT , 设置非阻塞 不论是 send 的时候msg queue满了, 还是 recv 的时候msg queue 是空的都不会阻塞住. 无阻塞调用
Return Val :
sucess : return 0
failure: return -1 && set errn
- msgrcv
msgtyp : 指定接收的消息的优先级, 优先接收消息队列哪里的消息
If msgtyp is 0, then the first message in the queue is read. 读取消息队列第一条消息, 不论message 的 type, 只是接收第一条消息
如果msgtyp > 0 则会接收具有相同消息类型的第一条消息
如果msgtyp < 0 则会接收类型 <= msgtyp绝对值的第一条消息
Return Val:
success: return the number of bytes actually copied into the mtext array 成功从mtext正文中读取的字节数
failure : return -1 && set errno
msgctl
功能 : 功能取决于 cmd, 其实就是对于IPC 执行 cmd 操作
cmd : IPC_RMID : 删除消息队列.
消息队列模型的优缺点以及应用场景总结:
场景: 两个进程频繁的进行数据交换.
优点: 避免了管道的同步阻塞问题, 提高了效率. 相比匿名管道只能是血缘进程间通信, 消息队列支持毫不相干的两个进程间的通信
缺点1:数据块存在长度限制, 不适合大数据的传输
缺点2:通信不及时
缺点3: 存在用户空间和内核空间数据的拷贝. --- 留个思考, 共享内存是最快的通信机制, 向较于msg queue, 它连内核空间跟用户空间的数据拷贝代价都省去了
案例实操: 现在存在 A B 两个进程, 通过消息队列, 实现 A 向 B 发送消息, B 接收并且打印消息.
code msgA.c
#include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #define TEXT_SIZE 1024 typedef struct msgbuff { long int type; //消息数据类型 char text[TEXT_SIZE];//正文大小 } msgbuff; int main(int argc, char* argv[]) { if (argc != 2) { fprintf(stderr, "usage: %s <filepath>\n", argv[0]); return -1; } msgbuff buff; //1获取key key_t key = ftok(argv[1], 'c');//当前目录 if (key == -1) { perror("ftok"); return -2; } //2.创建msg queue int msgid = msgget(key, 0666 | IPC_CREAT | IPC_EXCL); if (-1 == msgid) { perror("msgget"); return -3; } //3.循环写入数据 while (1) { printf("请输入消息类型type:\n"); scanf("%d", &buff.type); printf("请输入消息正文text:\n"); scanf("%s", buff.text); if (-1 == msgsnd(msgid, &buff, TEXT_SIZE, IPC_NOWAIT)) { perror("msgsnd"); return -4; } } return 0; }
code msgB.c
#include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define TEXT_SIZE 1024 #define TYPE 2 typedef struct msgbuff { long int type; //消息数据类型 char text[TEXT_SIZE];//正文大小 } msgbuff; int main(int argc, char* argv[]) { if (argc != 2) { fprintf(stderr, "usage: %s <filepath>\n", argv[0]); return -1; } msgbuff buff; buff.type = TYPE; //1获取key key_t key = ftok(argv[1], 'c');//当前目录 if (-1 == key) { perror("ftok"); return -2; } //2.获取消息队列msgid int msgid = msgget(key, 0666 | IPC_CREAT); if (-1 == msgid) { perror("msgget"); return -3; } //3.循环读取数据 while (1) { memset(&buff.text, 0, TEXT_SIZE); int read_size = msgrcv(msgid, &buff, TEXT_SIZE, TYPE, IPC_NOWAIT); if (-1 == read_size && errno == ENOMSG) { continue;//还没有消息到来 } if (-1 == read_size) { perror("msgrcv"); return -4; } buff.text[read_size] = 0;//设置结束字符,习惯 if (strcmp("quit", buff.text) == 0) { break;//通信结束 } printf("recv: %s\n", buff.text); } //4.结束通讯删除msg queue if (-1 == msgctl(msgid, IPC_RMID, NULL)) { perror("msgctl"); return -5; } return 0; }
ans:
编程模型: 模子.
- 创建key = ftok() 唯一标识
- 创建消息队列, msgid = msgget()
- 收发数据 msgrcv() + msgsnd()
- 结束通信, 删除消息队列 msgctl(IPC_RMID) or ipcrm -q
System V 共享内存
理解共享内存
- 正常情况下, 进程地址空间中的虚拟内存,映射到的物理内存都一定是互不相同的, 是不会存在重合的, 因为进程的独立性。 --- 但是为了通信, 可以固定一块物理内存, 将其挂载到进程A, 进程B上去, 这样, A 和 B 对于这块物理内存都是可见的, 于是在这块物理内存为介质, 多进程可以实现通信
最快速,最高效的IPC 通信.
- shmget
功能:申请共享内存
shmflag : 9位权限标志位: 我们通常使用 0x666 | IPC_CREAT | IPC_EXCL
插一个权限介绍: r : 读权限 w : 可写权限 X : 可执行权限. 所谓的9位权限标识位, 就是针对: 文件拥有者, 文件拥有组人员, 其他人员的访问权限的限制
- shmat
功能:将物理内存连接,挂载,关联到进程地址空间中去
shmaddr :NULL 自动选取连接地址, 不为NULL, 人为指定, 希望挂载到的地址
我们还是填写 NULL
Return Val : 返回一个ptr, 指向共享物理内存的首地址. 虚拟空间中的连接首地址
- shmdt
功能:解除挂载, 将共享内存和当前进程进行脱离.
shmaddr : 之前 shmat 的 return val, 共享内存映射在虚拟地址空间上的首地址.
Return Val: success return 0 failure return -1
- shmctl
cmd: IPC_RMID 删除共享内存
共享内存模型的优缺点以及应用场景总结:
场景: 多个进程都需要加载一份配置文件的场景下, 由一个进程负责将配置文件加载到共享内存中, 需要这份配置文件的进程直接挂载这个共享内存使用即可
优点: 高效, 快速, 是最快的IPC通信机制了.
缺点: 需要借助信号量来进行进程间的同步工作, 自身不支持进程间同步
案例实操: 现在存在 A B 两个进程, 共享一块内存 m, A 往里面写入数据, B 从中读取 A 写入的数据
code A:
#include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <errno.h> #define SHM_SIZE 4096 int main(int argc, char* argv[]) { if (argc != 2) { fprintf(stderr, "usage: %s <filename>", argv[0]); return -1; } //1创建key key_t key = ftok(argv[1], 'a'); if (-1 == key) { perror("ftok"); return -2; } //2.创建共享内存 int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT | IPC_EXCL); if (-1 == shmid) { perror("shmget"); return -3; } //3. 挂载共享内存 char* p = (char*)shmat(shmid, NULL, 0); if ((char*)-1 == p) { perror("shmat"); return -4; } //4.通信阶段, 往共享内存中写入数据 *p = 0;//先设置为NULL printf("请输入共享数据:\n"); scanf("%s", p); return 0; }
code B
#include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <errno.h> #include <string.h> #define SHM_SIZE 4096 int main(int argc, char* argv[]) { if (argc != 2) { fprintf(stderr, "usage: %s <filename>", argv[0]); return -1; } //1创建key key_t key = ftok(argv[1], 'a'); if (-1 == key) { perror("ftok"); return -2; } //2.获取共享内存 int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT); if (-1 == shmid) { perror("shmget"); return -3; } //3. 挂载共享内存 char* p = (char*)shmat(shmid, NULL, 0); if ((char*)-1 == p) { perror("shmat"); return -4; } //4.通信阶段,从共享内存中读取数据 while (1) { if (*p) break; //等A传输数据 } printf("%s\n", p); //5.卸载共享内存 if (-1 == shmdt(p)) { perror("shmdt"); return -5; } return 0; }
编程模型: 模子.
- 创建key = ftok() 唯一标识
- 创建共享内存 int shmid = shmget()
- 挂载共享内存 p = shmat()
- 利用共享内存完成通信
- 卸载共享内存 shmdt()
- 删除共享内存 shmctl()
- POSIX信号量
- 理解信号量
信号量是一种资源, 是临界资源, 是操作系统封装好的, 具有保护作用的临界资源, 不需要我们手动上锁, 因为对于信号量资源的请求和归还, 操作系统自身就已经帮我们保护好了的.
目的: 是为了在多线程同步访问临界资源的时候不会产生资源竞争造成的冲突问题. 用于线程间同步访问临界资源. 这个临界资源就是sem 信号量
- sem_init
功能:初始化信号量
Return Val : 还是老规矩, success return 0 failure return -1
- sem_destroy
功能: 销毁信号量
- sem_wait
功能: 申请, 消费一个sem资源.
Return Val : success return 0 and failure return -1
- sem_post
功能: 归还, 增加一个sem 资源
Return Val : success return 0 and failure return -1
信号量的优缺点以及应用场景总结:
场景: 多线程同步访问资源, 或者是多进程同步访问资源
优点: 比文件锁有优势,效率高一些,起码不用打开文件关闭文件这些耗时间的工作, 相较加锁操作更简单方便
缺点: 不够安全,PV操作使用不当容易产生死锁,遇到复杂同步互斥问题实现复杂, 针对代码块需要加锁保护的问题无能为力, 因为代码块毕竟不是能用数字衡量的呀.
信号
- 何为信号?
- 常见的信号学习
- 如何注册自己的信号处理函数
全文终,综述留续
送君入门, 各位大哥们, 可以看到此处,实属不易呀, 或许是被小杰的文章引的入迷了, 哈哈哈,开了玩笑.
说正事, 摊在寝室床头与小学生抢人头, 说实话咱真不如去看看博文, 出去走走,锻炼锻炼, 学习学习, 计算机的学习之途本就是一头扎进汪洋大海.
没有绝对的强者, 没有绝对的天赋, 只有针对一个方向的强者, 所谓天赋偷偷告诉你,是高手们的凡尔赛, 他们肯定也是经过了无数日夜的煎熬, 刷题, 从初学, 到读书, 到总结出属于自己的知识点. 不容易呀.
看到此处,小杰真心佩服,说明您也有炙热的心, 无限的渴望提升的信念. 如果觉得小杰写的还蛮不错的, 可以伸手给小杰一个小心心, 小杰万分感谢, 对于哪个信号章节, 请原谅小杰, 创作不易, 如果硬要写完, 发文就得再拖一周, 小杰希望自己可以写出有质量, 对自己有提升的总结 --- 祝学习的学业有成, 工作的前辈们升