一、管道
管道是最简单,效率最差的一种通信方式。
管道本质上就是内核中的一个内核缓冲区(4k),当进程创建一个管道后,Linux会返回两个文件描述符,一个是写入端的描述符,一个是输出端的描述符,可以通过这两个描述符往管道写入或者读取数据。
注意,操作系统将管道看作一个抽象的文件,但管道并不是普通的文件,管道存在于内核空间中而不放置在磁盘,访问速度更快,但存储量较小,管道是临时的,是随进程的,当进程销毁,所有端口自动关闭,此时管道也是不存在的
匿名管道
匿名管道只适用于具有亲缘关系的进程,如果想要实现两个进程通过管道来通信,则需要让创建管道的进程fork子进程,这样子进程们就拥有了父进程的文件描述符,这样父子进程之间也就有了对同一管道的操作。
父子进程各关闭一个文件描述符。
命名管道
Linux下,采用mkfifo函数创建,可以传入要指定的‘文件名’,然后其他进程就可以调用open方法打开这个特殊的文件,并进行write和read操作(那肯定是字节流对吧)。
注意,命名管道适用于任何进程,除了这一点不同外,其余大多数都与匿名管道相同。
管道的读写行为
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
① 读管道: 1. 管道中有数据,read返回实际读到的字节数。
2. 管道中无数据:
(1) 管道写端被全部关闭,read返回0 (好像读到文件结尾);
(2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
② 写管道:
1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
2. 管道读端没有全部关闭:
(1) 管道已满,write阻塞。
(2) 管道未满,write将数据写入,并返回实际写入的字节数。
管道的优劣
优点:简单,相比信号,套接字实现进程间通信,简单很多。
缺点:1. 只能单向通信,双向通信需建立两个管道。
2. 只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用fifo有名管道解决。
int pipe(int pipefd[2]); //成功:0;失败:-1,设置errno
long fpathconf(int fd, int name);//成功:返回管道的大小 失败:-1,设置errno
二、消息队列
1.创建或访问一个消息队列
int msgget(key_t key, int msgflag);
参数:
key:某个消息队列的名字,用ftok()产生,消息队列为PIC资源,该key标识了此消息队列,如果传入key存在,则返回对应消息队列ID,否则,创建并返回消息队列ID。
msgflag:有两个选项IPC_CREAT和IPC_EXCL
单独使用IPC_CREAT,如果消息队列不存在则创建之,如果存在则打开返回;
单独使用IPC_EXCL是没有意义的,两个同时使用,如果消息队列不存在则创建之,否则出错返回。
返回值:成功返回一个非负整数,即消息队列的标识码,失败返回-1
2.把一条消息添加到消息队列中
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
msgid:由msgget函数返回的消息队列标识码
msgp:指针指向准备发送的消息
msgze:msgp指向的消息的长度(不包括消息类型的long int长整型)
msgflg:默认为0
返回值:成功返回0,失败返回-1
3.是从一个消息队列接受消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:与msgsnd相同
返回值:成功返回实际放到接收缓冲区里去的字符个数,失败返回-1
优点:
- 与管道不同,消息队列的生命周期随内核,不会随进程销毁而销毁,需要我们显示的调用接口删除或使用命令删除。
- 消息队列可以双向通信。
- 克服了管道只能承载无格式字节流的缺点。
缺点:
- 每个消息体有一个最大长度的限制,并且队列所包含消息体的总长度也是有上限的
- 消息队列通信过程中存在用户态和内核态之间的数据拷贝问题。进程往消息队列写入数据时,会发送用户态拷贝数据到内核态的过程,同理读取数据时会发生从内核态到用户态拷贝数据的过程。
三、共享内存
共享内存解决了消息队列存在的内核态和用户态之间数据拷贝的问题。 现代操作系统对于内存管理采用的是虚拟内存技术,也就是说每个进程都有自己的虚拟内存空间,虚拟内存映射到真实的物理内存。共享内存的机制就是,不同的进程拿出一块虚拟内存空间,映射到相同的物理内存空间。
当使用共享内存的通信方式,如果有多个进程同时往共享内存写入数据,有可能先写的进程的内容被其他进程覆盖了。
因此需要一种保护机制,信号量本质上是一个整型的计数器,用于实现进程间的互斥和同步。
信号量代表着资源的数量,操作信号量的方式有两种:
P操作:这个操作会将信号量减一,相减后信号量如果小于0,则表示资源已经被占用了,进程需要阻塞等待;如果大于等于0,则说明还有资源可用,进程可以正常执行。
V操作:这个操作会将信号量加一,相加后信号量如果小于等于0,则表明当前有进程阻塞,于是会将该进程唤醒;如果大于0,则表示当前没有阻塞的进程。
四、存储映射I/O
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
返回:成功:返回创建的映射区首地址;失败:MAP_FAILED宏
参数:
addr: 建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL
length: 欲创建映射区的大小
prot: 映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags: 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
MAP_SHARED: 会将映射区所做的操作反映到物理设备(磁盘)上。
MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。
fd: 用来建立映射区的文件描述符
offset: 映射文件的偏移(4k的整数倍)