别再纠结 select 和 poll 了!epoll 才是 I/O 复用的顶流担当!

科技   2024-11-24 12:45   河北  

前言:

selectpoll,是时候见识下 epoll 的威力了!

还记得咱们之前聊的 select poll 吗?每次要监听一堆连接时,它们会一遍一遍地“挨个问”:“有事没?有事没?” 这种方式效率低得让人心累。再多来点请求,服务器分分钟就要瘫了!

今天,我们要隆重介绍 epoll — 这个 Linux I/O 多路复用机制的“王者”!它是如何做到让 CPU 轻松高效处理成千上万连接的?只需简单几步,让需要响应的连接自己“找上门来”,而其他不活跃的连接安安静静“打酱油”去吧。是不是听着就很酷?今天我们就来揭开 epoll 的神秘面纱,让你彻底掌握它。

1、什么是 epoll?省时省力的 I/O 管家

说到 epoll,它的聪明之处就在于不再去主动找那些“沉默”的连接,而是设置好监听条件,只有符合条件的连接“自己来找你”!这就像你是公司客服,不用总去问每个客户“有什么问题吗”,而是让有问题的客户来找你,省时省力。

2、为什么 epoll 比 select 和 poll 更强?

1. 事件驱动,省时省力epoll 使用事件通知机制,只有真的有事件的连接才触发通知,这就大大节省了资源。

2. 支持大规模连接select poll 在处理大量连接时效率会下降,并且 select 有文件描述符数量的限制。但 epoll 没有!哪怕几千上万个连接,它照样轻松应对。

3. 支持水平触发和边缘触发:水平触发(Level Triggered)类似“待办事项”一直显示,直到处理完毕;而边缘触发(Edge Triggered)更高效,只在状态变化时通知一次,非常适合高性能场景。

3、epoll 的三步走:创建、登记、等待事件

为了说明 epoll 是如何高效管理大量客户端连接,我们可以把它想象成一个 VIP 俱乐部。在这个俱乐部里,每位 VIP 客户只有在需要时才会联系俱乐部,而我们只处理这些有需求的客户。epoll 就是这个俱乐部的管理系统,通过特定的数据结构来高效管理和响应每位 VIP 客户的请求(这里的 VIP 客户可以类比成网络客户端)。

3.1 第一步:创建 epoll 对象 —— 开设 VIP 俱乐部

首先,我们需要创建一个 VIP 俱乐部,把所有 VIP 客户集中管理起来。这一步在代码中通过 epoll_create 函数实现:


int epoll_fd = epoll_create();

这里的 epoll_fd 是 VIP 俱乐部的“钥匙”,有了它,我们就可以管理俱乐部中的所有 VIP 客户(即:客户端的连接 fd)。

图解:VIP 俱乐部刚成立,还没有客户加入,等待后续登记。


红黑树结构:VIP名单

              VIP 俱乐部 (epoll_fd)
                    |
                    |
                    |
       +------------+-------------+
       |                          |
   [暂无客户]                 [暂无客户]

epoll 使用的数据结构:红黑树

在系统内核中,epoll 使用 红黑树 来存储所有 VIP 客户的“身份信息”(即客户端连接的文件描述符 fd)。红黑树就像俱乐部的 VIP 名单,主要有以下特点:

  1. 自平衡、节点有序:红黑树是一种自平衡二叉树,确保 VIP 名单(fd 列表)始终保持有序,方便快速查找。
  2. 操作高效:红黑树的增、删、查操作的时间复杂度为 O(log N),即使面对成百上千的 VIP 客户(fd),也能迅速找到或更新信息。

类比:当一个新客户加入俱乐部时,他们的“身份信息”(fd)会按照规则被加入到红黑树中,方便随时快速查找和处理。就像 VIP 名单按顺序排列,每次新增或删除客户时,名单会自动调整,确保查询效率始终保持高效。

这样通过创建一个 epoll 对象,我们就相当于开设了 VIP 俱乐部,并准备好随时接纳和管理更多 VIP 客户(即客户端连接)。

3.2 第二步:登记 VIP 客户 —— 添加客户并设定监听事件

俱乐部建立好后,接下来我们要“登记”每位 VIP 客户的信息(即文件描述符 fd),并设定他们的“需求”。这一步通过 epoll_ctl 函数来完成:

struct epoll_event ev;
ev.events = EPOLLIN;        // 设置监听“有新请求”事件
ev.data.fd = sock_fd;       // 客户的文件描述符
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &ev);

这段代码的作用是把 VIP 客户的信息和需求注册到 epoll 系统中,就像给 VIP 客户设定了一个“叫号规则”。

可以把这一步理解为给每个 VIP 客户设置了“有事叫我”的规则。

  • 比如,VIP 客户 sock_fd 设置的是“当有新请求时叫我”。也就是说,每位 VIP 客户在俱乐部设定了一个规则:“有需求时叫我一声。”
  • 当 VIP 客户真的有新请求(比如网络上有数据到达)时,系统就会根据这个规则提醒我们去处理这个客户的请求。

这样一来,epoll 可以高效地管理所有 VIP 客户,不需要每次去问每个客户“有什么需求吗”,而是等着他们自己“叫号”,大大节省了系统资源。

epoll 使用红黑树管理 VIP 客户信息

在 epoll 内部,红黑树被用来管理所有 VIP 客户的信息。这里的每一个 VIP 客户(即每一个客户端文件描述符 fd)都会成为红黑树中的一个节点。

随着客户的逐渐登记,红黑树会逐渐填满 VIP 客户的节点。每个节点代表一个客户端连接fd。

图解:

                      [ 客户10 (fd10, 黑) ]
/ \
[ 客户5 (fd5, 红) ] [ 客户15 (fd15, 黑) ]
/ \ \
[ 客户3 (fd3, 黑) ] [ 客户7 (fd7, 黑) ] [ 客户18 (fd18, 红) ]

// 红黑树特点说明:红黑树的节点不是黑色就是红色,根节点是黑色,且红色节点的子节点必须是黑色。

红黑树的关键作用:

  • 高效管理客户信息:红黑树可以快速找到每个客户的位置,新增、查找、删除 VIP 客户的效率都很高。
  • 自动平衡:红黑树有自动平衡的机制,不会因为 VIP 客户多了而影响查询效率。

小结

通过“叫号规则”的设置,epoll 可以高效地管理大量 VIP 客户,在有需求时迅速找到对应的客户,不浪费资源。而红黑树为 epoll 提供了一个有序、平衡的管理系统,即使 VIP 客户再多,也能保持高效的注册和查找。

3.3 第三步:等待事件触发 —— 集中处理 VIP 客户(客户端fd)的请求

当所有 VIP 客户都登记好之后,epoll 就进入了“待命模式”,它会专注于那些“真的有事”的 VIP 客户,其他客户保持静默就不用理会。这个等待事件触发的过程通过 epoll_wait 完成:


struct epoll_event events[10];          // 用来存储触发事件的客户
int nfds = epoll_wait(epoll_fd, events, 10-1);  // 等待事件
for (int i = 0; i < nfds; ++i) {
    if (events[i].events & EPOLLIN) {
        // 处理客户的请求
    }
}

类比:智能秘书模式

可以把 epoll_wait 理解为 epoll 的“智能秘书”, 只会通知我们那些“有需求”的 VIP 客户。每当调用 epoll_wait,它会检查所有已登记的 VIP 客户,并把有事件的客户集中放在 events 数组里,返回给我们。这样一来,我们只需处理这些真正有需求的 VIP 客户,其他静默的客户则可以忽略,省时省力。

epoll 采用的另一个数据结构:双向链表

在内核中,epoll 会把所有“发出请求”的 VIP 客户(真正有数据到来的客户端fd)从红黑树移到双向链表中。双向链表中只存储那些有数据到来的客户端 fd,这样 epoll_wait 能一次性返回所有“有数据到来”的客户端fd,进一步提高效率。

图解:双向链表专门存储有需求的 VIP 客户


+-----------------------------------------------------------------+
| 双向链表 (只包含有需求的客户) |
| |
| [ 客户5 (fd5) ] <--> [ 客户10 (fd10) ] <--> [ 客户18 (fd18) ] |
+------------------------------------------------------------------+

红黑树 + 双向链表:epoll 的高效组合

  • 红黑树:负责管理所有 VIP 客户的注册信息,确保增删查的效率。
  • 双向链表:只存放那些已触发事件的客户,保证我们只需集中处理“有需求”的客户。

这种红黑树和双向链表的组合,让 epoll 能高效筛选出有请求的 VIP 客户,把系统资源集中在真正有需求的连接上,大大提高了性能。epoll_wait 就像 epoll贴心的“智能秘书”,只提醒我们需要处理的 VIP 客户,保证我们高效完成所有请求。

让我们再来看一个图,这张图可以帮助我们更直观地理解整个 epoll 三步走的过程:

图解的简单说明:

  • 1、创建 epoll 对象:首先,调用 epoll_create,在内核空间中创建一个 epoll 对象 eventpoll。这个对象包含两个数据结构:rbtree 和 rdlist。其中,rbtree 是红黑树,用来存储所有注册的文件描述符(fd),而 rdlist 是双向链表,用来存放已经触发事件的文件描述符。

  • 2、登记 VIP 客户 :接着,使用 epoll_ctl 将文件描述符(如图中 fd 5、10、15、3、7、8 等)添加到 rbtree 中。这些文件描述符表示不同的客户端连接,被挂载到红黑树上以便快速查找和管理。

  • 3、等待事件触发:当某个文件描述符上有事件发生时(例如 fd 5、10、18 发生了事件),内核会通过回调机制将这些触发的文件描述符从红黑树移到 rdlist(双向链表)中。随后,调用 epoll_wait 时,系统将返回 rdlist 中的触发事件文件描述符。这样,我们只需处理这个链表中的“有数据到来”的客户端,而不用关注其他无事的客户。

通过这张图,我们可以更直观地理解 epoll 的三步走流程,以及 rbtree 和 rdlist 的作用,epoll 就是借助这两个数据结构高效管理并筛选出有事件的文件描述符,从而实现高效的 I/O 处理。

4、实战代码:简单上手 epoll

说了这么多,来个简易代码示例,手把手带你用 epoll 写个基本的 IO 复用实例。假设我们有一个网络服务器,需要能够处理多个 socket,以下代码展示了 epoll 的使用流程。

#include <sys/epoll.h>

int setup_server(int port)// 假设 setup_server() 函数已实现,返回监听 socket

int main() {
    int listen_fd = setup_server(8080);       // 设置服务器监听端口
    int epoll_fd = epoll_create();           // 创建 epoll 对象
    struct epoll_event evevents[10];
    ev.events = EPOLLIN;                      // 设置监听可读事件
    ev.data.fd = listen_fd;          // 文件描述符
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);  // 将监听 socket 注册到 epoll

    while (1) {
        int nfds = epoll_wait(epoll_fd, events, 10-1); // 等待事件触发
        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == listen_fd) {
                int conn_fd = accept(listen_fd, NULLNULL); // 接受新连接
                ev.events = EPOLLIN;
                ev.data.fd = conn_fd;
                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev); // 注册新连接
            } else if (events[i].events & EPOLLIN) {
                // 处理客户端数据
                char buffer[512];
                int conn_fd = events[i].data.fd;
                int count = read(conn_fd, buffer, sizeof(buffer));
                if (count > 0) {
                    write(conn_fd, buffer, count); // 简单回显收到的数据
                } else {
                    close(conn_fd); // 客户端关闭,移除连接
                }
            }
        }
    }

    close(epoll_fd);  // 关闭 epoll 对象
    close(listen_fd); // 关闭监听 socket
    return 0;
}

这段代码展示了如何创建 epoll 实例,注册文件描述符,并等待事件触发。实用又高效,对吧?只要理解了三步走,epoll 其实很简单!

5. epoll 的两种触发模式:像门铃一样的“ET”和“LT”

在了解了 epoll 的基本工作流程后,我们再来看一下它的两种触发模式:ET 和 LT

继续我们的 VIP 俱乐部例子。VIP 客户每次来访都会按门铃,这时工作人员就知道“哎,有客户来了”,然后去处理。但是门铃有两种模式:一种是“持续提醒”,另一种是“按一下就行”。这两种模式就分别对应了 epoll 的 LT 模式和 ET 模式。

5.1 LT(Level Triggered : 水平触发)—— 持续提醒模式

LT 模式可以理解为“门铃一直响”:VIP 客户每次按了门铃,门铃就一直响个不停,提醒工作人员“门口有人等着”,直到工作人员去接待。也就是说,LT 模式就是“有事就一直叫”。在 LT 模式下,如果有事件(比如客户端发来的数据)还没有处理完,epoll_wait 就会不停地提醒你,直到你把数据都读完了为止。

实际应用:网络数据处理

在处理网络数据时,LT 模式这种“持续提醒”特别适合需要确保所有数据都被读取的场景。比如客户端发送一大块数据给服务器,这些数据会先放在内核缓冲区中,等待服务器去读取。
LT 模式下,只要内核缓冲区中还有没读完的数据,epoll_wait 就会一遍又一遍地提醒你,确保你能把所有数据都拿到手,绝不会漏掉。

举个例子:假设有个 VIP 客户发送了 100 字节数据给你,但你只读了 50 字节。这时候 LT 模式会怎么做呢?
在 LT 模式下,epoll_wait 会不停提醒你:“还有数据呢,还没读完呢!” 直到你把剩下的 50 字节全都读完,它才会停下来。

代码示例:


struct epoll_event ev;
ev.events = EPOLLIN;   // 默认 LT 模式,持续提醒
ev.data.fd = sock_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &ev);

while (1) {
    int nfds = epoll_wait(epoll_fd, events, 10-1);
    for (int i = 0; i < nfds; ++i) {
        if (events[i].events & EPOLLIN) {
            int client_fd = events[i].data.fd;
            char buffer[512];
            ssize_t count = read(client_fd, buffer, sizeof(buffer));

            if (count == -1) {
                perror("read");
                close(client_fd);
            } else if (count == 0) {
                // 客户端关闭连接
                close(client_fd);
            } else {
                // 处理读取到的数据
                process_data(buffer, count);
            }
        }
    }
}

在这个代码中,LT 模式的 epoll_wait 会不断提醒我们有数据可以读。即使你只读了一部分,LT 也会继续提醒你,直到你读完所有数据。

小结:

LT 模式就像“持续提醒”模式的门铃,确保你不会漏掉事件。即使数据没处理完,它也会一直提醒你,确保你把所有数据读干净。

5.2 ET(Edge Triggered : 边缘触发)—— “敲一下就知道”模式

ET 模式则像是“敲一下就知道”的门铃:VIP 客户来访时按了一下门铃,门铃只响一次,提醒工作人员“有人来了”。也就是说,ET 模式就是“只敲一下”。如果工作人员没有立刻去处理,门铃就不会再响了,所以必须在听到门铃后马上接待客户,不然就可能错过他们的需求。

在 ET 模式下,如果有事件(比如客户端发来的数据)还没有处理完,epoll_wait 只会在事件触发的那一刻提醒一次,而不会持续提醒。因此,一旦收到通知,就必须一次性把数据全部处理完,避免遗漏。

实际应用:ET 模式处理网络数据

在网络数据处理中,ET 模式的“敲一下就知道”特别适合快速响应的场景。比如客户端发送数据给服务器,这些数据会先放到内核缓冲区中。ET 模式下,epoll_wait 只会在数据到达的一瞬间提醒你,但只提醒一次。

这意味着你需要在事件触发后,立刻把内核缓冲区的数据都读出来。如果没有一次性读完,ET 模式下就不会再提醒了,可能会错过剩下的数据。所以在 ET 模式下,一定要在触发时把数据读干净。

举个例子:

假设客户端发送了 100 字节数据,而你只读了 50 字节,ET 模式下 epoll_wait 不会再提醒你了,剩下的 50 字节就可能被错过。所以 ET 模式下通常要一次性将数据读完。

代码示例:非阻塞 + 循环读取

在 ET 模式下,通常需要设置 socket 为非阻塞,并用循环读取来确保所有数据被读完。

// 设置 socket 为非阻塞模式
int flags = fcntl(sock_fd, F_GETFL, 0);
fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK);

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;    // 设置 ET 模式
ev.data.fd = sock_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &ev);

while (1) {
    int nfds = epoll_wait(epoll_fd, events, 10-1);
    for (int i = 0; i < nfds; ++i) {
        if (events[i].events & EPOLLIN) {
            int client_fd = events[i].data.fd;
            while (1) {
                ssize_t count = read(client_fd, buffer, sizeof(buffer));
                if (count == -1) {
                    if (errno == EAGAIN) { // 数据读完了
                        break;
                    }
                    // 处理错误
                    close(client_fd);
                    break;
                } else if (count == 0) {
                    // 客户端关闭连接
                    close(client_fd);
                    break;
                }
                // 处理读取到的数据
                process_data(buffer, count);
            }
        }
    }
}

在这个代码中,ET 模式的 epoll_wait 只会提醒一次,所以我们必须在事件触发后立即读取所有数据,防止遗漏。

小结:

ET 模式就像“敲一下”的门铃:数据来时系统只提醒一次。如果没有一次性读完所有数据,系统不会再提醒。这种模式适合需要高效快速处理的场景,比如 Web 服务器,可以减少系统的反复提醒,提升性能。

5.3 怎么选?ET 还是 LT?

LT 模式更稳妥因为它会反复提醒;ET 模式更高效,但要求处理速度快。一般来说,如果我们想保证每个事件都不漏,就选 LT;而在高并发下,为了减少提醒开销,可以考虑 ET,但需要代码确保每次把数据读取或写入处理完整。

通过这个“门铃”例子,大家应该能比较直观地理解 epoll 的 LT 和 ET 模式了。选择哪种模式,看我们是需要“稳妥”,还是追求“效率”!

5.4 如何在代码中设置 ET 和 LT 模式?

我们已经了解了 LT 模式(一直提醒)和 ET 模式(只提醒一次)的区别,那么在代码里该如何选择呢?其实很简单,通过 epoll_event 结构体中的 events 字段,我们可以灵活设置。

代码示例:LT 模式(默认)

epoll 中,LT 模式是默认的,我们不需要特别指定。只要注册事件时不额外加 EPOLLET 标志,系统就会自动按照 LT 模式来处理。例如:

struct epoll_event ev;
ev.events = EPOLLIN;               // 默认是 LT 模式(一直提醒)
ev.data.fd = sock_fd;              // 设置文件描述符
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &ev);  // 注册事件

这样写,epoll 会在 sock_fd 有数据可读时,每次调用 epoll_wait 都提醒我们,直到我们把数据读完。

代码示例:ET 模式

如果我们要切换到 ET 模式(只提醒一次),就需要在 events 字段中加上 EPOLLET 标志。例如:

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;     // ET 模式,敲一次提醒一次
ev.data.fd = sock_fd;              // 设置文件描述符
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &ev);  // 注册事件

加上 EPOLLET 后,epoll 会只在 sock_fd 状态第一次变为可读时通知我们,如果我们没有及时处理完所有数据,epoll_wait 不会再次提醒。因此,使用 ET 模式时要确保在每次触发后尽可能一次性读完或写完。

这样,通过简单的设置就可以在 epoll 中使用 LT 或 ET 模式,灵活调整系统性能和稳定性。

5.5 epoll LT 和 ET 模式的适用场景:

LT 模式的适用场景

LT 模式适用于需要确保所有数据都被完整处理的情况,尤其是在数据量不确定、需要逐步读取的场景。因为 LT 模式会在有数据可读时不断提醒,即使一次只读取一部分,系统也会继续提醒你“这里还有数据没处理完”。

典型场景

  • 文件传输:当接收大文件或连续的数据流时,LT 模式非常合适。这种情况下可能无法一次性读完数据,LT 模式的“持续提醒”可以确保所有数据被处理。
  • 普通客户端连接:适合连接不多、流量不高的服务。LT 模式让开发者更放心,不用担心遗漏数据。

ET 模式的适用场景:

ET 模式适合高并发、高性能的场景,因为它减少了系统的通知次数,降低了 CPU 的开销。在 ET 模式下,系统只会在数据到达时提醒一次,所以适合那些需要快速响应、能在事件触发后一次性处理完数据的情况。

典型场景

  • 高并发服务器:例如 Web 服务器,通常采用非阻塞加 ET 模式,避免多次提醒,减少系统负担,提高性能。
  • 实时性要求高的场景:需要快速处理事件、不希望频繁提醒的情况。ET 模式效率更高,但要确保每次都能读完所有数据,以免数据遗漏。

总结

LT 模式:适合数据量不确定、需要确保数据不遗漏的场景,特点是“持续提醒”。ET 模式:适合高并发、需要高性能的场景,特点是“只提醒一次”。

6、epoll 的优缺点

到这里,相信大家已经对 epoll 的 ET 和 LT 模式有了了解。接下来,我们看看 epoll 的整体优缺点,看看它为什么如此受欢迎,又在哪些场景最能发挥它的威力。

epoll 的优点:高并发中的“资源节省大咖”

  1. 高效管理大量连接:epoll 使用了红黑树和双向链表的数据结构:红黑树保存所有连接,双向链表只保存触发事件的连接。这样,epoll 只关注“有事”的连接,其他静默的连接就不管了,即使同时管理上千个连接也能游刃有余。
  2. fd 数量无上限,轻松突破 1024:传统的 select 有 1024 个文件描述符的上限,而 epoll 没有这种限制——想监听多少连接都可以,只要服务器内存够用。这让它在高并发场景中有了更多可能性。
  3. 支持 ET 模式,减少系统调用次数:ET 模式(边缘触发)减少了系统不必要的重复提醒,只在事件刚发生时提醒一次,大大减少了 CPU 资源的开销,更加高效。
  4. 减少内核拷贝,提高效率:epoll 直接将触发事件的列表拷贝到用户空间,不像 select 那样每次都要拷贝整个连接列表,减少了系统调用的次数,也节省了内存和 CPU 资源。

这些优点,使 epoll 成为了高并发场景的理想之选。尤其是在网络服务器领域,它的高效机制让它成为 Linux 服务器程序的首选之一。

epoll 的缺点:跨平台的“绊脚石”

  1. Linux 特有,不跨平台:epoll 是 Linux 内核的特有功能,其他系统(如 macOS 和 Windows)不支持。如果需要跨平台,epoll 不是最佳选择。
  2. ET 模式要求高,代码复杂:虽然 ET 模式高效,但要求开发者将 socket 设置为非阻塞模式,并在读数据时使用循环。这对不熟悉高并发开发的程序员来说有些复杂,容易出现漏读问题,增加了调试难度。
  3. 长连接占用资源:epoll 适合高并发,但对于大量“长时间无数据”的长连接,仍然会占用系统内存。如果有成千上万的静默连接,epoll 监控的负担也会增加,对服务器带来压力。

7、epoll 的适用场景

综上,epoll 适合高并发、大流量的场景,特别是 Web 服务器、聊天室、游戏服务器等需要高效处理大量连接的应用。

总结:

epoll 就像个聪明又高效的 I/O 管家,帮我们解决了 select 和 poll 在高并发下的性能难题。通过创建 epoll 对象、登记监听事件、等待并集中处理事件三步走的流程 ,epoll 能够快速锁定有需求的连接,而不是浪费资源在安静的连接上。

它的“持续提醒”模式(LT)和“敲一下就知道”模式(ET)更是灵活实用,在不同场景下让我们既省心又省力。从底层的红黑树到双向链表,epoll 的数据结构设计为大规模连接提供了高效的管理方式。在处理高并发的 Web 服务器、实时通讯、负载均衡等场景中,epoll 无疑是“效率担当”。

不过,epoll 也有跨平台兼容性差的限制,但在 Linux 环境下,它仍是高并发系统中的绝佳利器。

看完这篇文章,对 epoll 是不是更了解了?想要真正玩转 epoll,还是要多写代码、亲自上手体验! 

在后台回复「epoll」即可获取完整代码示例。

end



一口Linux 


关注,回复【1024】海量Linux资料赠送


精彩文章合集

文章推荐

【专辑】ARM
【专辑】粉丝问答
【专辑】所有原创
专辑linux入门
专辑计算机网络
专辑Linux驱动
【干货】嵌入式驱动工程师学习路线
【干货】Linux嵌入式所有知识点-思维导图


一口Linux
《从零开始学ARM》作者,一起学习嵌入式,Linux,网络,驱动,arm知识。
 最新文章