一.前言

从上个世纪到现在,工程师们在优化服务器性能的过程中,提出了各种不同的io模型,比如非阻塞io,io复用,信号驱动式io,异步io。具体io模型在不同平台上的实现也不一样,比如io复用在bsd上可以由kqueue实现,在solaris系统上可以由/dev/poll实现。为了实现系统的可移植性,POSIX 确保 select和poll在 unix-like系统上得到广泛的支持。

在上个世纪,Dan Kegel 提出了C10K的设想,现在C10K 已经不是什么问题,比如nginx就可以做到百万级别的qps。于是又有人提出来了C10M的设想,Robert David Graham 从unix的最初设计初衷给出了自己的解决方案。

二.常见io模型

1.阻塞io

常见的read系统调用,是最常见的阻塞io:

2.非阻塞式io

非阻塞io的典型使用方式如下,设置非阻塞标志,并且常与io复用一起使用,使用起来比较复杂。

val = Fcntl(sockfd, F_GETFL, 0);
Fcntl(sockfd, F_SETFL, val | O_NONBLOCK); /* O_NONBLOCK 标志非阻塞 */

3.io 复用 (select/poll)

io复用在处理数量庞大的fd时非常有效,我们以select为例,select的核心api是select函数:

int select(int nfds, fd_set *_Nullable restrict readfds,
fd_set *_Nullable restrict writefds,
fd_set *_Nullable restrict exceptfds,
struct timeval *_Nullable restrict timeout);

看一个例子:

#include	"unp.h"

void
str_cli(FILE *fp, int sockfd)
{
int maxfdp1;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE]; FD_ZERO(&rset);
for ( ; ; ) {
FD_SET(fileno(fp), &rset); /* 设置要监听的socket fd */
FD_SET(sockfd, &rset); /* 设置要监听的file fd */
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL); /* select 调用 */ if (FD_ISSET(sockfd, &rset)) { /* socket 可读 */
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
} if (FD_ISSET(fileno(fp), &rset)) { /* input 可读 */
if (Fgets(sendline, MAXLINE, fp) == NULL)
return; /* all done */
Writen(sockfd, sendline, strlen(sendline));
}
}
}

4.信号驱动式io

但凡涉及到信号的程序都比较复杂。要使用信号驱动式io,先开启socket的信号驱动式io功能,并通过sigaction 系统调用安装一个信号处理函数:

void
dg_echo(int sockfd_arg, SA *pcliaddr, socklen_t clilen_arg)
{
int i;
const int on = 1;
sigset_t zeromask, newmask, oldmask; sockfd = sockfd_arg;
clilen = clilen_arg; for (i = 0; i < QSIZE; i++) { /* init queue of buffers */
dg[i].dg_data = Malloc(MAXDG);
dg[i].dg_sa = Malloc(clilen);
dg[i].dg_salen = clilen;
}
iget = iput = nqueue = 0; Signal(SIGHUP, sig_hup); /* 安装信号处理函数 */
Signal(SIGIO, sig_io);
Fcntl(sockfd, F_SETOWN, getpid()); /* 设置属主 */
Ioctl(sockfd, FIOASYNC, &on); /* 开启信号驱动式io */
Ioctl(sockfd, FIONBIO, &on); /* non-bloking */ Sigemptyset(&zeromask); /* init three signal sets */
Sigemptyset(&oldmask);
Sigemptyset(&newmask);
Sigaddset(&newmask, SIGIO); /* signal we want to block */ Sigprocmask(SIG_BLOCK, &newmask, &oldmask);
for ( ; ; ) {
while (nqueue == 0)
sigsuspend(&zeromask); /* wait for datagram to process */ /* 4unblock SIGIO */
Sigprocmask(SIG_SETMASK, &oldmask, NULL); Sendto(sockfd, dg[iget].dg_data, dg[iget].dg_len, 0,
dg[iget].dg_sa, dg[iget].dg_salen); if (++iget >= QSIZE)
iget = 0; /* 4block SIGIO */
Sigprocmask(SIG_BLOCK, &newmask, &oldmask);
nqueue--;
}
}

5.异步io

我们来看一个aio的例子(由于aio的例子过于复杂,我们这里只截取部分关键代码):

for (i = 0; i < NBUF; i++) {
switch (bufs[i].op) {
case UNUSED:
/*
* Read from the input file if more data
* remains unread.
*/
if (off < sbuf.st_size) {
bufs[i].op = READ_PENDING;
bufs[i].aiocb.aio_fildes = ifd;
bufs[i].aiocb.aio_offset = off;
off += BSZ;
if (off >= sbuf.st_size)
bufs[i].last = 1;
bufs[i].aiocb.aio_nbytes = BSZ;
if (aio_read(&bufs[i].aiocb) < 0) /* aio_read */
err_sys("aio_read failed");
aiolist[i] = &bufs[i].aiocb;
numop++;
}
break; case READ_PENDING:
if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS) /* aio_error */
continue;
if (err != 0) {
if (err == -1)
err_sys("aio_error failed");
else
err_exit(err, "read failed");
} /*
* A read is complete; translate the buffer
* and write it.
*/
if ((n = aio_return(&bufs[i].aiocb)) < 0) /* 调用aio_return成功则 说明数据已经返回 */
err_sys("aio_return failed");
if (n != BSZ && !bufs[i].last)
err_quit("short read (%d/%d)", n, BSZ);
for (j = 0; j < n; j++)
bufs[i].data[j] = translate(bufs[i].data[j]);
bufs[i].op = WRITE_PENDING;
bufs[i].aiocb.aio_fildes = ofd;
bufs[i].aiocb.aio_nbytes = n;
if (aio_write(&bufs[i].aiocb) < 0) /* aio_write */
err_sys("aio_write failed");
/* retain our spot in aiolist */
break; case WRITE_PENDING:
if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS) /* aio_error */
continue;
if (err != 0) {
if (err == -1)
err_sys("aio_error failed");
else
err_exit(err, "write failed");
} /*
* A write is complete; mark the buffer as unused.
*/
if ((n = aio_return(&bufs[i].aiocb)) < 0)
err_sys("aio_return failed");
if (n != bufs[i].aiocb.aio_nbytes)
err_quit("short write (%d/%d)", n, BSZ);
aiolist[i] = NULL;
bufs[i].op = UNUSED;
numop--;
break;
}
}

6.同步和异步的分类

网络上对io同步和异步的争论很多,这里给出Stevens的分类标准:

同步 阻塞io,非阻塞io,io复用,信号驱动式io
异步 异步io

三.C10K io策略

在上个世纪,Dan Kegel 提出了C10K的设想,即单机实现10k的并发量,主要提出了以下四种类型的解决方法:

服务器范式 例子 备注 软件实现
Serve many clients with each thread, and use nonblocking I/O(level-triggered) select, poll(posix), /dev/poll(solaris), kqueue(bsd) 轮询  
Serve many clients with each thread, and use nonblocking I/O (readiness change) kqueue(bsd), epoll(linux), Realtime Signals(linux) 事件通知 nginx, redis
Serve many clients with each server thread, and use asynchronous I/O aio 异步,没有得到广泛支持  
Serve one client with each server thread

LinuxThreads, Java threading support in JDK 1.3.x and earlier

早期的java使用绿色线程

 
  • 在实现的过程中有诸多限制,比如打开fd的限制,创建thread数量的限制,根据不同内核而异。
  • 32 位系统,用户态的虚拟空间只有3G,如果创建线程时分配的栈空间是10M,那么一个进程最多只能创建300 个左右的线程。 64 位系统,用户态的虚拟空间大到有128T,理论上不会受虚拟内存大小的限制(10M个线程),而会受系统的参数或性能限制(线程上下文切换)。

四.C10M

Robert David Graham认为如果要解决C10M的问题,必须对unix内核进行改造。当下的unix系统的设计目标是为了满足非常广泛的需求,于是加上了许多通用的功能,比如进程管理,内存管理等等。C10M的问题不是通用的问题,需要自己处理数据控制,而不是依赖unix内核,而且需要做到packet scalability, multi-core scalability, memory scalability。

专项问题,需要特殊的解决方案。

五.总结

本文从常见io模型出发,梳理了高并发服务器可能涉及到的io模型,这些经典io模型在过去十年基本没有发生变化。了解这些底层技术对我们了解深入理解服务器是非常有必要的。

六.参考

《unix网络编程》

http://www.kegel.com/c10k.html#threads.java

http://highscalability.com/blog/2013/5/13/the-secret-to-10-million-concurrent-connections-the-kernel-i.html

https://man7.org/linux/man-pages/man2/select.2.html

从read 系统调用到 C10M 问题的更多相关文章

  1. 【网络】高性能网络编程--下一个10年,是时候考虑C10M并发问题了

    转载:http://www.52im.net/thread-568-1-1.html 1.前言 在本系列文章的上篇中我们回顾了过云的10年里,高性能网络编程领域著名的C10K问题及其成功的解决方案(上 ...

  2. c 进程和系统调用

    这一篇博客讲解进程和系统调用相关的知识 有这样一个场景,我需要输入一串文字,然后把我输入的文字加上一个本地的时间戳 保存在一个文件中,可以初步理解为一个备忘录也行 #include <stdio ...

  3. 我的操作系统复习——I/O控制和系统调用

    上篇博客介绍了存储器管理的相关知识——我的操作系统复习——存储器管理,本篇讲设备管理中的I/O控制方式和操作系统中的系统调用. 一.I/O控制方式 I/O就是输入输出,I/O设备指的是输入输出设备和存 ...

  4. xv6的作业翻译——作业1 - shell和系统调用

    Xv6的lecture LEC 1 Operating systems   L1: O/S overview L1:O/S概述   * 6.828 goals 6.828的目标   Understan ...

  5. Linux系统编程:基本I/O系统调用

    文件描述符 进程每打开一个文件的时候,会获得该文件的文件描述符,而后续的读写操作都把文件描述符作为参数.在用户空间或者内核空间,都是通过文件描述符来唯一地索引一个打开的文件.文件描述符使用int类型表 ...

  6. Linux系统调用和库函数调用的区别

    Linux下对文件操作有两种方式:系统调用(system call)和库函数调用(Library functions).系统调用实际上就是指最底层的一个调用,在linux程序设计里面就是底层调用的意思 ...

  7. linux自定义系统调用

    1 Linux3.10.21内核系统调用设置 以前看的内核版本时2.6.11的,里面的系统调用设置一目了然啊!在文件entry.S中直接定义了sys_call_table表,并在这个文件中用各个系统调 ...

  8. 【linux草鞋应用编程系列】_1_ 开篇_系统调用IO接口与标准IO接口

    最近学习linux系统下的应用编程,参考书籍是那本称为神书的<Unix环境高级编程>,个人感觉神书不是写给草鞋看的,而是 写给大神看的,如果没有一定的基础那么看这本书可能会感到有些头重脚轻 ...

  9. Linux0.11内核--系统调用机制分析

    [版权所有,转载请注明出处.出处:http://www.cnblogs.com/joey-hua/p/5570691.html ] Linux内核从启动到初始化也看了好些个源码文件了,这次看到kern ...

  10. Linux 下系统调用的三种方法

    系统调用(System Call)是操作系统为在用户态运行的进程与硬件设备(如CPU.磁盘.打印机等)进行交互提供的一组接口.当用户进程需要发生系统调用时,CPU 通过软中断切换到内核态开始执行内核系 ...

随机推荐

  1. 5分钟实现调用ChatGPT接口API实现多轮问答

    5分钟实现调用ChatGPT接口API完成多轮问答 最近ChatGPT也是火爆异常啊,在亲自使用了几个月之后,我发现这东西是真的好用,实实在在地提高了生产力.那么对于开发人员来说,有时候可能需要在自己 ...

  2. #Power Query 分组依据,数据的分类汇总

    一:概述 Power Query中的分组依据,类似于Excel中的分类汇总功能,可以按照某一分类对某列数据或某几列数据进行去重操作和聚合计算(求和.计数.求平均.非重复行计数等),并在去重的过程中将其 ...

  3. Windows server 2012 安装ad域

    Windows server 2012 安装ad域   安装ad域(active directory)服务的作用:存储目录数据并管理域之间的通信,包括用户登录处理,身份验证和目录搜索等. 1.使用ad ...

  4. 500行代码手写docker开篇-goland远程编译环境配置

    (1)500行代码手写docker开篇-goland远程编译环境配置 本系列教程主要是为了弄清楚容器化的原理,纸上得来终觉浅,绝知此事要躬行,理论始终不及动手实践来的深刻,所以这个系列会用go语言实现 ...

  5. boot-admin 项目数据库缺省字段设计之最佳实践

    数据库(Database)中的缺省字段(也称为默认字段),就是在一般情况下,每个数据表(Table)必须包含的字段(Field),这类字段用于满足特定的数据需求,字段值的填充或更改一般遵照一定的逻辑要 ...

  6. Hugging News #0519: 我们把 HuggingChat 开源了

    每一周,我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新,包括我们的产品和平台更新.社区活动.学习资源和内容更新.开源库和模型更新等,我们将其称之为「Hugging Ne ...

  7. 一篇文章告诉你什么是Java内存模型

    在上篇 并发编程Bug起源:可见性.有序性和原子性问题,介绍了操作系统为了提示运行速度,做了各种优化,同时也带来数据的并发问题, 定义 在单线程系统中,代码按照顺序从上往下顺序执行,执行不会出现问题. ...

  8. 源码解析:数据批量导入bukl_crete()原理

    在Django中需要向数据库中插入多条数据(list).使用如下方法,每次save()的时候都会访问一次数据库.导致性能问题: for i in resultlist: p = Account(nam ...

  9. JavaWeb入门必备JavaEE规范!

    前言 对于学习 Java 的同学,大都是 Web 方向的.我们学习 JavaWeb 开发肯定是一个循序渐进的过程,学习前有一些前置知识要掌握,比如 JavaSE 相关知识,HTML.CSS.JavaS ...

  10. Hive执行计划之只有map阶段SQL性能分析和解读

    目录 目录 概述 1.不带函数操作的select-from-where型简单SQL 1.1执行示例 1.2 运行逻辑分析 1.3 伪代码解释 2.带普通函数和运行操作符的普通型SQL执行计划解读 2. ...