簡體   English   中英

子進程的異步雙向IO重定向

[英]Asyncronous Bidirectional IO Redirection for a child process

我正在嘗試找出子進程的異步雙向IO重定向的通用方法。 基本上,我想產生一個交互式的子進程,等待輸入,任何輸出都應被讀回。 我試圖通過產生一個新的python進程來嘗試python.subprocess。 嘗試實現的一個簡單的基本示例如下

process = subprocess.Popen(['/usr/bin/python'],shell=False,stdin=subprocess.PIPE, stdout=subprocess.PIPE)
while True:
    output = process.stdout.readline()
    print output
    input = sys.stdin.readline()
    process.stdin.write(input)

並執行上面的代碼片段只是掛起而沒有任何輸出。 我嘗試使用/usr/bash/usr/bin/irb但是結果都是一樣的。 我的猜測是,緩沖IO與IO重定向完全不兼容。

所以我的問題是,在不刷新緩沖區或退出子流程的情況下讀取子流程的輸出是否可行?

以下文章提到了IPC套接字,但為此我將不得不更改子進程,這可能不可行。 還有其他方法可以實現嗎?

注意***我的最終目標是創建一個可以與遠程Web客戶端交互的服務器REPL流程。 盡管給出的示例是Python的,但我的最終目標是通過通用包裝器包裝所有可用的REPL。


借助答案中的一些建議,我提出了以下建議

#!/usr/bin/python
import subprocess, os, select
proc = subprocess.Popen(['/usr/bin/python'],shell=False,stdin=subprocess.PIPE, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
for i in xrange(0,5):
    inputready, outputready, exceptready = select.select([proc.stdout, proc.stderr],[proc.stdout, proc.stderr],[proc.stdout, proc.stderr],0)
    if not inputready: print "No Data",
    print inputready, outputready, exceptready
    for s in inputready: print s.fileno(),s.readline()
proc.terminate()
print "After Terminating"
for i in xrange(0,5):
    inputready, outputready, exceptready = select.select([proc.stdout, proc.stderr],[proc.stdout, proc.stderr],[proc.stdout, proc.stderr],0)
    if not inputready: print "No Data",
    print inputready, outputready, exceptready
    for s in inputready: print s.fileno(),s.readline() 

現在,盡管程序沒有陷入僵局,但不幸的是沒有輸出。 運行上面的代碼我得到

No Data [] [] []
No Data [] [] []
No Data [] [] []
No Data [] [] []
No Data [] [] []
After Terminating
No Data [] [] []
No Data [] [] []
No Data [] [] []
No Data [] [] []
No Data [] [] []

僅供參考,以python身份運行

/usr/bin/python 2>&1|tee test.out

似乎工作正常。

我還想出了一個“ C”代碼。 但是結果並沒有不同。

int kbhit() {
    struct timeval tv;
    fd_set fds;
    tv.tv_sec = tv.tv_usec = 0;
    FD_ZERO(&fds);
    FD_SET(STDIN_FILENO, &fds);
    select(STDIN_FILENO+1, &fds, NULL, NULL, &tv);
    return FD_ISSET(STDIN_FILENO, &fds);
}
void receive(char *str) {
    char ch;
    fprintf(stderr,"IN1\n");
    if(!kbhit()) return;
    fprintf(stderr,"IN2\n");
    fprintf(stderr,"%d\n",kbhit());
    for(;kbhit() && (ch=fgetc(stdin))!=EOF;) {
        fprintf(stderr,"%c,%d",ch,kbhit());
    }
    fprintf(stderr,"Done\n");
}
int main(){
    pid_t pid;
    int rv, pipeP2C[2],pipeC2P[2];  
    pipe(pipeP2C);
    pipe(pipeC2P);
    pid=fork();
    if(pid){
        dup2(pipeP2C[1],1); /* Replace stdout with out side of the pipe */
        close(pipeP2C[0]);  /* Close unused side of pipe (in side) */
        dup2(pipeC2P[0],0); /* Replace stdin with in side of the pipe */
        close(pipeC2P[1]);  /* Close unused side of pipe (out side) */
        setvbuf(stdout,(char*)NULL,_IONBF,0);   /* Set non-buffered output on stdout */
        sleep(2);
        receive("quit()\n");
        wait(&rv);              /* Wait for child process to end */
        fprintf(stderr,"Child exited with a %d value\n",rv);
    }
    else{
        dup2(pipeP2C[0],0); /* Replace stdin with the in side of the pipe */
        close(pipeP2C[1]);  /* Close unused side of pipe (out side) */
        dup2(pipeC2P[1],1); /* Replace stdout with the out side of the pipe */
        close(pipeC2P[0]);  /* Close unused side of pipe (out side) */
        setvbuf(stdout,(char*)NULL,_IONBF,0);   /* Set non-buffered output on stdout */
        close(2), dup2(1,2); /*Redirect stderr to stdout */
        if(execl("/usr/bin/python","/usr/bin/python",NULL) == -1){
            fprintf(stderr,"execl Error!");
            exit(1);
        }
    }
    return 0;
}

有不同的方法可以做到這一點。 您可以,例如:

  • 使用SysV消息隊列並輪詢隊列中的超時以使消息到達
  • 使用O_NONBLOCK標志為孩子創建一個pipe(),為父親創建一個pipe(),然后在文件描述符上選擇select()以便數據到達(即使沒有數據到達也可以處理超時)
  • 使用socket()AF_UNIX或AF_INET,將其設置為非阻塞,然后選擇select()或epoll()以使數據到達
  • mmap()MAP_SHARED存儲段並在數據到達時向其他進程發出信號,請注意具有鎖定機制的共享段。

我用雙管道用C語言編寫了一個示例:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/select.h>
#include <fcntl.h>
#include <signal.h>

#define BUFLEN (6*1024)
#define EXECFILE "/usr/bin/python"

char *itoa(int n, char *s, int b) {
        static char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
        int i=0, sign;

        if ((sign = n) < 0)
                n = -n;

        do {
                s[i++] = digits[n % b];
        } while ((n /= b) > 0);

        if (sign < 0)
                s[i++] = '-';
        s[i] = '\0';

        return s;
}

/*
int set_nonblock(int sockfd) { // set socket to non blocking
        int arg,i;

        if ((arg=fcntl(sockfd, F_GETFL, NULL)) < 0) {
                printf("error getting socket flag for fd %i: fcntl(..., F_GETFL): %i\n", sockfd, errno);
                return -1;
        }
        // set O_NONBLOCK flag
        arg |= O_NONBLOCK;
        if ((i=fcntl(sockfd, F_SETFL, arg)) < 0) {
                printf("error setting socket flag for fd %i: fcntl(..., F_SETFL): %i\n", sockfd, errno);
                return -1;
        }
        return i;
}

int set_block(int sockfd) { // set socket to blocking
        int arg,i;

        if ((arg=fcntl(sockfd, F_GETFL, NULL)) < 0) {
                printf("error getting socket flag for fd %i: fcntl(..., F_GETFL): %i\n", sockfd, errno);
                return -1;
        }
        // clean O_NONBLOCK flag
        arg &= (~O_NONBLOCK);
        if ((i=fcntl(sockfd, F_SETFL, arg)) < 0) {
                printf("error setting socket flag for fd %i: fcntl(..., F_SETFL): %i\n", sockfd, errno);
                return -1;
        }
        return i;
}
*/
int main() {
        FILE *input;
        char slice[BUFLEN];
        int status = 0;
        pid_t pid;
        int err;
        int newfd;
        // if you want you can pass arguments to the program to execute
        // char *const arguments[] = {EXECFILE, "-v", NULL};
        char *const arguments[] = {EXECFILE,  NULL};
        int father2child_pipefd[2];
        int child2father_pipefd[2];
        char *read_data = NULL;
        FILE *retclam;
        fd_set myset;
        int x=1;

        signal(SIGPIPE, SIG_IGN);
        newfd = dup(0);
        input = fdopen(newfd, "r");

        pipe(father2child_pipefd); // Father speaking to child
        pipe(child2father_pipefd); // Child speaking to father

        pid = fork();
        if (pid > 0) { // Father
                close(father2child_pipefd[0]);
                close(child2father_pipefd[1]);

                // Write to the pipe reading from stdin
                retclam = fdopen(child2father_pipefd[0], "r");


                // set the two fd non blocking
                //set_nonblock(0);
                //set_nonblock(child2father_pipefd[0]);
                //set_nonblock(fileno(retclam));

                while(x==1) {
                        // clear the file descriptor set
                        FD_ZERO(&myset);
                        // add the stdin to the set
                        FD_SET(fileno(input), &myset);
                        // add the child pipe to the set
                        FD_SET(fileno(retclam), &myset);

                        // here we wait for data to arrive from stdin or from the child pipe. The last argument is a timeout, if you like
                        err = select(fileno(retclam)+1, &myset, NULL, NULL, NULL);
                        switch(err) {
                        case -1:
                                // Problem with select(). The errno variable knows why
                                //exit(1);
                                x=0;
                                break;
                        case 0:
                                // timeout on select(). Data did not arrived in time, only valid if the last attribute of select() was specified
                                break;
                        default:
                                // data is ready to be read
                                bzero(slice, BUFLEN);
                                if (FD_ISSET(fileno(retclam), &myset)) { // data ready on the child
                                        //set_block(fileno(retclam));
                                        read_data = fgets(slice, BUFLEN, retclam); // read a line from the child (max BUFLEN bytes)
                                        //set_nonblock(fileno(retclam));
                                        if (read_data == NULL) {
                                                //exit(0);
                                                x=0;
                                                break;
                                        }
                                        // write data back to stdout
                                        write (1, slice, strlen(slice));
                                        if(feof(retclam)) {
                                                //exit(0);
                                                x=0;
                                                break;
                                        }
                                        break;
                                }
                                bzero(slice, BUFLEN);
                                if (FD_ISSET(fileno(input), &myset)) { // data ready on stdin
                                        //printf("father\n");
                                        //set_block(fileno(input));
                                        read_data = fgets(slice, BUFLEN, input); // read a line from stdin (max BUFLEN bytes)
                                        //set_nonblock(fileno(input));
                                        if (read_data == NULL) {
                                                //exit (0);
                                                close(father2child_pipefd[1]);
                                                waitpid(pid, &status, 0);
                                                //fclose(input);
                                                break;
                                        }
                                        // write data to the child
                                        write (father2child_pipefd[1], slice, strlen(slice));
                                        /*
                                        if(feof(input)) {
                                                exit(0);
                                        }*/
                                        break;
                                }
                        }
                }

                close(father2child_pipefd[1]);
                fclose(input);
                fsync(1);
                waitpid(pid, &status, 0);

                // child process terminated
                fclose (retclam);

                // Parse output data from child
                // write (1, "you can append somethind else on stdout if you like");
                if (WEXITSTATUS(status) == 0) {
                        exit (0); // child process exited successfully
                }
        }

        if (pid == 0) { // Child
                close (0); // stdin is not needed
                close (1); // stdout is not needed
                // Close the write side of this pipe
                close(father2child_pipefd[1]);
                // Close the read side of this pipe
                close(child2father_pipefd[0]);

                // Let's read on stdin, but this stdin is associated to the read pipe
                dup2(father2child_pipefd[0], 0);
                // Let's speak on stdout, but this stdout is associated to the write pipe
                dup2(child2father_pipefd[1], 1);

                // if you like you can put something back to the father before execve
                //write (child2father_pipefd[1], "something", 9);
                //fsync(child2father_pipefd[1]);
                err = execve(EXECFILE, arguments, NULL);

                // we'll never be here again after execve succeeded!! So we get here only if the execve() failed
                //fprintf(stderr, "Problem executing file %s: %i: %s\n", EXECFILE, err, strerror(errno));
                exit (1);
        }

        if (pid < 0) { // Error
                exit (1);
        }

        fclose(input);

        return 0;
}

在您發布的Python代碼中,您沒有使用正確的流:

inputready, outputready, exceptready = select.select(
    [proc.stdout, proc.stderr], # read list
    [proc.stdout, proc.stderr], # write list
    [proc.stdout, proc.stderr], # error list.
    0)                          # time out.

我沒有嘗試修復它,但是我敢打賭對同一組流進行讀寫是不正確的。


您的示例中有很多錯誤。 首先是作為子進程啟動的python可執行文件不產生任何輸出。 第二個原因是存在競爭條件,因為您可以在子進程產生輸出之前連續調用select() 5次,在這種情況下,您將在讀取任何內容之前終止該進程。

我修復了上面提到的三個問題(寫列表,啟動產生輸出和競爭條件的過程)。 試用此示例,看看它是否適合您:

#!/usr/bin/python
import subprocess, os, select, time

path = "/usr/bin/python"
proc = subprocess.Popen([path, "foo.py"], shell=False,
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE)
for i in xrange(0,5):
    time.sleep(1)
    inputready, outputready, exceptready = select.select(
        [proc.stdout, proc.stderr], [proc.stdin,],
        [proc.stdout, proc.stderr, proc.stdin], 0)
    if not inputready:
        print "No Data",
    print inputready, outputready, exceptready
    for s in inputready:
        print s.fileno(),s.readline()

proc.terminate()
print "After Terminating"

for i in xrange(0,5):
    inputready, outputready, exceptready = select.select(
        [proc.stdout, proc.stderr], [proc.stdin,],
        [proc.stdout, proc.stderr, proc.stdin], 0)
    if not inputready:
        print "No Data",
    print inputready, outputready, exceptready
    for s in inputready:
        print s.fileno(),s.readline()

我使用的foo.py文件包含以下內容:

#!/usr/bin/python
print "Hello, world!"

以下版本(大部分刪除了冗余輸出以使結果更易於閱讀):

#!/usr/bin/python
import subprocess, os, select, time

path = "/usr/bin/python"
proc = subprocess.Popen([path, "foo.py"], shell=False,
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE)
for i in xrange(0,5):
    time.sleep(1)
    inputready, outputready, exceptready = select.select(
        [proc.stdout, proc.stderr], [proc.stdin,],
        [proc.stdout, proc.stderr, proc.stdin], 0)
    for s in inputready:
        line = s.readline()
        if line:
            print s.fileno(), line

proc.terminate()
print "After Terminating"

for i in xrange(0,5):
    time.sleep(1)
    inputready, outputready, exceptready = select.select(
        [proc.stdout, proc.stderr], [proc.stdin,],
        [proc.stdout, proc.stderr, proc.stdin], 0)
    for s in inputready:
        line = s.readline()
        if line:
            print s.fileno(), line

提供以下輸出:

5您好,世界!

終止后

請注意,由於某些原因,在select.select()使用timeout參數不會在我的系統上產生預期的結果,因此我改為使用time.sleep()


僅供參考,以python身份運行

 /usr/bin/python 2>&1|tee test.out 

似乎工作正常。

您無法獲得這種效果,因為此示例仍然為python解釋器提供了控制tty。 如果沒有控制tty,則python解釋器不會打印Python版本,也不會顯示>>>提示。

一個接近的例子將是如下所示。 您可以將/dev/null替換為包含要發送到解釋器的命令的文件。

/usr/bin/python </dev/null 2>&1|tee test.out

如果將控制tty(鍵盤)以外的任何內容重定向為該過程的標准輸入,則python解釋器將沒有輸出。 這就是為什么您的代碼似乎不起作用的原因。

我在bash中使用2-way io像這樣:

mkfifo hotleg
mkfifo coldleg

program <coldleg |tee hotleg &

while read LINE; do
 case $LINE in
  *)call_a_function $LINE;;
 esac
done <hotleg |tee coldleg &

(請注意,您可以只用“>”代替tee,但是您可能希望首先看到輸出)

您認為應該歸咎於緩沖I / O是最正確的。 編寫循環的方式是,讀取將一直阻塞,直到填充所需的緩沖區為止,直到返回,您才能處理任何輸入。 這很容易導致死鎖。

Popen.communicate通過使線程與每個管道一起工作並確保將所有數據都寫入stdin來解決此問題,從而在文件對象等待緩沖區填充或填充時,不會延遲實際寫入用於要刷新/關閉的文件對象。 我認為您可以根據需要提供一個涉及線程工作的解決方案,但這並不是真正的異步方法,並且可能不是最簡單的解決方案。

您可以通過不使用Popen提供的文件對象訪問管道,而使用fileno()方法獲取其fd來避免python的緩沖。 然后,可以將fd與os.read,os.write和select.select一起使用。 os.read和os.write函數將不進行緩沖,但它們將阻塞直到至少可以讀取/寫入一個字節。 在調用管道之前,您需要確保管道可讀/可寫。 最簡單的方法是使用select.select()等待要讀取/寫入的所有管道,並在select()返回時對每個就緒的管道進行單個讀取或寫入調用。 如果您進行搜索,則應該能夠找到選擇循環的示例(它們可能使用套接字而不是管道,但是原理是相同的)。 (此外,切勿在未先檢查其不會阻塞的情況下進行讀取或寫入,否則最終可能會導致子進程陷入僵局。即使沒有還寫了你想要的一切。)

如果您需要控制Python解釋器會話,最好使用

順便說一句,在后一種情況下,服務器可以在任何地方運行,並且PyScripter已經有一個正常工作的服務器模塊(客戶端模塊在Pascal中,需要翻譯)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM