2025-01-19 00:37:00
说实话,编译器是否该利用 Undefined Behavior 进行优化目前都还是一个争议话题,主要是 gcc 开了个坏头,不予余力的在默认参数下利用 UB 来优化,举个例子,C 语言里带符号整数溢出是未定义行为,编译器应该假设它实际上以某种方式定义了:
int foo(unsigned char c) {
int value = 2147483600;
value += c;
if (value < 2147483600)
bar();
return value;
}
但利用这个 UB 进行优化的编译器会认为,既然 x 不会是负数,那么 value < 2147483600 就永远不会发生,所以整个 if 语句以及后面的 bar() 调用将可以被忽略,变成:
int foo(unsigned char c) {
int value = 2147483600;
value += c;
return value;
}
这其实是一种很危险的做法,因为 C 语言可以跨越各种 CPU 架构编程,当年标准定义时,CPU 架构的差异比今天还大,在处理上面这类问题时,即便在今天,不同的架构结果并不一定相同,比如有的平台用补码表示负数,所以溢出了就会变成 -2147483648,而碰到反码或者原码表示负数的架构下,溢出了可能就变成 0 或者其它,所以一些事情根本就没法具体定义,必须留给具体编译器具体平台去处理。
Undefined Behavior 并不是说代码这么写是错的,相反他们都是语法正确的代码,真是错的应该就编译错误了,而是标准留个编译器实现者以自由,不去做限制,让他们根据实际平台,根据自己实现情况自行选择实现方式,而某些编译器实现选择利用它来进行优化了,那么如果本来就想利用特定平台的特性完成某些特定功能时,所以这类代码将没法写了。
关于这个问题,有个 X 友说的很准确,这里贴下译文(原文贴后面):
亲爱的 C 程序员们,既然你们似乎无法阅读关于你们编程语言的文档,让我为你们解释一下什么是未定义行为,更重要的是它不是什么。
引用 C 标准:
未定义行为:本国际标准未做任何强制要求的行为。(Undefined behavior: behavior for which this International Standard imposes no requirements)
这意味着标准并没有规定某些表达式该如何表现,编译器的创造者们可以自由选择他们想要的行为,例如:
即,编译器的职责是合理处理未定义行为的情况。C 标准并没有对编译器作者提出挑战,这不是 “尽情发挥,给我惊喜”,它仅仅是对某些表达式没有施加限制,因为给予编译器作者自由选择其解决方案是合情合理的,因为这可能依赖于平台。
未定义行为并不是关于“被禁止的表达式”,它仅仅是语法上正确的 C 代码,而 C 标准对此并不关心。
到目前为止,这一切都是合理的。
在处理未定义行为时,编译器作者有多种选择:
正确的答案总是 “默认优先语义而非性能”。可以有破坏语义的优化,但这些优化应该通过适当的编译选项来启用,以便那些知道自己在做什么的人(或至少在继续破坏代码之前,编译器会得到明确的同意)使用。
(点击 more/continue 继续阅读)
让编译器默认具有一致的语义不仅减少了各种开发人员(包括经验丰富的开发人员)引入的错误,还极大地改善了开发人员的用户体验,因为这样可以让他们更好地推理代码,毫无畏惧地进行重构,并且总体上不必时刻担心优化器会搞砸他们的代码。
至于 “但您必须支持多个平台” 的论点,如果 GCC 的开发者想要这样,他们就会让整数溢出在编译时的行为与运行时的行为相同。也就是说,如果在运行时是环绕运算,那么在编译时也是环绕运算。或者,他们可以简单地拒绝承诺任何特定的整数溢出策略,而只是不过度尝试优化涉及算术运算的表达式。
不幸的是,GCC 的开发者和他们的社区一样愚蠢,他们不仅没有合理地定义未定义行为,反而假装这一切都与禁止表达式有关,并默认启用他们那些让语义破裂的优化。“看,妈的,代码快了5%,却带来了 5% 的更多 bug,搞得全世界的基础设施和生产代码都乱了!”
至于他们在整数溢出问题上的选择有多愚蠢,我们会另行讨论,但这则推文的要点并不是某个具体优化的糟糕,而是 GCC 将 C标准中的未定义行为作为借口来搞砸自己编译器的整体方法,这真是让人难以置信——这是只有少数人才能理解的全面退化。
任何有不同看法的人都应该被视为愚蠢,应该被限制接触任何计算机,保持50米的安全距离。
原文:https://x.com/effectfully/status/1876231418357092534
这里还有个 原文长截图。
The post GCC 利用未定义行为进行优化正确么? appeared first on Skywind Inside.
2024-11-04 11:41:09
异步事件模型中有一个重要问题是,当你的 select/poll 循环陷入等待时,没有办法被另外一个线程被唤醒,这导致了一系列问题:
1)在没有 pselect/ppoll 的系统上,信号无法中断 select/poll 等待,得不到即时处理;
2)另一个线程投递过来的消息,由于 select/poll 等待,无法得到即时处理;
3)调短 select/poll 的超时时间也无济于事,poll 的超时精度最低 1ms,粗糙的程序可能影响不大,但精细的程序却很难接受这个超时;
4)有的系统上即便你传了 1ms 进去,可能会等待出 15ms 也很正常。
比如主线程告诉网络线程要发送一个数据,网络线程还在 select/poll 那里空等待,根本没有机会知道自己自己的消息队列里来了新消息;或者多个 select/poll 循环放在不同线程里,当一个 accept 了一个新连接想转移给另一个时,没有办法通知另一个醒来即时处理。
解决这个问题的方法就叫做 self-pipe trick,顾名思义,就是创建一个匿名管道,或者 socketpair,把它加入 select/poll 中,然后另外一个线程想要唤醒它的话,就是往这个管道或者 socketpair 里写一个字节就行了。
类似 java 的 nio 里的 selector 里面的 notify()
函数,允许其他线程调用这个函数来唤醒等待中的一个 selector。
具体实现有几点要注意,首先是使用 notify()
唤醒,不用每次调用 notify()
都往管道/socketpair 里写一个字节,可以加锁检测,没写过才写,写过就不用写了:
// notify select/poll to wake up
void poller_notify(CPoller *poller) {
IMUTEX_LOCK(&poller->lock_pipe);
if (poller->pipe_written == 0) {
char dummy = 1;
int hr = 0;
#ifdef __unix
hr = write(poller->pipe_writer_fd, &dummy, 1);
#else
hr = send(poller->pipe_writer_fd, &dummy, 1);
#endif
if (hr == 1) {
poller->pipe_written = 1;
}
}
IMUTEX_UNLOCK(&poller->lock_pipe);
}
大概类似这样,在非 Windows 下面把 pipe()
创建的两个管道中的其中一个放到 select/poll 中,所以用 write()
,而 Windows 下的 select 不支持放入管道,只支持套接字,所以把两个相互连接的套接字里其中一个放入 select。
两个配对的管道命名为 reader/writer,加入 select 的是 reader,而唤醒时是向 writer 写一个字节,并且判断,如果写过就不再写了,避免不停 notify 导致管道爆掉,阻塞线程。
而作为网络线程的 select/poll 等待,每次被唤醒时,甭管有没有网络数据,都去做一次管道复位:
static void poller_pipe_reset(CPoller *poller) {
IMUTEX_LOCK(&poller->lock_pipe);
if (poller->pipe_written != 0) {
char dummy = 0;
int hr;
#if __unix
hr = read(poller->pipe_reader_fd, &dummy, 1);
#else
hr = recv(poller->pipe_reader_fd, &dummy, 1);
#endif
if (hr == 1) {
poller->pipe_written = 0
}
}
IMUTEX_UNLOCK(&poller->lock_pipe);
}
每次 select/poll 醒来,都调用一下这个 poller_pipe_reset()
,这样确保管道里的数据被清空后,就可以复位 pipe_written
标志了。
让后紧接着,处理完所有网络事件,就检查自己内部应用层的消息队列是否有其他消息投递过来,再去处理这些事件去;而其他线程想给这个线程发消息,也很简单,消息队列里塞一条,然后调用一下 notify()
,把该线程唤醒,让他可以马上去检查自己的消息队列。
主循环大概这样:
while (is_running) {
// 1)调用 select/poll 等待网络事件,超时设置成 1ms-10ms;
// 2)醒来后先处理所有网络事件;
// 3)如果和上次等待之间超过 1毫秒,则马上处理所有时钟超时事件;
// 4)检查自己消息队列,并处理新到来的事件。
}
差不多就是这样。
PS:有人说用 eventfd 也能实现类似效果,没错,但不能跨平台,只有 Linux 特有,而且还有一些坑,但 self-pipe trick 是跨平台的通用解决方案,不管你用 Windows / FreeBSD / Linux / Solaris 都可以使用这个功能。
The post 异步事件模型的 Self-pipe trick appeared first on Skywind Inside.
2024-11-01 17:04:09
在 Linux/Unix 等 posix 环境中,每个套接字都是一个文件描述符 fd
,类型是 int
,使用起来非常方便;但在 Win32 环境中是 SOCKET
类型被定义成 UINT_PTR
,是一个指针,在 x64 环境中一个 SOCKET
占用 8 个字节。
那么是否能将 SOCKET
类型强制转换成 int
类型保存没?这样就能统一用 int
在所有平台下表示套接字了,同时在 x64 环境下这样将 64 位的指针转换为 32 位的整数是否安全?
答案是可以的,下面将从三个方面说明一下。
每个 SOCKET 背后其实都是一个指向 Kernel Object 的 Handle,而每个进程的 Handle 的数量是有限的,见 MSDN 的 Kernel Objects:
Kernel object handles are process specific. That is, a process must either create the object or open an existing object to obtain a kernel object handle. The per-process limit on kernel handles is 2^24. However, handles are stored in the paged pool, so the actual number of handles you can create is based on available memory.
单进程不会超过 2^24 个,每个 Kernel Object 需要通过一个 Handle 来访问:
这些 Handle 保存于每个进程内位于低端地址空间的 Handle Table 表格,而这个 Handle Table 是连续的,见 MSDN 中的 Handles and objects:
Each handle has an entry in an internally maintained table. Those entries contain the addresses of the resources, and the means to identify the resource type.
这个 Handle Table 表格对用户进程只读,对内核是可读写,在进程结束时,操作系统会扫描整个表格,给每个有效 Handle 背后指向的 Kernel Object 解引用,来做资源回收。
所以看似是 UINT_PTR
指针的 SOCKET
类型,其实也只是一个表格索引而已,这个 Handle Table 表格的项目有数量限的(最多 2^24 个元素),内容又是连续的,那当然可以用 int
来保存。
故此不少开源项目也会选择在 Windows 环境下将 SOCKET
类型直接用 int
来存储,比如著名的 openssl 在 include/internal/sockets.h 里有解释:
/*
* Even though sizeof(SOCKET) is 8, it's safe to cast it to int, because
* the value constitutes an index in per-process table of limited size
* and not a real pointer. And we also depend on fact that all processors
* Windows run on happen to be two's-complement, which allows to
* interchange INVALID_SOCKET and -1.
*/
# define socket(d,t,p) ((int)socket(d,t,p))
# define accept(s,f,l) ((int)accept(s,f,l))
所以 openssl 不论什么平台,都将套接字看作 int
来使用:
int SSL_set_fd(SSL *ssl, int fd);
int SSL_set_rfd(SSL *ssl, int fd);
int SSL_set_wfd(SSL *ssl, int fd);
所以它的这些 API 设计,清一色的 int
类型。
道理前面都讲完了,下面写个程序验证一下:
void create(int n) {
std::vector<SOCKET> sockets;
for (int i = 0; i < n; i++) {
SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
if (s == INVALID_SOCKET) {
printf("socket failed with error %d\n", WSAGetLastError());
break;
}
sockets.push_back(s);
printf("index=%d socket=%llu\n", i, (uint64_t)s);
}
for (int i = 0; i < n; i++)
closesocket(sockets[i]);
printf("\n");
}
int main(void) {
WSADATA WSAData;
WSAStartup(0x202, &WSAData);
printf("Round 1:\n");
create(10);
printf("Round 2:\n");
create(10);
return 0;
}
在 64 位环境下,创建 10 个套接字,然后释放,再创建 10 个:
Round 1:
index=0 socket=352
index=1 socket=324
index=2 socket=340
index=3 socket=332
index=4 socket=336
index=5 socket=356
index=6 socket=360
index=7 socket=364
index=8 socket=368
index=9 socket=372
Round 2:
index=0 socket=372
index=1 socket=368
index=2 socket=364
index=3 socket=360
index=4 socket=376
index=5 socket=356
index=6 socket=336
index=7 socket=332
index=8 socket=340
index=9 socket=324
可以看出,即便在 64 位下面:1)SOCKET
指向的表格项目是连续的;2)前面释放掉的表格项目,是会被后面复用的;3)他们都在表格范围内,不会由于不停创建/销毁导致 SOCKET
的数值持续增长。
成功的印证了前面关于 Kernel Object 和 Handle Table 的解释。
—
补充:刚才我又写了个程序,不停创建 socket ,看能创建多少个,以及 socket 的值最高去到多少:
最终结果(64 位程序,台式机内存 96GB):
差不多就这样。
The post WinSock 可以把 SOCKET 类型转换成 int 保存么? appeared first on Skywind Inside.
2024-10-31 23:31:38
在做跨平台网络编程时,Windows 下面能够对应 epoll/kevent 这类 reactor 事件模型的 API 只有一个 select,但是却有数量限制,一次传入 select 的 socket 数量不能超过 FD_SETSIZE
个,而这个值是 64。
所以 java 里的 nio 的 select 在 Windows 也有同样的数量限制,很多移植 Windows 的服务程序,用了 reactor 模型的大多有这样一个限制,让人觉得 Windows 下的服务程序性能很弱。
那么这个数量限制对开发一个高并发的服务器显然是不够的,我们是否有办法突破这个限制呢?而 cygwin 这类用 Win32 API 模拟 posix API 的系统,又是如何模拟不受限制的 poll 调用呢?
当然可以,大概有三个方法让你绕过 64 个套接字的限制。
首先可以看 MSDN 中 winsock2 的 select 帮助,这个 FD_SETSIZE
是可以自定义的:
Four macros are defined in the header file Winsock2.h for manipulating and checking the descriptor sets. The variable FD_SETSIZE determines the maximum number of descriptors in a set. (The default value of FD_SETSIZE is 64, which can be modified by defining FD_SETSIZE to another value before including Winsock2.h.)
而在 winsock2.h
中,可以看到这个值也是允许预先定义的:
#ifndef FD_SETSIZE
#define FD_SETSIZE 64
#endif
只要你在 include 这个 winsock2.h
之前,自定义了 FD_SETSIZE
,即可突破 64 的限制,比如在 cygwin 的 poll 实现 poll.cc,开头就重定义了 FD_SETSIZE
:
#define FD_SETSIZE 16384 // lots of fds
#include "winsup.h"
#include <sys/poll.h>
#include <sys/param.h>
定义到了一个非常大的 16384,最多 16K 个套接字一起 select,然后 cygwin 后面继续用 select 来实现 posix 中 poll 函数的模拟。
这个方法问题不大,但有两个限制,第一是到底该定义多大的 FD_SETSIZE
呢?定义大了废内存,每次 select 临时分配又一地内存碎片,定义少了又不够用;其次是程序不够 portable,头文件哪天忘记了换下顺序,或者代码拷贝到其它地方就没法运行。
因此我们有了更通用的方法2。
(点击 more/continue 继续)
这个方法更为通用,按照 MSDN 里的 fd_set 定义:
typedef struct fd_set {
u_int fd_count;
SOCKET fd_array[FD_SETSIZE];
} fd_set, FD_SET, *PFD_SET, *LPFD_SET;
结构体里第一个成员 fd_count
代表这个 fd_set
里总共保存了多少个套接字,而后面的 fd_array
数组则存储了各个套接字的值,它的大小由前面的 FD_SETSIZE
宏决定,这决定了最大可存储数量。
我们来看看 winsock2.h
中几个操作 fd_set
的宏的实现:
#ifndef FD_ZERO
#define FD_ZERO(set) (((fd_set *)(set))->fd_count=0)
#endif
清空操作很简单,就是把 fd_count
设置成零就行了,而增加一个套接字:
#define FD_SET(fd, set) do { u_int __i;\
for (__i = 0; __i < ((fd_set *)(set))->fd_count ; __i++) {\
if (((fd_set *)(set))->fd_array[__i] == (fd)) {\
break;\
}\
}\
if (__i == ((fd_set *)(set))->fd_count) {\
if (((fd_set *)(set))->fd_count < FD_SETSIZE) {\
((fd_set *)(set))->fd_array[__i] = (fd);\
((fd_set *)(set))->fd_count++;\
}\
}\
} while(0)
简单来讲就是先判断数组里是否已经包含,如果没包含并且 fd_count
小于 FD_SETSIZE
的话就追加到 fd_array
后面并且增加 fd_count
值。
那么方案就是用一个动态结构模拟这个 fd_set
就行了,要用时直接强制类型转换成 fd_set
指针传递给 select
即可,微软的 devblogs 里一篇文章讲过这个方法:
但是它的实现是用模板做了个新的 fd_set
结构体,一旦实例化就定死了,我给一个更好的跨平台实现:
#define ISOCK_ERECV 1 /* event - recv */
#define ISOCK_ESEND 2 /* event - send */
#define ISOCK_ERROR 4 /* event - error */
/* iselect: fds(fd set), events(mask), revents(received events) */
int iselect(const int *fds, const int *events, int *revents,
int count, long millisec, void *workmem);
这个函数第一个参数 fds
传入 fd 数组,然后 events
传入对应 fd 需要捕获的事件,相当于 poll 里的 events,而 revents
用于接受返回的事件,最后 count
代表总共有多少个 fd,前面的参数模仿了 poll 函数只是没用 struct pollfd
这个结构体表达而已。
最后一个参数 workmem
代表需要用多少内存,如果为 NULL
的话,这个函数不会调用下层的 select/poll 而会根据 count
数量计算出需要用到的内存并返回给你,让你安排好内存,第二次调用时用 workmem
传入内存指针:
int my_select1(const int *fds, const int *event, int *revent, int count, long millisec) {
int require = iselect(NULL, NULL, NULL, count, 0, NULL);
if (require > current_buffer_size) {
current_buffer = realloc(current_buffer, require);
current_buffer_size = require;
}
return iselect(fds, event, revent, count, millisec, current_buffer);
}
这样用就行了,这个 current_buffer
可以是一个全局变量,也可以放在你封装的 selector/poller 对象里。
或者栈上开辟一块空间,如果少量 select 就用栈空间,否则临时分配:
int my_select2(const int *fds, const int *event, int *revent, int count, long millisec) {
#define MAX_BUFFER_SIZE 2048
char stack[MAX_BUFFER_SIZE];
char *buffer = stack;
int require = iselect(NULL, NULL, NULL, count, 0, NULL);
if (require > MAX_BUFFER_SIZE) buffer = (char*)malloc(require);
int hr = iselect(fds, event, revent, count, millisec, buffer);
if (buffer != stack) free(buffer);
return hr;
}
这样可以避免维护一个全局变量。
下面给出 iselect
这个函数的实现,它能完全模拟 poll 的行为,突破 FD_SETSIZE
的限制,并且在非 Windows 下用 poll 而 Windows 下用 select:
/* iselect: fds(fd set), events(mask), revents(received events) */
int iselect(const int *fds, const int *events, int *revents, int count,
long millisec, void *workmem)
{
int retval = 0;
int i;
if (workmem == NULL) {
#if defined(__unix) || defined(__linux)
return count * sizeof(struct pollfd);
#else
size_t unit = (size_t)(&(((FD_SET*)0)->fd_array[1]));
size_t size = count * sizeof(SOCKET) + unit + 8;
return (int)(size * 3);
#endif
}
else {
#if defined(__unix) || defined(__linux)
struct pollfd *pfds = (struct pollfd*)workmem;
for (i = 0; i < count; i++) {
pfds[i].fd = fds[i];
pfds[i].events = 0;
pfds[i].revents = 0;
if (events[i] & ISOCK_ERECV) pfds[i].events |= POLLIN;
if (events[i] & ISOCK_ESEND) pfds[i].events |= POLLOUT;
if (events[i] & ISOCK_ERROR) pfds[i].events |= POLLERR;
}
poll(pfds, count, millisec);
for (i = 0; i < count; i++) {
int event = events[i];
int pevent = pfds[i].revents;
int revent = 0;
if ((event & ISOCK_ERECV) && (pevent & POLLIN))
revent |= ISOCK_ERECV;
if ((event & ISOCK_ESEND) && (pevent & POLLOUT))
revent |= ISOCK_ESEND;
if ((event & ISOCK_ERROR) && (pevent & POLLERR))
revent |= ISOCK_ERROR;
revents[i] = revent & event;
if (revents[i]) retval++;
}
#else
struct timeval tmx = { 0, 0 };
size_t unit = (size_t)(&(((FD_SET*)0)->fd_array[1]));
size_t size = count * sizeof(SOCKET) + unit + 8;
FD_SET *fdr = (FD_SET*)(((char*)workmem) + 0);
FD_SET *fdw = (FD_SET*)(((char*)workmem) + size);
FD_SET *fde = (FD_SET*)(((char*)workmem) + size * 2);
void *dr, *dw, *de;
int maxfd = 0;
int j;
fdr->fd_count = fdw->fd_count = fde->fd_count = 0;
for (i = 0; i < count; i++) {
int event = events[i];
int fd = fds[i];
if (event & ISOCK_ERECV) fdr->fd_array[(fdr->fd_count)++] = fd;
if (event & ISOCK_ESEND) fdw->fd_array[(fdw->fd_count)++] = fd;
if (event & ISOCK_ERROR) fde->fd_array[(fde->fd_count)++] = fd;
if (fd > maxfd) maxfd = fd;
}
dr = fdr->fd_count? fdr : NULL;
dw = fdw->fd_count? fdw : NULL;
de = fde->fd_count? fde : NULL;
tmx.tv_sec = millisec / 1000;
tmx.tv_usec = (millisec % 1000) * 1000;
select(maxfd + 1, (fd_set*)dr, (fd_set*)dw, (fd_set*)de,
(millisec >= 0)? &tmx : 0);
for (i = 0; i < count; i++) {
int event = events[i];
int fd = fds[i];
int revent = 0;
if (event & ISOCK_ERECV) {
for (j = 0; j < (int)fdr->fd_count; j++) {
if (fdr->fd_array[j] == (SOCKET)fd) {
revent |= ISOCK_ERECV;
break;
}
}
}
if (event & ISOCK_ESEND) {
for (j = 0; j < (int)fdw->fd_count; j++) {
if (fdw->fd_array[j] == (SOCKET)fd) {
revent |= ISOCK_ESEND;
break;
}
}
}
if (event & ISOCK_ERROR) {
for (j = 0; j < (int)fde->fd_count; j++) {
if (fde->fd_array[j] == (SOCKET)fd) {
revent |= ISOCK_ERROR;
break;
}
}
}
revents[i] = revent & event;
if (revent) retval++;
}
#endif
}
return retval;
}
这就是我目前用的方法,刚好一百多行,这个方法我测试过,在我的台式机上同时维护一万个 socket 连接问题不大,做 echo server,每个连接每秒一条消息往返,只是 CPU 占用回到 70% 左右。
对于 Windows 下的客户端程序,维护的连接不多,这个函数足够用;而对于服务端程序,则可以做到能跑,可以让你平时跑在 Linux 下的服务端程序保证能在 Windows 下正常工作,正常开发调试,不论连接有多少。
这个方法唯一问题是 CPU 占用过高,那么 Windows 下面是否有像 kevent/epoll 一样丝滑的异步事件模型,既能轻松 hold 上万的套接字,又不费 CPU 呢?当然有,但是在说方案三之前先说两个错误的例子。
不少人提过函数 WSAEventSelect
,它可以把套接字事件绑定到一个 WSAEVENT
上面:
int WSAAPI WSAEventSelect(
[in] SOCKET s,
[in] WSAEVENT hEventObject,
[in] long lNetworkEvents
);
这个 WSAEVENT
是一个类似 EVENT
的东西,看起来好像没有 FD_SETSIZE
的个数限制,但问题 WSAWaitForMultipleEvents
里你同样面临 WSA_MAXIMUM_WAIT_EVENTS
的限制,在 winsock2.h
里:
#define WSA_MAXIMUM_WAIT_EVENTS (MAXIMUM_WAIT_OBJECTS)
后面这个 MAXIMUM_WAIT_OBJECTS 的数量就是 64,你还是跳不开。另外一个函数 WSAAsyncSelect
可以把 socket 事件关联到窗口句柄上:
int WSAAsyncSelect(
[in] SOCKET s,
[in] HWND hWnd,
[in] u_int wMsg,
[in] long lEvent
);
这的确没有个数限制了,问题是你需要一个窗口句柄 HWND
,你需要创建一个虚拟窗口,那么为了模拟 posix 的 poll 行为,你打算把这个虚拟窗口放哪里呢?它的消息循环需要一个独立的线程来跑么?
Unix 的哲学是一切皆文件,Windows 的哲学是一切皆窗口,没想到有一天写网络程序也要同窗口打交道了吧?总之也是个不太干净的做法。
是的可以用 iocp 完全模拟实现 epoll,让你拥有一个高性能的 reactor 事件模型,轻松处理 10w 级别的套接字,听起来很诱惑但是很难实现,没关系,有人帮你做了:
这个 wepoll 的项目意在使用 iocp 实现 Windows 下的高性能 epoll,支持 vista 以后的系统,并且只有两个文件 wepoll.h
和 wepoll.c
,十分方便集成,接口也是对应 epoll 的:
HANDLE epoll_create(int size);
HANDLE epoll_create1(int flags);
int epoll_close(HANDLE ephnd);
int epoll_ctl(HANDLE ephnd,
int op,
SOCKET sock,
struct epoll_event* event);
int epoll_wait(HANDLE ephnd,
struct epoll_event* events,
int maxevents,
int timeout);
完全跟 epoll 一样用就完事了,不过只支持 Level-triggered 不支持 Edge-triggered,不过有性能测试表明 Edge-triggered 并没有太大优势,且并不跨平台,其它平台的异步事件 API 大多也不兼容这个模式,所以用 Level-triggered 问题不大。
PS:libevent 新版本在 Windows 下就是用的这个 wepoll,应该是经过考验的项目了。
那么假设你在做一个跨平台的 poll 模块,在 Windows 下上面的三套方案用哪套好呢?我的做法是内部实现的是第二套方案自定义 fd_set
,它可以兼容到 Windows 95,算是个保底的做法,同时提供插件机制,可以由外部实现来进行增强。
然后主程序检测到系统在 vista 以后,并且包含了 wepoll 的时候,把 wepoll 的实现做一层插件封装安装进去。
The post WinSock 的 select 如何超过 64 个套接字限制?(三种方法) appeared first on Skywind Inside.
2024-10-16 15:55:31
二十多年前的某一天,我盯着资源管理器里很久没用却一直舍不得删除的 UCDOS 文件夹犹豫了半天,最终却为了给硬盘腾点空间一狠心 shift+delete 把他们彻底删除了,当时我没意识到,一个时代就这样彻底的离我远去;二十多年后的今天,我又在最新版的 DOSBOX 里把这些当年的工具一个个重新装了回去,软件没变,但是消逝的青春却再也回不来了。
做了一个《上古软件仓》,包含上古时代的编程工具,汉字系统和设计软件等,都是一些我以前经常用的软件,主打怀旧和娱乐。
截图:中文系统
(点击 more/continue 继续)
截图:WPS
截图:整人专家 2000
其它工具包括:
CCED,Borland C++,Watcom C++,Turbo C,QBasic,FoxBase,sea 1.3 等。
欢迎访问:
https://skywind.me/wiki/%E4%B8%8A%E5%8F%A4%E8%BD%AF%E4%BB%B6%E4%BB%93
The post DOS 经典软件下载 appeared first on Skywind Inside.