UNIX 下 C 實(shí)現(xiàn) Pipe Descriptors 映射

翻譯整理自 http://unixwiz.net/techtips/remap-pipe-fds.html

在UNIX中,通過pipe()可以創(chuàng)建一對單向的pipe descriptors;通過fork()可以創(chuàng)建一個(gè)子進(jìn)程(即創(chuàng)建一個(gè)當(dāng)前進(jìn)程的“鏡像”),兩者初始時(shí)狀態(tài)相同,執(zhí)行互不干擾。利用這兩點(diǎn),我們可以實(shí)現(xiàn)進(jìn)程間的數(shù)據(jù)交互,進(jìn)而可以將一個(gè)進(jìn)程的幾個(gè)standard POSIX file通過dup2()函數(shù)被自定義的pipe所映射。這里主要介紹該實(shí)現(xiàn)原理和幾個(gè)注意點(diǎn)以避免descriptor之間產(chǎn)生沖突引起的I/O errors。

UNIX Pipes

pipe(int fd[2])函數(shù)可得到兩個(gè)file descriptors形成一個(gè)單向的pipe,fd[0]固定為讀端,fd[1]固定為寫端。

單向pipe

通常pipe作為進(jìn)程間的讀寫通道,并不會用在單獨(dú)的一個(gè)進(jìn)程中。

The "usual" way

UNIX pipe由一對file descriptors組成,即一端寫,一端讀。對于單進(jìn)程來說并沒有意義,但可以作為多個(gè)進(jìn)程之間的數(shù)據(jù)通道,由于UNIX pipe是單向的,所以在數(shù)據(jù)交換時(shí)常會用到多個(gè)pipe。例如兩個(gè)進(jìn)程之間需要完成數(shù)據(jù)的交互,那么就需要兩個(gè)pipe,即四個(gè)file descriptors。
使用舉例如下:

...

int writepipe[2] = {-1,-1}, /* parent -> child */
    readpipe [2] = {-1,-1}; /* child -> parent */
pid_t   childpid;

/*------------------------------------------------------------------------
 * CREATE THE PAIR OF PIPES
 *
 * Pipes have two ends but just one direction: to get a two-way
 * conversation you need two pipes. It's an error if we cannot make
 * them both, and we define these macros for easy reference.
 */
writepipe[0] = -1;

if ( pipe(readpipe) < 0  ||  pipe(writepipe) < 0 )
{
    /* FATAL: cannot create pipe */
    /* close readpipe[0] & [1] if necessary */
}

#define PARENT_READ readpipe[0]
#define CHILD_WRITE readpipe[1]
#define CHILD_READ  writepipe[0]
#define PARENT_WRITE    writepipe[1]

if ( (childpid = fork()) < 0)
{
    /* FATAL: cannot fork child */
}
else if ( childpid == 0 )   /* in the child */
{
    close(PARENT_WRITE);
    close(PARENT_READ);

    dup2(CHILD_READ,  0);  close(CHILD_READ);
    dup2(CHILD_WRITE, 1);  close(CHILD_WRITE);

    /* do child stuff */
}
else                /* in the parent */
{
    close(CHILD_READ);
    close(CHILD_WRITE);

    /* do parent stuff */
}

descriptors崩潰

在pipe(fd)執(zhí)行時(shí),通常會為fd分配兩個(gè)當(dāng)前最小且可用的值作為文件描述符放入fd[2]數(shù)組;正常情況下不會造成問題,但如果恰好此時(shí)FD#0和#1(stdin, stdout)均可用且在該狀態(tài)下是最小的,那么這兩個(gè)描述符會被分配到fd中。此時(shí),若進(jìn)行上述的dup2操作,會產(chǎn)生矛盾,先看下面一段代碼:

#define PARENT_READ  pipe1[0]   /* fd#0 */
#define CHILD_WRITE  pipe1[1]   /* fd#1 */
#define CHILD_READ   pipe2[0]   /* fd#2 */
#define PARENT_WRITE pipe2[1]   /* fd#3 */

dup2(CHILD_READ=2,  0);  close(CHILD_READ=2);
dup2(CHILD_WRITE=1, 1);  close(CHILD_WRITE=1);

假如為pipe1分配了 FD#0 #1,為pipe2分配了FD#2 #3,而第一個(gè)dup2函數(shù)又將pipe2[0](CHILD_READ)指向FD#0,即指向了pipe[0](PARENT_READ)。后臺守護(hù)進(jìn)程檢測到PARENT_READ不再工作,便會關(guān)閉PARENT_READ。接下來CHILD_READ也被關(guān)閉,那么此時(shí)PARENT進(jìn)程的read和write操作將會不再有意義。

Getting it right

如果要進(jìn)行映射的file descriptor的值和目標(biāo)均不同,(即所有dup2(souce, target)調(diào)用中souces和targets的值均不同),那么可以按照上述代碼片段1進(jìn)行file descriptor映射,否則就需要重新設(shè)計(jì)調(diào)用邏輯。
這里我們以上述的#0#1(stdin, stdout)為例,需要考慮souce這一file descriptor的三種情況:

  • 0 - 可能造成沖突
  • 1 - 也可能造成沖突
  • >1 - 安全

考慮到一個(gè)pipe的兩端(同一個(gè)pipe的read和write文件描述符)值是不會相等的,可以通過一個(gè)表格來審計(jì)所有需要考慮的操作。

#define DUP2CLOSE(oldfd, newfd) ( dup2(oldfd, newfd), close(oldfd) )

每個(gè)特定情況下所需的操作如下表:

| read FD (should be 0) | write FD (should be 1) | Action |
| :-----------: | :-------------: | : ----- |
| 0 | 1 | nothing - it's already done |
| >=1 | >1 | DUP2CLOSE(rfd, 0); DUP2CLOSE(wfd, 1); |
| 0 | >1 | DUP2CLOSE(wfd, 1); |
| >1 | 1 | DUP2CLOSE(rfd, 0); |
| >1 | 0 | DUP2CLOSE(wfd, 1); DUP2CLOSE(rfd, 0); |
| 1 | 0 | tmp = dup(wfd); close(wfd); DUP2CLOSE(rfd, 0); DUP2CLOSE(tmp, 1); |

在類似的映射操作中,通過上述遍歷情況矩陣的形式審計(jì)映射操作會大大減少錯(cuò)誤的發(fā)生。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容