核心态代码拥有完全的底层资源控制权限,可以执行任何CPU指令,访问任何内存地址,其占有的处理机是不允许被抢占的。内核态的指令包括:启动I/O,内存清零,修改程序状态字,设置时钟,允许/终止中断和停机。内核态的程序崩溃会导致PC停机。
用户态是用户程序能够使用的指令,不能直接访问底层硬件和内存地址。用户态运行的程序必须委托系统调用来访问硬件和内存。用户态的指令包括:控制转移,算数运算,取数指令,访管指令(使用户程序从用户态陷入内核态)。
既然我们已经区分了核心态和用户态,而且说了用户态不能完成实质的IO,那么一次完整的IO应该是什么样子的呢?
我们前面说了,CPU会在两个model之间切换,那么切换机制是什么?其实是有三种机制
当有多个程序的时候,那么CPU就会以时间片划分,轮流执行程序,当CPU执行到某个程序的时候,应该当前程序的相关信息,比如上下文,计数器,寄存器,这些都需要内核的支持。
同步、异步描述的是多个线程之间的协作关系,线程之间要么是同步要么是异步 举个例子,前端人以前都会用到的jquery发起ajax请求
$(document).ready(function(){
var a=1
var b=2
var add=function(){
$.ajax({url:"5.txt",async:false,success:function(result){
a=parseInt(result);
}});
};
add();
var c=a + b
console.log(c);
});
如果async 为FALSE,那么代表是同步,不执行完毕add方法就不会继续往下执行,那么此时c的结果就是5+2=7(5.txt返回值是5),而如果async 为true代表异步,那么我不再关注add方法有没有完成,而是继续执行代码,那么此时,c的值可能就是1+2=3,但是add方法执行完毕我怎么能知道呢?就是调用success里面的方法,其实就是执行完毕后的回调函数。
也就是说,同步,就是一段调用一旦开始,就必须要返回结果才能继续往下运行。异步就是我不需要等调用的结果而可以继续往下进行,你什么时候执行完毕,通过回调函数通知我。
再通俗点讲就是,这个结果如果是你主动去获取的(不管是一直等还是轮询),那么就是同步,如果是被动通知得到的(回调),那么就是异步。
阻塞与非阻塞强调的是一个线程的内部执行状态,阻塞是指调用结果返回前,发起调用的进程会被挂起,不能干其他的活。非阻塞就是调用结果没有返回,但是调用的进程可以继续干其他的活。 比如八九十年代去银行取钱,你得人在那排队,排队期间,你啥都干不了,这就是阻塞,非阻塞就是后来银行有了取号机,你取号之后可以去外面抽根烟,刷个手机啥的,等着服务台喊你就行了。
比如这样一个场景,小明说,妈妈,我吃完饭要去打篮球,那么小明的妈妈就去做饭了,小明此时有以下几种方案, 1.小明哪也不去,就在那等妈妈做饭,然后吃完饭去打篮球,这个过程就是同步阻塞 2.小明去感觉等的无聊,就去看电视了,但是每五分钟过来问一下妈妈做好饭了吗,这个就叫 同步非阻塞 3.一会一群小朋友喊小明去打篮球,但是妈妈饭没做好,小明等不及了,跟妈妈说,妈,一会做好了喊我,我去打篮球了。这个就叫做 异步非阻塞 4.小明已经让妈妈做好饭喊他了,但是他还是在等妈妈,哪也不去,这就是异步阻塞,这种场景一般不会出现
同步、异步与阻塞、非阻塞说完了,那么接下来,我们就可以敞开聊聊IO模型了
当进程发起一次IO调用后,程序就一直等待操作系统准备数据,将数据从内核态拷贝到用户态,然后IO函数返回成功指示。
应用程序定时去询问内核的IO函数,询问数据是否准备好,如果准备好了,就进行拷贝,如果没有准备好,内核直接返回未就绪,程序就过一会再来询问。
程序发起IO调用后立刻返回结果,表示我已经调用成功,程序继续执行,等数据准备就绪而且已经从内核态拷贝到用户态的时候,发送信号给调用程序
当数据报准备好的时候,内核会向应用程序发送一个信号,进程对信号进行捕捉,并且调用信号处理函数来获取数据报。
在UDP上,SIGIO信号会在下面两个事件的时候产生:
1 数据报到达套接字
2 套接字上发生错误
因此我们很容易判断SIGIO出现的时候,如果不是发生错误,那么就是有数据报到达了。
而在TCP上,由于TCP是双工的,它的信号产生过于频繁,并且信号的出现几乎没有告诉我们发生了什么事情。因此对于TCP套接字,SIGIO信号是没有什么使用的。
我们前面说了非阻塞IO,进程不会一直等内核的结果,但是进程会隔一段时间就去轮询内核,是否准备好了数据。这样明显有点浪费资源。前面我们还说了异步调用,就是结果返回的时候通过回调函数通知客户端。那么非阻塞IO能不能也这样呢?不要进程一次次的轮询了,内核什么时候准备好,什么时候通知我。
或者我们可以思考一下,当我们浏览网页的时候,有时候是敲键盘,有时候是点击鼠标,电脑是怎么捕获我的动作的呢?难道一直跑一个死循环不停的监听我的动作吗?那我敲键盘,鼠标点击,鼠标悬浮 这些动作 是不是需要分好几个线程阻塞在那等着我们?从实现方式上来说,这种方式固然可以实现。但是会有几个问题,如果你操作太多太快,可能会导致延迟。甚至会由于一个动作的阻塞导致后面的动作全部失效。
那么怎么解决这种问题?UI编程的事件驱动模型是这样做的,你的每个动作对于我来说都是事件,你可能敲键盘了,可能鼠标点击了,我都给你记录到事件队列里面。然后有另外一个循环去消费队列里面的事件,根据事件类型调用不同的函数。
那么我们的操作系统里面,是不是也有这样的一套事件处理机制呢?
fd 是 File descriptor 的缩写,中文名叫做:文件描述符。文件描述符是一个非负整数,本质上是一个索引值(这句话非常重要),指向内核为每一个进程所维护的该进程打开文件的记录表。文件描述符在unix系统中几乎无处不在。 当打开一个文件时,内核向进程返回一个文件描述符( open 系统调用得到 ),后续 read、write 这个文件时,则只需要用这个文件描述符来标识该文件,将其作为参数传入 read、write 。
在 POSIX 语义中,0,1,2 这三个 fd 值已经被赋予特殊含义,分别是标准输入( STDIN_FILENO ),标准输出( STDOUT_FILENO ),标准错误( STDERR_FILENO )。
文件描述符是有一个范围的:0 ~ OPEN_MAX-1 ,最早期的 UNIX 系统中范围很小,现在的主流系统单就这个值来说,变化范围是几乎不受限制的,只受到系统硬件配置和系统管理员配置的约束。
IO复用模型核心思路:系统给我们提供一类函数(比如select、poll、epoll函数),它们可以同时监控多个fd的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom系统调用。
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,通过遍历fdset,来找到就绪的描述符,将数据从kernel拷贝到用户进程。
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对 于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache就是这样实现的),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。
IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。
select 和epoll最大的区别在于对fd的遍历方式不同。
select 模式,就像在医院里,有一个病人喊护士拔针,护士不知道谁喊的,所以护士就一个一个的挨个问一遍。可以说select的调用复杂度是线性的,即O(n)。
后来医院改进了,哪个病人需要帮助,请按电铃,电铃响了,护士办公室根据电铃的编号可以迅速定位到病人在几号病床,这就是epoll的改进。此时,如果护士听到铃声马上去处理了,这种方式就是同步阻塞的,交做ET。如果护士在忙其他的事情,没有马上处理,那么过了一会铃声自动再响,提醒护士做这件事情,这种模式叫做lt。