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

外设工作起来

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

  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; // 第一个空闲的fdf->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 队列中的数据实际写入显存。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/76498.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

琥珀扫描 2.0.5.0 | 文档处理全能助手,支持扫描、文字提取及表格识别

琥珀扫描是一款功能强大的文档处理应用程序。它不仅仅支持基本的文档扫描功能&#xff0c;还涵盖了文字提取、证件扫描、表格识别等多种实用功能。无论是学生、职员还是教师&#xff0c;都能从中找到适合自己的功能。该应用支持拍照生成电子件&#xff0c;并能自动矫正文档边缘…

jQuery UI 小部件方法调用详解

jQuery UI 小部件方法调用详解 引言 jQuery UI 是一个基于 jQuery 的用户界面和交互库,它提供了一系列小部件,如按钮、对话框、进度条等,这些小部件极大地丰富了网页的交互性和用户体验。本文将详细介绍 jQuery UI 中小部件的方法调用,帮助开发者更好地理解和应用这些小部…

浮点数比较在Eigen数学库中的处理方法

浮点数比较在Eigen数学库中的处理方法 在Eigen数学库中进行浮点数比较时&#xff0c;由于浮点数的精度问题&#xff0c;直接使用运算符通常不是推荐的做法。Eigen提供了几种更安全的方法来进行浮点数比较&#xff1a; 1. 近似相等比较 使用isApprox()函数进行近似比较&#…

Linux-----驱动

一、内核驱动与启动流程 1. Linux内核驱动 Nor Flash: 可线性访问&#xff0c;有专门的数据及地址总线&#xff08;与内存访问方式相同&#xff09;。 Nand Flash: 不可线性访问&#xff0c;访问需要控制逻辑&#xff08;软件&#xff09;。 2. Linux启动流程 ARM架构: IRAM…

Wincc脚本全部不运行

Wincc脚本全部不运行 前言解决办法操作步骤 前言 这里主要是指旧项目移植到Wincc的高版本&#xff0c;移植后界面的一些功能均会失效。&#xff08;例如脚本不执行&#xff0c;项目编辑器不可用等情况&#xff09; 解决办法 Wincc的项目文件中有Dcf文件&#xff0c;Dcf文件包…

使用numpy构建逻辑回归模型及训练流程

逻辑回归模型构建及训练流程 关于逻辑回归的数据&#xff0c;有很多学习⽤的⽰例样本。这⾥我们使⽤scikit learn提供的数据集⽣成函数来创建 具体参数可参照官网 Scikit-learn 是⽤ Python 开发的开源机器学习库&#xff0c;⼴泛⽤于数据挖掘和数据分析。 特点&#xff1a;易…

python的多线程和多进程程序编程

CPU密集型使用多进程&#xff0c;IO密集型使用多线程 查看进程ID和线程ID的命令分别是os.getpid()和threading.current_thread() 多进程使用multiprocessing就可以了&#xff0c;通常使用进程池来完成操作&#xff0c;阻塞主进程使用join方法 多线程使用threading模块&#…

代码随想录算法训练营第十五天

LeetCode题目: 654. 最大二叉树617. 合并二叉树700. 二叉搜索树中的搜索98. 验证二叉搜索树2843. 统计对称整数的数目 其他: 今日总结 往期打卡 654. 最大二叉树 跳转: 654. 最大二叉树 学习: 代码随想录公开讲解 问题: 给定一个不重复的整数数组 nums 。 最大二叉树 可以用…

[GN] Uart协议解码器源码各个方法

系列文章目录 sigrokdecode 模块学习指南 — 准备阶段 通讯协议 - Uart sigrokdecode 模块 UART协议解码器源码解析 Uart协议解码器源码各个方法 文章目录 系列文章目录引入库parity_ok注解类型枚举options参数annotations 注解annotation_rows 注解分组接收&#xff08;RX&a…

技术分享|iTOP-RK3588开发板Ubuntu20系统旋转屏幕方案

iTOP-3588开发板采用瑞芯微RK3588处理器&#xff0c;是全新一代AloT高端应用芯片&#xff0c;采用8nmLP制程&#xff0c;搭载八核64位CPU&#xff0c;四核Cortex-A76和四核Cortex-A55架构&#xff0c;主频高达2.4GHz。是一款可用于互联网设备和其它数字多媒体的高性能产品。 在…

Unity IL2CPP内存泄漏追踪方案(基于Memory Profiler)技术详解

一、IL2CPP内存管理特性与泄漏根源 1. IL2CPP内存架构特点 内存区域管理方式常见泄漏类型托管堆(Managed)GC自动回收静态引用/事件订阅未取消原生堆(Native)手动管理非托管资源未释放桥接层GCHandle/PInvoke跨语言引用未正确释放 对惹&#xff0c;这里有一个游戏开发交流小组…

消融实验_草稿

五列数据 \begin{table}[htbp]\caption{Performance Comparison of Standalone KD Variants vs MIRKD-enhanced Variants on ACNE04 Dataset\label{AblationKD}}\centering\renewcommand{\arraystretch}{1.2}\scriptsize\begin{tabularx}{\linewidth}{{}l *{3}{>{\centering…

面向对象高级(1)

文章目录 final认识final关键字修饰类&#xff1a;修饰方法&#xff1a;修饰变量final修饰变量的注意事项 常量 单例类什么是设计模式&#xff1f;单例怎么写?饿汉式单例的特点是什么&#xff1f;单例有啥应用场景&#xff0c;有啥好处&#xff1f;懒汉式单例类。 枚举类认识枚…

不用额外下载jar包,idea快速查看使用的组件源码

以nacos为例子&#xff0c;在idea中引入了nacos依赖&#xff0c;就可以查看源码了。 2. idea选择open&#xff08;不关闭项目直接选择file-open也可以&#xff09;, 在maven的仓库里找到对应的包&#xff0c;打开 2.idea中选择 jar包&#xff0c;选择 add as library 3.这样j…

小白学习java第12天:IO流之缓冲流

1.IO缓冲流&#xff1a; 之前我们学习的都是原始流&#xff08;FileInputStream字节输入流、FileOutputStream字节输出流、FIleReader字符输入流、FIleWriter字符输出流&#xff09;其实我们可以知道对于这些其实性能都不是很好&#xff0c;要么太慢一个一个&#xff0c;要么就…

高速电路设计概述

1.1 低速设计和高速设计的例子 本节通过一个简单的例子&#xff0c;探讨高速电路设计相对于低速电路设计需要考虑哪些不同的问题。希望读者通过本例&#xff0c;对高速电路设计建立一个表象的认识。至于高速电路设计中各方面的设计要点&#xff0c;将在后续章节展开详细的讨论…

MySQL8.0.31安装教程,附pdf资料和压缩包文件

参考资料&#xff1a;黑马程序员 一、下载 点开下面的链接&#xff1a;https://dev.mysql.com/downloads/mysql/ 点击Download 就可以下载对应的安装包了, 安装包如下: 我用夸克网盘分享了「mysql」&#xff0c;链接&#xff1a;https://pan.quark.cn/s/ab7b7acd572b 二、解…

在Java项目中,引入【全局异常处理器】

目录 一.为什么引入全局异常处理器&#xff08;目前项目碰到了什么问题&#xff09;&#xff1f; 1.问题描述 2.与预期的差别 3.解决方案 二.解决上述问题 1.定义【业务异常类】 2.在serviceImpl层&#xff0c;手动抛出【违反唯一性约束】这个异常 3.定义【全局异常处理…

newspaper公共库获取每个 URL 对应的新闻内容,并将提取的新闻正文保存到一个文件中

示例代码&#xff1a; from newspaper import Article from newspaper import Config import json from tqdm import tqdm import os import requestswith open(datasource/api/news_api.json, r) as file:data json.load(file)print(len(data)) save_path datasource/sourc…

前端核心知识:Vue 3 编程的 10 个实用技巧

文章目录 1. **使用 ref 和 reactive 管理响应式数据**原理解析代码示例注意事项 2. **组合式 API&#xff08;Composition API&#xff09;**原理解析代码示例优势 3. **使用 watch 和 watchEffect 监听数据变化**原理解析代码示例注意事项 4. **使用 provide 和 inject 实现跨…