操作系统
# 进程间通信
1、无名管道( pipe );2、高级管道(popen);3、有名管道 (named pipe);4、消息队列( message queue );5、信号量( semophore );7、共享内存( shared memory );8、套接字( socket )。
# 线程间通信
1.锁机制:包括互斥锁、条件变量、读写锁 2.信号量机制(Semaphore) 3.信号机制(Signal)
# 内核态与用户态的信息交互如何实现
出于效率和代码大小的考虑,内核程序不能使用标准库函数(当然还有其它的顾虑,详细原因请查阅参考资料2)因此内核开发不如用户程序开发那么方便。
内核中启动用户程序还是要通过execve这个系统调用原形,只是此时的调用发生在内核空间,而一般的系统调用则在用户空间进行。
# procfs(/proc)<–sysctl
procfs 是 进程文件系统 的缩写,它本质上是一个伪文件系统,为什么说是 伪 文件系统呢?因为它不占用外部存储空间,只是占用少量的内存,通常是挂载在 /proc 目录下。
我们在该目录下看到的一个文件,实际上是一个内核变量。内核就是通过这个目录,以文件的形式展现自己的内部信息,相当于 /proc 目录为用户态和内核态之间的交互搭建了一个桥梁,用户态读写 /proc 下的文件,就是读写内核相关的配置参数。
# netlink<–iproute2
netlink 是 Linux 用户态与内核态通信最常用的一种方式。Linux kernel 2.6.14 版本才开始支持。它本质上是一种 socket,常规 socket 使用的标准 API,在它身上同样适用。
# IO原理 epoll等
# IO数据到达网卡之后的流程
输入Frames通过DMA模块把数据拷贝到内存(不需要消耗CPU资源),直接进行内存映射。之所以这样做,是因为网卡没有大量的内存空间,只能做简单的缓冲,所以必须赶紧将它们保存下来。
Buffer是一个双向链表作为缓冲区,看上去像一个有很多个凹槽的线性结构,每个凹槽(节点)可以存储一个封包,这个封包可以从网络层看(IP 封包),也可以从传输层看(TCP 封包)。触发CPU中断交给操作系统处理不断地从Buffer中取出数据,数据通过一个协议栈,你可以把它理解成很多个协议的集合。协议栈中数据封包找到对应的协议程序处理完之后,就会形成Socket文件,一个 Socket 文件内部类似一个双向的管道,进程读取 Socket 文件,可以从 Buffer 中对应节点读走数据。
高并发的请求量级实在太大,有可能把 Buffer 占满,此时,操作系统就会拒绝服务
# select/poll/epoll
进程如何监听关注集合的状态变化,比如说在有数据进来,如何通知到这个进程
一个线程需要处理所有关注的 Socket 产生的变化,或者说消息。实际上一个线程要处理很多个文件的 I/O。所有关注的 Socket 状态发生了变化,都由一个线程去处理,构成了 I/O 的多路复用问题。
一个 Socket 文件,可以由多个进程使用;而一个进程,也可以使用多个 Socket 文件。进程和 Socket 之间是多对多的关系。
这样在进程内部就需要一个数据结构来描述自己会关注哪些 Socket 文件的哪些事件(读、写、异常等)。
一种是利用线性结构,比如说数组、链表等,这类结构的查询需要遍历。每次内核产生一种消息,就遍历这个线性结构,select 和 poll 都采用线性结构,每次 select 操作会阻塞当前线程,在阻塞期间所有操作系统产生的每个消息,都会通过遍历的手段查看是否在集合中。poll 虽然优化了编程模型,但是从性能角度分析,它和 select 差距不大。因为内核在产生一个消息之后,依然需要遍历 poll 关注的所有文件描述符来确定这条消息是否跟用户程序相关。
另一种是索引结构,内核发生了消息可以通过索引结构马上知道这个消息进程关不关注。epoll 将进程关注的文件描述符存入一棵二叉搜索树,通常是红黑树的实现。
# 总结与同步异步阻塞非阻塞
总结一下,select/poll 是阻塞模型,epoll 是非阻塞模型。当然,并不是说非阻塞模型性能就更好。在多数情况下,epoll 性能更好是因为内部有红黑树的实现。
上面的模型当中,select/poll 是阻塞(Blocking)模型,epoll 是非阻塞(Non-Blocking)模型。阻塞和非阻塞强调的是线程的状态,所以阻塞就是触发了线程的阻塞状态,线程阻塞了就停止执行,并且切换到其他线程去执行,直到触发中断再回来。
还有一组概念是同步(Synchrounous)和异步(Asynchrounous),select/poll/epoll 三者都是同步调用。
**同步强调的是顺序,所谓同步调用,就是可以确定程序执行的顺序的调用。比如说执行一个调用,知道调用返回之前下一行代码不会执行。这种顺序是确定的情况,就是同步。
而异步调用则恰恰相反,异步调用不明确执行顺序。比如说一个回调函数,不知道何时会回来。异步调用会加大程序员的负担,因为我们习惯顺序地思考程序。因此,我们还会发明像协程的 yield 、迭代器等将异步程序转为同步程序。
由此可见,非阻塞不一定是异步,阻塞也未必就是同步。比如一个带有回调函数的方法,阻塞了线程 100 毫秒,又提供了回调函数,那这个方法是异步阻塞。
# 零拷贝
从上面可以看出,应用进程的每一次写操作,都会把数据写到用户空间的缓冲区中,再由CPU将数据拷贝到系统内核的缓冲区中,之后再由DMA将这份数据拷贝到网卡中,最后由网卡发送出去。也就是说,一次写操作数据要拷贝两次才能通过网卡发送出去,而用户进程的读操作则是将整个流程反过来,数据同样会拷贝两次才能让应用程序读取到数据。
零拷贝技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域,所谓的零拷贝,就是取消用户空间和内核空间之间的数据拷贝操作,应用程序每一次的读写操作,可以通过一种方式,直接将数据写入内核或者从内核中读取数据,再通过DMA将内核中的数据拷贝到网卡,或者将网卡中的数据copy到内核。
# 实现方式
分为三种:
- 让数据拷贝完全在内核里进行,通过增加新的系统调用,比如mmap(),sendfile() 以及 splice(),其核心原理都是通过虚拟内存来解决的。
- 绕过内核的直接IO,内核在传输过程中只负责一些管理和辅助的工作
- 内核缓冲区和用户缓冲区之间的传输优化:这种方式侧重于在用户进程的缓冲区和操作系统的页缓存之间的 CPU 拷贝的优化。这种方法延续了以往那种传统的通信方式,但更灵活。