[英]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
之前終止(即在已經有waitpid
和accept
地方之間)。 克服這個有兩種主要可能性:
在SIGCHLD
處理程序中執行所有子終止處理,而不是主循環。 但是,這可能是不可行的,因為在信號處理程序中允許執行的操作存在重大限制。 例如,您可能不會調用printf
(盡管您可以使用write
)。
我並不建議你沿着這條路走下去,雖然它起初可能看起來更簡單但它是最不靈活的選擇,可能在以后證明是行不通的。
寫入SIGCHLD
信號處理程序中非阻塞管道的一端。 在主循環中,不是直接調用accept
,而是使用poll
(或select
)在套接字和管道的讀取端查找准備情況,並適當地處理每個。
在Linux(和OpenBSD,我不確定其他人)你可以使用ppoll
( 手冊頁 )來避免創建管道(在這種情況下你應該保持信號被屏蔽,並在輪詢操作期間將其取消屏蔽) ;如果ppoll
在EINTR
失敗,你知道收到了一個信號,你應該調用waitpid
)。 您仍然需要為SIGCHLD設置信號處理程序,但它不需要做任何事情。
Linux上的另一個選擇是使用signalfd
( 手冊頁 )來避免創建管道和設置信號處理程序(我認為)。 如果使用此信號,則應屏蔽SIGCHLD信號(使用sigprocmask
)。 當poll
(或等效)指示signalfd處於活動狀態時,從中讀取信號數據(清除信號),然后調用waitpid
來收集孩子。
在各種BSD系統上,您可以使用kqueue
( OpenBSD手冊頁 )而不是poll
並觀察信號,而無需建立信號處理程序。
在其他POSIX系統上,您可能能夠以與上述ppoll
類似的方式使用pselect
( 文檔 )。
還可以選擇使用諸如libevent之類的庫來抽象出特定於操作系統的內容。
Glibc手冊有一個使用select
的例子 。 有關這些功能的更多信息,請參閱poll
, ppoll
, pselect
手冊頁。 有一本關於使用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.