BLOG

Record, summarize, and improve.

pipe

一、pipe特点

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,内核中用于进程间通信的技术有很多,管道是其中的一种。

Image in a image block

有两种类型的管道,一种是匿名管道,也是类Unix系统上传统的管道,用与将一个进程的输出作为另一个进程的输入。另一种则是命名管道(又叫FIFO),是对类Unix系统上匿名管道概念的扩展,并且是进程间通信(IPC)的方法之一

匿名管道仅在过程中持续使用,过程结束后管道会被释放。命名管道则可以在系统启动时一直运行到被删除为止,遵循先进先出机制,是一种的特殊文件,但可以像普通文件一样使用它。如果不再使用它,也可以将其删除。通常,命名管道以文件的形式出现,并且通常会附加用于IPC的进程。命名管道利用了filesystem,可用于将信息从一个应用程序传输到另一个应用程序,而无需使用中间临时文件。

fifo和pipe之间的关系:

  • pipe是匿名管道,只能用于两个拥有pipe读写两端fd的进程通信;
  • fifo在文件系统中有自己的名称,操作fifo与操作普通文件几无差别,可以用于两个没有关系的进程间通信
  • fifo和pipe在kernel层面上都实现在fs/pipe.c中,本质上是一个东西。
  • pipe作为linux文件系统的一部分,与epoll一样,都是在向kernel注册了自己的文件系统,可以使用VFS提供的通用接口,比如open、read和write等操作
  • pipe的容量不是无限大的,早期linux版本中pipe容量只能是4KB大小,新版本可以在运行时动态扩大到64KB

实现机制:

管道是由内核管理的一个缓冲区,其一端连接一个进程的输出,这个进程会向管道中放入信息,而另一端连接一个进程的输入,这个进程取出被放入管道的信息。这个缓冲区不需要很大,因而被设计成为环形数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

二、数据结构

pipe buffer在内存中保存pipe的缓存,其中page为真正内存对应的page结构,offset、len则为page的起始地址偏移和page的长度,即多少个page,而ops则是buffer操作的方法,private指向下面的管道描述符

/* 用来描述pipe所使用的page结构 */
struct pipe_buffer {
	struct page *page;
	unsigned int offset, len;
	const struct pipe_buf_operations *ops;
	unsigned int flags;
	unsigned long private;
};

在pipe_inode_info结构中有很多的限制方法来对pipe进行限制,例如mutex、counter、readers和writers等,都是为了限制对pipe所对应的临界区访问

/* 管道描述符,表示一个管道,存储管道相应的信息 */
struct pipe_inode_info {
	struct mutex mutex;
	wait_queue_head_t rd_wait, wr_wait;
	unsigned int head;
	unsigned int tail;
	unsigned int max_usage;
	unsigned int ring_size;
#ifdef CONFIG_WATCH_QUEUE
	bool note_loss;
#endif
	unsigned int nr_accounted;
	unsigned int readers;
	unsigned int writers;
	unsigned int files;
	unsigned int r_counter;
	unsigned int w_counter;
	struct page *tmp_page;
	struct fasync_struct *fasync_readers;
	struct fasync_struct *fasync_writers;
	struct pipe_buffer *bufs;
	struct user_struct *user;
#ifdef CONFIG_WATCH_QUEUE
	struct watch_queue *watch_queue;
#endif
};

三、内核源码实现

首先在应用层使用pipe会通过pipe系统调用去调用pipefs中的方法,所以首先需要初始化pipefs。需要先创建一个file_system_type对象,然后在初始化的时候注册并挂载。

static struct file_system_type pipe_fs_type = {
    .name       = "pipefs",
    .init_fs_context = pipefs_init_fs_context,
    .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); //挂载
        ...
    }
    return err;
}

注册完pipefs就可以使用其中的方法,定义在

static const struct super_operations pipefs_ops = {
    .destroy_inode = free_inode_nonrcu,
    .statfs = simple_statfs,
};

其方法是释放inode和获取pipefs的状态信息,用于在pipe缓冲使用完毕需要进行回收时使用。pipe的调用都是通过系统调用进而进入内核空间,在内核中由do_pipe2这个函数去完成标志的检测和file描述符的分配,同时在分配file描述符的同时为pipe分配内存空间,pipe缓冲区的分配则是在create_pipe_files函数中,pipe实现在file system的inode、superblock、file基础结构之上,一个pipe就是一个特殊的文件,使用方法与其他文件一样,都需要open,之后去调用其读写方法

static int __do_pipe_flags(int *fd, struct file **files, int flags)
{
    ...
    if (flags & ~(O_CLOEXEC | O_NONBLOCK | O_DIRECT | O_NOTIFICATION_PIPE))
        return -EINVAL;

    error = create_pipe_files(files, flags);
    if (error)
        return error;
    ...
}

在__do_pipe_flags函数中会创建管道并返回读写端文件描述符,首先会检查传进来的flag,O_CLOEXEC、O_NONBLOCK、O_DIRECT、O_NOTIFICATION_PIPE四种标志都可以创建管道,分别代表发生clone、没有映射的block、直接磁盘传输以及无通知的pipe,进而去创建读写端的文件句柄

创建pipe的文件描述符会先通过superblock找到一个inode节点,同时这个inode对应的缓冲区不能发生回写,因为它只存在与内存,并将这个inode标记为dirty,所以说pipe其实是一个特殊的文件,然后分配一个pipe_inode_info,将其初始化并将地址保存在inode的i_pipe成员中

int create_pipe_files(struct file **res, int flags)
{
    ...
    f = alloc_file_pseudo(inode, pipe_mnt, "",
                O_WRONLY | (flags & (O_NONBLOCK | O_DIRECT)),
                &pipefifo_fops);
    f->private_data = inode->i_pipe;
    res[0] = alloc_file_clone(f, O_RDONLY | (flags & O_NONBLOCK),
                  &pipefifo_fops);

    res[0]->private_data = inode->i_pipe;
    res[1] = f;
    ...
}

alloc_file_pseudo函数则会将pipefifo_fops里面的方法与file结构和inode绑定,之后就能够通过open打开文件句柄,并使用其中的方法,file结构中的private_data也会被初始化成inode中的i_pipe,可以看到res[0]和res[1]都绑定了pipefifo_fops,只是res[0]的分配是从res[1]clone过来的,最后指定stream方式的打开,最后还需要检测一下flags,其中fd[0]被设置为读端口,fd[1]设置为写端口

3.2 读写

pipe的读写就是去操作缓冲区中的数据

读:

static ssize_t
pipe_read(struct kiocb *iocb, struct iov_iter *to)
{
    ...
    /* 只有当pipe中的数据已满时并且reader开始读取数据才唤醒writer */
    was_full = pipe_full(pipe->head, pipe->tail, pipe->max_usage);
    for (;;) {
        if (!pipe_empty(head, tail)) {
            struct pipe_buffer *buf = &pipe->bufs[tail & mask];

            /* 最大buffer限制,不能超过这个最大值 */
            if (chars > total_len) {
                chars = total_len;
            }
            /* 验证缓冲区中的内容,是buffer方法中的一种 */
            error = pipe_buf_confirm(pipe, buf);

            written = copy_page_to_iter(buf->page, buf->offset, chars, to);
            /* 包缓冲区则退出 */
            if (buf->flags & PIPE_BUF_FLAG_PACKET) {
                total_len = chars;
                buf->len = 0;
            }
            /* 长度为0则释放pipe buffer, */
            if (!buf->len) {
                pipe_buf_release(pipe, buf);
            }
            /* 总长度减掉 */
            total_len -= chars;
            if (!total_len)
                break;  /* 读取成功直接break */
            if (!pipe_empty(head, tail))    /* 没读取完则再来一次 */
                continue;
        }
        }
        /* 结束,为pipe解锁 */
        __pipe_unlock(pipe);
        /* 仍然满,需要去挂起writer,并将writer的同步模式清除 */
        if (unlikely(was_full)) {
            wake_up_interruptible_sync_poll(&pipe->wr_wait, EPOLLOUT | EPOLLWRNORM);
            kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);
        }
        __pipe_lock(pipe);
        /* 为下一次判断pipe buffer是否为满 */
        was_full = pipe_full(pipe->head, pipe->tail, pipe->max_usage);
        /* 下一次读取标志 */
        wake_next_reader = true;
    }
    /* pipe buffer已经为空,不需要再去读 */
    if (pipe_empty(pipe->head, pipe->tail))
        wake_next_reader = false;
    __pipe_unlock(pipe);

    /* 到这里不论是读还是写都需要挂起 */
    if (was_full) {
        wake_up_interruptible_sync_poll(&pipe->wr_wait, EPOLLOUT | EPOLLWRNORM);
        kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);
    }
    if (wake_next_reader)
        wake_up_interruptible_sync_poll(&pipe->rd_wait, EPOLLIN | EPOLLRDNORM);
    ...
}

读过程就是在一个循环中,将缓冲区中数据通过copy_page_to_iter函数写到用户态空间缓冲区中,需要判断读完成标志和为full状态

static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
    ...
    was_empty = pipe_empty(head, pipe->tail);
    chars = total_len & (PAGE_SIZE-1);
    if (chars && !was_empty) {
        unsigned int mask = pipe->ring_size - 1;
        struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
        int offset = buf->offset + buf->len;

        if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
            offset + chars <= PAGE_SIZE) {
            ret = pipe_buf_confirm(pipe, buf);
            if (ret)
                goto out;

            ret = copy_page_from_iter(buf->page, offset, chars, from);
            if (unlikely(ret < chars)) {
                ret = -EFAULT;
                goto out;
            }

            buf->len += ret;
            if (!iov_iter_count(from))
                goto out;
        }
    }

    for (;;) {

        head = pipe->head;
        if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
            unsigned int mask = pipe->ring_size - 1;
            struct pipe_buffer *buf = &pipe->bufs[head & mask];
            struct page *page = pipe->tmp_page;
            int copied;

            if (!page) {
                page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
                if (unlikely(!page)) {
                    ret = ret ? : -ENOMEM;
                    break;
                }
                pipe->tmp_page = page;
            }

            spin_lock_irq(&pipe->rd_wait.lock);

            head = pipe->head;
            if (pipe_full(head, pipe->tail, pipe->max_usage)) {
                spin_unlock_irq(&pipe->rd_wait.lock);
                continue;
            }

            pipe->head = head + 1;
            spin_unlock_irq(&pipe->rd_wait.lock);

            /* Insert it into the buffer array */
            buf = &pipe->bufs[head & mask];
            buf->page = page;
            buf->ops = &anon_pipe_buf_ops;
            buf->offset = 0;
            buf->len = 0;
            if (is_packetized(filp))
                buf->flags = PIPE_BUF_FLAG_PACKET;
            else
                buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
            pipe->tmp_page = NULL;

            copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
            if (unlikely(copied < PAGE_SIZE && iov_iter_count(from))) {
                if (!ret)
                    ret = -EFAULT;
                break;
            }
            ret += copied;
            buf->offset = 0;
            buf->len = copied;

            if (!iov_iter_count(from))
                break;
        }

        if (!pipe_full(head, pipe->tail, pipe->max_usage))
            continue;

        /* Wait for buffer space to become available. */
        if (filp->f_flags & O_NONBLOCK) {
            if (!ret)
                ret = -EAGAIN;
            break;
        }
        if (signal_pending(current)) {
            if (!ret)
                ret = -ERESTARTSYS;
            break;
        }

        __pipe_unlock(pipe);
        if (was_empty) {
            wake_up_interruptible_sync_poll(&pipe->rd_wait, EPOLLIN | EPOLLRDNORM);
            kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);
        }
        wait_event_interruptible_exclusive(pipe->wr_wait, pipe_writable(pipe));
        __pipe_lock(pipe);
        was_empty = pipe_empty(pipe->head, pipe->tail);
        wake_next_writer = true;
    }
out:
    if (pipe_full(pipe->head, pipe->tail, pipe->max_usage))
        wake_next_writer = false;
    __pipe_unlock(pipe);

    if (was_empty) {
        wake_up_interruptible_sync_poll(&pipe->rd_wait, EPOLLIN | EPOLLRDNORM);
        kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);
    }
    if (wake_next_writer)
        wake_up_interruptible_sync_poll(&pipe->wr_wait, EPOLLOUT | EPOLLWRNORM);
    if (ret > 0 && sb_start_write_trylock(file_inode(filp)->i_sb)) {
        int err = file_update_time(filp);
        if (err)
            ret = err;
        sb_end_write(file_inode(filp)->i_sb);
    }
    return ret;
}

写缓冲区则是先分配page,属于虚拟内存空间,然后将数据写入到这块内存