silly中socket犯的错误

silly的socket模块最初是使用epoll实现的, 后来为了在mac上开发就加入了kevent, 然后使用socket_poll.h来封装成了socket_poll。

但是由于对epoll和kevent的不了解,导致了在实现silly_socket.c文件中的_process函数时犯了一些很silly的错误。 直到昨天去实现异步发送时才突然发现epoll和kevent的不同之处。


linux下,在调用epoll_wait函数时将一个struct epoll_event结构体的数组作为参数传入,当epoll_wait函数返回且返回值大于0时, 则会将当前已经发生的事件和发生事件的文件描述符设置到传入的sctruct epoll_event结构体的数组中。

struct epoll_event结构体中的events成员用于设置相应的事件,data成员用于存储设置的用户自定义数据。问题的关键就在于events可以是几个事件的成员组合,如他的值可能是EPOLLIN | EPOLLOUT.

而我在代码中使用了if…else if … else的结构

这就会导致一个问题,如果一个socket fd即是可读又是可写的, 代码中就会将写事件略过,直到下次调用epoll_wait时,写事件又会再次被置入struct epoll_event数组中去,这样就无形中增加了epoll_wait的调用次数。

由于epoll_wait是一个系统调用, 所以会增加很多不必要的开销。而且在极端情况下如果此socket一直有读事件, 将会导致此socket fd的写被饿死。

由于是异步发送, 如果一直得不到写事件,就会导致所有的数据被挂入wlist链表上去,内存会持续增加直到wlist上的数据被写出。

其实这个结构是我第一次学epoll时从网上抄来了,由于一直没用过写事件,所以也没有察觉这种写法的问题。


在调用kevent函数之后同样将事件结果一个struct kevent结构体数组中。

struct kevent结构体中filter成员用于标志出当前struct kevent中所发生的事件, udata成员则用于存储用户自定义数据。 但是与epoll恰好相反的是每一个struct kevent变量中filter仅仅只能表示一个事件。而且这些事件的值并不是某一个二进制bit如:
#define EVFILT_READ (-1)
#define EVFILT_WRITE(-2)

因此拿filter成员与相应的事件标志位去做与操作会造成即使仅仅EVFILT_READ触发也会被误判EVFILT_READ和EVFILT_WRITE同时触发的假象。


最后就是在命令设处理上的一个设计上的问题了。

socket模块必须要实现异步发送才能才能够让worker线程尽可能的去处理逻辑,而不是阻塞在数据发送上去。

所以socket模块提供的发送接口应该仅仅是将要发送的数据以消息的形式发送给socket线程,然后当调用epoll_wait/kevent函数得到此描述符WRITE事件时, 将数据写入socket。由于epoll_wait/kevent函数均为system call。为了降低开销,一般超时时间则设为-1, 即永久等待。

为了能够在调用socket模块提供的异步发送接口时能够让epoll_wait/kevent函数从内核中及时返回, 给socket线程通知要发送数据的消息应该通过pipie或Uinx域socket的方式来发送。由于pipe在一次发送小于PIPE_BUF大小的数据时write为原子操作,而异步发送接口可能会被多个线程同时调用。因此选择使用pipe而不是Unix域socket。

最开始设计时这样的,在epoll_wait/kevent返回之后如果pipe中有数据时取出一条命令去执行, 然后执行其他socket的事件。之所以这样设计是担心如果一直处理pipe中的数据,而pipe中又一直有数据来, 那么其他socket就会被饿死。

但是今天早上在地铁上想了一路,发现这样做其实是有问题的。

首先这增加了epoll_wait/kevent的调用次数是毋容置疑的。
其次即然最坏的情况下pipe中会一直有数据, 那么如果一次pipe只去处理一个命令,就会导致后续调用socket模块提供的异步发送接口阻塞,最坏的情况下所有的worker都会被塞住, 造成所有client无响应。

将程序改为循环执行完pipe中所有命令之后再去处理其他socket事件, 再分析一下pipe中会一直有数据的情况。

如果pipe中一直有数据, 那么同样所有的socket不会得到处理,在socket事件没有得到处理的情况下, pipe中还持续有命令产生, 只能说明一点整个系统过载了。

如果整个系统过载了不管哪种实现方式都不能避免处理缓慢的情况。

然后在系统不过载的情况下, 显然循环处理完当前pipe中的所有命令之后, 再去处理其他socket事件会更有效率。

BTW,我想应该可以能过压力测试来确定最大连接数来避免系统过载。


8月2日补充:

在处理pipe的命令时, 我采用了与一般socket相同的处理方式, 即当此事件如果为pipe的READ事件则处理, 后来去瞅了瞅skynet, 发现他是先去处理pipe中的命令。

仔细想了许久终于发现这样做的好处了:

如果按照epoll_wait/kevent函数返回的事件列表去顺序处理, 最坏的情况则可能pipe被放在事件数组中的最后一个。那么在pipe中的命令被处理完以前,所有的worker线程将全被阻塞。

假如epoll_wait/kevent返回了100个event,而pipe恰好被放在event[99]。那么当pipe被处理之前就已经有99个socket被处理, 而socket线程处理前99个socket的事件时,由于worker线程将被阻塞,因此其他cpu均被浪费在空转上了。那么系统对第一个数据包的响应速度则为(处理99个fd的时间 + pipe的处理时间)

如果首先处理完pipe中的命令,其他线程就处于就绪状态, 一旦接收到socket的数据就可以立即开始处理。那么系统对于整个数据包的响应速度则为(pipe的处理时间 + 一个socket fd的读时间)

因此首先处理pipe可以更充分的使用cpu.

《silly中socket犯的错误》有1条评论

发表评论

five × = thirty