Linux内核中如何实现I/O通道?

Kaw*_*iKx 8 linux io streams

stdin、stdout、stderr 是一些整数,它们索引到一个数据结构中,该数据结构“知道”哪些 I/O 通道将用于该过程。我知道这个数据结构对于每个流程都是独一无二的。I/O 通道只是一些具有动态内存分配的数据数组结构吗?

Tho*_*man 15

在类 Unix 操作系统中,标准输入、输出和错误流由文件描述符01、标识2。在 Linux 上,这些proc/proc/[pid]/fs/{0,1,2}. 这些文件实际上是指向目录下终端设备的符号链接/dev/pts

一个伪终端 (PTY) 是一对虚拟设备,一个伪终端主机(PTM) 和一个伪终端从机(PTS)(统称为伪终端对),它们提供一个 IPC 通道,有点像程序之间的双向管道,它期望连接到终端设备,以及使用伪终端向前一个程序发送输入和接收输入的驱动程序。

一个关键点是伪终端从机看起来就像一个常规终端,例如它可以在非规范和规范模式(默认)之间切换,在这种模式下它解释某些输入字符,例如SIGINT中断字符(通常生成通过按键盘上的Ctrl+ C)被写入伪终端主机或在遇到文件结束字符(通常由+生成)时导致下一个read()返回。终端支持的其他操作有打开或关闭回显、设置前台进程组等。0CtrlD

伪终端有多种用途:

  • 它们允许程序ssh在通过网络连接的另一台主机上运行面向终端的程序。面向终端的程序可以是通常在交互式终端会话中运行的任何程序。此类程序的标准输入、输出和错误不能直接连接到套接字,因为套接字不支持上述与终端相关的功能。

  • 它们允许expect程序从脚本驱动交互式终端导向程序。

  • 它们由终端模拟器使用,例如xterm提供与终端相关的功能。

  • 它们被程序使用,例如screen在多个进程之间多路复用单个物理终端。

  • 它们被程序script用来记录在 shell 会话期间发生的所有输入和输出。

Linux 中使用的Unix98-style PTYs设置如下:

  • 驱动程序在 处打开伪终端主多路复用器dev/ptmx,在该处接收 PTM 的文件描述符,并在/dev/pts目录中创建 PTS 设备。通过打开获得的每个文件描述符/dev/ptmx都是一个独立的 PTM,具有自己关联的 PTS。

  • 驱动程序调用fork()创建子进程,子进程依次执行以下步骤:

    • 孩子打电话setsid()开始一个新的会话,孩子是会话的领导者。这也导致孩子失去其控制终端

    • 孩子继续打开与驱动程序创建的 PTM 对应的 PTS 设备。由于孩子是会话领导者,但没有控制终端,PTS 成为孩子的控制终端。

    • dup()进程用于在其标准输入、输出和错误上复制从属设备的文件描述符。

    • 最后,子进程调用exec()以启动要连接到伪终端设备的面向终端的程序。

此时,驱动程序写入 PTM 的任何内容都显示为 PTS 上面向终端的程序的输入,反之亦然。

在规范模式下运行时,PTS 的输入逐行缓冲。换句话说,就像使用常规终端一样,从 PTS 读取的程序只有在将换行符写入 PTM 时才会接收一行输入。当缓冲容量耗尽时,进一步write()调用阻塞直到一些输入被消耗。

在Linux内核,文件相关的系统调用open()read()write() stat()等在虚拟文件系统(VFS)层,它提供一个统一的文件系统接口为用户空间程序被实现。VFS 允许不同的文件系统实现在内核中共存。当用户空间程序调用上述系统调用时,VFS 会将调用重定向到适当的文件系统实现。

下面的 PTS 设备由/dev/ptsdevpts定义的文件系统实现管理/fs/devpts/inode.c,而提供 Unix98 风格ptmx设备的 TTY 驱动程序在 中定义drivers/tty/pty.c

TTY 设备和 TTY线路规程(例如伪终端)之间的缓冲为每个 tty 设备提供了一个缓冲区结构,定义在include/linux/tty.h

在内核版本 3.7 之前,缓冲区是一个翻转缓冲区

#define TTY_FLIPBUF_SIZE 512

struct tty_flip_buffer {
        struct tq_struct tqueue;
        struct semaphore pty_sem;
        char             *char_buf_ptr;
        unsigned char    *flag_buf_ptr;
        int              count;
        int              buf_num;
        unsigned char    char_buf[2*TTY_FLIPBUF_SIZE];
        char             flag_buf[2*TTY_FLIPBUF_SIZE];
        unsigned char    slop[4];
};
Run Code Online (Sandbox Code Playgroud)

该结构包含分成两个相等大小的缓冲区的存储。缓冲区被编号0(前半部分char_buf/flag_buf)和1(后半部分)。驱动程序将数据存储到由 标识的缓冲区buf_num。另一个缓冲区可以刷新到线路规则。

通过buf_num0和之间切换来“翻转”缓冲区1。当buf_num改变,char_buf_ptrflag_buf_ptr 设置为查明的缓冲区的开始buf_num,并count设置为0

从内核版本 3.7 开始,TTY 翻转缓冲区已被替换为通过kmalloc()组织在环中分配的对象。在 IRQ 驱动的串行端口以典型速度的正常情况下,它们的行为与旧的翻转缓冲区几乎相同;最终分配了两个缓冲区,内核像以前一样在它们之间循环。但是,当出现延迟或速度增加时,新的缓冲区实现会表现得更好,因为缓冲池可以稍微增长。