Linux IPC之管道通信
2017-04-07
管道通信在linux中使用较为频繁的进程通信机制。基于unix一切皆文件的传统,管道也是一种文件。所以可以使用一般的VFS接口对管道进行读写操作,如read、write。具体管道分为有名管道和无名管道。无名管道的使用场景较为局限,仅仅限制在有亲缘关系的进程之间通信,多由于父子进程。而有名管道使用就广泛一些,可以在任何有权限的进程之间进行通讯。而这正是有其本质的实现机制所导致的。
一、无名管道
在linux中,管道的实现没有具体的数据结构,而是借助了文件系统的file结构和VFS的inode节点,在之前的文件系统章节介绍过,file结构代表进程打开的一个文件,记录着关于本次打开这个文件的动态信息,是局部于进程的。通过将两个file结构指向同一个inode节点,inode节点又指向一个物理页面,实现两个通过两个文件描述符操作同一文件。关于具体文件系统的知识这里不赘述,感兴趣可以参考之前的博文。正如前面所述,文件描述符不是通过open系统调用打开具体的文件获得,而是通过pipe直接通过内核创建的,所以其他进程无法获取文件描述符也就无法使用其进行通信。这在一定程度上保证了通信的安全,但是也限制了其应用场景。
管道的读写函数是pipe_read和pipe_write函数,管道写函数通过将字节复制到inode节点指向的物理内存而写入数据,而管道读函数通过复制物理内存中的字节而读出数据。既然涉及到两个进程操作同一资源,就避免不了使用同步机制,为此,内核使用了锁,等待队列和信号机制。当写进程向管道写入数据时,利用标准库函数write(),系统根据库函数传递的文件描述符,找到文件的file结构,(文件描述符其实是file数组中的索引),根据file结构找到inode节点,在正式写入数据时,必须检查节点中的信息,在满足如下条件时,才能进行实际的复制工作。
- 内存中有足够的空间可容纳所要写入的数据。
- 内存中没有被读程序锁定。
如果满足条件,写函数要先锁定内存,防止读进程干扰,然后从写进程地址空间复制数据到内存。如果不满足,写进程就休眠在节点的等待队列中,而休眠函数会触发内核调度。当条件满足时,读取进程会唤醒写入进程,写入进程收到信号,重新挂入就绪队列,待再次被调度就可以执行写操作。写入完成后,释放锁,此时所有休眠在该节点的读取进程会被唤醒,因为读操作并不是排他的。
管道的读取过程和写入过程类似,这里就不在重复。默认情况下,当管道为空时,读进程会阻塞;而在管道空间不足时,写进程会阻塞。当然,这是可以设置的,具体见fcntl()函数的介绍。
二、有名管道
前面已经介绍到无名管道的局限性,在看无名管道的时候我也在想,为何不直接通过一个磁盘文件来通信,这样还可以任意进程之间的通信,难道当初就是为了避免这样,才设计的无名管道??
和无名管道相反,有名管道真真实实的有名字,即它在磁盘上有自己对应的文件,所以不同的进程也可以通过打开该文件,对其进行读写。这正是克服了无名管道仅用于亲缘进程之间通信的缺点。有名管道的操作方式基于先进先出的原理,所以有称有名管道为FIFO。通过mknod函数可以创建FIFO文件,该文件一旦创建成功,任何具备权限的进程都可以对其执行打开操作,进而发生读写。只是当多个进程操作文件时,也需要同步机制的保障。
三、管道通信面临的问题
1、缓冲区的限制
2、读写速度不一致
由于不管是有名管道和无名管道都不能像普通文件那样任意扩展空间,其通信的数据量就受到限制,linux下缓冲区的大小为1页面即4kb,所以管道比较适合数据量不大的进程通信。而面对读写速度不一致的问题,读进程只能通过阻塞自己来等待,一定程度上也会影响管道通信的效率。
参考资料:
1、《深入分析linux内核源码》