操作系统 4.1-I/O与显示器

发布于:2025-04-13 ⋅ 阅读:(11) ⋅ 点赞:(0)

外设工作起来

操作系统让外设工作的基本原理和过程,具体来说,它概括了以下几个关键步骤:

  1. 发出指令:操作系统通过向控制器中的寄存器发送指令来启动外设的工作。这些指令通常是通过I/O指令(如out指令)来实现的。

  2. 控制器处理:控制器接收到指令后,根据寄存器中的内容来操控硬件。控制器内部可能包含有计算电路,能够根据CPU发出的指令来具体操作设备。

  3. 中断处理:一旦外设完成其任务,它会向CPU发送一个中断信号。CPU接收到中断后,会暂停当前的工作,转而处理中断,这可能涉及到数据传输等操作。

  4. 统一的文件接口:为了让外设的使用变得简单,操作系统提供了一种统一的视图,即文件视图。这意味着,无论操作哪种外设,用户都可以通过统一的文件操作接口(如openreadwrite等)来进行。

总结来说,操作系统让外设工作的核心原理非常简单,即通过发出指令让外设工作,然后编写中断处理程序来响应外设完成任务后的中断信号。此外,操作系统通过提供统一的文件接口,使得用户可以方便地使用各种外设,而无需关心底层的硬件细节。接下来我们将围绕这三个方面讲解。

外设工作的开始

提取的代码如下:

int fd = open("/dev/xxx");
for (int i = 0; i < 10; i++) {
    write(fd, i, sizeof(int));
}
close(fd);

外设工作的开始可以总结为以下几个步骤:

  1. 打开设备

    • 使用open函数打开指定的设备文件(/dev/xxx),这个文件是系统中外设的接口。

    • open函数返回一个文件描述符fd,用于后续对该设备的操作。

  2. 数据传输

    • 通过write函数将数据传输到外设。在这个例子中,数据是一个整数i,大小为sizeof(int)

    • 这个过程在一个循环中进行,循环10次,每次写入一个整数。

  3. 关闭设备

    • 完成数据传输后,使用close函数关闭设备文件,释放文件描述符fd所占用的资源。

文件视图概念

文件视图是操作系统提供的两大视图之一,它将所有的I/O设备统一抽象为文件,使得用户可以通过一组标准的文件操作接口(如openreadwriteclose)来访问和操作这些设备。这种抽象极大地简化了用户与硬件设备的交互,并隐藏了底层硬件的具体细节。

在文件视图中,操作系统将设备属性数据和设备驱动程序结合在一起,通过系统调用接口与用户空间进行交互。当用户程序调用这些系统调用时,操作系统会进行解释,并将其转换为对特定设备的命令。这些命令随后被发送到相应的设备控制器(如键盘控制器或磁盘控制器),并由控制器执行具体的硬件操作。

文件视图的样貌可以总结如下:

  1. 统一接口:无论什么设备,用户都通过统一的系统调用接口(openreadwriteclose)来进行操作。

  2. 设备抽象:不同的设备对应不同的设备文件(如/dev/xxx),操作系统根据这些设备文件找到控制器的地址、内容格式等信息。

  3. 设备驱动:设备驱动程序是操作系统与硬件设备之间的桥梁,它负责将系统调用转换为对特定硬件的操作。

  4. 中断处理:当设备完成操作后,会通过中断机制通知操作系统,操作系统再进行相应的中断处理。

  5. I/O系统:操作系统中的I/O系统负责管理设备属性数据和设备驱动程序,协调用户程序与硬件设备之间的交互。

通过这种文件视图,操作系统为用户提供了一个简单、统一的方式来操纵外设,同时隐藏了底层硬件操作的复杂性。这种抽象不仅简化了用户程序的开发,还提高了系统的可移植性和可扩展性。

代码思路讲解

提取的代码如下:

int sys_write(unsigned int fd, char *buf, int count) {
    struct file* file;
    file = current->filp[fd];  // fd是找到file的索引
    inode = file->f_inode;     // file的目的是得到inode
}

总结显示器输出的过程:

  1. 系统调用

    • 用户程序通过printf函数输出信息,printf函数内部会先创建一个缓存区(buf),将格式化后的输出写入该缓存区。

  2. 写入系统调用

    • printf函数最终会调用write系统调用,将缓存区中的数据写入指定的文件描述符(fd)。

  3. 文件描述符索引

    • 在Linux内核中,sys_write函数通过文件描述符(fd)找到对应的文件结构体(file)。文件描述符是用户空间和内核空间之间的索引。

  4. 获取inode

    • 从文件结构体中获取inode结构体,inode包含了文件的元数据和设备信息。对于设备文件(如显示器),inode中包含了设备驱动的相关信息。

wirte->filp

提取的代码如下:

int copy_process(...){
    *p = *current;
    for (i = 0; i < NR_OPEN; i++)
        if ((f = p->filp[i])) f->f_count++;
}
​
void main(void) {
    if (!fork()) { init(); }
}
​
void init(void) {
    open("/dev/tty0", O_RDWR, 0);
    dup(0);
    dup(0);
    execve("/bin/sh", argv, envp);
}

fd=1filp从哪里来?

在UNIX和Linux系统中,当一个进程创建一个新的子进程时(通常是通过fork系统调用),子进程会继承父进程的文件描述符。这意味着子进程会拥有与父进程相同的文件描述符集合,包括指向相同文件结构(struct file)的指针。

在提供的代码中,copy_process函数负责复制父进程的文件描述符信息到子进程。这是通过遍历父进程的filp数组(每个元素都是指向struct file的指针)并递增相应文件结构的引用计数来实现的。这样做确保了文件在两个进程间正确共享。

main函数中,通过调用fork创建了一个新的子进程。如果fork返回0(表示这是子进程),则调用init函数。在init函数中,首先打开/dev/tty0(通常是控制台设备),然后使用dup(0)将标准输入、输出和错误都重定向到这个控制台设备。最后,通过execve调用替换当前进程映像为/bin/sh(shell),从而启动一个新的shell进程。

因此,fd=1(通常用于标准输出)的filp指针是从父进程继承来的,并且在子进程中通过dup(0)调用被重定向到/dev/tty0设备。这样,当shell进程写入标准输出时,数据会被发送到控制台设备。

filp->open

提取的代码如下:

int sys_open(const char* filename, int flag) {
    i = open_namei(filename, flag, &inode);
    current->filp[fd] = f; // 第一个空闲的fd
    f->f_mode = inode->i_mode;
    f->f_inode = inode;
    f->f_count = 1;
    return fd;
}

open系统调用完成了什么?

open系统调用主要完成了以下步骤:

  1. 解析目录,找到inode:系统需要解析传入的文件名,找到对应的inode结构,inode包含了文件的元数据和设备信息。

  2. 分配文件描述符(fd):在进程的文件描述符数组(filp)中找到一个空闲的文件描述符,并将其分配给这个文件。

  3. 建立文件结构体(file):创建一个文件结构体(file),该结构体包含了文件的状态信息,如文件模式(f_mode)和指向inode的指针(f_inode)。

开始输出

提取的代码如下:

// sys_write function in linux/fs/read_write.c
int sys_write(unsigned int fd, char *buf, int cnt) {
    inode = file->f_inode;
    if (S_ISCHR(inode->i_mode))
        return rw_char(WRITE, inode->i_zone[0], buf, cnt);
    ...
}
​
// rw_char function in linux/fs/char_dev.c
int rw_char(int rw, int dev, char *buf, int cnt) {
    crw_ptr call_addr = crw_table[MAJOR(dev)];
    call_addr(rw, dev, buf, cnt);
    ...
}
  1. 系统调用:用户程序通过 write 系统调用向内核请求写操作,传递文件描述符(fd)、缓冲区地址(buf)和要写入的字节数(cnt)。

  2. 字符设备检查sys_write 函数中,首先获取文件结构体的inode,并检查该inode表示的是否为字符设备(通过 S_ISCHR(inode->i_mode) 判断)。

  3. 调用设备驱动:如果是字符设备,调用 rw_char 函数,传入写操作标志(WRITE)、inode中的设备信息(i_zone[0])、缓冲区地址和字节数。

  4. 设备驱动操作:在 rw_char 函数中,根据设备的主要号码(MAJOR(dev))从字符设备驱动表(crw_table)中获取对应的操作函数指针,并调用该函数执行实际的写操作。

  5. 输出到屏幕:对于显示器这样的字符设备,rw_char 函数最终会调用显示器的驱动函数,将缓冲区中的数据写入显示器的显存,实现向屏幕的输出。

这个过程展示了从用户空间的 printf 调用开始,经过系统调用接口,到内核空间的文件操作,再到设备驱动程序,最终实现数据向硬件设备的输出。这是操作系统中I/O系统工作的一个典型流程。

rw_char->crw_table

提取的代码如下:

// 定义字符设备操作函数指针数组
static crw_ptr crw_table[] = {..., rw_ttyx, ...};
​
// 函数指针类型定义
typedef (*crw_ptr)(int rw, unsigned minor, char *buf, int count);
​
// 字符设备读写函数
static int rw_ttyx(int rw, unsigned minor, char *buf, int count) {
    return ((rw == READ) ? tty_read(minor, buf) : tty_write(minor, buf));
}
​
// 真正的写函数
int tty_write(unsigned channel, char *buf, int nr) {
    struct tty_struct *tty;
    tty = channel + tty_table;
    sleep_if_full(&tty->write_q);
    ...
}

总结代码所做的事情及用途:

  1. 定义字符设备操作函数指针数组(crw_table

    • crw_table 是一个数组,包含了指向不同字符设备操作函数的指针。这些函数负责对字符设备进行读写操作。

  2. 函数指针类型定义(crw_ptr

    • crw_ptr 是一个函数指针类型,用于指向符合特定签名的函数,即接受读写标志、次要设备号、缓冲区指针和计数作为参数的函数。

  3. 字符设备读写函数(rw_ttyx

    • rw_ttyx 函数根据传入的读写标志(rw),决定调用 tty_read 还是 tty_write 函数。这个函数作为字符设备的通用入口点,根据操作类型分发到具体的读写处理函数。

  4. 真正的写函数(tty_write

    • tty_write 是实现字符设备(如终端)写操作的核心函数。它负责将数据从内核缓冲区写入到设备。

    • 函数首先通过 channeltty_table 获取到 tty_struct 结构体,该结构体包含了终端设备的相关信息和状态。

    • 然后检查输出队列(write_q)是否已满,如果已满,则调用 sleep_if_full 函数使进程休眠,等待队列有空间。

    • 一旦队列有空间,数据就被写入队列,后续操作(可能是中断处理程序)会负责将队列中的数据实际输出到设备。

crw_table->tty_write

提取的代码如下:

// 在 linux/kernel/tty_io.c 中的 tty_write 函数
int tty_write(unsigned channel, char *buf, int nr) {
    char c, *b = buf;
    while (nr > 0 && !FULL(tty->write_q)) {
        c = get_fs_byte(b); // 从用户缓存区读
        if (c == '\r') { PUTCH(13, tty->write_q); continue; }
        if (O_LCUC(tty)) c = toupper(c);
        b++; nr--;
        PUTCH(c, tty->write_q);
    } // 输出完事或写队列满
    tty->write(tty);
}

总结代码所做的事情及用途:

  1. 初始化

    • 定义字符变量 c 和字符指针 b 指向缓冲区 buf 的起始位置。

  2. 循环处理每个字符

    • 使用 while 循环,当还有字符要写入(nr > 0)且写队列未满(!FULL(tty->write_q))时,继续处理。

    • 从用户空间的缓冲区中读取一个字符到 c

  3. 处理回车字符

    • 如果字符是回车符('\r'),将其转换为换行符('\n')并继续下一个循环。

  4. 字符大小写转换

    • 如果终端设置为转换为大写(O_LCUC(tty)),将字符 c 转换为大写。

  5. 写入队列

    • 将处理后的字符放入终端的写队列 tty->write_q 中。

    • 更新缓冲区指针 b 和字符计数 nr

  6. 触发实际写操作

    • 一旦所有字符都已处理或写队列满,调用 tty->write(tty) 触发实际的写操作,将队列中的数据输出到屏幕上。

  • 提取的代码如下:

  • // 在 include/linux/tty.h 中定义的 tty_struct 结构体
    struct tty_struct {
        void (*write)(struct tty_struct *tty);
        struct tty_queue read_q, write_q;
    };
    ​
    // tty_struct 结构体数组的初始化
    struct tty_struct tty_table[] = {
        {con_write, {0,0,0,0,""}, {0,0,0,0,""}},
        {}, ...
    };
    ​
    // con_write 函数在 linux/kernel/chr_drv/console.c 中的定义
    void con_write(struct tty_struct *tty) {
        GETCH(tty->write_q, c);
        if (c > 31 && c < 127) {
            __asm__ (
                "movb _attr, %%ah\n\t"
                "movw %%ax, %1\n\t::" "a"(c),
                "m"(*(short*)pos):"ax");
            pos += 2;
        }
    }
    • con_write 函数是 Linux 内核中负责将字符输出到控制台显示器的关键函数。它通过直接操作显存来实现字符的显示,这是 Linux 内核中实现控制台输出的底层机制。

    • 通过这种方式,内核可以将用户程序的输出(如通过 printf 函数)转换为屏幕上的可见字符,实现用户与系统的交互。

  • 总结代码所做的事情及用途:

    con_write 函数定义
    1. 如果字符 c 在可打印范围内(ASCII码 32 到 126),则通过内联汇编代码将其写入显存(视频内存)的特定位置。

    2. 函数从 tty->write_q 队列中获取一个字符 c

    3. con_write 函数是 tty_struct 结构体中的 write 函数指针所指向的实际函数,负责将字符写入显示器。

tty_write->mov pos

  • 这两张图片提供了关于如何在Linux内核中实现向屏幕输出字符的详细信息。以下是提取的代码和总结:

    提取的代码:

  • // 在 include/linux/tty.h 中定义的 tty_struct 结构体
    struct tty_struct {
        void (*write)(struct tty_struct *tty);
        struct tty_queue read_q, write_q;
    };
    ​
    // tty_struct 结构体数组的初始化
    struct tty_struct tty_table[] = {
        {con_write, {0,0,0,0,""}, {0,0,0,0,""}}, {}, ...
    };
    ​
    // con_write 函数在 linux/kernel/chr_drv/console.c 中的定义
    void con_write(struct tty_struct *tty) {
        GETCH(tty->write_q, c);
        if (c > 31 && c < 127) {
            __asm__ (
                "movb _attr, %%ah\n\t"
                "movw %%ax, %1\n\t"::"a"(c),
                "m"(*(short*)pos):"ax");
            pos += 2;
        }
    }

    总结代码的作用:

    用途:

    • con_write 函数是 Linux 内核中负责将字符输出到控制台显示器的关键函数。它通过直接操作显存来实现字符的显示,这是 Linux 内核中实现控制台输出的底层机制。

    • 通过这种方式,操作系统能够统一管理不同程序的输出,提供一致的接口给用户程序,同时隐藏了硬件操作的复杂性。

    • 这种机制是操作系统中设备驱动程序的一部分,它展示了如何通过编程接口与硬件设备进行交互,是学习操作系统工作原理和设备驱动开发的重要内容。

    关于 mov pos 的解释:

    • mov pos, c 是完成显示中最核心的秘密,它将字符 c 的值移动到 pos 指向的显存位置,从而在屏幕上显示字符。

    • pos 指向显存的起始地址(例如 0xA0000),每次写入一个字符后,pos 的值会增加,以指向下一个字符的位置。

    • 这种直接操作显存的方法是早期计算机系统中常见的屏幕输出方式,它允许操作系统直接控制屏幕上的每个像素点。

    关于 pos += 2 的解释:

    • 在彩色图形适配器(CGA)中,屏幕上的一个字符在显存中除了字符本身还应该有字符的属性(如颜色等)。因此,每个字符及其属性占用两个字节。

    • pos += 2 表示在写入一个字符后,pos 的值增加2,以指向下一个字符及其属性的起始位置。

    • 这种机制确保了字符及其属性能够正确地存储在显存中,从而在屏幕上正确显示。

总结

printf 的整个过程涉及多个步骤和组件,具体如下:

  1. 库函数(printf)

    • 用户程序调用标准库中的 printf 函数来输出格式化的文本。

  2. 系统调用(write)

    • printf 函数处理完格式化字符串后,通过系统调用 write 将数据写入文件描述符指向的设备。

  3. 字符设备接口(crw_table[])

    • 系统调用 write 通过字符设备接口数组 crw_table[] 找到对应的设备处理函数。

  4. tty设备写(tty_write)

    • 对于终端设备,tty_write 函数负责将数据写入 write_q 队列。

  5. write_q队列

    • write_q 队列用于暂存要写入设备的数据,直到设备准备好接收数据。

  6. 显示器写(con_write)

    • con_write 函数负责将 write_q 队列中的数据实际写入显存。


网站公告

今日签到

点亮在社区的每一天
去签到