簡體   English   中英

C:fork()在子進程斷開連接時通知父進程

[英]C: fork() inform parent when child process disconnects

我在C中做一個簡單的服務器/客戶端程序,它監聽網絡接口並接受客戶端。 每個客戶端都在分叉的過程中處理。

我的目標是,一旦客戶端與子進程斷開連接,就讓父進程知道。

目前我的主循環看起來像這樣:

for (;;) {

    /* 1. [network] Wait for new connection... (BLOCKING CALL) */
    fd_listen[client] = accept(fd_listen[server], (struct sockaddr *)&cli_addr, &clilen);

    if (fd_listen[client] < 0) {
        perror("ERROR on accept");
        exit(1);
    }


    /* 2. [process] Call socketpair */
    if ( socketpair(AF_LOCAL, SOCK_STREAM, 0, fd_comm) != 0 ) {
        perror("ERROR on socketpair");
        exit(1);
    }


    /* 3. [process] Call fork */
    pid = fork();

    if (pid < 0) {
        perror("ERROR on fork");
        exit(1);
    }

    /* 3.1 [process] Inside the Child */
    if (pid == 0) {

        printf("[child] num of clients: %d\n", num_client+1);
        printf("[child] pid: %ld\n", (long) getpid());

        close(fd_comm[parent]);     // Close the parent socket file descriptor
        close(fd_listen[server]);   // Close the server socket file descriptor

        // Tasks that the child process should be doing for the connected client
        child_processing(fd_listen[client]);

        exit(0);
    }
    /* 3.2 [process] Inside the Parent */
    else {

        num_client++;
        close(fd_comm[child]);      // Close the child socket file descriptor
        close(fd_listen[client]);   // Close the client socket file descriptor

        printf("[parent] num of clients: %d\n", num_client);

        while ( (w = waitpid(-1, &status, WNOHANG)) > 0) {
            printf("[EXIT] child %d terminated\n", w);
            num_client--;
        }
    }

}/* end of while */ 

這一切都很好,我唯一的問題是(可能)由於阻塞accept調用。

當我連接到上面的服務器時,會創建一個新的子進程並child_processing

但是,當我與該客戶端斷開連接時,主父進程不知道它並且不輸出printf("[EXIT] child %d terminated\\n", w);

但是,當我第一個客戶端斷開連接后與第二個客戶端連接時,主循環能夠最終處理while ( (w = waitpid(-1, &status, WNOHANG)) > 0)部分並告訴我第一個客戶已斷開連接。

如果之后只有一個客戶端連接和斷開連接,我的主要父進程將永遠無法判斷它是否已斷開連接。

有沒有辦法告訴父進程我的客戶已經離開了?

UPDATE

因為我是一個真正的初學者,如果你提供一些簡短的片段給你的答案,那么我真的可以理解它:-)

您的waitpid使用情況不正確。 你有一個非阻塞的調用,所以如果孩子沒有完成,那么調用得到0:

waitpid() :成功時,返回狀態已更改的子進程ID; 如果指定了WNOHANG並且存在由pid指定的一個或多個子(ren),但尚未更改狀態,則返回0。 出錯時,返回-1。

所以你馬上就會離開while循環。 當然,這可以在第一個孩子終止時被捕獲,而第二個孩子可以讓你再次處理waitpid

因為你需要有一個非阻塞的等待調用,我建議你不要直接管理終止,但是通過SIGCHLD信號可以讓你捕獲任何孩子的終止,然后在處理程序中適當地調用waitpid

void handler(int signal) {
  while (waitpid(...)) { // find an adequate condition and paramters for your needs
}

...
struct sigaction act;
act.sa_flag = 0;
sigemptyset(&(act.sa_mask));
act.sa_handler = handler;
sigaction(SIGCHLD,&act,NULL);
... // now ready to receive SIGCHLD when at least a children changes its state

如果我理解正確,您希望能夠同時為多個客戶端提供服務,因此您的waitpid調用是正確的,因為如果沒有子節點終止,它就不會阻塞。

但是,您遇到的問題是,您需要能夠在通過accept等待新客戶端時處理異步子終止。 假設你正在處理POSIXy系統,只是建立了一個SIGCHLD處理程序並且信號未被屏蔽(通過sigprocmask ,雖然IIRC默認情況下是未屏蔽的),應該足以導致如果子EINTR終止時accept失敗並使用EINTR您正在等待新客戶端連接 - 然后您可以適當地處理EINTR

這樣做的原因是當子進程終止時,SIGCHLD信號將自動發送到父進程。 通常,如果在等待時接收到信號,則諸如accept系統調用將返回EINTR (“中斷”)的錯誤。

然而,仍然存在競爭條件,其中一個孩子你打電話給accept 之前終止(即在已經有waitpidaccept地方之間)。 克服這個有兩種主要可能性:

  1. SIGCHLD處理程序中執行所有子終止處理,而不是主循環。 但是,這可能是不可行的,因為在信號處理程序中允許執行的操作存在重大限制。 例如,您可能不會調用printf (盡管您可以使用write )。

    我並不建議你沿着這條路走下去,雖然它起初可能看起來更簡單但它是最不靈活的選擇,可能在以后證明是行不通的。

  2. 寫入SIGCHLD信號處理程序中非阻塞管道的一端。 在主循環中,不是直接調用accept ,而是使用poll (或select )在套接字和管道的讀取端查找准備情況,並適當地處理每個。

    在Linux(和OpenBSD,我不確定其他人)你可以使用ppoll手冊頁 )來避免創建管道(在這種情況下你應該保持信號被屏蔽,並在輪詢操作期間將其取消屏蔽) ;如果ppollEINTR失敗,你知道收到了一個信號,你應該調用waitpid )。 您仍然需要為SIGCHLD設置信號處理程序,但它不需要做任何事情。

    Linux上的另一個選擇是使用signalfd手冊頁 )來避免創建管道和設置信號處理程序(我認為)。 如果使用此信號,則應屏蔽SIGCHLD信號(使用sigprocmask )。 poll (或等效)指示signalfd處於活動狀態時,從中讀取信號數據(清除信號),然后調用waitpid來收集孩子。

    在各種BSD系統上,您可以使用kqueueOpenBSD手冊頁 )而不是poll並觀察信號,而無需建立信號處理程序。

    在其他POSIX系統上,您可能能夠以與上述ppoll類似的方式使用pselect文檔 )。

    還可以選擇使用諸如libevent之類的庫來抽象出特定於操作系統的內容。

Glibc手冊有一個使用select 的例子 有關這些功能的更多信息,請參閱pollppollpselect手冊頁。 有一關於使用Libevent的在線書籍

使用select粗略示例,借用Glibc文檔(並修改):

/* Set up a pipe and set signal handler for SIGCHLD */

int pipefd[2]; /* must be a global variable */
pipe(pipefd); /* TODO check for error return */
fcntl(pipefd[1], F_SETFL, O_NONBLOCK); /* set write end non-blocking */

/* signal handler */
void sigchld_handler(int signum)
{
    char a = 0; /* write anything, doesn't matter what */
    write(pipefd[1], &a, 1);
}

/* set up signal handler */
signal(SIGCHLD, sigchld_handler);

您當前已accept ,您需要檢查服務器套接字的狀態和管道的讀取端:

fd_set set, outset;
struct timeval timeout;

/* Initialize the file descriptor set. */
FD_ZERO (&set);
FD_SET (fdlisten[server], &set);
FD_SET (pipefds[0], &set);
FD_ZERO(&outset);

for (;;) {
    select (FD_SETSIZE, &set, NULL, &outset, NULL /* no timeout */));
    /* TODO check for error return.
       EINTR should just continue the loop. */

    if (FD_ISSET(fdlisten[server], &outset)) {
        /* now do accept() etc */
    }

    if (FD_ISSET(pipefds[0], &outset)) {
        /* now do waitpid(), and read a byte from the pipe */
    }
}

使用其他機制通常更簡單,所以我把它們留作練習:)

暫無
暫無

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

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