同一个TCP端口(地址相同)是不能被多次绑定、监听的。因此,多线程程序无法并发地accept连接。而多进程可以并发accept新连接,办法就是先创建、绑定好端口,完成listen调用,然后fork出子进程,子进程继承父进程的文件描述符,然后子进程、父进程都可以accept。
很多石器时代的C/C++程序员认为,如果有一个新连接建立完成,此时阻塞在accept系统调用上的进程都会被唤醒,但是只有一个进程accept成功(这种现象叫做惊群,哈哈哈)。
事实果真如此吗???这个问题的答案取决于内核(Unix/类Unix/Linux,以及具体哪个内核版本,都可能导致答案不同,鄙人只探讨Linux平台,没有BSD操作系统的历史包袱,也没有AIX/Solaris等商业操作系统的环境和必要)。我不喜欢不去思考内核行为的程序员,因为他们太过自负,而且不懂得与时俱进地更新自己的知识。
操作系统内核是干什么的?这很明显是取决于内核的一个特性呀。
操作系统内核版本迭代,做了什么?他们以为仅仅是修复下bug,增加下版本号吗?
测试环境:
VirtualBox在Windows工作站上虚拟4核心,4GB内存机器,安装CentOS-6.10,来提供2.6.32内核环境(这里很可能对测试结果造成了影响,即同一个进程被唤醒,但是我条件限制暂时无法确认)。
阿里云上本人有一台4核心8G内存机器,来提供3.10内核测试环境:
4核心 3.10.0-693.el7.x86_64 #1 SMP Tue Aug 22 21:09:27 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
不过,有一点很明确,对于3.10内核来说,accept/select/epoll都解决了惊群问题
代码如下(子进程没有优雅地退出,新连接也没有优雅地关闭,要手动杀死子进程,原谅小哥哥这里的偷懒):
#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
using namespace std;
int set_opts(int fd)
{
const int enable = 1;
int ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable));
if(ret != 0)
{
cout << "setsockopt.addr.ret=" << ret << ",fd=" << fd << endl;
return ret;
}
ret = setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(enable));
if(ret != 0)
{
cout << "setsockopt.port.ret=" << ret << ",fd=" << fd << endl;