深入理解 Go | 内存管理:栈

基于 Go 1.14 应用程序的内存分为:

Go 语言使用 goroutine 作为执行上下文,而 goroutine 的初始栈内存时 2KB,比线程小很多。

分段栈与连续栈

Go 1.3 开始使用的栈策略,不再将栈分段。

Go Memory Stack Image

参考

epoll的那些坑

可能当初 epoll 设计没有考虑太多并发的情况,单进程单线程下 epoll 工作良好,但是多进程或者多线程下就有一些坑。

文件对象:在讲这个之前,需要了解和文件有关的三个数据结构,具体可以参考《Unix环境高级编程》3.10节,或者The method to epoll’s madness的内容。文件涉及三个数据结构:进程维护 file descriptor 表,每个 fd 包含 fd 标志和指向内核 file description 表项的指针。内核维护所有打开文件的 file description 表,每个 file description 包含当前文件的 offset、文件状态标志(读、写、阻塞、非阻塞等)和指向该文件 v 节点表项的指针。每个打开文件/设备都有一个 v 节点结构,包含 inode 信息等。

epoll 相关的数据结构:调用 epoll_create 成功,返回 epoll instance fd,进程通过 efd 对 epoll 进行操作。同时在内核也会创建 epoll instance 对应的 file description,在文件系统也会创建 inode 信息。例如图中,fd9 和相关联的其它数据结构,就是 epoll instance 相关的数据结构(淡黄色部分)。同时注册 fd 到 epoll instance,epoll set 其实监听的是 file description,而不是 file descriptor(fd)。所以 epoll set 里面的 fd0 其实是 fd0 指向的 file description(橙色部分)。

复制 epoll fd 问题:假设 Process A 创建了 epoll,并将 fd0 注册到 epoll 中。Process A fork 子进程 B,此时 B 也拥有和 A 同样的 fd table,B 的 epoll file description 和 A 是同一个。现在 A 创建了一个新的 fd8 并注册到 epoll 中,如果 fd8 有事件,不仅仅 A 能接收到这个事件,B 也能(虽然 B 都不知道有这个 fd8)。

复制 fd 问题:继续上面的例子,假设 A close(fd0),A 以为自己已经关闭了 fd0,不会收到 fd0 任何事件了。但是由于如下原因:epoll 监听的是 file description、只有指向 file description 的 file descriptor 都关闭,file description 才会删除、虽然 A 关闭 fd0,但是file description 还有 B 的 fd0 指着,所以不会删除。所以 A 还是会继续收到 fd0 的事件。由此可以看出,epoll 注册对象的生命周期和对象对应的 fd 生命周期不完全一致。

再比如,epoll 监听了 fd,程序执行 fd2 = dup(fd),然后调用 close(fd),会出现如下问题:程序还是能接收到 fd 的事件、fd 不能从 epoll 里面删除,即使做如下 epoll_ctl 操作也不行。所以在 close 之前,一定要记得先从 epoll 里面删除 fd。

多线程问题:假设有两个线程,线程 A 和线程 B,线程 A 创建了 epoll,并注册了 fd0。线程 B 创建了 fd1,并注册到线程 A 创建的 epoll 中。线程 A 和线程 B 都调用 epoll_wait,线程 A 和线程 B 都能收到 fd0 和 fd1 的事件。

多进程问题:假设有两个进程,进程 A 和进程 B,进程 A 创建了 epoll,并注册了 fd0。进程 A fork 子进程 B,此时 B 也拥有和 A 同样的 fd table,B 的 epoll file description 和 A 是同一个。现在 B 创建了一个新的 fd1 并注册到 epoll 中,如果 fd1 有事件,不仅仅 A 能接收到这个事件,B 也能(虽然 B 都不知道有这个 fd1)。

总结:epoll 的坑主要是和 fd 生命周期相关,以及多进程/多线程下 epoll 的坑。解决方法是:不要复制 epoll fd、不要复制注册到 epoll 的 fd、多线程下,每个线程都创建自己的 epoll。

参考