https://www.cnblogs.com/rockyching2009/p/11032230.html
一、背景
端對端的通信中存在的一個問題是:如何唯一地標識通信主體。對於socket,解決這個問題的方式是四元組:自身IP,自身端口,對方IP,對方端口。
在socket編程中,作為client,端口號是由操作系統管理和分配的,所以不存在端口占用的情況。如果作為server,在bind某個端口的時候,或多或少遇到過如下錯誤:
bind: Address already in use
在進程異常終止或重啟網絡服務的時候,出現上述問題的情況很常見;而作為服務端又需要進程馬上步入正軌,這個問題就顯得尤其重要。
二、原因及解決方案
/** From APUE */
int initserver(int type, const struct sockaddr *addr, socklen_t alen)
{
int fd;
if ((fd = socket(addr->sa_family, SOCK_STREAM, 0)) < 0) {
perror("socket");
return (-1);
}
if (bind(fd, addr, alen) < 0) {
perror("bind");
goto errout;
}
if (listen(fd, SOMAXCONN) < 0) {
perror("listen");
goto errout;
}
return (fd);
errout:
err = errno;
close(fd);
errno = err;
return(-1);
}
以上代碼段,進程終止后立即投入運行的話,就會產生開篇說的問題。原因在APUE這本書也有提及:
Normally, the implementation of TCP will prevent us from binding the same address until a timeout expires,
which is usually on the order of several minutes.
同時也提供了解決方法,給socket設置SO_REUSEADDR屬性:
int reuse = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int);
三、未完再續
但是,如果你的進程調用了fork()或system()函數,上述方案則無能為力。
對於子進程,創建成功后將復制父進程的數據空間、堆和棧副本,這其中也包括文件描述符(fork過程);而且,一般我們fork子進程后接着執行一個新程序(execve過程),這就導致copy過來的文件描述符一直無法關閉,如此即使父進程終止,其監聽的socket也無法釋放,進而相應端口一直處於Listen狀態(netstat命令),導致進程無法bind指定端口。
知道了原因,對症下葯即可:創建的子進程execve時禁止對文件描述符的復制,即close-on-exec。
在Linux系統中,這是通過fcntl()設置文件描述符的FD_CLOEXEC標志實現:
int set_cloexec(int fd)
{
int val;
val = fcntl(fd, F_GETFD, 0);
val |= FD_CLOEXEC; /* enable close-on-exec */
return fcntl(fd, F_SETFD, val);
}
莎翁說:簡潔是智慧的靈魂,上面一段畢竟顯得臃腫,所以socket引入了SOCK_CLOEXEC實現同樣的功能
對於通過open()函數創建的文件描述符,可以設置flags入參O_CLOEXEC選項達到close-on-exec的目的。
參考資料
2、《UNIX環境高級編程》
