Linux
IO模式
https://segmentfault.com/a/1190000003063859
https://www.jianshu.com/p/7fbda1696789
用户空间与内核空间
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
缓存IO
又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。
在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点: 数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
IO请求流程:两个阶段
- 等待数据准备 (Waiting for the data to be ready) ------ (阻塞/非阻塞)
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) ------(同步/异步)
五种网络模式
- 阻塞IO (blocking IO)
- 非阻塞IO (nonblocking IO)
- IO多路复用 (IO multiplexing)
- 信号驱动IO (signal driven IO) 忽略
- 异步IO (asynchronous IO)
1.阻塞IO(blocking IO)
同步 + 阻塞IO,1、2阶段都是阻塞的。
用户进程请求后等待阶段1阻塞:当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)
阶段1完成后等待阶段2仍然阻塞:数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞
整个过程只需要一次系统调用。
2.非阻塞IO (nonblocking IO)
同步 + 非阻塞 IO, 阶段二阻塞
- 用户进程轮询请求数据, 没有数据时并不会block,而是kernel返回错误状态error,用户进程收到后会重试.
- 某次请求后如果数据到达,kernel返回数据到达状态,阶段1结束,用户进程调用read,将数据从kernel拷贝到用户内存
用户进程需要不断的主动询问kernel数据好了没有,需要两次有效的系统调用
3.IO多路复用(IO multiplexing)
和阻塞IO一样是synchronous阻塞IO,这里的 1 2是阻塞的 ,唯一的区别是一个用户进程负责多个socket,也是IO多路复用的优势
基本原理就是:
- select,poll,epoll不断轮询所有负责的socket, 在阶段1被阻塞, 当某个socket有数据到达了就通知用户进程。
- 用户进程调用read操作,将数据从kernel拷贝到用户内存,在阶段2被阻塞
阻塞io只需要一次系统调用,IO多路复用需要两次,如果连接数不是很高时 select/epoll不一定比multi-threading+blocking IO更快
select
单个进程就可以同时处理多个网络连接的io请求(同时阻塞多个io操作)。基本原理就是程序呼叫select,然后整个程序就阻塞状态,这时候,kernel内核就会轮询检查所有select负责的文件描述符fd,当找到其中那个的数据准备好了文件描述符,会返回给select,select通知系统调用,将数据从kernel内核复制到进程缓冲区(用户空间)。
poll
poll的原理与select非常相似,差别如下:
select和poll都需要在返回后遍历文件描述符来获取已经就绪的socket
- 描述fd集合的方式不同,poll使用 pollfd 结构而不是select结构fd_set结构,所以poll是链式的,没有最大连接数的限制
- poll有一个特点是水平触发,也就是通知程序fd就绪后,这次没有被处理,那么下次poll的时候会再次通知同个fd已经就绪。
select的几大缺点:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024\
epoll 事件驱动
-
epoll没有fd数量限制
epoll没有这个限制,我们知道每个epoll监听一个fd,所以最大数量与能打开的fd数量有关,一个g的内存的机器上,能打开10万个左右 -
epoll不需要每次都从用户空间将fd_set复制到内核kernel
epoll在用epoll_ctl
函数进行事件注册的时候,已经将fd复制到内核中,所以不需要每次都重新复制一次 -
select 和 poll 都是主动轮询机制,需要遍历每一个人fd;
epoll是被动触发方式,给fd注册了相应事件的时候,我们为每一个fd指定了一个回调函数,当数据准备好之后,就会把就绪的fd加入一个就绪的队列中,epoll_wait
的工作方式实际上就是在这个就绪队列中查看有没有就绪的fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数。 -
虽然epoll、poll、epoll都需要查看是否有fd就绪,但是epoll之所以是被动触发,就在于它只要去查找就绪队列中有没有fd,就绪的fd是主动加到队列中,epoll不需要一个个轮询确认。
换一句话讲,就是select和poll只能通知有fd已经就绪了,但不能知道究竟是哪个fd就绪,所以select和poll就要去主动轮询一遍找到就绪的fd。而epoll则是不但可以知道有fd可以就绪,而且还具体可以知道就绪fd的编号,所以直接找到就可以,不用轮询。
epoll相比于select和poll的高效在于以下事实:
同时连接的大量客户端在同一时刻可能只有很少处于就绪状态
内存管理
虚拟内存
计算机操作系统 - 内存管理
1. 为了更好的管理内存,操作系统将内存抽象成地址空间
每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页
。
这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。
当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。
2. 为了让物理内存扩充成更大的逻辑内存,让程序获得更多的可用内存
由下面一点可知,虚拟内存允许不用将地址空间的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行。可以使有限的内存运行更大的程序。
内存管理单元(MMU)
管理着地址空间和物理内存的转换,其中页表
(page table) 储存着页
(程序地址空间) 和页框
(物理内存空间) 的映射表。
虚拟地址 = 页面号 + 偏移量
栈内存和堆内存
栈:
- 操作系统自动分配和释放,程序员无法控制
- 栈内存空间有限
- 函数中定义的一些基本类型的变量,函数的参数和对象的引用变量
当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。
若分配失败,则提示栈溢出错误。
堆:
- 手动申请和释放,常用new来分配。由 Java 虚拟机的自动垃圾回收器来管理,C/C++必须手动释放。
- 空间很大,基本没有限制
- 数组和对象本身。数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。
程序员向操作系统申请一块内存,当系统收到程序的申请时,会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
分配的速度较慢,地址不连续,容易碎片化。
由程序员申请,同时也必须由程序员负责销毁,否则导致内存泄露。
Java中的堆与栈内存
声明的对象是先在栈内存
中为其分配地址空间,在对其进行实例化后则在堆内存
中为其分配地址。
Person p = null ; 只在Stack Memory中为其分配地址空间
p = new Person(); 则在Heap Memory中为其分配内存地址
C++内存管理
在C++中,内存分成5个区,分别是:堆、栈、自由存储区、全局/静态存储区、常量存储区。
-
静态存储区
内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据和常量。 -
栈区
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
-
堆区
亦称动态内存分配。程序在运行的时候用
malloc
或new
申请任意大小的内存,程序员自己负责在适当的时候用free或 delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。
但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。