目录
1.进程通信的概念
进程间通信简称为IPC(Inter process communication),进程间通信就是在不同进程之间传播或交换信息。
由于各个运行的进程之间具有独立性,这个独立性主要体现在数据层面,而逻辑层面可以是私有的也可以是共有的(例如父子进程),因此各个进程之间要实现通信是非常困难的。
各个进程之间要是想实现通信,一定要借助第三方资源,这些进程就可以向这个第三方资源写入或读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。
因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存、文件、内核缓存等)。由于这份资源可以由操作系统中不同的模块提供,因此出现了不同的进程间通信的方式。
进程间通信的目的:
数据传输:一个进程需要将它的数据发送给另外一个进程
资源共享:多个进行之间共享同样的资源
通知事件:一个进程需要向另一个或一组进程发送消息,通知他们发生了某种事件,比如进程终止时需要通知其父进程。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有的陷入和异常,并能够及时知道它的状态改变。
2.管道
把一个进程连接到另一个进程的数据流称为一个管道。
1.匿名管道
匿名管道用于进程间通信,且仅限于本地关联进程之间的通信。
进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是:让两个父子进程看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或者读取操作,进而实现父子间进程通信。
注意:
这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进写时拷贝。
管道虽然用的是文件的方案,但是操作系统一定不会把进程进行通信的数据刷新到磁盘党总,因为这样做有IO参与会降低效率,而且也没有必要。这种文件是一批不会把数据写到磁盘党总的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存存在,而不会在磁盘当中存在。
pipe函数
函数描述
创建匿名管道
函数原型
int pipe(int pipefd[2]);
函数参数:
pipefd是一个传出参数,用于返回两个管道读端和写端的文件描述符。
pipefd[0]:管道读端的文件描述符
pipefd[1]: 管道写端的文件描述符
函数返回值:
成功:返回0
失败:返回-1,并设置errno
在创建匿名管道实现父子进程通信的过程中,需要pipe()函数和fork()函数搭配使用,步骤如下:
1.父进程调用pipe函数创建管道
2.父进程创建子进程
3.父进程关闭写端,子进程关闭读端(根据情况而定)
注意:
1.管道只能进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
2.从管道写端写入的数据会被存到内核缓冲区,直到管道的读端被读取。
int main(int argc, char* argv[])
8 {
9 int pipefd[2];
10 pipe(pipefd);//创建管道文件
11 int pid=fork();
12 if(pid==0){
13 close(pipefd[0]);//关闭读端
14 int write_count=write(pipefd[1],"hello word",5);
15 printf("write_count=%d\n",write_count);
16 }else{
17 close(pipefd[1]);//关闭写端
18 char buf[512];
19 sleep(1);
20 int read_count=read(pipefd[0],buf,sizeof(buf));
21 printf("read_count=%d buf=%s\n",read_count,buf);
22
23 }
24 return 0;
25 } 5
在这里会有以下几种情况:
1.当管道中没有数据时:write返回成功写入的字节数,读端进程阻塞在read上()因为没有东西可以读到)
2.当管道中的数据没有满:write放回成功写入的字节数,read返回成功读取的字节数
3.管道已满:写端进行阻塞在write上(因为写不进去了),read返回成功读取的字节数
4.写端全部关闭:read正常读,返回读取的字节数
5.读端全部关闭:写端进程会异常结束。
2.命名管道
匿名管道只能用于共同祖先的进程(具有亲缘关系的进程)之间的通信。如果想实现两个毫无关系进程之间的通信,可以使用命名管道来实现。命名管道是一种特殊类型的文件,两个进程通过命名管道的文件打开同一个管道文件,此时两个进程就看到了同一份资源,进而就可以进行通信了。
命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘里有一个简单的映像,但是这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信的数据刷新到磁盘中。
mkfifo函数
函数描述:
程序中创建命名管道
头文件:
#include<sys/types.h>
#include<sys/stat.h>
函数原型:
int mkfifo(const char* pathname,mode_t mode);
函数参数:
pathname:表示要创建的命名管道
mode:表示要创建命名管道文件的默认权限
函数返回值:
成功:返回0
失败:返回-1
命名管道在父子进程间的通信
#include<sys/stat.h>
mkfifo("./fifo",0664);
int fd=open("./fifo",O_RDWR);
int pid=fork();
if(pid==0){
write(fd,"hello fifo",10);
}
else if(pid>0){
char buf[1024]={'\0'};
int ret=read(fd,buf,sizeof(buf));
printf("read count =%d,buf=%s\n",ret,buf);
}
命名管道在没有血缘关系的进程间通信
wfifo.c
mkfifo("./fifo",0664);
int fd=open("./fifo",O_WRONLY);
printf("fd=%d\n",fd);
write(fd,"hello fifo",10);
rfifo.c
int fd=open("./fifo",O_RDONLY);
char buf[1024];
int ret=read(fd,buf,sizeof(buf));
printf("read count=%d,buf=%s\n",ret,buf);
命名管道打开的规则:
这里又涉及到打开文件的第二个参数,对于管道来说,默认是阻塞的,以O_NONBLOCK方式打开可以做非阻塞。
读进程打开FIFO,并且没有写进程打开时:
没有O_NONBLOCK:阻塞直到有进程打开该FIFO
有O_NONBLOCK:立刻返回成功
写进程打开FIFO,并且没有读进程打开时:
没有O_NONBLOCK:阻塞直到有读进程打开该FIFO
有O_NONBLOCK:立刻返回失败
3.内存映射
内存映射是将磁盘文件的数据映射到内存中,用户通过修改内存就能修改磁盘文件。
文件映射:将文件的一部分映射到调用进程的虚拟内存中,对文件映射部分的访问转化为对应内存区域的字节操作。映射页面会按需自动从文件中加载。
匿名映射:一个匿名映射没有对应的文件。其映射页面的内容会被初始化为0.
一个进程所映射的内存可能与其他进程的映射共享,共享的两种方式
1.两个进程对同一文件的同一区域映射
2.fork()创建的子进程继承其父进程的映射
mmap()函数
函数描述:
在调用进程的虚拟地址空间创建一个新的内存映射
头文件:
<sys/mman.h>
函数原型:
void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);
函数参数:
addr:指向欲映射的内存起始地址,通常设置为NULL,由系统会自动选定地址
length:映射的长度
prot:映射区域的保护方式
PROT_READ:映射区域可读取
PROT_WRITE:映射区域可修改
flags:影响映射区域的特性。必须指定MAP_SHARED或MAP_PRIVATE
MAP_SHARED:创建共享映射。对映射的写入会写入文件里,其他共享映射的进程可见。
MAP_ANONYMOUS:创建匿名映射。此时会忽略参数fd,设置为-1,不涉及文件,没有血缘关系的进程不能共享。
fd:要映射的文件描述符,匿名映射设为-1
offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小(4k)的整数倍。
函数返回值:
若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1)报错原因存于errno中。
munmap()函数
函数描述:
解除映射区域
函数原型:
int munmap(void *addr,size_t length);
函数参数:
addr:指要解除映射的内存起始地址
length:解除映射的长度
例如,在父子进程之间的通信
char* ptr=(char*)mmap(NULL,1024,PORT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
int pid=fork();
if(pid==0){
strcpy(ptr,"hello world");
printf("child ptr=%s\n",ptr);
}else if(pid>0){
sleep(1);
printf("parent ptr=%s\n",ptr);
}
在没有血缘关系进程通信
wmmap.c
int fd=open("./mmap.txt",O_RDWR|O_CREAT|O_TRUNC,0644);
ftryncate(fd,1024);//扩展文件大小,空文件内存映射会报错
char* ptr=(char*)mmap(NULL,1024,PORT_WRITE,MAP_SHEARED,fd,0);
if(ptr==MAP_FAILED){
perror("mmap error");
exit(1);
}
int i=0;
while(1){
sprintf(ptr,"----%d---\n",i++);
sleep(1);
}
rmmap.c
int fd=open("./mmap.txt",O_RDONLY);
char* ptr=(char*)mmap(NULL,1024,PORT_READ,MAP_SHARED,fd,0);
if(ptr==MAP_FAILED){
perror("mmap error");
exit(1);
}
int i=0;
while(1){
printf("ptr=%s\n",ptr);
sleep(1);
}
4.消息队列
消息队列就是保存在内核的消息链表,消息队列是面向消息进行通信的,一次读取一条完整的消息,每条消息中还包含一个整数表示优先级,可以根据优先级读取消息。进程A可以往队列中写入消息,进程B读取信息。并且,进程A写入消息后就可以终止,进程B在需要的时候再去读取。
每条信息通常具有以下属性:
1.一个表示优先级的整数
2.消息数据部分的长度
3.消息数据本身
消息队列函数
头文件:
#include<fcntl.h>
#include<sys/stat.h>
#include<mqueue.h>
打开和关闭消息队列
mqd_t mq_open(const char* name,int oflag);
mqd_t mq_open(const char* name,int oflag,mode_t mode,struct mq_attr *attr);
int mq_close(mqd_t mqdes);
获取和设置消息队列属性
int mq_getattr(mqd_t mqdes,struct mq_attr *attr);
int mq_setattr(mqd_t mqdes,const struct mq_attr *newattr,struct mq_attr *oldattr);
在队列中写入和读取一条消息:
int mq_send(mqd_t mqdes,const char *msg_ptr,size_t msg_len,unsigned intmsg_prio);
ssize_t mq_receive(mqd_t mqdes,char *msg_ptr,size_t msg_len,unsignde int *msg_prio);
删除消息队列名
int mq_unlink(const char* name);
函数参数和返回值
name:消息队列名
oflag:打开方式,类似open函数
必选项:O_RDONLY,O_WRONLY,O_RDWR
可选项:O_NONBLOCK,O_CREAT,O_EXCL
mode:访问权限,oflag中含有O_CREAT且消息队列不存在时提供该参数
attr:队列属性,open时传NULL表示默认属性
mqdes:表示消息队列描述符
msg_ptr:指向缓冲区的指针
msg_len:缓冲区的大小
msg_prio:消息优先级
返回值:
成功返回0,open返回消息队列的描述符,mq——receive返回成功写入的字节数
失败返回-1
写程序wmsg.c
mqd_t mqd=mq_open("/mymsg",O_RDWR|O_CREAT,0664,NULL);
if(mqd==-1){
perror("mq_open error");
exit(1);
}
int ret=mq_send(mqd,argv[1],strlen(argv[1])+1,atoi(argv[2]));
if(ret==-1){
perror("mq_send error");
exit(1);
}
读程序rmsg.c
mqd_t mqd=mq_open("/mymsg",O_RDONLY);
if(mqd==-1){
perror("mq_open error");
exit(1);
}
char bud[8192];
unsigned int prio;
int ret=mq_receive(mqd,buf,size(buf),&prio);
if(ret==-1){
perror("mq_send error");
exit(1);
}
printf("buf=%s,prio=%d\n",buf,prio);
查看消息队列的程序msg.c
mqd_t mqd=msg_open("/mymsg",O_RDONLY);
if(mqd==-1){
perror("mq_open error");
exit(1);
}
struct mq_attr attr;
mq_getattr(mqd,&attr);
printf("mq_maxmsg=%lu\n",attr.mq_maxmsg);
printf("mq_msgsize=%lu\n",attr.mq_msgsize);
printf("mq_curmsg=%lu\n",attr.mq_curmsgs);
5.总结
管道:
优点:
1.简单易用:对于具有亲缘关系的进程通信,管道是一种非常简单直接的方式。它的实现机制相对比较容易理解,只需要通过系统调用pipe()或mkfifo()就可以建立通信管道,然后使用read()和write()函数进行数据读写。
2. 高效性:管道的通信是基于内存缓冲区的,数据的传输速度相对较快,在数据量不是特别大的情况下,能够很好地满足进程间通信的需求。
缺点:
1.半双工限制:管道只能单向传输数据。如果要实现双向通信,需要创建两个管道,增加了系统的复杂性。
2.数据量限制:管道的缓冲区大小是有限的,如果写入管道的数据量超过了缓冲区的大小,写入操作可能会被阻塞,直到有足够的空间来写入数据。
3. 生命周期限制:匿名管道随着创建它的进程结束而结束,命名管道虽然可以在不相关的进程之间通信,但是也需要正确地管理其生命周期,否则可能会出现问题。
内存映射
优点:
1.高效的数据访问:内存映射避免了传统的通过系统调用进行数据读写的方式,减少了数据在用户空间和内核空间之间的复制次数。进程可以像访问内存一样访问文件内容,对于大数据量的读写操作效率更高。
2.共享方便:多个进程可以方便地共享同一个内存映射区域,无论是共享一个文件还是共享一段内存,都可以通过简单的内存操作来实现数据的共享和传递。
缺点:
1.复杂性:内存映射的实现相对复杂,需要对虚拟内存管理、文件系统等有一定的了解。在使用内存映射时,需要正确地处理映射区域的大小、偏移量等参数,否则可能会出现错误。
2.同步问题:当多个进程同时访问内存映射区域时,需要考虑同步问题。如果没有合适的同步机制,可能会导致数据不一致或者竞争条件的出现。例如,两个进程同时对同一个内存映射区域进行写操作,可能会导致数据混乱。
消息队列
优点:
1.异步通信:消息队列允许进程以异步的方式进行通信。发送进程可以将消息发送到消息队列后继续执行其他任务,接收进程可以在自己方便的时候从消息队列中接收消息,这样可以提高系统的整体效率。
2.解耦性好:进程之间通过消息队列进行通信,彼此之间的依赖关系比较松散。发送进程不需要知道接收进程的具体细节,只要按照约定的消息格式发送消息即可。而且消息队列可以在多个不相关的进程之间传递消息,增强了系统的灵活性。
3. 消息选择:接收进程可以根据消息类型有选择地接收消息,这样可以对消息进行分类处理,提高了通信的灵活性。
缺点:
1.额外开销:消息队列的实现需要内核的支持,包括消息的存储、管理等,这会带来一定的系统开销。相比管道等简单的通信方式,消息队列的创建和维护成本更高。
2.可能的消息丢失:在一些情况下,如消息队列已满或者系统出现故障时,可能会出现消息丢失的情况。虽然可以通过一些机制来尽量避免消息丢失,如持久化消息等,但这也会增加系统的复杂性。
标签:操作系统,映射,int,通信,管道,mq,内存,Linux,进程 From: https://blog.csdn.net/gemluoye/article/details/144168420