前言
进程虽然是独立运行的个体,但它们之间有时候需要协作才能完成一项工作,比如有两个进程需要同步数据,进程 A 把数据准备好后,想把数据发往进程 B,进程 B 必须被提前通知有数据即将到来,或者进程 A 想发送信号给进程 B,以控制进程 B 的运行模式,再或者数据被多个进程共享时,数据变更后应该被所有进程看到,总之诸如此类的需求很多,操作系统必须要实现进程间的相互通信。
进程间通信方式有很多种,有管道、消息队列、共享内存、socket 网络通信等。在认识通信方式之前,先来认识几个概念。
公共资源
可以是公共内存、公共文件、公共硬件等,总之是被所有任务共享的一套资源。
临界区
若多个任务都访问同一公共资源,那么各任务中访问公共资源的指令代码组成的区域就称为临界区。临界区是指程序中那些访问公共资源的指令代码,即临界区是指令,并不是受访的静态公共资源。
互斥
是指某一时刻公共资源只能被 1 个任务独享,即不允许多个任务同时出现在自己的临界区中。公共资源在任意时刻只能被一个任务访问,即只能有一个任务在自己的临界区中执行,其他任务想访问公共资源时,必须等待当前公共资源的访问者完全执行完他自己的临界区代码后(使用完资源后)再开始访问。
管道
管道是进程间通信的方式之一,在 Linux 中一切皆文件,因此管道也被视为文件,只是该文件并不存在于文件系统上,而是只存在于内存中。
既然是文件,管道就要按照文件操作的函数来使用,因此也要使用open 、close 、read 、write 等方法来操作管道。管道通常被多个进程共享,而且存在于内存之中,因此共享的原理是所有进程在地址空间中都可以访问到它,管道其实就是内核空间中的内存缓冲区。
管道是用于存储数据的中转站,当某个进程往管道中写入数据后,该数据很快就会被另一个进程读取,之后可以用新的数据覆盖老数据,继续被别的进程读取,因此管道属于临时存储区,其中的数据在读取后可被清除。
管道是个环形缓冲区,环形缓冲区中一个指针用于读数据,另一个用于写数据。当缓冲区己满时,生产者要睡眠,并在睡眠前唤醒消费者,当缓冲区为空时,消费者要睡眠,并在睡眠前唤醒生产者。当缓冲区满或空时,使一方休眠,这是保证数据不丢失的方法。
管道有两端,一端用于从管道中读入数据,另一端用于往管道中写入数据。这两端使用文件描述符的方式来读取,故进程创建管道实际上是内核为其返回了用于读取管道缓冲区的文件描述符,一个描述符用于读,另一个描述符用于写。fd[0]用于读取管道,fd[1]用于写入管道。
一般情况下,父子进程中都是一个读数据,一个写数据,并不会存在一方又读又写的情况,因此在父子进程中会分别关掉不使用的管道描述符。比如父进程负责往管道中写数据,它只需要fd[1]描述符,因此只可以通过 close系统调用关闭fd[0]。子进程负责从管道中读数据,它只需要fd[0]描述符,因此只可以通过close系统调用关闭fd[1]。
匿名管道
pipe()
flags介绍
当没有数据可读时
1.O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
2.O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
1.O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
2.O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
管道读写规则
- 读端不读或者读的慢,写端要等读端
- 读端关闭,写端收到SIGPIPE信号直接终止
- 写端不写或者写的慢,读端等写端
- 写端关闭,读端读完pipe内部的数据,读到0说明到文件结尾。
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
//管道是单向,面向字节流,管道是有大小的,自带同步机制
int main()
{
int pipefd[2] = {0};
if(pipe(pipefd) != 0)
{
perror("pipe false!");
return 1;
}
printf("pipefd[0]:%d\n", pipefd[0]);
printf("pipefd[1]:%d\n", pipefd[1]);
// pipefd[0] refers to theread end of the pipe
// pipefd[1] refers to the write end of the pipe.
//子进程写
if(fork() == 0)
{
close(pipefd[0]);
const char* msg = "hello world\n";
while(1)
{
//只要有缓冲区就一直写
sleep(1);
write(pipefd[1], msg, strlen(msg));
//验证管道的大小
//write(pipefd[1], "c", 1);
// count++;
// printf("%d\n", count);
}
}
close(pipefd[1]);
//父进程读
while(1)
{
char buffer[1024] = {0};
//只要有数据就一直读
ssize_t s = read(pipefd[0], buffer, sizeof(buffer));
if(s == 0)
{
printf("写端退出\n");
break;
}
else if(s > 0)
{
buffer[s] = '\0';
printf("父进程读到子进程发来的:%s", buffer);
}
else break;
}
int status = 0;
waitpid(-1, &status, 0);
printf("exit code: %d\n", (status >> 8)&0xFF);
printf("exit signal: %d\n", (status)&0x7F);
}
总结
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道
- 管道提供流式服务
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行同步与互斥
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
命名管道
命名管道相比匿名管道,可以在不同的进程之前进行通信。相比直接继承父进程资源,命名管道要想让不同进程看到同一份资源,是通过文件名来实现的。
mkfifo()
实现一个client和server通信的例子。
client
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#define MY_FIFO "./fifo"
int main()
{
int fd = open(MY_FIFO, O_WRONLY);
if(fd < 0)
{
perror("open error\n");
return 1;
}
while(1)
{
printf("请输入字符");
fflush(stdout);
char buffer[32] = {0};
ssize_t s = read(0, buffer, sizeof(buffer) - 1);
if(s > 0)
{
buffer[s - 1] = 0;
//printf("%s\n", buffer);
write(fd, buffer, strlen(buffer));
}
}
close(fd);
}
server
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#define MY_FIFO "./fifo"
int main()
{
umask(0); //避免影响mkfifo的权限设置
if(mkfifo(MY_FIFO, 0666) < 0)
{
perror("mkfifo error");
return 1;
}
int fd = open(MY_FIFO, O_RDONLY);
if(fd < 0)
{
perror("open error");
return 2;
}
while(1)
{
char buffer[32] = {0};
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if(s > 0)
{
buffer[s] = 0;
if(strcmp(buffer, "ls") == 0)
{
if(fork() == 0)
{
execl("/usr/bin/ls", "ls", "-a","-l", NULL);
exit(1);
}
waitpid(-1, NULL, 0);
}
printf("client#:%s\n", buffer);
}
else if(s == 0)
{
printf("client quit...\n");
}
else
{
perror("read error");
}
}
close(fd);
return 0;
}
共享内存
共享内存是最快的通信方式,systemV的IPC资源生命周期是随内核的,要通过显示释放[ipcrm -m shmid 删除memory ],或者os重启。不提供同步互斥机制。
创建共享内存shmget()
参数
key是共享内存的名字,key能保证这个共享内存的名字是唯一的。
size:共享内存大小
shmflg:IPC_CREAT、IPC_EXCL 等IPC_CREAT和IPC_EXCL一起用能保证创建出来的共享内存是最新的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
共享内存链接到进程地址空间shmat()
参数
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);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段。
控制共享内存shmctl()
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
CMD
命令 | 说明 |
---|---|
IPC_STAT | 把shmid_ds结构中的数据设置为共享内存的当前关联值 |
IPC_SET | 在进程有足够的权限下,把共享内存的当前关联值设置为shmid_ds给出的值 |
IPC_RMID | 删除共享内存段 |
查看共享内存
例子
server
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4096 //最好是4096的整数倍
int main()
{
//ftok创建key (自定义路径,自定义ID)
key_t key=ftok(PATH_NAME,PROJ_ID);
if(key<0)
{
perror("ftok"); return 1;
}
//创建共享内存,返回的是共享内存的标识码
int shmid=shmget(key,SIZE,IPC_CREAT);
if(shmid<0)
{
perror("shmget"); return 2;
}
printf("key:%u,shmid:%d\n",key,shmid);
//attaches the shared memory
//返回指向共享内存的一个地址的指针
char* mesg=(char*)shmat(shmid,NULL,0);
printf("attaches the shared memory success!\n");
//通信开始
while(1){
sleep(1); printf("%s\n",mesg);
}
//detaches the shared memory
shmdt(mesg);
printf("detaches shm success!\n");
//删除共享内存段
shmctl(shmid,IPC_RMID,NULL);
printf("key:0x%x,shmid:%d->shm delete success\n",key,shmid);
return 0;
}
client
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4096 //最好是4096的整数倍
int main()
{
key_t key=ftok(PATH_NAME,PROJ_ID);//相同的方法创建出相同的key
if(key<0)
{
perror("ftok"); return 1;
}
//创建共享内存,返回的是共享内存的标识码
int shmid=shmget(key,SIZE,IPC_CREAT);
if(shmid<0)
{
perror("shmget"); return 1;
}
//attaches the shared memory
//返回指向共享内存的一个地址的指针
char* mesg=(char*)shmat(shmid,NULL,0);
printf("client attaches the shared memory success!\n");
//开始通信
//detaches
shmdt(mesg);
printf("client detaches success!\n");
return 0;
}