IO模型
操作系统,把内存分成了两个区域:
- 内核空间,这个内存空间只有内核程序可以访问;
- 用户空间,这个内存空间专门给应用程序使用;
用户空间的程序不能直接访问内核空间。当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间。
当应用程序发起 I/O 调用后,会经历两个步骤:
- 内核等待 I/O 设备准备好数据
- 内核将数据从内核空间拷贝到用户空间
BIO(Blocking I/O)
在同步阻塞IO模型中,应用程序发起 read 调用后,线程会一直阻塞,直到内核把数据拷贝到用户空间。在阻塞 I/O 模型中,每个连接都需要一个线程来处理。因此,对于大量并发连接的场景,阻塞 I/O 模型的性能较差。
伪代码如下:
listenfd = socket(); // 打开一个网络通信套接字
bind(listenfd); // 绑定
listen(listenfd); // 监听
while(true) {
buf = new buf[1024]; // 读取数据容器
connfd = accept(listenfd); // 阻塞等待建立连接
new Thread() {
int n = read(connfd, buf); // 检测 connfd 是否可读
if (n != -1) {
doSomeThing(buf); // 处理数据
}
}
close(connfd); // 关闭连接
}
int n = read(connfd, buf)这里会发生阻塞,执行以上所提到的两个步骤。

NIO(Non-blocking I/O)
同步非阻塞 IO 模型中,应用程序发起 read 调用后,会先询问内核数据是否已准备好,若没有准备好,可以立即返回-1,线程可以去执行其他的任务,这种模型允许一个线程去处理多个连接;应用程序通过轮询的方式来确认数据是否准备好,但是十分消耗CPU资源的。
arr = new Arr[]; // 创建连接数组
listenfd = socket(); // 打开一个网络通信套接字
bind(listenfd); // 绑定
listen(listenfd); // 监听
while(true) {
connfd = accept(listenfd); // 阻塞等待建立连接
arr.add(connfd); // 添加连接到数组
}
// 异步线程检测连接是否可读
new Thread() {
for (connfd : arr) {
buf = new buf[1024]; // 读取数据容器
// 非阻塞 read,最重要的是提供了我们在一个线程内管理多个文件描述符的能力
int n = read(connfd, buf); // 检测 connfd 是否可读
if (n != -1) {
newThreadDeal(buf); // 创建新线程处理
close(connfd); // 关闭连接
arr.remove(connfd); // 移除已处理的连接
}
}
}
// 处理数据的函数
newThreadDeal(buf) {
doSomeThing(buf); // 处理数据
}
逻辑如图所示:

IO多路复用模型
IO多路复用是指使用操作系统提供的多路复用功能(如select,poll,epoll等),使得单个线程可以同时处理多个IO事件。当某个连接的数据准备好时,操作系统会通知应用程序。
select
创建一个文件描述符集fd_set(基于位图实现,一个fd对应一个bit),使得一个线程可以监控多个文件描述符的状态变化(遍历整个集合),select监控的文件描述符有数量的限制(1024个)。跟NIO的区别在于NIO是用户线程去遍历,select是内核线程去遍历(可以减少大量的系统调用)
select伪代码示例:
arr = new Arr[]; // 创建连接数组
listenfd = socket(); // 打开一个网络通信套接字
bind(listenfd); // 绑定
listen(listenfd); // 监听
while (true) {
connfd = accept(listenfd); // 阻塞等待建立连接
arr.add(connfd); // 添加连接到数组
}
// 异步线程检测,通过 select 判断是否有连接可读
new Thread() {
while (select<

最低0.47元/天 解锁文章
5081

被折叠的 条评论
为什么被折叠?



