异步事件模型的 Self-pipe trick

异步事件模型中有一个重要问题是,当你的 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 都可以使用这个功能。

Loading

Posted in 网络编程 | Tagged | 9 Comments

WinSock 可以把 SOCKET 类型转换成 int 保存么?

在 Linux/Unix 等 posix 环境中,每个套接字都是一个文件描述符 fd,类型是 int,使用起来非常方便;但在 Win32 环境中是 SOCKET 类型被定义成 UINT_PTR ,是一个指针,在 x64 环境中一个 SOCKET 占用 8 个字节。

那么是否能将 SOCKET 类型强制转换成 int 类型保存没?这样就能统一用 int 在所有平台下表示套接字了,同时在 x64 环境下这样将 64 位的指针转换为 32 位的整数是否安全?

答案是可以的,下面将从三个方面说明一下。

Kernel Object

每个 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 类型。

程序验证

道理前面都讲完了,下面写个程序验证一下:

Continue reading

Loading

Posted in 网络编程 | Tagged , | Leave a comment

WinSock 的 select 如何超过 64 个套接字限制?(三种方法)

在做跨平台网络编程时,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 个套接字的限制。

方法1:重定义 FD_SETSIZE

首先可以看 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 继续)

Continue reading

Loading

Posted in 网络编程 | Tagged | Leave a comment

DOS 经典软件下载

二十多年前的某一天,我盯着资源管理器里很久没用却一直舍不得删除的 UCDOS 文件夹犹豫了半天,最终却为了给硬盘腾点空间一狠心 shift+delete 把他们彻底删除了,当时我没意识到,一个时代就这样彻底的离我远去;二十多年后的今天,我又在最新版的 DOSBOX 里把这些当年的工具一个个重新装了回去,软件没变,但是消逝的青春却再也回不来了。

做了一个《上古软件仓》,包含上古时代的编程工具,汉字系统和设计软件等,都是一些我以前经常用的软件,主打怀旧和娱乐。

截图:中文系统

(点击 more/continue 继续)

Continue reading

Loading

Posted in 未分类 | Tagged , | Leave a comment

Vim 文本过滤/文字处理插件

我经常有文本处理的需求,例如将 html 转换成纯文本,或者移除 markdown 里的所有连接,或者繁体转换简体。因此我做了一个插件来管理和执行各种外部文本过滤器。

所谓 “文本过滤器” 是一个命令行程序,它从标准输入读取文本,然后进行一些处理后写到标准输出,在 Vim 里可以用原生的 :{range}! xxx 命令将选中文本发送给 xxx 命令的标准输入,然后用该命令的标准输出替换选中文本,这个命令很有用,但每次输入一长串命令略显繁琐,并且过滤器多了以后也很难管理。

因此我做了这个插件来统一管理文本过滤程序,并且提供接口来执行他们:

比如上图演示了将 HTML 转换成文本,以及去除 markdown 中的连接,使用命令 :{range}TP {name} 就能调用名为 {name} 的文本过滤程序了。这些程序可以用你喜欢的语言编写,放到统一的目录,加上可执行属性就行,该插件就能找到它。

Continue reading

Loading

Posted in 随笔 | Tagged | Leave a comment

Python 的 asyncio 网络性能比 C 写的 Redis 还好?

先前我做过一个 asyncio/gevent 的性能比较《性能测试:asyncio vs gevent vs native epoll》,今天修改了一下 asyncio 的测试程序的消息解析部分,改用 Protocol,发现它甚至比 redis 还快了:

安装依赖:

pip install hiredis uvloop

编辑 echosvr.py 文件:

Continue reading

Loading

Posted in 网络编程 | Tagged | Leave a comment

EditPlus 的配置方法

作为一名编辑器爱好者,EditPlus 是我最喜欢的编辑器之一,超过 NotePad++,它启动速度比它快,打开文件比它快,功能比它强,颜值也比它高,但大小只有 2MB:

用了这么多年,我感觉我欠 EditPlus 一篇文章,介绍一下我平时是如何是用 EditPlus 搭建开发环境的,以及如何让它变得更好用:

(点击 more/continue 展开)

Continue reading

Loading

Posted in 未分类 | Tagged | 2 Comments

CD4:Windows XP 开发宝典

今天互联网上的内容,由于各种原因,正在以越来越快的速度消失,而习惯什么都从网上找的新一代网民们,却并没有备份和记录的习惯及意识。不远的将来,会有一天,当你特别想找某个工具却搜尽互联网你都找不到时,才会发现对珍贵资源做好收藏的必要性。

Windows XP 依然是一个完美的怀旧平台,它可以向后兼容到 Windows 95 的程序,是一个运行经典软件,玩经典游戏的完美方案。

图形界大佬 John Carmack 在推特上呼吁大家,现在应该有意识的保存你的开发环境,这样多年以后你想重新构建你的软件时才不会慌脚乱手,因为通常每过几年你常常会发现,自己之前的老代码已经没有合适的环境编译了:

本光盘包含了构建 Windows XP 程序所需要的必要工具,包括编译器,文本编辑器,集成开发环境和各种工具,他们全都能运行于 XP 下,并且能构建兼容 Windows XP 的项目。

制作原则:精选工具,断网可用,末日恢复,自包含无依赖,开发工具博物馆,帮你完全在 Windows XP 下工作,让你拥有 XP 下的沉浸式开发体验,容量却不超过一张 CD。

版权声明:本光碟采用 winworldpc.com 类似的版权声明,尽量收录开源或者不再销售的软件产品,目的是保护这些快绝版的资源。

光盘目录如下:

具体内容和下载地址见下文说明。

(点击 more 继续)

Continue reading

Loading

Posted in 未分类 | Tagged | 1 Comment