管道有名管道

之前老师一直说要自己写点东西,我总是想有很多人写了同样的东西,我何必还要再写呢?用的时候搜一搜就可以了。可是最近越来越发现把自己理解的东西写出来还是很有必要的。不为别人启发性的东西,就算是对自己学习过程的一些总结吧。

从今天开始把这条路走好,就算不知有多远,平心静气!


管道与文件系统


管道

1、管道的创建

2、管道的关闭

3、管道的读写

特征:管道通信时半双工的,即就是只能从一个方向到另一个方向传输数据,从一端写入数据从另一端读取数据。若要实现双向通信,就的建立两个方向的管道。管道又分为有名管道和匿名管道。

无名管道:

无名管道机制的主体是pipe系统调用,但pipe系统调用所建立的管道的两端在同一进程中,所以必须在fork的配合下,才能在父子进程间或兄弟进程间通信

创建:Int pipe(int  fd[2]);

pipe创建的两个文件描述符fd[0]fd[1]分别在管道的两端,读进程从fd[0]中读,写进程向fd[1]中写

注:由于管道两端都是已打开的文件的形式出现在相关进程中,在具体实现上也是作为匿名文件来实现,所以pipe的代码与文件系统密切相关

 

 结合2.6.11源码

File_struct结构中有两个最主要的关于文件对象的成员变量。

 Do_pipe()建立一个管道,通过作为调用的参数fd[]返回代表管道两端的两个打开文件号,再由copy_to_user()将数组复制到用户空间。

 Get_empty_filp就是分别为管道两端各分配一个file数据结构。

为什么要分配两个文件描述符????

为什么要创建一个iNode???通常对于普通文每个个文件都有一个iNode节点与之对应,并且通常iNode节点存在于磁盘上。但对于管道文件在创建管道之前并不存在这个文件,所以在创建管道时临时创建起一个iNode节点。实际上创建管道就是要创建这样一个文件

 Pipe_new是在获得iNode结构之后给它分配的的所需要的缓冲区。

 在创建的iNode数据结构中有一个指针i_pipe,它指向pipe_inode_info

 Get_unused_fd找当前进程current中最小的还未被使用的文件描述符

 Do_pipe的后半部分:

 在正常情况下,每个文件都至少有一个目录项,代表这个文件的一个路径名,而每个目录项只对应一个文件【文件和目录项之间的关系为一对多】。在dentry数据结构中有两个指针指向相应的iNode结构,在file中有个指针指向所打开文件的目录项dentry结构。

Do_alloc分配一个目录项D_add是已经分配的iNode与这个命令项项关联,并使两个已打开文件结构中的f_dentry指针都指向这个目录项。

管道是如何做到一端只读,一端只写的???????

对管道的两端来说,管道是单方向的。所以f1端被设置为只读(O_REONLY)【f_flags】,f2端被设置为只写(O_ERONLY)。同时两端文件的操作【f_mode】也分别设置为read_pipe_fopswrite_pipe_fops但是在管道的iNode节点中i_fop设置成指向rdwr_pipe_fops,它是双向的。

注:其实两者并不矛盾。对于代表管道两端的两个已打开文件【两个文件描述符】来说,一个以只读方式打开管道文件、一个以只写方式打开管道。事实上这两个文件描述符指向同一个iNode节点,同一个物理上存在的文件。在文件系统中,file结构中的f_op一般都来自与iNode结构中的i_fop,都指向同一个file_operators结构。而对于管道,则使管道的两端的file结构指向各自不同的file_operators结构,以此来确保他一端只能读,一端只能写。

 File结构中  文件标志。如O_RONLYO_NONBLOCKO_SYNC。为了检查用户请求是否非阻塞式的操作,驱动程序需要检查O_NONBLOCK标志,其他标志较少用到。 检查读写权限应该查看f_mode而不是f_flags

 File结构中 文件模式。FMODE_READFMODE_WRITE分别表示读写权限。

 

 

 

对管道应用的实例:

1、进程A先创建管道

2、A进程通过fork产生子进程。

3、父进程向管道写,子进程从管道读

4、父进程彻底关闭管道,使兄弟进程之间通信

   

 

 

 

管道的关闭

当一个进程调用close关闭代表着管道一端的已打开文件时,在内核中调用  sys_close()>file_close()>fput()。在fput()中将相应的file数据结构中的共享计数减1

从管道中读数据

Sys_read->read_pipe_fops->pipe_read

环形缓冲区:

每个管道只有一个页面作为缓冲区,这个页面作为环形缓冲区来使用,是一种典型的“生产者-----消费者”模式,进程中有大量数据要写入时,每当写入一个页面就需要等待,等待“消费者”从管道中取走数据,腾出空间,相应的,当读进程没有数据可读时,“消费者”进程要睡眠等待。(也有点像队列)

写端依次写入1 2 3 4 5 6 7 8 9 a b,读端读两次,读走1 2

 

 

环形缓冲区要维护写端(write)和读端(read)两个索引。写入数据时,必须先确保缓冲区没有满,然后才能将数据写入,最后将write指针指向下一个元素;读取数据时,首先要确保缓冲区不为空,然后返回read指针对应得元素,最后使read指向下一个元素的位置。

Linux内核用一个struct pipe_inode_info 结构来描述一个管道。

struct pipe_inode_info {

wait_queue_head_t wait;//pipe为空或为满时指向等待的readerwriter

unsigned int nrbufs, curbuf;//pipe中非空buff的数目,当前pipe的入口

struct pipe_buffer bufs[PIPE_BUFFERS];    //pipe的环形缓冲

struct page *tmp_page;   //临时释放的页

unsigned int start;//

unsigned int readers;   /pipe当前reader的个数

unsigned int writers;     //pipe当前写者的个数

unsigned int waiting_writers;

unsigned int r_counter;

unsigned int w_counter;

struct fasync_struct *fasync_readers;

struct fasync_struct *fasync_writers;

};

pipe为空或为满时采用等待队列,该等待队列用自旋锁进行保护

 

用struct pipe_buffer数据结构描述pipe的缓冲(buffer

 

从管道读数据:

1、如果写端不存在,则认为已经到达数据的末尾,读函数返回的读数据个数为0.

2、写端存在,当请求读的字节数大于PIPE_BUFF时,返回管道中现有的数据的字节数。

3、若写端存在但没有写入任何数据,读端将一直阻塞,知道有写端写入数据。

向管道中写数据:

1、若管道已写满,没有读端读取数据,则写端将一直阻塞,直到有读端读出数据。

2、若写时读端已经不存在,则写入的进程会收到SIGPIPE信号,此时进程可以选择处理该信号,也可以按照默认动作终止进程。

为了协调双方的通信管道通信机制必须提供以下三方面的协调能力。1、互斥:当一个进程正在对管道进行读写操作时,另一个进程必须等待。

2、同步。当写(输入)进程把一定数量(如4 KB)数据写入管道后,便去睡眠等待,直到读(输出)进程取走数据后,再把它唤醒。当读进程读到一空管道时,也应睡眠,直到写进程将数据写入管道后,才将它唤醒。

3、判断对方是否存在。只有确定对方已经存在时,方能进行通信。

注:为防止多个进程同时读写一个管道文件而产生混乱,在管道的iNode标志字里面设置了ilock字段,以设置软件锁的方式实现多进程间对管道的互斥操作。

2一般在父子进程间通信时,写进程关闭读端,读进程关闭写端。

如果没有这样做,父进程写的数据可能会被父进程中的读端读取到,之后管道中没有数据了,子进程读端虽然打开,但读不到任何数据。如果父进程写完数据之后sleep几秒数据就会被子进程读取到。所以关闭只是为了更好的节省系统资源。

管道的局限性:

1、只支持单向数据流

2、只能用于有亲缘关系的进程间

3、管道的缓冲区是有限的【管道在内存中且只分配了一页内存】

4、管道传输的是无格式字节流。通信双方要事先约定好数据格式。

static ssize_t

pipe_readv(struct file *filp, const struct iovec *_iov,

   unsigned long nr_segs, loff_t *ppos)

{

struct inode *inode = filp->f_dentry->d_inode;//建立的索引inode节点

struct pipe_inode_info *info;//描述一个管道

int do_wakeup;//用于唤醒等待队列上的进程

ssize_t ret;

struct iovec *iov = (struct iovec *)_iov;//缓冲区

size_t total_len;

total_len = iov_length(iov, nr_segs);//用于存放用户所给缓冲区的总长度

/* Null read succeeds. */

if (unlikely(total_len == 0))

return 0;

do_wakeup = 0;

ret = 0;         // 信号量的两种应用①实现互斥②实现同步

down(PIPE_SEM(*inode));   //iNode节点加信号量,实现对iNode节点的同步访问

info = inode->i_pipe;   //如果内核是一个管道则非0

for (;;) {

int bufs = info->nrbufs;//pipe中非空buffer的数目,当前pipe的入口

if (bufs) {   //pipebuffer非空时

int curbuf = info->curbuf;

struct pipe_buffer *buf = info->bufs + curbuf;//缓冲区加。

struct pipe_buf_operations *ops = buf->ops;//map  unmap release

void *addr;

size_t chars = buf->len;//缓冲区长度

int error;

 

if (chars > total_len)//系统的长度大于用户长度,

chars = total_len;

 

addr = ops->map(filp, info, buf);

//管道中的数据拷贝到用户缓冲区

error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars);

ops->unmap(info, buf);//删除映射的缓冲区

if (unlikely(error)) {

if (!ret) ret = -EFAULT;

break;

}//调整buffer中指针的值

ret += chars;

buf->offset += chars;

buf->len -= chars;

if (!buf->len) {

buf->ops = NULL;

ops->release(info, buf);

curbuf = (curbuf + 1) & (PIPE_BUFFERS-1);

info->curbuf = curbuf;

info->nrbufs = --bufs;

do_wakeup = 1;

}

total_len -= chars;

if (!total_len)

break; /* common path: read succeeded */

}

if (bufs) /* More to do? */

continue;

if (!PIPE_WRITERS(*inode))

break;

if (!PIPE_WAITING_WRITERS(*inode)) {

/* syscall merging: Usually we must not sleep

 * if O_NONBLOCK is set, or if we got some data.

 * But if a writer sleeps in kernel space, then

 * we can wait for that data without violating POSIX.

 */

if (ret)

break;

if (filp->f_flags & O_NONBLOCK) {

ret = -EAGAIN;

break;

}

}

if (signal_pending(current)) {

if (!ret) ret = -ERESTARTSYS;

break;

}

if (do_wakeup) {

wake_up_interruptible_sync(PIPE_WAIT(*inode));

  kill_fasync(PIPE_FASYNC_WRITERS(*inode), SIGIO, POLL_OUT);

}

pipe_wait(inode);

}

up(PIPE_SEM(*inode));

/* Signal writers asynchronously that there is more room.  */

if (do_wakeup) {

wake_up_interruptible(PIPE_WAIT(*inode));

kill_fasync(PIPE_FASYNC_WRITERS(*inode), SIGIO, POLL_OUT);

}

if (ret > 0)

file_accessed(filp);

return ret;

}

static ssize_t

pipe_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)

{

struct iovec iov = { .iov_base = buf, .iov_len = count };

return pipe_readv(filp, &iov, 1, ppos);

}

创建的管道是一个内存中的缓冲区,不属于任何一个文件系统。在创建管道时系统会创建一个新的文件系统再在创建的文件系统上创建了一个索引节点对象。然后分配一个目录项对象,并使用它把两个文件对象和索引节点连接在一起。最后把新的索引节点插入到pipefs特殊的文件系统中。iNode索引节点对应一页物理内存,因为Linux一页内存为4KB,所以管道的大小是受限制的。

管道写函数通过将字节复制到 VFS 索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。

写入函数在向内存中写入数据之前,必须首先检查 VFS 索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:

 1、内存中有足够的空间可容纳所有要写入的数据;

 2、内存没有被读程序锁定。

如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 VFS 索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。

 

 

有名管道

严格按照FIFO(先进先出),不支持在文件内移动读写指针位置的lseek()操作。

创建命名管道的命令:mknod mypiipe  p

创建:Int mkfifo(const char *pathname,mode_t mode);

第一个参数是路径名也就是之后命名管道的名字。第二个参数与打开文件是的mode参数函数相同。若第一个参数是路径名已经存在会返回EEXIST错误,此时只要直接调用打开函数就可以了。

命名管道创建好之后,一些进程就可以不断的将信息写入命名管道,而另一些进程可以直接不断的从里面读取数据,对命名管道的读写操作可以同时进行。

命名管道的优点:

1、既可以用于本地又可以用于网络

2、可以通过名称被引用

3、支持双向通信

4、支持异步重叠I/O操作

匿名管道和命名管道:

由于匿名管道是一种“无名、无形”的文件,只能通过fork的过程创建与进亲的进程之间,不能再任意两个进程之间通信。为克服匿名管道的缺点而提出命名管道。命名管道“有名”,任何进程都 可以通过文件名或路径名与文件挂钩,“有形”文件的iiNode存在于磁盘或其他系统介质上,使得任何进程在任何时间都可以建立。

匿名管道和命名管道的区别:

1、根本区别:

  PipeiNode节点是要自己创建的,FIFOiNode节点是原来就在磁盘中存在的,但仅仅只是一个iNode节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。

2、打开方式不同:【最主要的区别】

普通管道通过do_pipe建立。通过fork过程伸展到两个进程之间。对于父进程在调用pipe之后已经打开,对于子进程是与生俱来的,所以不需要打开。

而对于命名管道:参加通信的进程要通过调用“打开函数”通向已经建立在文件系统上的FIFO文件通道。【将FIFO文件的iNode节点读入内存时(因为FIFO文件的S_FIFO标志位为1)因为为特殊文件,所以会调用init_special_inode()来加以初始化。】因为管道的两端分别由两个进程打开,所以就有了个“同步”的问题。

 

FIFO文件的操作方法只有open方法。但这个open方法并不是FIFO文件真正的操作方法,其真正的读写方法是根据不同的打开方法而决定的。

 

 

FIFO的打开方式:

 

第一次打开FIFO文件的进程调用fifo_open时,该命名管道的缓冲页面还没有分配,在pipe_new里面分配缓冲页面。

 

FIFO文件可以以只读、只写、读写三种方式打开,open调用过程中如果设置了flag参数。如果调用fifo_open的进程开始设置了flag中的O_NONBLOCK参数为1,则在打开的过程中无论是否正常打开,进程都不能进入睡眠,必须立即返回。

在file结构中的f_mode的含义:

 

 

 

 

 

 

 

 以“只读”方式打开。即命名管道的读端的几种情况:

1、如果命名管道的写端已经打开,那么管道的创建就完成了。这时,一般写端(生产者)一般都在睡眠,因此要调用wake_up_partner()将其唤醒。

2、如果写端没有打开,而且设置了O_NONBLOCK标志,此时尽管读端已经打开,但是没有完成管道的打开,由于进程要求不能等待,因此必须立即返回。

3、写读没有打开,但是没有设置O_NONBLOCK标志,读进程调用wait_for_partner()函数,进入睡眠状态等待写读打开后将其唤醒。

 

1、如果命名管道的读端没有打开,并且设置了O_NONBLOCK标志,写端进程就要直接跳转到err处(判断是否有读进程或写进程在睡眠,如果没有就释放pipe_inode_info)执行。否则,让filp的f_op指向write_pipefifo_fops方法。然后管道的写进程计数加1

2、 如果命名管道的读端已经打开,那么写端就完成了命名管道的打开。此时,读端一般都在睡眠等待,应该调用wake_up_partner()将其唤醒。

3、如果命名管道读端没有打开,那么写端就要调用wait_for_partner()进入睡眠等待,直到读端打开,将其唤醒之后才能返回。

 

 

 

 

 

读写的方式打开命名管道,相当于同一个进程打开了命名管道的两端,因此不需要等待。但是,也有可能已经有进程已经打开了某一端正在睡眠等待,因此,任意一端第一次打开,就唤醒了正在睡眠的进程。这种打开方式下,真正的操作方法是rdwr_pipefifo_fops。

 

FIFO文件一不同的方式打开之后他们的操作方法:

 

 

 

 

 

 

 

 

 

 

 


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值