一. 概念
1.1 发展
- 管道
- System V进程间通信
- POSIX进程间通信
1.2 分类
1.2.1 管道
- 匿名管道pipe
- 命名管道
1.2.2 System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
1.2.3 POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
1.3 作用
- 数据传输
- 资源共享
- 通知事件
- 进程控制
二. 管道
2.1 简述
从一个进程连接到另一个进程的一个数据流称为一个“管道”。
2.2 匿名管道
2.2.1 pipe接口
int pipe(int pipefd[2]);
// 作用:创建一个匿名管道(以读写方式打开同一个文件)
// pipefd[0] --> 以读方式打开 pipefd[1] --> 以写方式打开
// 如果成功就返回0,错误就返回-1或者错误码
2.2.2 画图演示
2.2.2 代码演示
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> // for pid_t
#include <sys/wait.h>
int main()
{
int pipefd[2] = {0};
int ret_pipe = pipe(pipefd);
if (ret_pipe != 0)
{
perror("pipe");
return -1;
}
// 我们让子进程写,父进程读
pid_t pid = fork(); // 创建子进程
if (pid < 0)
{
perror("fork");
return -1;
}
if (pid == 0)
{
// child
close(pipefd[0]); // 子进程关闭读端,留下写端
while (1)
{
const char* msg = "hello world";
write(pipefd[1], msg, strlen(msg));
sleep(1);
}
}
// parent
close(pipefd[1]); // 父进程进程关闭写端,留下读端
char msg[64] = "";
while (1)
{
int n = read(pipefd[0], msg, sizeof(msg) - 1);
msg[n] = '\0';
printf("child say: %s\n", msg);
}
return 0;
}
2.2.3 关闭写端
// 只关闭写端
int main()
{
int pipefd[2] = {0};
int ret_pipe = pipe(pipefd);
// 让子进程写,父进程读
pid_t pid = fork();
if (pid == 0)
{
// child
close(pipefd[0]);
while (1)
{
char ch = 'a';
write(pipefd[1], &ch, 1);
close(pipefd[1]); // 将写端关闭
break;
}
}
// parent
close(pipefd[1]);
char msg[1024*4 + 1] = "";
while (1)
{
sleep(1);
int n = read(pipefd[0], msg, sizeof(msg) - 1);
if (n < 0)
{
break;
}
if (n == 0) // n为0,说明读端已经关闭了
{
printf("file is empty!\n");
break;
}
msg[n] = '\0';
printf("child say: %c\n", msg[0]);
}
return 0;
}
2.2.4 关闭读端
// 关闭读端
int main()
{
int pipefd[2] = {0};
int ret_pipe = pipe(pipefd);
// 让子进程写,父进程读
pid_t pid = fork();
if (pid == 0)
{
// child
close(pipefd[0]);
while (1)
{
char ch = 'a';
write(pipefd[1], &ch, 1);
}
}
// parent
close(pipefd[1]);
char msg[1024*4 + 1] = "";
while (1)
{
sleep(1);
int n = read(pipefd[0], msg, sizeof(msg) - 1);
close(pipefd[0]); // 关闭读端
printf("child say: %c\n", msg[0]);
break;
}
int status = 0;
wait(&status);
printf("exit_code: %d signal: %d\n", (status >> 8) & 0xff, status & 0x7f); // 打印signal为13
return 0;
}
2.2.5 五个特点
- 管道是一个只能单向通信的通信信道
- 管道是面向字节流的
- 仅限于具有血缘关系的进程进行通信
- 管道自带同步机制,原子性写入
- 管道的生命周期是随进程的
2.2.6 读写规则
没有数据读时
写端不写或者写得慢,读端要等写端
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满时
读端不读或者读得慢,写端要等读端
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
写端对应的文件描述符被关闭时
读端read读完pipe的数据后会读到0,可以根据检测到0判断写端已经关闭了
读端对应的文件描述符被关闭时
写端收到SIGPIPE信号直接终止程序
原子性
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
2.3 命名管道
2.3.1 概念
一个有名字的管道文件,可以像正常文件操作那样被打开,所以它可以用于不相关进程之间的通信。
2.3.2 创建命令和接口
# 1. 命令行上创建
mkfifo filename
// 2. C程序里面创建
// 成功返回0,失败返回-1
int mkfifo(const char* filename, mode_t mode);
2.3.3 server和client案例代码
// server
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h> // for open
#include <fcntl.h> // for open
int main()
{
umask(0);
int ret = mkfifo(FILE_NAME, 0666);
int fd = open("fifo", O_RDONLY);
while (1)
{
char buf[64] = {0};
ssize_t n = read(fd, buf, sizeof(buf));
printf("n -- %d\n", n);
buf[n-1] = '\0';
if (strcmp(buf, "ls") == 0)
{
if (fork() == 0)
{
execl("/bin/ls", "ls", "-al", NULL);
}
}
else if (strcmp(buf, "information") == 0)
{
if (fork() == 0)
{
execl("/usr/bin/information", "information", NULL);
}
}
else if (strcmp(buf, "clear") == 0)
{
if (fork() == 0)
{
execl("/usr/bin/clear", "clear", NULL);
}
}
else if (strcmp(buf, "close") == 0)
{
printf("OK, close the file!!!\n");
break;
}
else
{
printf("client say: %s\n", buf);
}
}
close(fd);
return 0;
}
// client
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h> // for open
#include <fcntl.h> // for open
int main()
{
int fd = open("fifo", O_WRONLY);
if (fd < 0)
{
perror("open");
}
printf("successful open, fd is %d\n", fd);
while (1)
{
printf("请输入:");
fflush(stdout);
char buf[64] = {0};
ssize_t n = read(0, buf, sizeof(buf));
buf[n] = '\0';
n = write(fd, buf, n);
if (strcmp(buf, "close\n") == 0)
{
break;
}
}
close(fd);
return 0;
}
2.3.4 读写规则
打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
2.4 匿名管道和命名管道的区别
- 匿名管道由pipe函数创建并且打开
- 命名管道由mkfifo命令或者函数创建,程序里面打开用open
- 匿名管道只能在有血缘关系的进程之间进行通信,而命名管道可以在不相关的进程之间通信
三. System V共享内存
3.1 简述
如下图,在物理内存上申请一块空间,然后让不同进程的虚拟内存通过页表映射挂接到这个这块空间,这块空间就能实现被不同进程共享了。共享内存在建立连接以后,在通信的时候不需要调用系统接口了,没有IO的处理,速度上是比管道快很多的。
3.2 函数接口
3.2.1 ftok
// 需要包含的头文件
#include <sys/types.h> // for key_t
#include <sys/ipc.h>
// 函数声明
key_t ftok(const char* pathname, int proj_id);
// 一. 功能:一般是给像shmget这种接口生成一个key
// 二. pathname:自定义路径的名字
// 四. proj_id:自定义项目id
// 五. 返回值:成功就返回一个对应key,失败就返回-1
3.2.2 shmget
// 需要包含的头文件
#include <sys/ipc.h>
#include <sys/shm.h>
// 函数声明
int shmget(key_t key, size_t size, int shmflg);
// 一. 功能: 向物理内存申请共享内存
// 二. key:也就是ftok函数接口生成的
// 三. size:是共享内存的大小,建议写4KB(4096Byte)的整数倍,因为内存分配操作是以页为单位的,一页的大小就是4KB
// 四. shmflag:是一个位图映射的数据变量,主要的标志位有IPC_CREAT和IPC_EXCL
// IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则打开操作
// IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误(单独使用没有意义)
// 五. 返回值(成功返回共享内存的标识码,失败返回-1)
// 1. 单独使用IPC_CREAT,如果key对应的共享内存不存在,就返回新申请共享内存的标识码,存在就返回返回其标识码
// 2. IPC_CREAT和IPC_EXCL一起使用,如果key对应的共享内存不存在,就返回新申请共享内存的标识码,存在就返回出错(意义:调用成功,保证返回的标识码的共享内存是新建的,而不是已经存在的)
3.2.3 shmctl
// 需要包含的头文件
#include <sys/ipc.h>
#include <sys/shm.h>
// 函数声明
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
// 一. 功能: 控制共享内存
// 二. shmid:需要控制的共享内存的标识码
// 三. cmd:将要采取的动作(IPC_STAT, IPC_SET, IPC_RMID)
// IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值
// IPC_SET:在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值
// IPC_RMID:删除共享内存段(常用!!!)
// 四. buf:指向一个保存着共享内存的模式状态和访问权限的数据结构的指针(删除共享内存时可以传NULL)
// 五. 返回值:成功返回0,失败返回-1
3.2.4 shmat
// 需要包含的头文件
#include <sys/types.h>
#include <sys/shm.h>
// 函数声明
void* shmat(int shmid, const void* shmaddr, int shmflg);
// 一. 功能: 将进程的虚拟地址通过页表映射挂接到共享内存
// 二. shmid:需要控制的共享内存的标识码
// 三. shmaddr:指定虚拟地址,一般传NULL让系统自动找一块地址
// 四. shmflg:SHM_RDONLY,传这个过去为只读模式,否则为其他模式
// 五. 返回值:成功返回挂接上的虚拟地址,失败返回(void*)-1
3.2.5 shmdt
// 需要包含的头文件
#include <sys/types.h>
#include <sys/shm.h>
// 函数声明
int shmdt(const void* shmaddr)
// 一. 功能: 解除虚拟内存与共享内存的页面映射关系
// 三. shmaddr:需要去关联的虚拟地址
// 四. 返回值:成功返回0,失败返回-1
3.2.6 案例
// common.h文件
#include <stdio.h>
#include <sys/types.h> // for key_t
#include <sys/ipc.h>
#include <sys/shm.h> // for shmget
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h> // for O_CREAT
#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4096
// server.c文件
#include "common.h"
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
printf("key = %d\n", key);
// 申请共享内存
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1)
{
perror("shmget");
exit(1);
}
printf("client success! shmid is %d\n", shmid);
// 让虚拟内存和共享内存关联起来
char* ptr = (char*)shmat(shmid, NULL, 0);
//sleep(10);
// 通信
char ch = 'A';
while(ch != 'Z')
{
*ptr = ch;
ptr++;
*ptr= '\0';
ch++;
sleep(1);
}
sleep(5);
// 去关联
shmdt(ptr);
// 销毁共享内存
int ret = shmctl(shmid, IPC_RMID, NULL);
if (ret == -1)
{
perror("shmctl");
}
printf("Destruction of successful!\n");
return 0;
}
// client.c文件
#include "common.h"
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
// 获取共享内存的标识码
int shmid = shmget(key, SIZE, IPC_CREAT);
if (shmid == -1)
{
perror("shmget");
exit(1);
}
// 建立链接关系
char* ptr = (char*)shmat(shmid, NULL, 0);
// 通信
int times = 26;
while (times--)
{
sleep(1);
printf("%s\n", ptr);
}
// 解除链接关系
shmdt(ptr);
return 0;
}
3.2.7 相关命令
# 1. 显示当前所有shmipc的信息
ipcs -m
# 2. 删除shmipc资源
ipcrm -m shmid # shmid为共享内存的标识符