分类 编程语言 下的文章

本周处理了好几例负载高问题,原因竟然都是因为微信对代理场景的支持不好导致的。

回顾十分曲折的排查过程,记录下来!顺带吐槽一下微信。

一、问题描述

背景:我们的设备作为客户处的上网出口,代理内网用户上网。

问题:设备流量不高,但是负载特别高,经常性产生断网事件,流量突降为0。

二、排查过程

2.1 分析负载和CPU

先画出问题时间点的负载趋势图,可以看到负载确实是很高的:

这个设备只是2核的,负载超过4就可以认为负载高了,而记录下来的负载基本都在10以上,远超出设备负载。

然后分析CPU采样,找到对应时间点的mpstat记录,采样中显示较高的是iowaitsoft

Fri Mar 13 08:40:08 CST 2020
Linux 2.6.30-gentoo-r8 (localhost)     03/13/20     _x86_64_    (2 CPU)

Average:     CPU    %sys %iowait    %irq   %soft  %idle
Average:     all    2.50   82.00    3.00   10.50   0.00
Average:       0    1.01   81.82    5.05   10.10   0.00
Average:       1    3.00   83.00    1.00   11.00   0.00

soft高一般都是流量过高或者连接数过大导致的,而iowait高大部分时候都是内存不足导致的,因为内存不足会导致内存cache回收,大量的磁盘脏数据刷到磁盘,导致IO高。因此下一步应该先分析内存。

2.2 分析内存占用

分析问题时间点前后的内存状态,发现了内存cache回收的情况:

2020-3-13 08:37:57
             total       used       free     shared    buffers     cached
Mem:          1809       1675        133          0          6        130

2020-3-13 08:38:02
             total       used       free     shared    buffers     cached
Mem:          1809       1546        262          0          1         27

设备只有2G内存,属于低端平台了,内存不足应该属于常态,所以经常回收cache,导致了io高。

然后统计slab占用,发现占用最高的是TCP和TCPv6,总共占用了接近三百兆:

正常情况下,这两个东西不可能占用这么大比例。这只能说明,网络流量有异常!!

内核给TCPv6和TCP分配slab的场景

为了搞清楚内核在什么场景下会分配这两块内存好对症下药,所以在内核中定位了相关代码,最后发现是创建TCP套接字的时候分配的。

slab是内核中的的内存分配机制,所有的内存都通过kmem_cache_create创建,而每块被分配的内存块都是有名字的(如上面的TCPv6和TCP)。因此在内核代码中直接搜索:

kmem_cache_create("TCPv6")

不出意料,没有搜索到相关代码。因为根据linux代码的特性,这个字符串肯定是被宏定义代替或者结构体引用了。

于是先搜索"TCPv6",在net/ipv6/tcpv6_prot中找到了定义:

struct proto tcpv6_prot = {
    .name = "TCPv6",
    // ...
}

整个内核代码中只有这一处出现了关键字段,除此之外没有其他地方有这个字符串,因此肯定一定是哪里引用这个字段。继续通过正则查找kmem_cache_create\(.*->namne,找到:

int proto_register(struct proto *prot, int alloc_slab)
{
    if (alloc_slab) {
        prot->slab = kmem_cache_create(prot->name, prot->obj_size, 0,
                    SLAB_HWCACHE_ALIGN | prot->slab_flags,
                    NULL);
    // ...
    }
}

proto_register是注册协议到内核的协议栈,产生对应协议请求的时候会调用这个方法来创建socket对象,TCPv6TCP内存块占用内存高,说明肯定是这里走得多了,这就间接说明设备肯定产生了大量连接。

2.3 分析网络流量

分析出流量有异常之后,第一个想到的是不是网络环境有问题,因为上周刚好也查了一个类似的问题:一次孤儿socket过多导致系统异常的问题排查过程。但是抓包分析后发现,流量并没有出现类似的特征,而且设备总共的孤儿socket并不多,这说明两个问题不是同一个原因。

于是只能继续分析了,因为流量特征(各种数据包的占比)都比较正常,所以排查的重点放在的包的内容身上。终于在经过了大量的分析后发现了数据异常:

以上是内网发到我们设备的代理请求,设备地址是10.115.5.2,因为是代理场景,所以目的地址是它,这没有问题。但问题出在了数据包身上,也就是最后一列,可以看到他们都是:

POST http://10.115.5.2/download HTTP/1.1

这个HTTP代理请求是请求代理本机,这是明显有问题的,也是不符合RFC规定的,正常的代理数据包应该是被代理的地址如www.biadu.com,绝不会是10.115.5.2,如:

POST http://www.baidu.com/download HTTP/1.1

以下是一些正常的代理请求:

如果代理的请求是本机,就会导致本机自己再去连接自己的80端口,造成的结果就是设备会多产生一个发往本机nginx的连接。nginx收到请求后因为不存在这个uri资源会返回404到代理进程,代理进程再返回给客户端。流程为:

本来到这里也没有很大的问题,只是会导致本机多一个连接而已。但关键的问题是,返回错误给PC客户端后,客户端会频繁发起重连,产生大量的重试请求。频率有多高呢?从下面的这个抓包截图来看,不到0.5ms的时间内内网就产生了10+个请求:

代理场景并不像路一样由,只要做NAT把地址转换一下发出去就可以了,路由对设备的资源占用并不大。代理场景除了要和内网PC执行三次握手以外,还要主动发起连接请求WAN端,要产生双倍的资源消耗。如此大规模的重连请求,占用了大量的连接资源,足以算得上dos攻击了。因此,可以认为本次问题就是内网的异常连接导致的。

这些异常的请求具体是什么呢,通过分析数据包内容和特征,很容易就能看出是微信的请求,是在请求下载小视频:

三、和微信的交涉

为了确认微信的问题,找微信客服反馈。他们承认确实是有问题,但是并不想改:

四、微信代理存在的问题

微信代理场景目前存在以下问题:

  1. 代理数据包构造不正确,也就是上面描述的场景。
  2. 微信视频不支持代理,使用代理后无法发起微信视频。
  3. 微信传输大文件时,不会经过代理,直接发到了服务端了。
  4. 不支持代理长连接。

第三个问题是出在另外一个客户身上,现象上是无法传输超出20M以上的文件。实际上通过抓包分析发现是微信直接绕过了代理,目的地址不是代理设备地址:

而不支持长连接问题就更有意思了,问题现象是微信代理后无法正常收到图片等相关资源,微信给出的反馈竟然是不支持嵌套,嵌套这个词说出来就很有意思了,刚说出来我还琢磨是什么嵌套。最后抓包发现竟然指的是HTTP长连接。

五、吐槽

从技术角度而言,这几个问题实际上并不能算成是问题,调整起来的技术难度并不高。不知道为什么就是不调整,反馈了很多次了。突然想起前两天微信的热搜,面对用户,好像并不“怂”:

一、异常处理

1.1 异常的基本用法

C语言中因为没有异常处理(只能通过返回值来判断错误)机制一直被诟病,因此C++也引入了try...catch机制,使得C++也能像java/python一样来捕获异常。

它的用法和大多数其他语言基本一致,非常简单:

try {
    throw "HelloException";
} catch (const char *msg) {
    cout << msg << endl;
}

除此之外,C++标准库中还提供了一个标准异常类exception, 内部有一个what函数可以打印异常信息:

try {
    throw std::exception();
} catch (exception &e) {
    cout << e.what() << endl;
}

执行后程序会抛出异常信息:

std::exception

不过std::exception类内部没有提供太多函数可以操作,只有基本的构造、拷贝构造以及析构函数等,自定义空间有限,很难完全依赖它打印出更详细的异常信息。因此,标准库中还提供了一些预定义的派生类来使用:

大部分的异常类都有提供自己的默认构造函数和带参构造函数,例如out_of_range异常类提供了一个char *的传入:

try {
    throw std::out_of_range("out of range");
} catch (std::out_of_range &e) {
    std::cout << e.what() << std::endl;
}

允许构造的时候带入错误信息字符串,执行what()的时候就会把这个字符串打印出来:

out of range

1.2 构造函数中的异常处理

构造函数执行初始化列表的时候,因为还没有执行到函数内部代码块,所以并不在try的捕获范围内,是无法捕获到异常的。

如若希望执行初始化列表的时候也能捕获异常,则需要在初始化列表之前加上try关键字:

my_exception() try: A(a), B(b) {}

二、自定义异常类

实际的项目中,往往会自己定义异常类,自定义异常类的方法很简单,从std::exception公有继承就可以了,内部还可以加上自己定义的成员和函数。

例如:

class my_exception : public std::exception {
public:
    int code;
    std::string msg;

    const char *what() const throw() {
        return msg.c_str();
    }

    my_exception(int code, const std::string &msg) : code(code), msg(msg) {}
};

使用方式:

try {
    throw my_exception(255, "this is a exception!");
} catch (my_exception &e) {
    cout << "ErrCode: " << e.code << ", ErrMsg: " << e.msg << endl;
    cout << e.what() << endl;
}

一、关于SQL注入

sql注入是目前web应用中一种常见的攻击方式,通过恶意构造参数生成不可预期的sql语句,来完成不可告人的秘密。危害极大!它的影响主要有以下两点:

第一:拖库,拖库的意思是直接把整个数据表甚至库中的数据都拖出来了。当今的互联网环境中,数据毫无疑问在任何公司都是最宝贵的财富,一旦数据泄露,轻者造成经济损失,重者可能造成法律责任。

第二:删库,拖库的危害可能只是和他人共享了劳动成果,而删库就不同了,数据被共享了不说,还把数据都删了。这就是典型的——走别人的路,让别人无路可走!

近期闹得沸沸扬扬的“微盟删库”事件,因为运维人员把数据库删了,导致业务接近一周都没有恢复,股价直接下跌10+亿。可见“删库”的危害实在太大!

- 阅读剩余部分 -

一、I/O模型分类

unix环境下有5中IO模型:

  • 阻塞式I/O
  • 非阻塞式I/O
  • I/O多路复用
  • 信号驱动I/O
  • 异步I/O(POSIX中的aio_系列函数)

常用的是前三种方式,特别是多路I/O复用是目前使用最广泛的I/O模型。它不仅包含了阻塞和非阻塞,同时也包含了异步调用。非阻塞+异步是效率最高的I/O方式。

二、阻塞式I/O

阻塞I/O的意思是:调用读写函数时,系统会卡在当前函数,直到有数据可读或者可写才返回。

工作流程图:

三、非阻塞式I/O

非阻塞I/O的意思是:调用函数时如果没有数据可读,立马返回。然后开始轮询,直到有数据返回为止。

工作流程图:

四、多路I/O复用

多路I/O复用:通过多路IO复用模型(select/poll/epoll)同时监听多个套接字,等待某个套接字有数据到达时再执行系统调用。

工作流程图:

epoll中的触发模式有两种,边缘触发和水平触发,默认情况下使用的是水平触发。

边缘触发(ET)的意思是当电平出现变化的时候才触发事件,如果设置了边缘触发,执行epoll_wait时,内核检测到数据到达后立马返回到应用层。但是这仅仅只返回这一次,如果缓冲区中的数据没有读取完,再次执行epoll_wait时不会继续触发,需要下一次来数据了才能触发。也就是说,一次数据不会重复发送到应用层,不管你是否读完了。

而水平触发(LT)的意思是只要存在高电平就一直触发事件,执行epoll_wait时,只要检测到有数据就返回。如果缓冲区中存在没有读完的数据,下一次执行epoll_wait还会继续触发事件,无需等到下一次数据来。

两者的触发时间点:

相比之下,ET的效率高于LT模式,因为产生的事件数更少,可以减少内核往应用层空间复制数据的次数。在进行高性能网络编程的时候,往往都是选择非阻塞IO+ET触发模式,这种模式下可以做到想读数据的时候就读,不想读就不读。同时,读不到也不阻塞,大大增加了程序的灵活性。而不是说不管是否想读数据,都强制要求读。

项目中使用ET模式时遇到了一个问题:我们开发了一个socks5代理程序,可以用来代理上网。正常的程序代理都没有问题,就是网页中的实时视频使用代理会出现延时,延时能达到5-10秒。最后查了很久之后发现是ET触发模式导致的, 发送数据的时候内核不会立马发出去,改成LT模式之后就好了。很神奇!

一、猴子拿苹果问题

逛脉脉时,看到一网友遇到的面试题:有9个苹果,2只猴子。一个猴子每次拿2个苹果,一个猴子每次拿3个苹果。如果剩余的苹果数量不够猴子拿的数量,则停止拿苹果。请用多线程的方式模拟上面的描述。

看到问题的第一眼,觉得很有趣,脑海中第一个想到的就是通过信号量来实现,因为信号量是最适合做线程和进程状态同步了,而问题的考点就是线程同步。

恰好这两天刚好有在看信号量,所以很容易就想到通过信号量来实现这个问题。

其实原题说的是通过java多线程实现,但是我不会java,只能用c来写了。

为什么说考查的问题点是线程同步?因为题目要求是通过多线程来实现,如果不对线程状态制约,9个苹果,很容易在一瞬间就被一个猴子拿完,或许第二个线程还没开始苹果就已经拿完了。这明显不是面试官想看到的。所以不难想到考点就是如何协调两个猴子(线程)有序的拿桃子,有序的意思就是:你拿一次,我拿一次,要有次序的进行,直到把桃子拿完。如何控制线程之间你执行一次,我执行一次,肯定就要利用同步了。

二、匿名信号

匿名信号比较适用于线程间同步,因为它没有名字,所以多个进程间无法通过一个载体来共享它。只有在线程中,通过变量或者参数传递的方式来共享,所以适用于线程。

有名信号的用法参考:进程间通信之信号量

匿名信号的原理也和有名信号一样,维持一个计数器,然后通过等待计数器的状态来继续往下执行。相关的函数:

#include <semaphore.h>
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 发送信号量
int sem_post(sem_t *sem);
// 等待信号量
int sem_wait(sem_t *sem);
// 关闭信号量
int sem_close(sem_t *sem);

四个函数中,下面三个和有名信号量中的一样,唯一有区别的是第一个初始化信号量函数。匿名信号量无需传入信号量的名字,直接传入信号量对象就可以初始化了。后面的两个参数也都是一样,一个代表共享属性,一个代表默认值。

三、实现猴子拿苹果问题

两个猴子,肯定是需要两个线程,线程中辨别是哪个猴子肯定要传入猴子的参数,因此先实现一个猴子信息的对象,传入线程参数:

// 每个猴子的信息
struct monkey_info {
    int id; // 猴子的ID
    int cnt; // 每次拿苹果的数量
    sem_t *my; // 自己的信号量
    sem_t *other; // 另外一个猴子的信号量
};

为什么需要两个信号量呢,逻辑是:我去拿猴子,拿完通知你拿,你去拿猴子,我等在这里,等你拿完通知我拿。一个是等待自己的信号量,一个是通知另外猴子的信号量,所以需要两个。

线程函数实现:

// 苹果总量
static int s_apple_cnt = 9;

void *get_apple(void *ptr) {
    struct monkey_info *monkey = ptr;
    if (monkey == NULL)
        return NULL;

    while (1) {
        // 等待信号量
        sem_wait(monkey->my);
        if (s_apple_cnt < monkey->cnt) { // 当前的苹果数量不足以给到当前猴子
            printf("monkey %d: apple has't enough! total: %d, need: %d\n", monkey->id, s_apple_cnt, monkey->cnt);
            // 退出前,通知另外猴子继续拿
            sem_post(monkey->other);
            break;
        } else {
            // 拿苹果,减库存
            s_apple_cnt -= monkey->cnt;
            printf("monkey %d: get %d apple! remaining apples: %d\n", monkey->id, monkey->cnt, s_apple_cnt);
        }
        // 通知另外猴子
        sem_post(monkey->other);
    }
    return NULL;
}

主函数实现:

int main() {
    sem_t sem1, sem2;
    pthread_t tid1, tid2;
    // 初始化两个猴子的状态
    struct monkey_info monkey1 = {1, 2, &sem1, &sem2};
    struct monkey_info monkey2 = {2, 3, &sem2, &sem1};

    // 初始化信号
    sem_init(&sem1, 0, 1);
    sem_init(&sem2, 0, 0);

    // 创建线程
    pthread_create(&tid1, NULL, get_apple, (void *)&monkey1);
    pthread_create(&tid2, NULL, get_apple, (void *)&monkey2);

    // 等待线程退出
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    // 退出信号量
    sem_close(&sem1);
    sem_close(&sem2);

    return 0;
}

执行结果:

过程描述:

  1. 第一个猴子拿2个苹果,剩余7个苹果。
  2. 第二个猴子拿3个苹果,剩余4个苹果。
  3. 第一个猴子拿2个苹果,剩余2个苹果。
  4. 第二个猴子想要拿3个苹果,但是苹果不够了,退出。
  5. 第一个猴子拿两个苹果,苹果被拿完。
  6. 第一个猴子想要拿2个苹果,但是苹果不够了,退出。

共享内存是所有IPC通信中效率最高的,它通过把文件映射到用户进程空间,然后直接通过地址访问来实现多进程通信。相对于其他IPC通信方式而言,少去了把数据从用户空间复制到内核空间,再从内核空间复制到用户空间的过程,因此效率相当高。

用图形来表示就是:

操作共享内存的函数:

void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t len);

第一个是映射共享内存的函数,参数说明:

  1. addr: 需要映射的共享内存起始地址,一般来说传入NULL。
  2. len: 映射出来的共享内存块大小。
  3. prot: 内存权限,长用的权限是PROT_READ | PROT_WRITE,表示可读可写。
  4. flags: 共享内存区域的属性,可以设置为MAP_SHARED或者MAP_PRIVATE,如果是前者,当前进程对共享内存区域的修改对其他进程可见,否则就不可见。
  5. fd: 共享内存文件的文件描述符。
  6. offset: 文件的偏移,从文件的offset处开始共享内存。

使用示例

以下代码展示了一个共享内存的操作示例,通过共享内存在父子进程间共享一个int类型的变量,然后父进程修改值,子进程读:

代码中忽略掉了一些不相关的错误处理。
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <semaphore.h>

int main() {
    int fd, n = 12345;
    int *ptr;
    sem_t *sem;
    pid_t pid;

    // 创建信号
    sem = sem_open("my_sem", O_RDWR | O_CREAT, 0755, 0);
    sem_unlink("my_sem");

    // 往文件写一个数据
    fd = open("mmapfile", O_RDWR | O_CREAT, 0755);
    write(fd, (void *)&n, sizeof(int));

    // 创建共享内存
    ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    pid = fork();
    if (pid == 0) {
        // 子进程,等待父进程修改完共享内存后打印出来
        sem_wait(sem);
        printf("child:\t%d\n", *ptr);
    } else if (pid > 0) {
        // 父进程,把共享内存加一
        printf("parent:\t%d\n", *ptr);
        (*ptr)++;
        sem_post(sem);
    } else {
        perror("fork error");
    }
    
    // 关闭信号量
    sem_close(sem);
    // 取消映射共享内存
    munmap(ptr, sizeof(int));
    // 删掉共享内存文件
    unlink("mmapfile");

    return 0;
}

代码中信号量的作用是确保子进程在父进程执行完成后才执行,父进程先把共享内存的数据加1,然后子进程读取共享内存的数据应该要变成12346。

执行结果符合预期:

一、信号量

信号量有两种,一种的有名信号,一种是无名信号。有名信号一般用于进程间同步,无名信号一般用于于线程间同步。创建或打开一个信号的函数:

#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ...);

name参数致命信号量的名字,由于信号量内部保存在系统内核中,多个进程间可以直接通过指定信号量的名字来相互通信。oflag表示信号的属性:可读、可写、可执行或者其他,如果指定了O_CREAT属性,则会创建信号量,否则表示打开已有信号量。后面的可选参数有两个:

  1. mode: 信号量的权限,和文件属性一样,0755或者其他。
  2. value: 默认值,信号量内部维持了一个计数,value就是设置计数的默认值。

操作信号量的方法:

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);

wait表示等待一个信号量,当信号量中的计数不为0时,会把计数减1,然后继续执行后面的逻辑,如果计数为0,wait操作就会阻塞,直到计数不为0。trywait是wait的非阻塞版本,它会尝试wait,如果计数为0也直接返回。post操作会把计数加1,它和wait相对。只有通过它把计数加1后,阻塞的信号量才最继续往下执行。

关闭信号量:

int sem_close(sem_t *sem);
int sem_unlink(const char *name);

close用于关闭信号量,这个操作会导致信号量的引用计数减1。当引用计数到达0后,执行过unlink的信号量会自动删除。

信号量的主要应用场景是同步,适合同步多个进程的状态同事到达某一个点。

二、示例代码

示例代码中创建了两个进程,每个进程都对一个变量执行三次自加操作。信号量的作用就是保证两个进程对变量自加操作的进度相同(意思是你自加了1次,我也自加1次,不存在你自加2次我才自加1次的情况)。

#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>

#define SEMAPHORE_CHILD_NAME "sem_child"
#define SEMAPHORE_PARENT_NAME "sem_parent"

int main() {
    int i, count;
    sem_t *sem_child, *sem_parent;
    pid_t pid;

    // 创建两个信号量,用于父子进程间相互制约
    sem_child = sem_open(SEMAPHORE_CHILD_NAME, O_RDWR | O_CREAT, 0755, 1);
    sem_parent = sem_open(SEMAPHORE_PARENT_NAME, O_RDWR | O_CREAT, 0755, 1);
    if (sem_child == NULL || sem_parent == NULL) {
        perror("sem_open error");
        return -1;
    }
    // 信号量使用完成后删除
    sem_unlink(SEMAPHORE_CHILD_NAME);
    sem_unlink(SEMAPHORE_PARENT_NAME);

    pid = fork();

    count = 0;
    if (pid == 0) {
        for (i = 0; i < 3; i++) {
            // 等待父进程信号量
            sem_wait(sem_parent);
            count++;
            // 设置子进程信号量
            sem_post(sem_child);
            printf("pid[%u]: count = %d\n", getpid(), count);
        }
    } else if (pid > 0) {
        for (i = 0; i < 3; i++) {
            // 等待子进程信号量
            sem_wait(sem_child);
            count++;
            // 发送父进程信号量
            sem_post(sem_parent);
            printf("pid[%u]: count = %d\n", getpid(), count);
        }
    } else {
        perror("fork error");
    }

    // 关闭信号量
    sem_close(sem_parent);
    sem_close(sem_child);

    return 0;
}

执行结果:

可以看到,两个进程间的字节都是交替进行的。

EAGAINEWOULDBLOCK是linux环境下的两个错误码,在非阻塞IO中经常会碰到,对新手而言,如何处理这两个值非常头疼。如果处理不当,很容易导致程序异常。

EAGAIN的官方定义:

“Resource temporarily unavailable.” The call might work if you try again later. The macro EWOULDBLOCK is another name for EAGAIN; they are always the same in the GNU C Library.

翻译:资源短暂不可用,这个操作可能等下重试后可用。它的另一个名字叫做EWOULDAGAIN,这两个宏定义在GNU的c库中永远是同一个值。

EWOULDBLOCK的定义:

“Operation would block.” In the GNU C Library, this is another name for EAGAIN (above). The values are always the same, on every operating system.

翻译:操作将会被阻塞,在GNU C的库中,它的另外一个名字是EAGAIN,在任何操作系统中他们两个的值都是一样的。

这两个错误码在大多数系统下是都同一个东西,特别是在使用了GNU的libc库平台(目前广泛使用的centos和ubuntu都是)下一定是相同的。这个错误产生的情况:

  • 尝试在一个设置了非阻塞模式的对象上执行阻塞操作,重试这个操作可能会阻塞直到其他条件让它可读、可写或者其他操作。
  • 对某些操作来说,资源短暂不可用。例如fork函数可能返回这个错误(当没有足够的资源能够创建一个进程时),可以采取的操作是休息一段时间然后再继续操作。

那么应该如何处理这个错误?

最好的办法是重试,重试一定次数后还不成功就退出操作。

为什么不能无限重试呢?假设在通过socket发送一段数据,发送缓冲区如果一直不可写,就会出现无限循环的情况,进程卡死。

其他

在某些较老的unix系统上,这两个值的意义可能不一样。

可以参考:Which systems define EAGAIN and EWOULDBLOCK as different values?

参考

Difference between 'EAGAIN' or 'EWOULDBLOCK'

Error Codes

来源:Latency Numbers Every Programmer Should Know

图片版:

文字版:

Latency Comparison Numbers (~2012)
----------------------------------
L1 cache reference                           0.5 ns
Branch mispredict                            5   ns
L2 cache reference                           7   ns                      14x L1 cache
Mutex lock/unlock                           25   ns
Main memory reference                      100   ns                      20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy             3,000   ns        3 us
Send 1K bytes over 1 Gbps network       10,000   ns       10 us
Read 4K randomly from SSD*             150,000   ns      150 us          ~1GB/sec SSD
Read 1 MB sequentially from memory     250,000   ns      250 us
Round trip within same datacenter      500,000   ns      500 us
Read 1 MB sequentially from SSD*     1,000,000   ns    1,000 us    1 ms  ~1GB/sec SSD, 4X memory
Disk seek                           10,000,000   ns   10,000 us   10 ms  20x datacenter roundtrip
Read 1 MB sequentially from disk    20,000,000   ns   20,000 us   20 ms  80x memory, 20X SSD
Send packet CA->Netherlands->CA    150,000,000   ns  150,000 us  150 ms

Notes
-----
1 ns = 10^-9 seconds
1 us = 10^-6 seconds = 1,000 ns
1 ms = 10^-3 seconds = 1,000 us = 1,000,000 ns

中文整理版:

操作耗时备注
CPU一级缓存寻址0.5纳秒
CPU二级缓存寻址7纳秒
互斥锁25纳秒
内存寻址100纳秒
使用zippy压缩1k文件3000纳秒(3微秒)
在1Gbps的网络中发送1k数据10000纳秒(10微秒)
从ssd中随机读取4KB数据150000纳秒(150微秒)SSD速率:1GB/s
从内存中顺序读取1MB250000纳秒(250微秒)
同一数据中心往返耗时500000纳秒(500微秒)
从ssd中随机读取1MB数据1000000纳秒(1000微秒,1毫秒)SSD速率:1GB/s
磁盘寻址10000000纳秒(10000微秒,10毫秒)约等于20次数据中心往返
从磁盘中顺序读取1MB数据20000000纳秒(20000微秒,20毫秒)

备注:1秒=$10^3$毫秒=$10^6$微秒=$10^9$纳秒。