Linux管道和系统调用pipe()
一、管道通信
父进程和子进程之间,或者两个兄弟进程之间,可以通过系统调用建立起一个单向的通信管道。但是这种管道只能由父进程开建立,对于子进程来说是静态的,与生俱来的。管道两端的进程各自都将该管道视作一个文件。一个进程写,另一个进程读。并且,通过管道传递的内容遵循“先入先出”(FIFO)的原则。每个管道都是单向的,需要双向通信时就要建立两个管道。
二、系统调用pipe()
管道机制的主体是系统调用pipe(),但是由pipe()所建立的管道的两端都在同一进程中,所以必须在fork的配合下,才能在父子进程之间或者两个子进程之间建立起进程间的通信管道。由于管道两端都是以(已打开)文件的形式出现在相关的进程中,在具体实现上也是作为匿名文件来实现的。所以pipe()的代码与文件系统密切相关。
asmlinkage int sys_pipe(unsigned long * fildes){
int fd[2];
int error;
error = do_pipe(fd);
if(!error){
if(copy_to_user(fildes, fd, 2 * sizeof(int)))
error = -EFAULT;
}
return error;
}
这里由do_pipe()建立起一个管道,通过作为调用参数的数组fd[]返回代表着管道两端的两个已经打开文件号,,再由copy_to_user()将数组fd[]复制到用户空间。显然,do_pipe是这个系统调用的主题。
三、管道两端是否可以共享同一个file数据结构
在文件系统中,进程对每个已经打开文件的操作都是通过一个file数据结构进行的,只有在 由同一进程按照相同模式打开同一文件 时才共享同一个数据结构。一个管道实际上就是一个无形(只存在于内存中)的文件,对这个文件的操作要通过两个已经打开的文件进行,分别代表该管道的两端。虽然最初创建时一个管道的两端都在同一进程中,但是在实际使用时却总是分别在两个不同的进程。,所以,管道的两端不能共享同一个file数据结构。而要为止各分配一个file数据结构。
四、管道为什么需要inode结构?
每个文件都是有一个inode数据结构代表的。虽然一个管道实际上是一个无形的文件。但是也得有一个inode数据结构。由于这个文件在创建管道之前并不存在,所以需要在创建管道时临时创建一个inode结构(调用get_pipe_inode()函数)。
五、关于pipe_new()函数
pipe_new()函数,先分配一个内存页面用做管道的缓冲区,再分配一个缓冲区用作pipe_inode_info数据结构。为什么要这么做?用来实现管道的文件是无形的,它并不出现在磁盘或者其他的文件系统存储介质上,而只存在于内存空间,其他进程也无法“打开”或者访问这个文件。所以,这个所谓文件实质上只是一个用作缓冲区的内存页面,只是把它纳入了文件系统的机制,借用了文件系统的各种数据结构和操作加以管理。
六、对于管道的操作
inode数据结构中有个重要的成分i_fop,是指向一个file_operations数据结构的指针。这个数据结构中给出了用于该文件的每种操作的函数指针。对于管道来说,这个数据结构是rdwr_pipe_fops:
struct file_operations rdwr_pipe_fops = {
llseek: pipe_lseek,
read: pipe_read,
write: pipe_write,
poll: pipe_poll,
ioctl: pipe_ioctl,
open: pipe_rdwr_open,
release:pipe_rdwr_release,
};
函数get_pipe_inode()中分配了inode结构以后,进行了一些初始化操作。但是,代码中并没有设置inode结构中的inode_operations结构指针i_op,所以该指针为0.可见,对于用来实现管道的inode并不允许对这里的inode进行常规操作,只有当inode代表着“有形”的文件时才使用。
对于管道是否必须有目录项的问题:在正常情况下,每个文件都至少有一个“目录项”,代表着这个文件的路径名;而每个目录项则只描述一个文件,在denty数据结构中有个指针指向相应的inode结构。因此,在file数据结构中有个指针f_dentry指向做打开文件的目录项dentry数据结构,这样,从file结构开始就可以一路通道文件inode结构。但是,对于管道,文件是无形的,本来并非得有个目录项不可。可是,在file数据结构中并没有直接指向相应inode结构的指针,一定要经过一个目录项转换一下才行。然而inode结构又是各种文件操作的枢纽,所以对于管道也得有一个目录项了。故调用d_alloc()函数分配一个目录项,然后通过d_add()使已经分配的inode结构,与这个函数挂上钩,并且让两个已经打开文件结构中的f_entry()指针都指向这个目录。另外,对于管道的目录项的操作只允许删除操作。
对于管道的两端来说,管道是单向的,所以一端设置成只读,另一端设置成只写。同时两端的文件操作也分别设置成read_pipe_fops,write_pipe_fops,结构如下:
struct file_operations read_pipe_fops = {
llseek: pipe_lseek,
read: pipe_read,
write: bad_pipe_w,//pipe_write,
poll: pipe_poll,
ioctl: pipe_ioctl,
open: pipe_read_open,
release:pipe_read_release,
};
struct file_operations write_pipe_fops = {
llseek: pipe_lseek,
read: bad_pipe_r,//pipe_read,
write: pipe_write,
poll: pipe_poll,
ioctl: pipe_ioctl,
open: pipe_write_open,
release:pipe_write_release,
};
其中,bad_pipe_w()和bad_pipe_r()函数分别用来返回错误代码。是的文件只支持读或者写。
6.1怎么做到进程间通信?
管道的两端在创建之初都在同一个进程中,显然起不到通信的作用。怎样才能将管道用于进程间通信呢
1)进程A创建一个管道,创建完成时代表管道两端的两个已打开文件都在进程A中(下图所示:父进程创建管道)
2)进程A通过fork()创建出进程B,在fork()的过程中进程A大打开文件表按照原样拷贝到进程B中(下图所示:父子进程共享管道)
3)A关闭管道的读端,而B关闭进程的写端。管道的写端在进程A中,而进程的读端在进程B中,成为父子进程之间的通信管道(下图:父子管道通过管道单向通讯)。
4)进程A又通过fork()创建进程C,然后关闭其管道写端而与管道脱离关系。使得管道的写端在进程C中,读端在进程B中。成为两个兄弟进程之间的管道(如下图所示,兄弟进程之间通过管道单向通讯)、
5)进程C和B各自通过exec()执行各自的目标程序,并通过管道进行单向通信、
6.2命名管道
由于管道是一种无形,无名的文件,它就只能通过fork()的过程创建在“近亲”的进程之间,而不可能成为可以在任意两个进程之间建立通信的机制,更不可能成为一般的,通用的进程间通信模型。同时,管道机制的这种缺点本身就强烈地暗示着人们,只要用有名,有形的文件来实现管道,就能克服这种缺点。这里所谓的有名是指这样一个文件应该有文件名,使得任何进程都可以通过文件名或者路径名与这个文件挂上钩;在这里,“有形”是指文件的inode应该存在于磁盘或者其他文件系统上,使得任何进程在任意时间(不仅仅是在fork()时)都可以建立(或断开)与这个文件的联系。所以命名管道的出现时必然的。
为了实现“命名管道”,在“普通文件”,“块设备文件”,“字符设备文件”之外由设立了一种文件类型,称为“FIFO”文件。对于这种文件的访问,严格遵循先进先出的原则,而不允许有在文件内移动读写指针位置的lseek()操作。这样一来,就可以像在磁盘上建立一个文件一样地建立一个命名管道,具体可以使用mkmod命令建立:
$mkmod mypipe p
这里的参数p表示所建立的节点(也就是特殊文件)的类型为命名管道。
建立了这样的节点以后,有关进程就可以想打开一个文件一样“打开”与这个命名管道的联系。对于FIFO文件上的操作可以通过下列几个file_operations数据结构确定:
struct file_operations read_fifo_fops = {
llseek: pipe_lseek,
read: pipe_read,
write: bad_pipe_w,//pipe_write,
poll: fifo_poll,
ioctl: pipe_ioctl,
open: pipe_read_open,
release:pipe_read_release,
};
struct file_operations write_fifo_fops = {
llseek: pipe_lseek,
read: bad_pipe_r,//pipe_read,
write: pipe_write,
poll: fifo_poll,
ioctl: pipe_ioctl,
open: pipe_write_open,
release:pipe_write_release,
};
struct file_operations rdwr_fifo_fops = {
llseek: pipe_lseek,
read: pipe_read,
write: pipe_write,
poll: fifo_poll,
ioctl: pipe_ioctl,
open: pipe_rdwr_open,
release:pipe_rdwr_release,
};
从上面代码,对照一下用于普通管道的数据结构read_pipe_fops,write_pipe_fops和rdwr_pipe_fops就可以看出它们几乎是完全一样的。fifo_poll和pipe_poll()都用于select()系统调用,与通信机制本身没有多大关系,这里我们并不关心。所不同的是,对于普通管道虽然也定义了相当于open()的操作pipe_read_open(),pipe_write和pipe_rdwr_open(),但是这些函数实际上在典型的应用中是不使用的。普通管道是通过do_pipe()建立,通过fork()的过程延伸到两个进程之间的。对于父进程,在系统调用fork()以后就已经打开,而对于子进程来说则是与生俱来的,所以都不需要再打开。然而,命名管道就不同了参加通信的进程确实要调用这些函数来“打开通向已经建立在文件系统中的FIFO文件的通道。
既然普通管道和命名管道的不同之处仅仅在于“”打开“”的过程,那么我们来分析一下,一个进程是怎样通过open()系统调用来建立与一个已经创建的FIFO文件之间的联系的。在文件系统中,进程在内核中由sys_open()进入file_open(),然后在open_namei()中调用一个函数path_walk(),根据文件的路径名在文件系统中找到代表这个文件的inode,在将磁盘上inode读入内存时,要根据文件的类型(FIFO文件的S_FIFO标志位1),将inode中的i_op和i_fop指针设置成指向相应的inode_operations数据结构和file_operations数据结构,但是对于FIFO这样的特殊文件则调用init_special_inode()来加以初始化。
6.3对FIFO文件打开的三种模式
FIFO文件可以按照三种不同的模式打开,就是“只读”,“只写”,“读写”。同时,在系统调用open()中还有个参数flags。如果flags的标志位O_NONBLOCK为1,就表示在打开的过程中即使某些条件得不到满足也不需要等待,而应立即返回。
在典型的应用中,就相对普通管道一样,一个进程按照只读模式打开命名通道,成为“消费者”;另一个进程按照只写模式打开命名管道,成为“生产者”。可是在普通管道的情况下,管道的两端是由同一进程do_pipe()中同时打开的,而在命名管道的情况下则管道的两端通常分别由两个进程先后打开,这就有了个“同步”的问题。除此之外,还有个不同,就是普通管道既然是“无名”,“无形”的,一般就不会由另一个进程也来打开这个管道。而在命名管道的情况下,任意一个进程都可以通过相同的路径名打开同一个FIFO文件。这些因素都使建立命名管道的过程比建立普通管道的过程要复杂一些。
先来看命名管道的“读端”,也就是按照“只读”模式打开一个FIFO文件时的几种情况:
1)如果管道的写端已经打开,那么现在读端的打开就完成了命名管道的建立过程,在这种情况下,写端的进程,也就是“生产者”进程一般都是正在睡眠中,等待着命名管道建立过程的完成,所以要将其唤醒。然后,两个进程差不多同时返回各自的用户空间,然后就可以通过这个命名管道进行通信了。
2)如果命名管道的写端尚未打开,而flags中的O_NONBLOCK标志位为1,表示不应该等待。此时读端虽已打开,但是命名管道只是部分建立了(写端未打开)。而标志的使用又要求系统调用不加等待立即返回,所以不做等待。
3)如果命名管道的写端尚未打开,而flags中的O_NONBLOCK标志位为0,。在这种情况下,读端的打开只是完成了命名管道建立过程的一半,所以消费者进程要通过wait_for_parter()进入睡眠,等待某个生产者进程来打开命名管道的写端已完成其建立过程。
相应的,命名管道的写端的打开也有以下几种不同的情况(只写):
1)如果命名管道的读端已经打开,那么写端的打开就完成了命名管道的建立过程。在这种情况下,命名管道读端的进程(“消费者进程”)有可能正在睡眠中等待,所以,如果当前进程是第一次打开该管道写端的进程,就要负责将其唤醒。
2)如果命名管道的读端尚未打开,而flags中的O_NONBLOCKED标志位为0。在这种情况下,生产者进程要睡眠等待至消费者进程打开命名管道的读端才可以返回。
3)如果命名管道的读端尚未打开,而flags中的O_NONBLOCKED标志位为1。此时对命名管道写端打开失败,所以要释放已经分配的各种资源并且返回-1.
对命名管道文件“读写”打开
此时相当于有一个进程同时打开了命名管道的两端,所以不管怎样都不需要等待。但是还有可能已经有某个进程已经打开了写端或者读端而正在睡眠等待,所以只要有任意一端是第一次打开,也就唤醒了,正在睡眠等待的进程。
命名管道一经建立们以后的读写以及关闭操作就和普通进程完全相同了。注意虽然FIFO文件的inode节点在磁盘上,但那只是一个节点,而文件的数据则只存在于内存缓冲页面中,与普通管道一样。