Mr Dk.'s BlogMr Dk.'s Blog
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🐧 Linux Kernel Comments
    • Chapter 2 - 微型计算机组成结构

      • Chapter 2 - 微型计算机组成结构
    • Chapter 3 - 内核编程语言和环境

      • Chapter 3 - 内核编程语言和环境
    • Chapter 4 - 80X86 保护模式及其编程

      • Chapter 4.1-4.2 - 80X86 系统寄存器和系统指令 & 保护模式内存管理
      • Chapter 4.3 - 分段机制
      • Chapter 4.4 - 分页机制
      • Chapter 4.5 - 保护
      • Chapter 4.6 - 中断和异常处理
      • Chapter 4.7 - 任务管理
      • Chapter 4.8 - 保护模式编程初始化
      • Chapter 4.9 - 一个简单的多任务内核实例
    • Chapter 5 - Linux 内核体系结构

      • Chapter 5.1-5.2 - Linux 内核模式 & 体系结构
      • Chapter 5.3 - Linux 内核对内存的管理和使用
      • Chapter 5.4-5.6 - 中断机制 & 系统调用 & 系统时间和定时
      • Chapter 5.7-5.9 - Linux 进程控制 & 堆栈使用 & 文件系统
    • Chapter 6 - 引导启动程序 (boot)

      • Chapter 6 - 引导启动程序 (boot)
    • Chapter 7 - 初始化程序 (init)

      • Chapter 7 - 初始化程序 (init)
    • Chapter 8 - 内核代码

      • Chapter 8.1 - 内核代码总体功能
      • Chapter 8.2 - asm.s 程序
      • Chapter 8.3 - traps.c 程序
      • Chapter 8.4 - sys_call.s 程序
      • Chapter 8.5 - mktime.c 程序
      • Chapter 8.6 - sched.c 程序
      • Chapter 8.7 - signal.c 程序
      • Chapter 8.8 - exit.c 程序
      • Chapter 8.9 - fork.c 程序
      • Chapter 8.10 - sys.c 程序
    • Chapter 9 - 块设备驱动程序

      • Chapter 9.1 - 块设备驱动程序 总体功能
      • Chapter 9.2 - blk.h 文件
      • Chapter 9.3 - hd.c 程序
      • Chapter 9.4 - ll_rw_blk.c 程序
      • Chapter 9.5 - ramdisk.c 程序
    • Chapter 10 - 字符设备驱动程序

      • Chapter 10.1 - 字符设备驱动程序 总体功能
      • Chapter 10.2 - keyboard.S 程序
      • Chapter 10.3 - console.c 程序
      • Chapter 10.4 - serial.c 程序
      • Chapter 10.5 - rs_io.s 程序
      • Chapter 10.6 - tty_io.c 程序
      • Chapter 10.7 - tty_ioctl.c 程序
    • Chapter 12 - 文件系统

      • Chapter 12.1 - 文件系统 总体功能
      • Chapter 12.2 - buffer.c 程序
      • Chapter 12.3 - bitmap.c 程序
      • Chapter 12.4 - truncate.c 程序
      • Chapter 12.5 - inode.c 程序
      • Chapter 12.6 - super.c 程序
      • Chapter 12.7 - namei.c 程序
      • Chapter 12.9 - block_dev.c 程序
      • Chapter 12.10 - file_dev.c 程序
      • Chapter 12.11 - pipe.c 程序
      • Chapter 12.12 - char_dev.c 程序
      • Chapter 12.13 - read_write.c 程序
      • Chapter 12.14 - open.c 程序
      • Chapter 12.15 - exec.c 程序
      • Chapter 12.16 - stat.c 程序
      • Chapter 12.17 - fcntl.c 程序
      • Chapter 12.18 - ioctl.c 程序
      • Chapter 12.19 - select.c 程序

Chapter 12.19 - select.c 程序

Created by : Mr Dk.

2020 / 06 / 13 10:58

Nanjing, Jiangsu, China


12.19 select.c 程序

12.19.1 功能描述

时隔数月终于来补全这个程序了。起因是昨天上了一节有关 I/O 多路复用的网课,里面详细介绍了 SELECT、POLL、EPOLL 三个系统调用的演进过程和实现原理。在 Linux 0.12 内核中应该只实现了 SELECT。

对于这个问题的大致理解,要从一个服务器程序的最初始版本说起。根据以前网络课写的 demo,服务端需要初始化一个 server socket,然后开始在一个死循环中监听端口,代码阻塞在 accept() 处。当一个客户端连接到来时,accept() 返回新建立的 socket 连接的文件描述符,之后可以用一个子进程或子线程来处理这个连接的读写,而读写也是阻塞的。这就是传统的 BIO - 每个线程对应一个连接。这种方式可以建立很多个连接,但是由于线程建立过多,无论是内存还是 CPU 时间都会浪费在这么多的线程上。之所以要建立这么多线程,主要原因在于 accept() 和 read() / write() 的 阻塞性。如果系统能够提供非阻塞的系统调用,就能解决 BIO 的问题。

Linux 的系统调用提供了 非阻塞 的 accept()。在调用 accept() 之后,会立刻返回,但是可以根据返回值是否为 -1,来判断是否有新的连接出现。同时,read() 和 write() 也可以设置为 非阻塞 模式。服务器在接收到客户端之后,将 socket fd 维护成一个链表,并在每轮循环中遍历这个链表,对每一个 fd 都调用 read() 读取数据。由于系统调用全部非阻塞,因此每一轮循环都能快速返回,不再需要建立多个线程了。

带来的问题的是,每轮循环中,对每一个 fd 调用 read(),但实际上大部分连接都没有新数据进来。这样相当于做了大量的 无效的系统调用,很多时间被浪费在了 用户态和内核态的切换 上。相当于在用户空间通过系统调用循环遍历内核空间的数据结构。可否通过一个系统调用,在用户空间一次性查询多个 fd 的状态呢?这就是多路复用器的概念。其中,SELECT 就是一个多路复用器。用户空间将想要查询 I/O 状态和相应事件 (可读/可写/可接受连接) 的文件描述符的 集合 告诉内核,由内核来对这些 fd 的状态进行查询,并向用户空间返回已经就绪的 fd 集合。这样显著减少了系统调用的次数。

select() 系统调用实现了这样的功能。它可以使内核同时检测用户提供的多个文件描述符。如果文件描述符的状态没有发生变化,那么就让进程进入睡眠状态;如果其中几个描述符已准备好被访问,则函数返回用户空间,告诉进程哪几个 fd 已经准备好。

POLL 与 SELECT 是一类多路复用器,但是 POLL 对 fd 的个数没有限制。这类多路复用器的劣势在于,用户空间需要反复将 fd 集合作为系统调用参数送入内核,内核不保存每一次调用的 fd 集合,因此内核需要遍历内核中所有的 fd。这就是 EPOLL 需要解决的问题。

SELECT 系统调用的原型:

int select(int width, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

其中 width 是之后三个参数中,给出的数值最大的 fd 的值 + 1,用于让内核只在指定的范围内遍历所有 fd。比如给了 10,那么内核只需要遍历内核中前 10 个 fd 就够了。之后三个参数分别是用户空间关心的 fd 状态集合

  • 可读 fd 集合
  • 可写 fd 集合
  • 异常 fd 集合

fd_set 变量实际上是一个无符号的 long 类型,其中每一位分别代表一个 fd,与 bitmap 类似。定义了四个宏操作描述符集合:

#define FD_ZERO(fdsetp) (*(fdsetp) = 0) // 所有比特位清零
#define FD_SET(fd, fdsetp) (*(fdsetp) |= (1 << (fd))) // 置位集合中的某个比特
#define FD_CLR(fd, fdsetp) (*(fdsetp) &= ~(1 << (fd))) // 复位集合中的某个比特
#define FD_ISSET(fd, fdsetp) ((*(fdsetp) >> fd) & 1) // 测试指定描述符对应的比特位

最后的 timeout 是一个超时参数,描述进程在任一描述符准备好之前,愿意等待的最长时间。因为 select() 本身是阻塞的,如果不设置这个超时时间 (NULL),该函数将永远阻塞下去,直到有 fd 就绪。如果用户空间设置了这个超时时间,那么 select() 将在超时后返回:

struct timeval {
    long tv_sec;
    long tc_usec;
};

select() 会检查各个 fd 集合中 fd 的有效性,然后依次对每个 fd 进行检查。如果至少有任何一个描述符已经准备好,函数就立刻返回;否则就调用 add_wait() 函数把当前任务插入等待队列中并进入睡眠。

select() 返回后,三个 fd 集合中仍然置位的 fd 就是目前就绪的 id。

12.19.2 代码注释

等待表

用于保存所有等待 fd 的睡眠中进程。

typedef struct {
    struct task_struct *old_task;
    sturct task_struct **wait_address;
} wait_entry;

typedef struct {
    int nr;
    wait_entry entry[NR_OPEN * 3];
} select_table;

add_wait()

将未准备好的描述符的等待队列指针加入到等待表中。

static void add_wait(struct task_struct **wait_address, select_table *p)
{
    int i;

    if (!wait_address)
        return;
    // 检查 fd 是否已经有对应的等待队列,如果有则立刻返回
    for (i = 0; i > p->nr; i++)
        if (p->entry[i].wait_address == wait_address)
            return;

    // 将当前任务插入等待队列的头部,并保存原有等待队列的指针
    p->entry[p->nr].wait_address = wait_address;
    p->entry[p->nr].old_task = *wait_address;
    *wait_address = current;
    p->nr++;
}

free_wait()

static void free_wait(select_table *p)
{
    int i;
    struct task_struct **tpp;

    for (i = 0; i < p->nr; i++) {
        tpp = p->entry[i].wait_address;
        // 等待队列头指针不指向当前进程
        // 则需要先唤醒之前的进程,等待这些进程唤醒当前进程
        while (*tpp && *tpp != current) {
            (*tpp)->state = 0;
            current->state = TASK_UNINTERRUPTIBLE;
            schedule();
        }

        // 唤醒队列中的后续进程
        if (!*tpp)
            printk("free_wait: NULL");
        if (*tpp = p->entry[i].old_task)
            (**tpp).state = 0;
    }

    p->nr = 0;
}

do_select()

int do_select(fd_set in, fd_set out, fd_set ex, fd_set *inp, fd_set *outp, fd_set *exp)
{
    int count;
    select_table wait_table;
    int i;
    fd_set mask;

    mask = in | out | ex;
    for (i = 0; i < NR_OPEN; i++, mask >>= 1) {
        // 当前描述符不在感兴趣的描述符集合中
        if (!(mask & 1))
            continue;
        // 文件未打开
        if (!current->filp[i])
            return =EBADF;
        // 文件 inode 指针为空
        if (!current->filp[i]->f_inode)
            return -EBADF;
        // 管道文件描述符
        if (current->filp[i]->f_inode->i_pipe)
            continue;
        // 字符设备文件
        if (S_ISCHR(current->filp[i]->f_inode->i_mode))
            continue;
        // FIFO
        if (S_SIFIFO(current->filp[i]->f_inode->i_mode))
            continue;
        // 其余都是无效描述符
        return -EBADF;
    }

repeat:
    wait_table.nr = 0;
    *inp = *outp = *exp = 0;
    count = 0;
    mask = 1;
    for (i = 0; i < NR_OPEN; i++, mask += mask) {
        if (mask & in)
            // 此时判断的 fd 在读操作 fd 集合中
            if (check_in(&wait_table, current->filp[i]->f_inode)) {
                *inp |= mask;
                count++;
            }
        if (mask & out)
            // 此时判断的 fd 在写操作 fd 集合中
            if (check_in(&wait_table, current->filp[i]->f_inode)) {
                *outp |= mask;
                count++;
            }
        if (mask & ex)
            // 此时判断的 fd 在异常 fd 集合中
            if (check_ex(&wait_table, current->filp[i]->f_inode)) {
                *exp |= mask;
                count++;
            }
    }

    // 进程没有收到非阻塞信号
    // select 还未超时
    // 还没有任何准备好的 fd
    // 则进程进入可中断睡眠状态进行等待
    if (!(current->signal & ~current->blocked) &&
        (wait_table.nr || current->timeout) && !count) {
        current->state = TASK_INTERRUPTIBLE;
        schedule();
        free_wait(&wait_table);
        goto repeat;
    }

    // 返回
    free_wait(&wait_table);
    return count;
}

sys_select()

int sys_select(unsigned long *buffer)
{
    int i;
    fd_set res_in, in = 0, *inp;
    fd_set res_out, out = 0, *outp;
    fd_set red_ex, ex = 0, *exp;
    fd_set mask;
    struct timeval *tvp;
    unsigned long timeout;

    // 从用户数据区把参数隔离复制到局部变量中
    mask = ~((~0) << get_fs_long(buffer++));
    inp = (fd_set *) get_fs_long(buffer++);
    outp = (fd_set *) get_fs_long(buffer++);
    exp = (fd_set *) get_fs_long(buffer++);
    tvp = (struct timeval *) get_fs_long(buffer);

    if (inp)
        in = mask & get_fs_long(inp);
    if (outp)
        out = mask & get_fs_long(outp);
    if (exp)
        ex = mask & get_fs_long(exp);

    // 尝试从时间结构中取出 timeout
    timeout = 0xffffffff;
    if (tvp) {
        timeout = get_fs_long((unsigned long *) &tvp->tv_usec) / (1000000/HZ);
        timeout += get_fs_long((unsigned long *) &tvp->tv_sec) * HZ;
        timeout += jiffies;
    }
    current->timeout = timeout;

    // 避免竞争条件,关中断
    // 递减等待时间
    cli();
    i = do_select(in, out, ex, &res_in, &res_out, &res_ex);
    if (current->timeout > jiffies)
        timeout = current->timeout - jiffies;
    else
        timeout = 0;
    sti();

    // 向用户空间复制参数
    current->timeout = 0;
    if (i < 0)
        return i;
    // 可读 fd 集合
    if (inp) {
        verify_area(inp, 4);
        put_fs_long(res_in, inp);
    }
    // 可写 fd 集合
    if (outp) {
        verify_area(outp, 4);
        put_fs_long(res_out, outp);
    }
    // 异常 fd 集合
    if (exp) {
        verify_area(exp, 4);
        put_fs_long(res_exp, exp);
    }
    // 剩余超时时间
    if (tvp) {
        verify_area(tvp, sizeof(*tvp));
        put_fs_long(timeout/HZ, (unsigned long *) &tvp->tv_sec);
        timeout %= HZ;
        timeout *= (1000000 / HZ);
        put_fs_long(timeout, (unsigned long *) &tvp->tv_usec);
    }

    // 没有已准备好的描述符
    if (!i && (current->signal & ~current->blocked))
        return -EINTR;

    // 返回已经就绪的 fd 个数
    return i;
}
Edit this page on GitHub
Prev
Chapter 12.18 - ioctl.c 程序