lvyilong316 阅读(428) 评论(0)

 execl对重定向的影响、execl对缓冲区类型的影响

——lvyilong316

这篇文章我们将要探讨三个问题:

(1) 对打开的文件描述符或者标准输入输出重定向后,在通过execl执行新的进程,重定向的效果还能保持吗?

(2) 如果我们将标准输出设置为无缓冲(默认是行缓冲)的,那么execl执行新的进程后,新进程的标准输出缓冲类型还是无缓冲吗?

(3) 文件描述符重定向对和文件描述符关联的IO流有什么影响?

为了回答以上三个问题,我们通过一个例子来说明和验证。

python程序pyth.py如下:


点击(此处)折叠或打开

  1. #! /usr/bin/python
  2. import sys
  3. import os
  4. print 'Hi!'
  5. while(raw_input()):
  6.     print 'hello'
  7.     print 'world'


  其功能是首先输出Hi!,随后在控制台上输入任何数据回车后就输出hello world(分两行输出),运行效果如图1.

1

我们想实现的效果是从一个机器上的客户端向这个python程序发送命令(相当于在控制台上输入数据),然后接收程序返回来的hello world串。

总体设计方案如图2

2

当服务端的server程序收到客户端的连接后,就创建子进程,并且将子进程的标准输入、标准输出、标准错误重定向到已连接的套接字,然后execl执行pyth.py,这样客户端clientpyth.py程序的交互就相当于在本机控制台上交互是一样的了。

编写客户端程序tcpclient.c如下:

tcpClient.c


点击(此处)折叠或打开

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <unistd.h>
  4. #include <sys/types.h>
  5. #include <sys/socket.h>
  6. #include <stdlib.h>
  7. #include <memory.h>
  8. #include <arpa/inet.h>
  9. #include <netinet/in.h>
  10. #define PORT 9999
  11. #define Buflen 1024
  12. int main(int argc,char *argv[])
  13. {
  14.     struct sockaddr_in server_addr;
  15.     int n,err;
  16.     int sockfd;
  17.     char recvline[Buflen];
  18.     char* cmd="a\n";
  19.     setbuf(stdout,NULL);
  20.     /********************socket()*********************/
  21.     sockfd= socket(AF_INET,SOCK_STREAM,0);
  22.     /*******************connect()*********************/
  23.     //设置服务器地址结构,准备连接到服务器
  24.     memset(&server_addr,0,sizeof(server_addr));
  25.     server_addr.sin_family = AF_INET;
  26.     server_addr.sin_port = htons(PORT);
  27.     server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  28.     server_addr.sin_addr.s_addr = inet_addr(argv[1]);
  29.     err = connect(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr));
  30.     if(err == 0)
  31.     {
  32.         printf("client : connect to server\n");
  33.     }
  34.     else
  35.     {
  36.         printf("client : connect error\n");
  37.         return -1;
  38.     }
  39.     //与服务器端进行通信
  40.     memset(recvline,0,sizeof(recvline));
  41.     if( (n=read(sockfd,recvline,Buflen))>0 )
  42.    {
  43.       recvline[n]=0;
  44.       printf("%s",recvline);
  45.    }
  46.     
  47.    write(sockfd,cmd,strlen(cmd)); //这里相当于在pyth.py的标准输入上输入数据
  48.    while( (n=read(sockfd,recvline,Buflen))>0 )
  49.   {
  50.      recvline[n]='\0';
  51.      printf("%s",recvline);
  52.   }
  53.  close(sockfd);
  54. }

编写服务端tcpServer.c如下。

tcpServer.c

点击(此处)折叠或打开

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <strings.h>
  4. #include <sys/types.h>
  5. #include <sys/socket.h>
  6. #include <memory.h>
  7. #include <unistd.h>
  8. #include <netinet/in.h>
  9. #include <arpa/inet.h>
  10. #include <string.h>
  11. #define PORT 9999 //定义通信端口
  12. #define BACKLOG 5 //定义侦听队列长度
  13. #define buflen 1024
  14. int listenfd,connfd;
  15. int main(int argc,char *argv[])
  16. {
  17.     struct sockaddr_in server_addr; //存储服务器端socket地址结构
  18.     struct sockaddr_in client_addr; //存储客户端 socket地址结构
  19.     int err; //返回值
  20.     pid_t pid;
  21.     /*****************socket()***************/
  22.     listenfd = socket(AF_INET,SOCK_STREAM,0);
  23.     /******************bind()****************/
  24.     //初始化地址结构
  25.     memset(&server_addr,0,sizeof(server_addr));
  26.     server_addr.sin_family = AF_INET; //协议族
  27.     server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //本地地址
  28.     server_addr.sin_port = htons(PORT);
  29.     err = bind(listenfd,(struct sockaddr *)&server_addr,sizeof(server_addr));
  30.     if(err<0)
  31.     {
  32.         printf("server : bind error\n");
  33.         return -1;
  34.     }
  35.     /*****************listen()***************/
  36.     err = listen(listenfd,BACKLOG); //设置监听的队列大小
  37.     if(err < 0)
  38.     {
  39.         printf("server : listen error\n");
  40.         return -1;
  41.     }
  42.     for(;;)
  43.     {
  44.         socklen_t addrlen = sizeof(client_addr);
  45.         //accept返回客户端套接字描述符
  46.         connfd = accept(listenfd,(struct sockaddr *)&client_addr,&addrlen);
  47.        
  48.         if((pid = fork()) == 0) //子进程,与客户端通信
  49.         {
  50.            close(listenfd);
  51.            dup2(connfd,STDOUT_FILENO); //重定向标准输出
  52.            dup2(connfd,STDERR_FILENO); //重定向标准错误
  53.            dup2(connfd,STDIN_FILENO); //重定向标准输入
  54.            if ( (err=execl("./pyth.py",(char*)0))<0 ) //执行pyth.py
  55.            {
  56.               perror("execl error");
  57.               exit(1);
  58.            }
  59.         }
  60.         else
  61.         {
  62.             close(connfd);
  63.         }
  64.     }
  65. }

分别编译运行tcpServer.cserver,编译tcpClient.cclient并运行,效果如图3.

3

我们发现客户端并没有收到服务端pyth.py输出的Hi!。难道是重定向的问题吗?经过考虑和一些验证我觉得问题应该在标准IO流的缓冲。我们知道标准IO的默认缓冲类型是:

(1) 标准错误是不带缓冲的;

(2) 若涉及终端设备,则标准输入和输出为行缓冲,否则是全缓冲。

   这里标准输入和输出重定向到了已连接套接字connfd,所以是全缓冲,即只有缓冲区满或者显式flush缓冲区中的内容才会真正输出。所以字符串Hi!应该停留在pyth.py的缓冲区中。

随后对程序tcpServer.c修改如下:


点击(此处)折叠或打开

  1. if((pid = fork()) == 0) //子进程,与客户端通信
  2. {
  3.     close(listenfd);
  4.     setbuf(stdout,NULL); //将标准输入、输出、错误都设置成无缓冲
  5.     setbuf(stdin,NULL);
  6.     setbuf(stderr,NULL);
  7.     dup2(connfd,STDOUT_FILENO); //将标准输入、输出、错误重定向到connfd
  8.     dup2(connfd,STDERR_FILENO);
  9.     dup2(connfd,STDIN_FILENO);
  10.     if ( (err=execl("./pyth.py",(char*)0))<0 ) //执行pyth.py
  11.    {
  12.      perror("execl error");
  13.      exit(1);
  14.    }
  15.  }

之后再次运行serverclient效果依然如图3所示。这是为什么呢?我们首先回答之前的两个问题:

 execl不影响之前打开的文件描述符,包括重定向的描述符,所以我们可以先将标准输入、输出、错误重定向,之后execl执行的新程序的标准输入、输出、错误依然保持之前的重定向。

每个程序有一个自己标准输入、输出、错误的IO流。当我们将这些IO流的缓冲区类型(行缓冲、全缓冲、无缓冲)改变后不影响execl后新进程的IO流类型。

所以之前我们在tcpServer.c中设置的IO缓冲类型其实对之后执行的pyth.py是无效的,那我们想改变pyth.pyIO缓冲类型应该怎么做呢?很简单,在pyth.py中设置就可以了。Python中设置标准输出为无缓冲有以下三种方法:

(1) python-u参数

(2)  PYTHONUNBUFFERED环境变量

(3)  sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

我们采用第三种方法,添加 sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)pyth.py中。再次运行client,结果如图4所示。

4

我们看到得到了我们想要的结果,服务端serverpyth.py的输出返回给了client

    现在我们来考虑第三个问题——重定向对标准IO流有什么影响?一个IO流是由一个FILE对象来管理的,FILE对象通常是一个结构,它包含了标准IO库为管理该流的所需要的所有信息,包括:用于实际I/O的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志等等——摘自apue

下面通过图5来说明重定向与标准IO流的关系。

5

从图5我们有如下结论:

重定向只是改变了FILE对象中实际关联的fd(对于我们的程序来说是由终端fd变为了已连接的connfd)

重定向后,根据实际关联fd指向的对象确定IO流缓冲区(图中绿色部分)的类型,是行缓冲、全缓冲、或无缓冲。对于我们之前的程序由于connfd指向的不是终端设备,所以标准输入、输出为全缓冲,标准错误仍然是无缓冲。

要区别标准IO流的缓冲(图中绿色部分)和套接字的发送缓冲(图中黄色部分),前者被IO流所管理,位于用户空间,后者由内核协议栈来管理,位于内和空间。

设置缓冲区的类型是指设置用户空间(绿色部分的缓冲)而不是内和空间(黄色部分的缓冲)