读书笔记:《Unix网络编程(第2版)》卷2:进程间通讯

这是《UNIX网络编程 卷2:进程间通讯》(W.Richard Stevens)的读书笔记以及批注。html

参考资料


第二部分 消息传递

第4章 管道和FIFO

4.3 管道

Page.33

pipe函数返回两个 文件描述符,前者用于读管道,后者用于写管道

这意味着,可使用writereadopen来像操做文件同样读写管道。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指针所指的内存中。若参数 count0,则 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,只要该管道存在一些数据就会立刻返回,没必要等待到达所请求的字节数”。

更多参考:

Page.35

对于客户端-服务器模型,使用单向的管道是很难完成数据的交互,由于涉及到收发双方的数据同步问题,极端例子是发送方发出的数据被本身收到了,这样致使管道内无数据可读,此后两者陷入阻塞。
解决这个问题的一个方法是使用两根管道,方向相反,这样,服务器进程一开始在管道A读入端陷入阻塞,等待客户端的数据到来;客户端从管道A发送数据后,开始在管道B的读入端等待服务器,服务器处理完数据后,将反馈信息顺着管道B的发出端口发出,完成一次信息交换。
因此做者的代码中有:

client(pipeB_readin_fd,pipeA_wirteout_fd);
server(pipeA_readin_fd,pipeB_writeout_fd);

4.6 FIFO

Page.41

FIFO的名字只有经过调用 unlink才能从文件系统中删除。

FIFO的建立流程是:

  1. 调用mkfifo建立FIFO,若返回-1,到第2步,不然到第3步;
  2. 若返回值是-1,且errno是EEXIST,则说明同名FIFO已存在,到第3步;不然说明建立失败,进行错误处理;
  3. 如今可使用open来调用打开FIFO进行文件写入
  4. 像关闭文件同样使用close来关闭FIFO
  5. 使用unlink来将FIFO从文件系统中移除

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.

Page.42

这篇笔记中使用父子进程用FIFO模拟了客户端-服务器模型用以交互信息。
特别注意open一个FIFO时,若是是以只读方式打开,那么若是这个FIFO尚未以只写方式打开过得话就要陷入阻塞。若是发送方发送完消息不close掉FIFO,那么读端将在readFIFO时陷入阻塞,假如写端此时也在等待读端反馈,那么颇有可能就会陷入死锁。

4.7 管道和FIFO的额外属性

Page.45

关于图4-21,第一列“当前操做”表示如今准备要进行的操做,第二列“管道或FIFO现有打开操做”表示在某个进程中已经完成的操做。例如:

  • 当前操做:open FIFO只读
  • 管道或FIFO现有打开操做:FIFO不是打开来写
  • 返回:阻塞:阻塞到FIFO打开来写为止

这表示,某FIFO已经在某个进程中打开,且没有设置O_WRONLY,即没有写端可以对该FIFO进行写操做,那么若是如今对该FIFO执行open操做且使用O_RDONLY表示读,则会陷入阻塞,由于没有写端,天然读不出任何东西。

选项O_NONBLOCK表示非阻塞,加上这个选项后,表示open调用是非阻塞的,若是没有这个选项,则表示open调用是阻塞的。

4.8 单个服务器,多个客户

Page.47

在这种一对多的服务器-客户端中,代码实现的是迭代服务器,即一次只服务于一个客户,其余客户须要等待。一个误区在于,可能认为一个客户端写入/tmp/fifo.serv的FIFO后,哪怕FIFO中还有空域区域,其余客户端就在write函数中阻塞,不会写入东西,一直等待服务器处理完该客户端消息后才会开始写入。
事实上注意到第16行读取的函数是Readline,也就是说,多个客户端能够并发地向同一个FIFO中写入数据,只有当FIFO满了之后其余客户端才会在write中阻塞,而服务器彻底是根据一行一行的读取内容,由于一行表明了一个客户端请求。由于在例子中的假设中,客户端请求老是小于PIPE_BUF,因此写入操做老是原子性的。

另外,在这里虽然服务器对服务器FIFO只进行读操做(写操做由客户端进行),可是仍然持有一个对该FIFO的写端(dummyfd),从而,就算没有任何一个客户端存在,由于存在该FIFO的写端,服务器得以在read中阻塞,等待着消息的到达,而不是遇到EOF而关闭FIFO。

Page.48

对客户端FIFO的unlink由客户端完成,服务器端只要close客户端FIFO,就可以使得客户端在read中读到EOF而结束。

Page.49

能够在shell中使用命令mkfifo来建立FIFO

在本页有一个命令行例子:

echo "$Pid message" > /tmp/fifo.serv
(间隔至关长的时间后)
cat < /tmp/fifo.$Pid
....(服务器应答消息)

两个命令之间能够间隔至关长的时间,可是仍然可以得到服务器消息。错误的理解是:服务器读取/tmp/fifo.serv内的请求后做处理,将处理结果写入/tmp/fifo.$PidFIFO中,而后服务器进程就关闭了。从而客户端进程能够在任什么时候候从/tmp/fifo.$Pid读取。

实际是,服务器读取客户端请求后处理,可是要将处理结果写入客户端的/tmp/fifo.$PidFIFO以前必需要进行open,然而在此时客户端还没有对FIFO进行只读打开,没有读端,因此服务器在open中阻塞,所以,在cat < /tmp/fifo.$Pid命令以前的时间中,服务器进程始终在阻塞,没有结束。直到客户端打开FIFO时,服务器才写入,而后客户端才能读取。

若是管道、FIFO所有被close(没有读端也没有写端,即文中的“最终close”),那么管道、FIFO的数据都被丢弃。

4.10 字节流与消息

4.11 管道和FIFO限制

Page.55

系统对 管道和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管道中的最大字节数。对于 fpathconffd参数要是管道或FIFO的描述符;而对于 pathconfpath是一个FIFO路径或者是目录名,若是是目录名,那么返回的值就是建立在该目录下的FIFOs的最大字节数。

能够看出,Posix认为PIPE_BUF是一个pathname variable,它的值可能会随着指定的路径名而发生变化。
这里值得注意的是pathconf函数的返回值,帮助手册写的是:

pathconf函数的返回值是以下状况中的一种:

  • 若是出现错误,那么返回-1,而且使用errno全局变量来指示错误缘由。
  • 若是name是关于限制最大/最小这方面性质的配置名,且其限制值是不明确的(indeterminate),那么返回-1,而且errno不变为了将这种状况和上一种区分开来,请先设置errno变量为0,调用完函数后再检查当-1返回时errno是不是非零值便可)。
  • 若是name参数是受支持的配置选项名,那么返回一个正值,不然返回-1。
  • 不然,其选项或限制的值将被返回。这个(动态返回的)值比与之相关的定义在头文件unistd.hlimits.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));

第5章 Posix消息队列

5.2 mq_openmq_closemq_unlink函数

Page.60

unlinkclose的区别:笔记:磁盘分区、文件系统、连接。若是调用mq_unlink时,那么指定的队列就会被从系统删除,可是注意,若是此时其连接计数(或引用计数)不为0,说明仍有进程在使用它,它就是“悬空的”,这就意味着,它已经被系统除名,已经不能经过它的名字找到它,可是在正在使用它的进程中仍是可以正常使用(由于持有它的描述符,能够理解为指针)。当最后一个mq_close关闭它后,它的引用计数就变成了0,说明它已经彻底不能被找到,这个时候队列就会被析构,彻底消失。

getopt函数的解释参考:使用 getopt() 进行命令行处理。书中实例代码中使用这个函数来处理命令行输入的命令字符串并得到相关设置,使用while是不断处理命令字符串中的选项,当该命令字符串被解析完,while就会退出(getopt返回-1)。也就是说,它只能一次处理一个命令字符串。

相关文章
相关标签/搜索