这是《UNIX网络编程 卷2:进程间通讯》(W.Richard Stevens)的读书笔记以及批注。html
pipe
函数返回两个
文件描述符,前者用于读管道,后者用于写管道
这意味着,可使用write
、read
、open
来像操做文件同样读写管道。linux
父进程建立一个管道,而后
fork
出子进程,接着父进程关闭这个管道的读出端,子进程关闭同一管道写出端,造成了从父进程流向子进程的单向管道。
fork
出的子进程显然和父进程具备亲缘关系,因此子进程会持有和父进程彻底相同的fd
文件描述符副本(做者在Page.25倒数第三段中写到:“……有内核维护的打开文件的文件描述符……只在单个进程内有意义……假如说文件描述符4……对于可能在另外一个与本进程无亲缘关系的进程中打开在文件描述符4上的文件而言根本没有意义”)。shell
若是父子进程均不关闭管道任何一端,此时若是从子进程发送消息,同时从父子进程都开始读取,以下所示,会怎样呢?:编程
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> #define MAXLEN 15 int main(int argc,char **argv){ int fd[2]; pid_t cpid; //child process pid ssize_t n; //indeed read-in size pipe(fd); //get pipe char buff[MAXLEN]; if((cpid=fork())==0){ //child process write(fd[1],"hello",6); printf("\nIn Child Process:Write ends\n"); while((n=read(fd[0],buff,MAXLEN))>0){ printf("In Child Process:message is %s\n",buff); } printf("\nIn Child Process:Read ends\n"); exit(0); }else{ //parent process while((n=read(fd[0],buff,MAXLEN))>0){ printf("In Father Process:message is %s\n",buff); } printf("\nIn Father Process:Read ends\n"); waitpid(cpid,NULL,0); exit(0); } }
#include<unistd.h>
:pipe
,read
,write
#include<sys/types.h>
、#include<sys/wait.h>
:waitpid
fd[0]
写出端编译运行上面的程序,结果有如:segmentfault
In Child Process:Write ends In Child Process:message is hello
或服务器
In Child Process:Write ends In Father Process:message is hello
语句顺序可能相反,可是不出意外的,它们都会在此时阻塞。也就是说,子进程经过write
写入fd[1]
的数据可能被子进程或父进程的read
读入,可是任何一个进程读入后,两个进程同时在下一轮或本轮的read
进入了阻塞状态。
对于read
函数的说明是:网络
ssize_t read(int fd,void * buf ,size_t count)
read()
会把参数fd
所指的文件传送count
个字节到buf
指针所指的内存中。若参数count
为0
,则read()
不会有做用并返回0
。返回值为实际读取到的字节数,若是返回0
,表示已到达 文件尾或是 无可读取的数据,此外,文件 读写位置会随读取到的字节 移动。
读取管道的信息时,文件读写指针也会发生移动,因此任何一个进程读完其中的数据后,没有向缓冲区填入新的数据,那么任何试图从空管道中读取数据的进程由于没有数据可读(file pointer indicates this scenario.),都会被阻塞在这里。并发
值得注意的是,管道与通常文件不一样的是,文件老是要有EOF
的,这标志着读取过程的结束;而管道由于写入信息具备偶发性,因此,当管道没有数据时是read
不能马上返回的,由于发送方的数据随时可能到达,那么read
如何知道读取结束了呢?显然,只有发送进程明确关闭了写入端close(fd[1])
,此时读取方才能得到一个EOF
。
而在这里,由于父子进程是有亲缘关系的进程,因此两者持有的fd
符其实是同一个内存缓冲区域,因此管道的读入读出端口实际上被两个进程引用着。这意味着,就算子进程发送数据出去后马上关闭写出端口,父进程仍持有写入端的描述符,从而两个进程的read
都没法关闭,由于内核会认为父进程随时可能从该写入端写入数据。socket
为了印证这一点,能够修改代码为:函数
//child process while((n=read(fd[0],buff,MAXLEN))>0){ printf("In Child Process:message is %s\n",buff); close(fd[1]); } ... //parent process while((n=read(fd[0],buff,MAXLEN))>0){ printf("In Father Process:message is %s\n",buff); close(fd[1]); }
不管哪个进程从管道中读入了数据,它都会马上关闭本身的写入端。假如子进程发送的消息被子进程读到了,从而关闭了子进程的写入端。那么父进程在执行read
时就会被阻塞,由于此时管道的惟一写入端就在父进程手中!对于父进程,这陷入一个悖论:“我必须等待,由于我不知道何时会给本身发送消息”。
所以,最佳实践是,让发送方一开始就关闭读入端口,让接收方一开始就开始关闭写出端口。从而保证单向管道只有惟一的读入写出端口。
另外,write
操做对管道和FIFO的操做,若是写入数据量小于PIPE_BUF
,那么就保证是原子的,不然不保证是原子的,这意味着若是发送方发送大量数据(远远多于PIPE_BUF
)后,发送/接收双方同时都开始收,那么可能会形成数据紊乱,由于哪一个进程在何时开始读取是不定的:
#define MAXLEN 1048576 //修改成大量数据 //child process write(fd[1],str,MAXLEN); printf("\nIn Child Process:Write ends\n"); while((n=read(fd[0],buff,MAXLEN))>0){ printf("In Child Process:message length=%d\n",(int)strlen(buff)); } //parent process while((n=read(fd[0],buff,MAXLEN))>0){ printf("In Father Process:message length=%d\n",(int)strlen(buff)); }
执行结果可能以下:
In Child Process:Write ends In Child Process:message length=65535 In Father Process:message length=983039 ...(blocking)
子进程接收到65536
字节、父进程接收到983040
字节数据,这是由于strlen
函数统计时没有吧'/0'
算在其中,它们加和刚好是1048576
。
做者在Page.36下方写有:“对于管道的read,只要该管道存在一些数据就会立刻返回,没必要等待到达所请求的字节数”。
更多参考:
对于客户端-服务器模型,使用单向的管道是很难完成数据的交互,由于涉及到收发双方的数据同步问题,极端例子是发送方发出的数据被本身收到了,这样致使管道内无数据可读,此后两者陷入阻塞。
解决这个问题的一个方法是使用两根管道,方向相反,这样,服务器进程一开始在管道A读入端陷入阻塞,等待客户端的数据到来;客户端从管道A发送数据后,开始在管道B的读入端等待服务器,服务器处理完数据后,将反馈信息顺着管道B的发出端口发出,完成一次信息交换。
因此做者的代码中有:
client(pipeB_readin_fd,pipeA_wirteout_fd); server(pipeA_readin_fd,pipeB_writeout_fd);
FIFO的名字只有经过调用
unlink
才能从文件系统中删除。
FIFO的建立流程是:
mkfifo
建立FIFO,若返回-1,到第2步,不然到第3步;unlink
函数的定义是:
#include <unistd.h> int unlink(const char *pathname);Delete a name and possibly the file it refers to.
unlink()
deletes a name from the filesystem.
If that name was the last link to a file and no processes have the file open, the file is deleted and the space it was using is made available for reuse.
If the name was the last link to a file but any processes still have the file open, the file will remain in existence until the last file descriptor referring to it is closed.
If the name referred to a symbolic link, the link is removed.
▲ If the name referred to a socket, FIFO, or device, the name for it is removed but processes which have the object open may continue to use it.
在这篇笔记中使用父子进程用FIFO模拟了客户端-服务器模型用以交互信息。
特别注意open
一个FIFO时,若是是以只读方式打开,那么若是这个FIFO尚未以只写方式打开过得话就要陷入阻塞。若是发送方发送完消息不close
掉FIFO,那么读端将在read
FIFO时陷入阻塞,假如写端此时也在等待读端反馈,那么颇有可能就会陷入死锁。
关于图4-21,第一列“当前操做”表示如今准备要进行的操做,第二列“管道或FIFO现有打开操做”表示在某个进程中已经完成的操做。例如:
- 当前操做:
open
FIFO只读- 管道或FIFO现有打开操做:FIFO不是打开来写
- 返回:阻塞:阻塞到FIFO打开来写为止
这表示,某FIFO已经在某个进程中打开,且没有设置
O_WRONLY
,即没有写端可以对该FIFO进行写操做,那么若是如今对该FIFO执行open
操做且使用O_RDONLY
表示读,则会陷入阻塞,由于没有写端,天然读不出任何东西。
选项O_NONBLOCK
表示非阻塞,加上这个选项后,表示open
调用是非阻塞的,若是没有这个选项,则表示open
调用是阻塞的。
在这种一对多的服务器-客户端中,代码实现的是迭代服务器,即一次只服务于一个客户,其余客户须要等待。一个误区在于,可能认为一个客户端写入/tmp/fifo.serv
的FIFO后,哪怕FIFO中还有空域区域,其余客户端就在write
函数中阻塞,不会写入东西,一直等待服务器处理完该客户端消息后才会开始写入。
事实上注意到第16行读取的函数是Readline
,也就是说,多个客户端能够并发地向同一个FIFO中写入数据,只有当FIFO满了之后其余客户端才会在write中阻塞,而服务器彻底是根据一行一行的读取内容,由于一行表明了一个客户端请求。由于在例子中的假设中,客户端请求老是小于PIPE_BUF
,因此写入操做老是原子性的。
另外,在这里虽然服务器对服务器FIFO只进行读操做(写操做由客户端进行),可是仍然持有一个对该FIFO的写端(dummyfd
),从而,就算没有任何一个客户端存在,由于存在该FIFO的写端,服务器得以在read
中阻塞,等待着消息的到达,而不是遇到EOF
而关闭FIFO。
对客户端FIFO的unlink
由客户端完成,服务器端只要close
客户端FIFO,就可以使得客户端在read
中读到EOF
而结束。
能够在shell中使用命令mkfifo
来建立FIFO
在本页有一个命令行例子:
echo "$Pid message" > /tmp/fifo.serv (间隔至关长的时间后) cat < /tmp/fifo.$Pid ....(服务器应答消息)
两个命令之间能够间隔至关长的时间,可是仍然可以得到服务器消息。错误的理解是:服务器读取/tmp/fifo.serv
内的请求后做处理,将处理结果写入/tmp/fifo.$Pid
FIFO中,而后服务器进程就关闭了。从而客户端进程能够在任什么时候候从/tmp/fifo.$Pid
读取。
实际是,服务器读取客户端请求后处理,可是要将处理结果写入客户端的/tmp/fifo.$Pid
FIFO以前必需要进行open
,然而在此时客户端还没有对FIFO进行只读打开,没有读端,因此服务器在open
中阻塞,所以,在cat < /tmp/fifo.$Pid
命令以前的时间中,服务器进程始终在阻塞,没有结束。直到客户端打开FIFO时,服务器才写入,而后客户端才能读取。
若是管道、FIFO所有被close
(没有读端也没有写端,即文中的“最终close
”),那么管道、FIFO的数据都被丢弃。
系统对 管道和FIFO 的惟一限制 是:
OPEN_MAX
:一个进程在任意时刻打开的最大描述符数PIPE_BUF
:可原子的写入任何一个 管道和FIFO 的最大数据量。
OPEN_MAX
能够经过sysconf
函数来查询,查询的宏是_SC_OPEN_MAX
。在这个网站能够查看到该函数的详细状况。PIPE_BUF
能够经过pathconf
函数来查询,查询的宏是_PC_PIPE_BUF
。在这个网站能够查看到该函数的详细状况。pathconf
函数pathconf
函数的接口定义是:
#include <unistd.h> long fpathconf(int fd, int name); long pathconf(const char *path, int name);
其做用是得到文件名path
/文件描述符fd
的名为name
的配置的值。
这些值在unistd.h
头文件中也定义了相关的宏能够获取,可是这些宏只是规定了这些值的最小值,是静态不可变的。若是应用想要获取实时的值(这些值可能发生变更),那么就要调用这两个函数。
其中name
能够指定为已经预约好的宏名,例如:
name = _PC_PIPE_BUF
: 能够 原子的写到FIFO管道中的最大字节数。对于fpathconf
,fd
参数要是管道或FIFO的描述符;而对于pathconf
,path
是一个FIFO路径或者是目录名,若是是目录名,那么返回的值就是建立在该目录下的FIFOs的最大字节数。
能够看出,Posix认为PIPE_BUF
是一个pathname variable,它的值可能会随着指定的路径名而发生变化。
这里值得注意的是pathconf
函数的返回值,帮助手册写的是:
pathconf
函数的返回值是以下状况中的一种:
- 若是出现错误,那么返回-1,而且使用
errno
全局变量来指示错误缘由。- 若是
name
是关于限制最大/最小这方面性质的配置名,且其限制值是不明确的(indeterminate),那么返回-1,而且errno
不变(为了将这种状况和上一种区分开来,请先设置errno
变量为0,调用完函数后再检查当-1返回时errno
是不是非零值便可)。- 若是
name
参数是受支持的配置选项名,那么返回一个正值,不然返回-1。- 不然,其选项或限制的值将被返回。这个(动态返回的)值比与之相关的定义在头文件
unistd.h
或limits.h
中的用于描述该应用的(静态)值更宽泛(not be more restrictive)
注意到前两点,就能够明白为何做者在wrapunix.c
中将包裹pathconf
的包裹函数定义为
//in wrapunix.c by W.Richard Stevens long Pathconf(const char *pathname, int name) { long val; errno = 0; /* in case pathconf() does not change this */ if ( (val = pathconf(pathname, name)) == -1) { if (errno != 0) err_sys("pathconf error"); else err_sys("pathconf: %d not defined", name); } return(val); }
同时还注意到了errno
这个变量。errno
是记录系统的最后一次错误代码。代码是一个int
型的值,在errno.h
头文件中定义。在博客《Linux errno详解》一文中,做者写道:
Linux中 系统调用的错误都存储于errno
中,errno
由操做 系统维护,存储 就近发生的错误,即下一次的错误码会 覆盖掉上一次的错误。只有当 系统调用 或者 调用lib 函数时出错, 才会置位errno。
可使用定义在string.h
中的strerror
函数来根据errno
得到错误说明字符串,或是经过定义在stdio.h
头文件中的perror
函数来把系统调用错误信息字符串发送到标准输出。这篇帮助文档给出了可能的错误代码。
sysconf
函数sysconf
函数接口是
#include <unistd.h> long sysconf(int name);
是用来在运行时得到配置信息。在这篇帮助手册中详细说明了sysconf
函数。若是name
是 _SC_OPEN_MAX
,那么它表示一个进程最多能打开的文件数。其返回状况与上面的pathconf
是彻底一致的。
因此做者定义了相似了包裹函数
long Sysconf(int name) { long val; errno = 0; /* in case sysconf() does not change this */ if ( (val = sysconf(name)) == -1) { if (errno != 0) err_sys("sysconf error"); else err_sys("sysconf: %d not defined", name); } return(val); }
使用以下语句便可完成查询:
printf("PIPE_BUF=%ld, OPEN_MAX=%ld\n", Pathconf(argv[1],_PC_PIPE_BUF),Sysconf(_SC_OPEN_MAX));
mq_open
、mq_close
、mq_unlink
函数unlink
和close
的区别:笔记:磁盘分区、文件系统、连接。若是调用mq_unlink
时,那么指定的队列就会被从系统删除,可是注意,若是此时其连接计数(或引用计数)不为0,说明仍有进程在使用它,它就是“悬空的”,这就意味着,它已经被系统除名,已经不能经过它的名字找到它,可是在正在使用它的进程中仍是可以正常使用(由于持有它的描述符,能够理解为指针)。当最后一个mq_close
关闭它后,它的引用计数就变成了0,说明它已经彻底不能被找到,这个时候队列就会被析构,彻底消失。
getopt
函数的解释参考:使用 getopt() 进行命令行处理。书中实例代码中使用这个函数来处理命令行输入的命令字符串并得到相关设置,使用while
是不断处理命令字符串中的选项,当该命令字符串被解析完,while就会退出(getopt
返回-1)。也就是说,它只能一次处理一个命令字符串。