Linux进程间通讯
Linux进程间通讯(三)管道
一、匿名管道
匿名管道通过系统调用 pipe 创建,其定义如下
int pipe(int fd[2])
通过这个系统调用,可以在内核中创建一个管道,返回两个文件描述符,表示管道的两端,一个用于读,一个用于写
下面看一看内核时如何实现的
SYSCALL_DEFINE1(pipe, int __user *, fildes)
{
return sys_pipe2(fildes, 0);
}
SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)
{
struct file *files[2];
int fd[2];
int error;
error = __do_pipe_flags(fd, files, flags);
if (!error) {
if (unlikely(copy_to_user(fildes, fd, sizeof(fd)))) {
......
error = -EFAULT;
} else {
fd_install(fd[0], files[0]);
fd_install(fd[1], files[1]);
}
}
return error;
}
sys_pipe 调用了 sys_pipe2
这里面有一个 struct file 数组,用来存放管道的两端的打开文件。还有一个 fd 数组,用于存放管道两端对应的文件描述符
如果 __do_pipe_flags 成功返回,那么就调用 fd_install,将 两个 fd 和两个 struct file 关联起来
下面我们好好分析 __do_pipe_flags 函数,其定义如下
static int __do_pipe_flags(int *fd, struct file **files, int flags)
{
int error;
int fdw, fdr;
......
error = create_pipe_files(files, flags);
......
error = get_unused_fd_flags(flags);
......
fdr = error;
error = get_unused_fd_flags(flags);
......
fdw = error;
fd[0] = fdr;
fd[1] = fdw;
return 0;
......
}
这里面调用 create_pipe_files 创建管道文件,然后生成两个 fd,从代码可以看出,fd[0] 用于读,fd[1] 用于写
我们再来看看 create_pipe_files 是如何创建管道文件的,其定义如下
int create_pipe_files(struct file **res, int flags)
{
int err;
struct inode *inode = get_pipe_inode();
struct file *f;
struct path path;
......
path.dentry = d_alloc_pseudo(pipe_mnt->mnt_sb, &empty_name);
......
path.mnt = mntget(pipe_mnt);
d_instantiate(path.dentry, inode);
f = alloc_file(&path, FMODE_WRITE, &pipefifo_fops);
......
f->f_flags = O_WRONLY | (flags & (O_NONBLOCK | O_DIRECT));
f->private_data = inode->i_pipe;
res[0] = alloc_file(&path, FMODE_READ, &pipefifo_fops);
......
path_get(&path);
res[0]->private_data = inode->i_pipe;
res[0]->f_flags = O_RDONLY | (flags & O_NONBLOCK);
res[1] = f;
return 0;
......
}
匿名管道是一种特殊的文件,它在特殊的文件系统上创建,每一个文件都会对应一个 inode,管道文件也不例外,它对应一个特殊的 inode,通过 get_pipe_inode 获取
get_pipe_inode 的定义如下
static struct file_system_type pipe_fs_type = {
.name = "pipefs",
.mount = pipefs_mount,
.kill_sb = kill_anon_super,
};
static int __init init_pipe_fs(void)
{
int err = register_filesystem(&pipe_fs_type);
if (!err) {
pipe_mnt = kern_mount(&pipe_fs_type);
}
......
}
static struct inode * get_pipe_inode(void)
{
struct inode *inode = new_inode_pseudo(pipe_mnt->mnt_sb);
struct pipe_inode_info *pipe;
......
inode->i_ino = get_next_ino();
pipe = alloc_pipe_info();
......
inode->i_pipe = pipe;
pipe->files = 2;
pipe->readers = pipe->writers = 1;
inode->i_fop = &pipefifo_fops;
inode->i_state = I_DIRTY;
inode->i_mode = S_IFIFO | S_IRUSR | S_IWUSR;
inode->i_uid = current_fsuid();
inode->i_gid = current_fsgid();
inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode);
return inode;
......
}
从 get_pipe_inode 的实现,我们可以知道匿名管道来自于一个特殊的文件系统 pipefs,当它挂载后,就会生成对应的 struct vfsmount* pipe_mnt,然后对应的超级块就是 pipe_mnt->mnt_sb。这里通过 new_inode_pseudo 创建一个新的 inode,然后填写 inode
这里面需要的是 struct pipe_inode_info 成员,这个结构中有个成员是 struct pipe_buffer* bufs,这是缓存的意思。由此我们可以想象,所谓的匿名管道,就是内核中的一段缓存
另一个需要注意的是 pipefifo_fops,它定义了管道文件的操作集,定义如下
const struct file_operations pipefifo_fops = {
.open = fifo_open,
.llseek = no_llseek,
.read_iter = pipe_read,
.write_iter = pipe_write,
.poll = pipe_poll,
.unlocked_ioctl = pipe_ioctl,
.release = pipe_release,
.fasync = pipe_fasync,
};
我们回到 create_pipe_files 函数,创建完 inode 之后,还需要创建一个 dentry 和它对应。之后就开始创建 struct file 对象了,先创建用于写入的 struct file,对应的操作为 pipefifo_fops;然后再创建用于读取的 struct file,对应的操作也是 pipefifo_fops。之后再将 private_data 指向 pipe_inode_info,这样子,就可以通过 struct file 来操作管道文件了
至此,一个匿名管道就创建好了。对于写操作,通过 fd[1] 调用 pipe_write,写到 pipe_buffer;对于读操作,通过 fd[0] 调用 pipe_read,从 pipe_buffer 中读取
但是此时,两个文件描述符都在同一个进程里面,现在无法进行进程间通讯,如何实现进程间通讯呢?
通过 fork 系统调用,在 fork 的时候,子进程会将父进程的 struct file_struct 复制一份,所以里面的文件描述符数组也会被复制,所以子进程会复制父进程的管道文件描述符,两个进程相应的文件描述符都会指向相同的 struct file,所以父子进程通过各自的 fd 写入或者读取,操作的都是同一个管道文件,如下图所示
由于管道只能一端读取一端写入,所以一个管道是单向的,一般如果我们需要需要父进程写,子进程读,那么就需要让父进程把读端的文件描述符关闭,让子进程把写端的文件描述符关闭,如下图所示
在Linux命令行中,我们也经常使用管道,如下面命令
find -name "*.cpp" | grep test
这个命令是将 find 的查找结果输出给 grep,让 grep 查找,如果能够让 find 的结果输出给 grep 呢?
find 是一个程序,grep 也是一个程序,运行的时候,它们分别对应两个进程,我们在 shell 中输入的命令,所以 这两个进程都是由 shell 创建的
那么既然是两个进程,它们之间就需要通过进程间通讯的方式,这里使用的就是匿名管道的技术
具体是如何实现的呢?
首先 shell 创建一个进程运行程序,创建匿名管道,得到读端和写端的文件描述符,然后创建子进程,子进程对应 find 程序,父进程对应 grep 程序。在子进程中,将标准输出设置为管道的写端,然后运行 find 程序;在父进程中,将标准输入设置为管道的读端,然后运行 grep 程序。这样子,find 程序的输出结果就会发送到管道文件中,grep 就会从管道文件中读取内容,如下如所示
那么怎么重定向标准输入和标准输出呢?
这就需要 dup2 系统调用了,其定义如下
int dup2(int oldfd, int newfd);
这个系统调用的功能是,将新的文件描述符赋值给老的文件描述符,其实在内核中就是使老的文件描述符对应的 struct file 指向新的文件描述符对应的 struct file
匿名管道讲解到这里,接下讲有名管道
二、有名管道
有名管道,顾名思义,就是在文件系统中有文件名
有名管道可以通过 mkfifo 创建,它不是对应一个系统调用,而是 glibc 提供的一个函数,定义如下
int
mkfifo (const char *path, mode_t mode)
{
dev_t dev = 0;
return __xmknod (_MKNOD_VER, path, mode | S_IFIFO, &dev);
}
int
__xmknod (int vers, const char *path, mode_t mode, dev_t *dev)
{
unsigned long long int k_dev;
......
/* We must convert the value to dev_t type used by the kernel. */
k_dev = (*dev) & ((1ULL << 32) - 1);
......
return INLINE_SYSCALL (mknodat, 4, AT_FDCWD, path, mode,
(unsigned int) k_dev);
}
mkfifo 最终是通过系统调用 mknodat 来实现的,其定义如下
SYSCALL_DEFINE4(mknodat, int, dfd, const char __user *, filename, umode_t, mode, unsigned, dev)
{
struct dentry *dentry;
struct path path;
unsigned int lookup_flags = 0;
......
retry:
dentry = user_path_create(dfd, filename, &path, lookup_flags);
......
switch (mode & S_IFMT) {
......
case S_IFIFO: case S_IFSOCK:
error = vfs_mknod(path.dentry->d_inode,dentry,mode,0);
break;
}
......
}
首先是通过 user_path_create 来创建一个 dentry,然后因为是 S_IFIFO,所以调用的是 vfs_mknod
由于这个文件是创建在 ext4 文件上的,所以会调用 ext4_dir_inode_operations 的 mknod,其定义如下
const struct inode_operations ext4_dir_inode_operations = {
......
.mknod = ext4_mknod,
......
};
static int ext4_mknod(struct inode *dir, struct dentry *dentry,
umode_t mode, dev_t rdev)
{
handle_t *handle;
struct inode *inode;
......
inode = ext4_new_inode_start_handle(dir, mode, &dentry->d_name, 0,
NULL, EXT4_HT_DIR, credits);
handle = ext4_journal_current_handle();
if (!IS_ERR(inode)) {
init_special_inode(inode, inode->i_mode, rdev);
inode->i_op = &ext4_special_inode_operations;
err = ext4_add_nondir(handle, dentry, inode);
if (!err && IS_DIRSYNC(dir))
ext4_handle_sync(handle);
}
if (handle)
ext4_journal_stop(handle);
......
}
#define ext4_new_inode_start_handle(dir, mode, qstr, goal, owner, \
type, nblocks) \
__ext4_new_inode(NULL, (dir), (mode), (qstr), (goal), (owner), \
0, (type), __LINE__, (nblocks))
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops;
inode->i_rdev = rdev;
} else if (S_ISFIFO(mode))
inode->i_fop = &pipefifo_fops;
else if (S_ISSOCK(mode))
; /* leave it no_open_fops */
else
......
}
ext4_mknod 会通过 ext4_new_inode_start_handle 在内存中创建一个 inode,然后调用 init_special_inode,由于是管道文件,所以 inode 的 fop 会被赋值为 pipefifo_fops,这样子管道文件就创建完毕了
接下来要打开这个管道文件,就需要通过 open 系统调用,最后会调用到 pipefifo_fops 的 open 函数,也即 fifo_open,其定义如下
static int fifo_open(struct inode *inode, struct file *filp)
{
struct pipe_inode_info *pipe;
bool is_pipe = inode->i_sb->s_magic == PIPEFS_MAGIC;
int ret;
filp->f_version = 0;
if (inode->i_pipe) {
pipe = inode->i_pipe;
pipe->files++;
} else {
pipe = alloc_pipe_info();
pipe->files = 1;
inode->i_pipe = pipe;
spin_unlock(&inode->i_lock);
}
filp->private_data = pipe;
filp->f_mode &= (FMODE_READ | FMODE_WRITE);
switch (filp->f_mode) {
case FMODE_READ:
pipe->r_counter++;
if (pipe->readers++ == 0)
wake_up_partner(pipe);
if (!is_pipe && !pipe->writers) {
if ((filp->f_flags & O_NONBLOCK)) {
filp->f_version = pipe->w_counter;
} else {
if (wait_for_partner(pipe, &pipe->w_counter))
goto err_rd;
}
}
break;
case FMODE_WRITE:
pipe->w_counter++;
if (!pipe->writers++)
wake_up_partner(pipe);
if (!is_pipe && !pipe->readers) {
if (wait_for_partner(pipe, &pipe->r_counter))
goto err_wr;
}
break;
case FMODE_READ | FMODE_WRITE:
pipe->readers++;
pipe->writers++;
pipe->r_counter++;
pipe->w_counter++;
if (pipe->readers == 1 || pipe->writers == 1)
wake_up_partner(pipe);
break;
......
}
......
}
在 fifo_open 里面,会通过 alloc_pipe_info 创建一个 pipe_inode_info,我们知道这里面有一个 struct pipe_buffer* buf 缓存。所以,所谓的有名管道也是在内存中创建一段缓存
接下来,对于管道的写入,会通过 pipefifo_ops 中的 pipe_write 函数,向 pipe_buffer 中写入数据;对于读取,通过 pipefifo_ops 的 pipe_read 函数,从 pipe_buffer 中读取数据
三、总结
无论是匿名管道还是有名管道,在内存中都是对应一个文件,在内存中对应一个特殊的 inode,它指向内核空间中的一段缓存
inode 中的 file_operations 指向了管道的特殊操作 pipe_fops
当我们打开一个管道的时候,内核会为进程分配 struct file 对象,其中的 inode 就是指向这个管道的 inode,file_operations 就是指向 pipe_fops
读写管道的时候,就是通过 struct file 的 file_operations,也即 pipe_fops,去操作管道对应的缓存