Linux作为一个开源的操作系统,是我们进行操作系统和提高编程水平的最佳途径之一。
好的程序如同好的音乐一样,完成的完美、巧妙。开放源码的程序都是经过无数人检验地,本文将以linux-kernel-
2.6.5为例对pipe的工作机制进行阐述。
一、
进程间通信的分类
大型程序大多会涉及到某种形式的进程间通信,一个较大型的应用程序设计成可以相互通信的“碎片”,从而就把一个任务分到多个进程中去。进程间通信的方法有
三种方式:
管道(pipe)
套接字(socket)
System v IPC 机制
管道机制在UNIX开发的早期就已经提供了,它在本机上的两个进程间的数据传递表现的
相当出色;套接字是在BSD(Berkeley Software Development)中出现的,现在的应用也相当的广泛;而System V
IPC机制Unix System V 版本中出现的。
二、工
作机制
管道分为pipe(无名管道)和FIFO(命名管道),它们都是通过内核缓冲区按先进先出的方式数据传输,管道一端顺序地写入数据,另一端顺序地读入数据
读写的位置都是自动增加,数据只读一次,之
后就被释放。在缓冲区写满时,则由相应的规则控制读写进程进入等待队列,当空的缓冲区有写入数据或满的缓冲区有数据读出时,就唤醒等待队列中的写进程继续
读写。
管道的读写规则:管道两端可分别用描述字
fd[0]以及
fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字
fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字
fd[1]来
表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的
I/O函
数都可以用于管道,如
close、
read、
write等等。
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
- 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
- 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立
门户,单独构成一种文件系统,并且只存在与内存中。
- 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从
缓冲区的头部读出数据。
从管道中读取数据:
- 如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0;
- 当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不大于
PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据
量)。注:(PIPE_BUF在include/linux/limits.h中定义,不同的内核版本可能会有所不同。Posix.1要求
PIPE_BUF至少为512字节,red hat 7.2中为4096)。
四、pipe的
数据结构
首先要定义一个文件系统类型:
pipe_fs_type。
fs/pipe.c
static struct
file_system_type pipe_fs_type = {
.name = "pipefs",
.get_sb = pipefs_get_sb,
.kill_sb =
kill_anon_super,
};
变量
pipe_fs_type其类型是
struct
file_system_type 用于向系统注册文件系统。
Pipe以类似文件的方式与进程交互,但在磁盘上无对应节点,因此效率较高。
Pipe主要包括一
个
inode和两个
file结构——分别用
于读和写。
Pipe的缓冲区首地址就存放在
inode的
i_pipe域指向
pipe_inode_info结
构中。但是要注意
pipe的
inode并没
有磁盘上的映象,只在内存中交换数据。
static
struct super_block *pipefs_get_sb(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data)
{
return get_sb_pseudo(fs_type, "pipe:", NULL, PIPEFS_MAGIC);
}
上为超级的生成函数。
nclude/linux/pipe.h
#ifndef _LINUX_PIPE_FS_I_H
#define
_LINUX_PIPE_FS_I_H
#define PIPEFS_MAGIC 0x50495045 struct pipe_inode_info {
wait_queue_head_t wait; //1
char
*base; //2
unsigned int len; //3
unsigned int start;
//4
unsigned int readers; //5
unsigned int
writers; //6
unsigned int waiting_writers; //7
unsigned int r_counter;
//8
unsigned int
w_counter; //9
struct fasync_struct *fasync_readers; //10
struct fasync_struct
*fasync_writers; //11
}; 2
管道等待队列指针wait
3 内核缓冲区基地址base
4 缓冲区当前数据量
6 管道的读者数据量
7 管道的写者数据量
8 等待队列的读者个数
9 等待队列的写者个数
11、12 主要对 FIFO
五、管道的创建:
通过pipe系统调用来创建管道。
int
do_pipe(int *fd)
{
struct qstr this;
char name[32];
struct dentry *dentry;
struct inode * inode;
struct file *f1, *f2;
int error;
int i,j;
error = -ENFILE;
f1 =
get_empty_filp();//分配文件对象,得到文件对象指针用于读管道
if (!f1)
goto no_files;
f2 =
get_empty_filp();//分配文件对象,得到文件对象指针用于读管道
if (!f2)
goto close_f1;
inode = get_pipe_inode(); //调用get_pipe_inode获得管道类型的索引节点的指针inode
if (!inode)
goto
close_f12;
error = get_unused_fd();
//获得当前进程的两个文件描述符。在当前的
if (error <
0) //进程的进程描述符file域中,有一个fd 域,
goto close_f12_inode; //指向该进程当前打开文件指针数组,数组
i=error; //元素是指向文件对象的指针。
error = get_unused_fd();
if (error < 0)
goto close_f12_inode_i;
j =
error;
error =
-ENOMEM;
sprintf(name, "[%lu]", inode->i_ino);
//生成对象目录dentry,
this.name =
name; //并通过它将上述两个文
this.len = strlen(name); //件对象将的指针与管道
this.hash = inode->i_ino; /* will go */ //索引节点连接起来。
dentry = d_alloc(pipe_mnt->mnt_sb->s_root, &this);
if (!dentry)
goto close_f12_inode_i_j;
dentry->d_op = &pipefs_dentry_operations;
d_add(dentry, inode);
f1->f_vfsmnt =
f2->f_vfsmnt = mntget(mntget(pipe_mnt));
f1->f_dentry = f2->f_dentry = dget(dentry);
f1->f_mapping = f2->f_mapping = inode->i_mapping;
/* read file */
f1->f_pos = f2->f_pos =
0; //为用于读的两个文件对象设置属性值
f1->f_flags =
O_RDONLY; //f_flage设置为只读,f_op设置为
f1->f_op =
&read_pipe_fops; //read_pipe_fops 结构的地址。
f1->f_mode = 1;
f1->f_version = 0;
/* write file */ //为用于写的两个文件对象设置属性值
f2->f_flags = O_WRONLY; //f_flage设置为只写,f_op设置为
write_pipe_fops 结构的地址。
f2->f_op =
&write_pipe_fops;
f2->f_mode = 2;
f2->f_version = 0;
fd_install(i,
f1);
fd_install(j, f2);
fd[0] =
i; //将两个文件描述符放入参数fd数组返回
fd[1] = j;
return 0;
close_f12_inode_i_j:
put_unused_fd(j);
close_f12_inode_i:
put_unused_fd(i);
close_f12_inode:
free_page((unsigned long) PIPE_BASE(*inode));
kfree(inode->i_pipe);
inode->i_pipe = NULL;
iput(inode);
close_f12:
put_filp(f2);
close_f1:
put_filp(f1);
no_files:
return error;
}
六、管道的释放
管道释放时f-op的release域在读管道和写管道中分别指向
pipe_read_release()和pipe_write_release()。而这两个函数都调用release(),并决定是否释放pipe的
内存页面或唤醒该管道等待队列的进程。
以下为管道释放的代码:
static int
pipe_release(struct inode *inode, int decr, int decw)
{
down(PIPE_SEM(*inode));
PIPE_READERS(*inode)
-= decr;
PIPE_WRITERS(*inode) -= decw;
if (!PIPE_READERS(*inode) && !PIPE_WRITERS(*inode)) {
struct pipe_inode_info *info = inode->i_pipe;
inode->i_pipe = NULL;
free_page((unsigned long)
info->base);
kfree(info);
}
else {
wake_up_interruptible(PIPE_WAIT(*inode));
kill_fasync(PIPE_FASYNC_READERS(*inode), SIGIO,POLL_IN);
kill_fasync(PIPE_FASYNC_WRITERS(*inode), SIGIO, POLL_OUT);
}
up(PIPE_SEM(*inode));
return 0;
}
七、
管道的读写
1.
从管道中读取数据:如果管道的写端不存在,则认为已经读到了数据的
末尾,读函数返回的读出字节数为0; 当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果
请求的字节数目不大于PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求
的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。2.
向管道中写入数据: 向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管
道缓冲区中的数据,那么写操作将一直阻塞。- 注:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信
号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。
八、管道的局限性
管
道的主要局限性正体现在它的特点上:
只支持单向数据流;
只能用于具有亲缘关系的进程之间;
没
有名字;
管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓
冲区分配一个页面大小);
管道所传送的是无格式字节流,这就要求管道的读
出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等。
九、后记
写完本文之后,发现有部分不足之处。在由于管道读写的代码过于冗长,限于篇幅不一一列出。有不足和错误之处还请各位老师指正。通过一段时间对
Linux的内核代码的学习,开源的程序往往并非由“权威人士”、“享誉海内外的专家”所编写,它们的由一个
个普通的程序员写就。但专业造就专家,长时间集中在某个领域中能够创建出据程序员应该珍视的财富。
完成之时特别感谢我的搭档周欣和张博的大力支持和帮助。