通识

内核两大作用:对硬件资源抽象以及提供应用程序运行环境。
程序访问硬件资源通过系统调用。系统调用时使用操作系统的最小单位,一般操作系统提供200到300个系统调用。系统调用就像汉字里的笔画。库相当于偏旁。

进程相关

wait

proc.wait()不调用,父进程退出后子进程托孤给init进程才会清理此子进程。

setsid

进程组长调用出错返回-1。
Python 3.2 之后 subprocess.Popen 新增了一个选项 start_new_session, Popen(args, start_new_session=True) 即等效于 preexec_fn=os.setsid 。
会话组长终止会发发送SIGHUP信号。

Python实例

popen

fcntl

file control的缩写。
O_NONBLOCK使I/O变成非阻塞,在读不到数据或写缓冲区满时,直接return,不会阻塞等待。

  1. fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)

O_ASYNC 当I/O可用的时候,允许SIGIO信号发送到进程组,例如:当有数据可以读的时候


close-on-exec

fork父子进程相同文件的描述符指向系统文件表统一项,所以共享同一文件偏移量。
一般我们会调用exec执行另一个程序,此时会用全新的程序替换子进程的正文,数据,堆和栈等。此时保存文件描述符的变量当然也不存在了,我们就无法关闭无用的文件描述符了。所以通常我们会fork子进程后在子进程中直接执行close关掉无用的文件描述符,然后再执行exec。
但是在复杂系统中,有时我们fork子进程时已经不知道打开了多少个文件描述符(包括socket句柄等),这此时进行逐一清理确实有很大难度。我们期望的是能在fork子进程前打开某个文件句柄时就指定好:“这个句柄我在fork子进程后执行exec时就关闭”。其实时有这样的方法的:即所谓的 close-on-exec。

FD_CLOEXEC fork此文件描述符不关闭,

I/O基本认知

有流就一定有缓冲区。一方写缓冲区、一方读缓冲区,形成一个流。
阻塞I/O模式下,一个进程或一个线程只能同时处理一个I/O事件。

非阻塞I/O

忙轮询:定期不停轮询,是否有事件。

I/O多路复用

select

解决忙轮询问题。当有IO事件发生是会唤醒阻塞的线程。无差别轮询所有文件描述。时间复杂度O(n)

三大问题:

- 1024fd限制。

  1. fd从用户态拷贝到内核态。需要自己手动循环每次调用select()注册文件描述符,所以要不停拷贝。
  2. 每次都要遍历fd。

    poll

    突破1024限制。
    但是调用方式和select一样每次都要注册所有需要监听的fd。

    epoll

    select和poll都没有解决个别文件描述符触发就遍历所有注册的文件描述符的低效情况。
    会把哪个文件描述符发生IO事件告诉用户。时间复杂度O(1)。

解决拷贝问题:

使用Linux mmap机制,将用户态的一块内存和内核态的一块内存映射到同一块物理内存上。用户态和内核态可以同时访问这块地址,就不需要拷贝了。

epoll通过epoll_ctl来对监控的fds集合来进行增、删、改,那么必须涉及到fd的快速查找问题。于是,一个低时间复杂度的增、删、改、查的数据结构来组织被监控的fds集合是必不可少的了。
linux 2.6.8之前的内核,epoll使用hash来组织fds集合,于是在创建epoll fd的时候,epoll需要初始化hash的大小。于是epoll_create(int size)有一个参数size,以便内核根据size的大小来分配hash的大小。
在linux 2.6.8以后的内核中,epoll使用红黑树来组织监控的fds集合,于是epoll_create(int size)的参数size实际上已经没有意义了。

解决遍历问题:

进程只注册到epoll的睡眠队列中。
遍历就绪列表(ready_list)

参考资料

https://cloud.tencent.com/developer/article/1005481