一、进程间通信介绍
目的
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个进程或一个组发送消息,通知它(它们)发生了某种事件
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
发展
- 管道
- System V进程通信
- POSIS进程间通信
分类
管道
- 匿名管道
- 命名管道
System V IPV
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
二、管道
- 管道是最古老的进程间通信的形式。
- 我们把从一个进程连接另一个进程是一个数据流称为一个"管道"
2.1 匿名管道
匿名管道:创建一种无名的管道,提供具有血缘关系的进程进行通信。
#include<unistd.h>
int pipe(int fd[2])
参数:fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
返回值:成功返回0,失败返回错误代码
int main()
{
int fd[2];
// 创建匿名管道
if (pipe(fd) < 0)
{
perror("pipe!");
return 1;
}
// 创建子进程
int id = fork();
if (id == 0)
{
// child
close(fd[0]);
const char *mag = "hello linux\n";
int count = 5;
while (count)
{
write(fd[1], mag, strlen(mag));
count--;
sleep(1);
}
exit(0);
}
// father
close(fd[1]);
char buff[64];
while (1)
{
ssize_t s = read(fd[0], buff, sizeof(buff));
if (s > 0)
{
buff[s] = '\0';
printf("child --->father#%s", buff);
sleep(1);
}
else if (s == 0)
{
printf("read end\n");
break;
}
else
{
printf("error\n");
break;
}
}
// 等待子进程结束,回收资源,防止僵尸进程。
waitpid(id, NULL, 0);
close(fd[0]);
return 0;
}
<1> 如果说通信可以通过文件来作为媒介,那么为什么不直接open一个文件来呢?要用pipe来创建管道?
答:pipe创建的文件是内存文件,数据一定不会刷新到磁盘。并且用普通文件会有很多问题(同步与互斥),有IO参与会降低效率,没有必要。
<2> 创建子进程进行写入难道不会发生写时拷贝吗?
答:不会,管道是OS提供的,子进程写入时,不会改变父进程的数据区,故不会发生写时拷贝。
<3> 子进程还没写完,父进程就不会读取吗?
答:管道是自带同步与互斥的。不会发生子进程还没写完,父进程就开始读了。
管道读写规则
当没有数据可读时:
- 子进程不write,父进程会被挂起。
当管道满了时:
- 子进程会被挂起,等待父进程read
如果管道写端对应的文件描述符被关闭,read返回0
如果父进程write关闭时,子进程read没有意义,子进程会接收到13号信号退出。
用代码演示子进程接收信号退出:
int main()
{
int fd[2];
if (pipe(fd) < 0)
{
perror("pipe!");
return 1;
}
pid_t id = fork();
if (id == 0)
{
// child
close(fd[0]);
const char *mag = "hello linux\n";
write(fd[1], mag, strlen(mag));
exit(0);
}
// father
// 关闭读端和写端
close(fd[1]);
close(fd[0]);
int status = 0;
waitpid(id, &status, 0);
// 打印信号
printf("child get signal:%d\n", status & 0x7F);
return 0;
}
接收的是13号信号,信号是由OS发送的。
当要写入的数据量不大于PIPE_BUF时,Linux将保持原子性。
当要写入的数据量大于PIPE_BUF时,Linux将不保持原子性。
管道特点
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
- 管道提供流式服务
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行同步与互斥
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
2.2 命名管道
- 匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
- 命名管道是一种特殊类型的文件
创建一个命名管道
mkfifo filename
利用命名管道让两个毫不相干的进程进行通信
//server.c ->写
int main()
{
int fd = open("filename", O_WRONLY);
char *mag = "ni hao a\n";
write(fd, mag, strlen(mag));
close(fd);
return 0;
}
//client.c ->读
int main()
{
char buff[64];
int fd = open("filename", O_RDONLY);
ssize_t s = read(fd, buff, sizeof(buff));
buff[s] = '\0';
printf("srever----->client:%s", buff);
close(fd);
return 0;
}
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
第一个参数是文件名或者路径。第二个参数是文件的权限。
成功是返回0,失败是返回-1。
2.3 匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
- 命名管道作用于毫不相干的进程,匿名管道作用于具有血缘关系的进程
三、system V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存示意图
共享内存函数
shmget函数
功能:用来创建共享内存。 原型:int shmget(key_t key,size_t size,int shmflg); 参数:key:共享内存段的名字。 size:共享内存的大小。 shmflg:权限。由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。 返回值:成功返回一个非负整数,该共享内存段的标识码。失败返回-1。
shmat函数
功能:将共享内存段连接到进程地址空间 原型 void *shmat(int shmid, const void *shmaddr, int shmflg); 参数 shmid: 共享内存标识 shmaddr:指定连接的地址 shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY 返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
【说明】
shmaddr为NULL,内核会自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存。
shmdt函数
功能:将共享内存段与当前进程脱离 原型:int shmdt(const void *shmaddr) 参数:shnaddr:由shmat所返回的指针 返回值:成功返回0;失败返回-1 注意:将共享内存段与当前进程脱离不等于删除共享内存段。
shmctl函数
功能:用于控制共享内存 原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf) 参数:shmid:key的标识符 cmd:将要采取的动作 buf:指向一个保存着共享内存的模式状态和访问权限的数据结构 返回值:成功返回0,失败返回-1。