如何分层与隔离

linux 中有一个 内核态,用户态的概念。

一直知道有这个概念, 但之前没仔细看过他的具体实现

直到最近项研究一下 redis 怎么用 epoll的, 然后发现

redis 的源码又一个令人疑惑的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
* 关联给定事件到 fd
*/

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee;

/* If the fd was already monitored for some event, we need a MOD
* operation. Otherwise we need an ADD operation.
*
* 如果 fd 没有关联任何事件,那么这是一个 ADD 操作。
*
* 如果已经关联了某个/某些事件,那么这是一个 MOD 操作。
*/

int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;

// 注册事件到 epoll
ee.events = 0;
mask |= eventLoop->events[fd].mask; /* Merge old events */
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.u64 = 0; /* avoid valgrind warning */
ee.data.fd = fd;

// segment error?
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;

return 0;
}

乍一看,感觉没问题, 但是仔细一想, ee 不是一个栈上的对象嘛, 怎么能吧栈上的地址 提交到函数内部呢?

函数return 之后, 栈上面的空间就被回收了, 在epoll 注册进去的这个ee 岂不会segment fault ?

然后翻了一下 epoll_ctl 源码

https://code.woboq.org/linux/linux/fs/eventpoll.c.html#1999

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
struct epoll_event __user *, event)
{
int error;
int full_check = 0;
struct fd f, tf;
struct eventpoll *ep;
struct epitem *epi;
struct epoll_event epds;
struct eventpoll *tep = NULL;
error = -EFAULT;
if (ep_op_has_event(op) &&
copy_from_user(&epds, event, sizeof(struct epoll_event)))
goto error_return;

发现进入 epoll 的对象 其实内部都会将 用户区的对象 copy 一份。

所以, segment fault 没有发生。

但是 这种写法也没有实际意义上的错误。

操作系统的这一次 copy, 区分了, 内核态 和 用户态, 2态通过一次copy,从而让系统和用户之间完全物理隔离.

那么不隔离可以吗?

可以,只不过,系统可能会比较危险, 你可以修改内核的内存空间。 也有可能 用户 不小心犯了一个 bug, 把提供搞出问题了, 抛异常抛的是系统的异常,其实是自己的。

所以,不隔离的话对用户的要求很高。

linux 这种想法经过这么久的实践,应该是非常靠谱的。 照例应该尊崇。

因为, 用户万万千, 能保证所有用户都去翻看一边linux 源码再来编程嘛?

肯定不行。

然而, 我们的框架当初设计时没有这么考虑, 最终也付出了一定的代价。

我们在我们的 rpc 框架中, 直接将 读写操作返回的 NettyFuture 直接返回给业务方。

future 默认是由 netty 的IOThread setValue的。

future 如果还绑定了 listener的话, 一般来说,这个listener 为了减少线程切换,也是在setValue 后直接在 IOThread 执行的。

于是问题就来了, 有人就在 listener 内执行了 IO操作,而一般我们的 IOThread 不会太多, 于是大量的 IOThread 都被阻塞了, 一旦IOThread 全被阻塞,就无法继续处理请求了。

虽然这个问题可以通过文档,提醒用户注意, 但是感觉最好的做法就是,不管用户怎么使用,都不会触发问题。 就好像 linux 区分 内核态 用户态一样。

自此之后也理解了,为啥 dubbo 和 sofa 以及微博 motan 默认对用户都使用同步的连接池, 毕竟并不是所有用户都需要高并发, 反而使用越简单, 大家各自的维护成本就越低, 效率也就越高, 感觉我们真是走上了一条不归路….

avatar

lelouchcr's blog