IPC学习笔记(2) 管道和FIFO

1、概述

管道是最初的UNIX IPC形式,由于管道没有名字,所以只能在用于有亲缘关系的进程(所谓的亲缘关系是指进程间有共同的祖先)。FIFO则被称为命名管道。


先说明fork,exec,_exit函数对管道及fifo函数的影响:
fork:子进程取得父进程的管道以及fifo描述字的拷贝。
exec:所有代开的描述字依旧打开,除非已经设置描述字的FD_CLOEXEC位。
_exit:关闭所有打开的描述字,最后一个描述字关闭时删除管道与FIFO中的数据。

管道和FIFO涉及的函数有:pipe, mkfifo, fcntl, open, read, write, close, unlink.

2、管道函数及说明:int pipe(int fd[2]);

(1)、这个函数提供一个单向管道。fd是输出参数,fd[0]用来读出,fd[1]用来写入(这里的读出和写入是的宾语是管道)。函数成功返回0,错误返回-1.

注意:a、linux的system call(相当于系统API)函数的返回值是成功返回0,返回其他的就表示出错,而windows的api函数则刚好相反,错误返回0,成功返回其他。b、这里不写函数的头文件,要知道它用到那些头文件,在类unix下用man来查看即可,如:man 2 pipe,2表示pipe是一个系统调用,如果是c函数库里面的函数则按3。


(2)、管道的典型用途如下(用来作为父子进程消息的共享):
a、创建管道1(int fd1[2]) 和管道2(int fd2[2]);
b、fork创建子进程
c、父进程关闭管道1的读出端和2的写入端(fd1[0],fd2[1])
d、子进程关闭管道2的读出端和1的写入端(fd1[1],fd2[0])
代码如下:

  1. int pipe1[2];
  2. int pipe2[2];

  1. pid_t childPid;
  2. pipe(pipe1);
  3. pipe(pipe2);

  4. if( ( childPid = fork()) == 0 ) /* child process */
  5. {
  6.     close(pipe1[1]);
  7.     close(pipe2[0]);
  8.     ...   /* your code */
  9. }
  10. /* parent process */
  11. close(pipe1[0]);
  12. close(pipe2[1]);
  13. ...
  14. waitpid(childPid, NULL, 0);
  15. ...
子进程在这里用来专门做一件事情,像服务器。而父进程就相当于客户端。如是,子进程在完成任务之后就exit(0),那么但子进程终止时候变成僵尸进程(zombie),内核给父进程发送一个信号SIGCHILD,但是父进程没有扑抓,缺省行为是忽略;父进程后面调用到waitpid的时候就可以等待childPid进程的终止,取得终止状态。如果没有waitpid而直接返回,则父进程返回后那个僵尸子进程将成为托孤给init进程的孤儿进程。内核将为init进程发送SIGCHILD信号。

这样子,就变成: 客户-->管道fd1-->服务器;服务器-->fd2-->客户

3、fifo 有名管道(fifo的意思是:先进先出)
也是一个半双工的单向数据流,但是有一个路径名与之相连。函数如下:
int mkfifo(const char *pathname, mode_t mode);

说明:
a、mkfifo函数已经隐含制订了O_CREAT | O_EXCL也就是说如果pathname那个管道已经存在,则返回一个错误值EEXIST,如果想打开一个已经存在的管道,用open()即可。
b、由于fifo是先进先出的,所以write函数总是往管道末尾写入数据,而read函数总是从管道的开头返回数据,如果用lseek定位文件指针,会返回ESPIPE错误。

应用:

  1. /* server.c */
  2. ...
  3. if( ( mkfifo(FIFO1, FILE_MODE ) < 0 ) && (errno != EEXIST ) )
  4.     /* error handle and exit(1) */
  5. if( ( mkfifo(FIFO2, FILE_MODE ) < 0 ) && (errno != EEXIST ) )
  6. {
  7.     unlink(FIFO1);
  8.     /* error handle and exit(1) */
  9. }
  10. readfd = open(FIFO1, O_RDONLY, 0);
  11. writefd = open(FIFO2, O_WRONLY, 0);
  12. /* your code */
  13. exit(0);
这是服务器进程的FIFO,其中FIFO1,FIFO2可以定义一个宏来指定,比如,#define FIFO1 "/temp/fifo.1"(路径名随意,不过由于是临时的文件,一般放在/temp目录下,权限比较低嘛)

打开文件之后就可以用read,write函数往管道读写信息了,FIFO1,FIFO2就是像文件描述符。

再看client的代码:

  1. /* client.h */
  2. writefd = open(FIFO1, O_WRONLY, 0);
  3. readfd = open(FIFO2, O_RDONLY, 0);
  4. /* your code */

  5. close(writefd);
  6. close(readfd);

  7. unlink(FIFO1);
  8. unlink(FIFO2);

  9. exit(0);
说明:a、通常管道是由服务器建立,由客户端销毁(unlink函数),close只是关闭管道而已。b、注意clinet.h上的那两个open和server.h上的那两个open的顺序,否则会引起死锁(见后面)

4、阻塞态下的规则(阻塞态是默认的状态):

(1)、管道和FIFOopen函数的返回结果:
writefd = open(FIFO1, O_WRONLY, 0); 这个是用只写方式打开管道,如果FIFO1此时已经有别的进程以只读方式打开(就是说在这条代码运行之前,已经有代码open(FIFO1, O_RDONLY, 0)运行),则此函数返回成功,否则,将会阻塞到有别的进程以只读方式打开FIFO1为止。反过来也一样。

(2)、read函数作用于管道和FIFO 的返回结果:
如果FIFO1为空(就是说里面没有数据):该管道以只读方式打开,则返回0;FIFO1以只写方式打开,则阻塞到FIFO1有数据或者是FIFO1不再以写方式打开为止。

(3)、write函数作用于管道和FIFO的返回结果:
如果FIFO1没有以只读方式打开,则给进程产生SIGPIPE信号(缺省行为是终止该进程);如果已经以只读方式打开,则见下。

(4)、write操作的原子性:
如果写入数据的字节数小于等于PIPE_BUF,则该函数保证其原子性;否则不能保证。

现在来看一下上面那个程序,服务器代码先运行,因此: 当起运行到readfd = open(FIFO1, O_RDONLY, 0);的时候,还没有任何进程以O_WRONLY方式打开FIFO1,进程阻塞在这里;

然后客户端代码开始运行,但运行到这个地方的时候:
writefd = open(FIFO1, O_WRONLY, 0);服务器阻塞的地方开始释放,而在客户端,因为FIFO1在服务器已经是以O_RDONLY打开了,所以继续运行。

如果客户端的这两个open交换一个顺序,那么 readfd = open(FIFO2, O_RDONLY, 0);先运行,则由于FIFO2还没有以O_WRONLY方式打开,所以客户端也阻塞,客户和服务器都阻塞,大家都在等对方的资源,这种情况我们称之为死锁(deadlock)

5、非阻塞态下的规则:

(1)、非阻塞态的设置。
a、调用open时可以指定 readfd = open(FIFO1, O_RDONLY | O_NONBLOCK );
b、如果readfd已经打开,则可以用fcntl来设置O_NONBLOCK标志。

(2)、对open操作的影响:
如果当前操作是wrfd = open(FIFO1, O_WRONLY | O_NONBLOCK, 0); 那么如果是FIFO1在此以前没有用O_RDONLY方式打开过,返回ENOXIO错误,否则都成功返回。

(3)、对空管道或空FIFO read操作的影响:read(readfd, ...); 如果该readfd对应的FIFO用O_WRONLY打开返回0,否则返回EAGAIN。

(4)、对write操作的影响同阻塞态下。

6、技巧:单个服务器多个客户时候,服务器中有连续的两行代码:
readfifo = open(SERV_FIFO, O_RDONLY, 0);
dummy = open(SERV_FIFO, O_WRONLY, 0);
作用:a、服务器运行到readfifo,阻塞,直到有客户用O_WRONLY打开SERV_FIFO为止。然后因为SERV_FIFO已经以O_RDONLY打开,因此这个dummy成功返回。
b、到客户完成任务,关闭SERV_FIFO时,SERV_FIFO变成空,因此,服务器在运行到read语句的时候,阻塞知道下一个客户以O_WRONLY方式打开SERV_FIFO为止(也就是直到有下一个用户请求为止)

7、Dos攻击(拒绝服务型攻击)
有见上面的阻塞,有个攻击方法就是说,发送一条请求,但是从来不打开自己的FiFO,让服务器死等。服务器一直阻塞,没办法用了。

当然,现在的服务器都是并发性服务器,最多只能阻塞他的一个子进程;但是即使在这种情况下一样可以进行Dos攻击,方法是发送大量的请求,以至于服务器开辟的进程达到极限,而且每个子进程都用上面的办法来阻塞。(此时不但服务器进程被阻塞了,整个服务器系统都被阻塞了)。

8、字节流
管道和FIFO是以字节流的方式来传递信息的,类似于TCP。那么怎样区分信息的界限呢?有下面三种常用的技巧:

a、带内特殊终止符:分隔标志(类似与SLIP的标记)
b、显示长度在信息头:像TCP协议栈实现。
c、每次连接一个记录:像HTTP1.0协议实现。

你可能感兴趣的:(api,unix,服务器,dos,File,任务)