外设工作起来
操作系统让外设工作的基本原理和过程,具体来说,它概括了以下几个关键步骤:
发出指令:操作系统通过向控制器中的寄存器发送指令来启动外设的工作。这些指令通常是通过I/O指令(如
out
指令)来实现的。控制器处理:控制器接收到指令后,根据寄存器中的内容来操控硬件。控制器内部可能包含有计算电路,能够根据CPU发出的指令来具体操作设备。
中断处理:一旦外设完成其任务,它会向CPU发送一个中断信号。CPU接收到中断后,会暂停当前的工作,转而处理中断,这可能涉及到数据传输等操作。
统一的文件接口:为了让外设的使用变得简单,操作系统提供了一种统一的视图,即文件视图。这意味着,无论操作哪种外设,用户都可以通过统一的文件操作接口(如
open
、read
、write
等)来进行。
总结来说,操作系统让外设工作的核心原理非常简单,即通过发出指令让外设工作,然后编写中断处理程序来响应外设完成任务后的中断信号。此外,操作系统通过提供统一的文件接口,使得用户可以方便地使用各种外设,而无需关心底层的硬件细节。接下来我们将围绕这三个方面讲解。
外设工作的开始
提取的代码如下:
int fd = open("/dev/xxx");
for (int i = 0; i < 10; i++) {
write(fd, i, sizeof(int));
}
close(fd);
外设工作的开始可以总结为以下几个步骤:
打开设备:
使用
open
函数打开指定的设备文件(/dev/xxx
),这个文件是系统中外设的接口。open
函数返回一个文件描述符fd
,用于后续对该设备的操作。
数据传输:
通过
write
函数将数据传输到外设。在这个例子中,数据是一个整数i
,大小为sizeof(int)
。这个过程在一个循环中进行,循环10次,每次写入一个整数。
关闭设备:
完成数据传输后,使用
close
函数关闭设备文件,释放文件描述符fd
所占用的资源。
文件视图概念
文件视图是操作系统提供的两大视图之一,它将所有的I/O设备统一抽象为文件,使得用户可以通过一组标准的文件操作接口(如open
、read
、write
、close
)来访问和操作这些设备。这种抽象极大地简化了用户与硬件设备的交互,并隐藏了底层硬件的具体细节。
在文件视图中,操作系统将设备属性数据和设备驱动程序结合在一起,通过系统调用接口与用户空间进行交互。当用户程序调用这些系统调用时,操作系统会进行解释,并将其转换为对特定设备的命令。这些命令随后被发送到相应的设备控制器(如键盘控制器或磁盘控制器),并由控制器执行具体的硬件操作。
文件视图的样貌可以总结如下:
统一接口:无论什么设备,用户都通过统一的系统调用接口(
open
、read
、write
、close
)来进行操作。设备抽象:不同的设备对应不同的设备文件(如
/dev/xxx
),操作系统根据这些设备文件找到控制器的地址、内容格式等信息。设备驱动:设备驱动程序是操作系统与硬件设备之间的桥梁,它负责将系统调用转换为对特定硬件的操作。
中断处理:当设备完成操作后,会通过中断机制通知操作系统,操作系统再进行相应的中断处理。
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
}
总结显示器输出的过程:
系统调用:
用户程序通过
printf
函数输出信息,printf
函数内部会先创建一个缓存区(buf
),将格式化后的输出写入该缓存区。
写入系统调用:
printf
函数最终会调用write
系统调用,将缓存区中的数据写入指定的文件描述符(fd
)。
文件描述符索引:
在Linux内核中,
sys_write
函数通过文件描述符(fd
)找到对应的文件结构体(file
)。文件描述符是用户空间和内核空间之间的索引。
获取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=1
的filp
从哪里来?
在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
系统调用主要完成了以下步骤:
解析目录,找到inode:系统需要解析传入的文件名,找到对应的inode结构,inode包含了文件的元数据和设备信息。
分配文件描述符(fd):在进程的文件描述符数组(
filp
)中找到一个空闲的文件描述符,并将其分配给这个文件。建立文件结构体(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);
...
}
系统调用:用户程序通过
write
系统调用向内核请求写操作,传递文件描述符(fd
)、缓冲区地址(buf
)和要写入的字节数(cnt
)。字符设备检查:在
sys_write
函数中,首先获取文件结构体的inode,并检查该inode表示的是否为字符设备(通过S_ISCHR(inode->i_mode)
判断)。调用设备驱动:如果是字符设备,调用
rw_char
函数,传入写操作标志(WRITE
)、inode中的设备信息(i_zone[0]
)、缓冲区地址和字节数。设备驱动操作:在
rw_char
函数中,根据设备的主要号码(MAJOR(dev)
)从字符设备驱动表(crw_table
)中获取对应的操作函数指针,并调用该函数执行实际的写操作。输出到屏幕:对于显示器这样的字符设备,
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);
...
}
总结代码所做的事情及用途:
定义字符设备操作函数指针数组(
crw_table
):crw_table
是一个数组,包含了指向不同字符设备操作函数的指针。这些函数负责对字符设备进行读写操作。
函数指针类型定义(
crw_ptr
):crw_ptr
是一个函数指针类型,用于指向符合特定签名的函数,即接受读写标志、次要设备号、缓冲区指针和计数作为参数的函数。
字符设备读写函数(
rw_ttyx
):rw_ttyx
函数根据传入的读写标志(rw
),决定调用tty_read
还是tty_write
函数。这个函数作为字符设备的通用入口点,根据操作类型分发到具体的读写处理函数。
真正的写函数(
tty_write
):tty_write
是实现字符设备(如终端)写操作的核心函数。它负责将数据从内核缓冲区写入到设备。函数首先通过
channel
和tty_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);
}
总结代码所做的事情及用途:
初始化:
定义字符变量
c
和字符指针b
指向缓冲区buf
的起始位置。
循环处理每个字符:
使用
while
循环,当还有字符要写入(nr > 0
)且写队列未满(!FULL(tty->write_q)
)时,继续处理。从用户空间的缓冲区中读取一个字符到
c
。
处理回车字符:
如果字符是回车符(
'\r'
),将其转换为换行符('\n'
)并继续下一个循环。
字符大小写转换:
如果终端设置为转换为大写(
O_LCUC(tty)
),将字符c
转换为大写。
写入队列:
将处理后的字符放入终端的写队列
tty->write_q
中。更新缓冲区指针
b
和字符计数nr
。
触发实际写操作:
一旦所有字符都已处理或写队列满,调用
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
函数定义:如果字符
c
在可打印范围内(ASCII码 32 到 126),则通过内联汇编代码将其写入显存(视频内存)的特定位置。函数从
tty->write_q
队列中获取一个字符c
。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
的整个过程涉及多个步骤和组件,具体如下:
库函数(printf):
用户程序调用标准库中的
printf
函数来输出格式化的文本。
系统调用(write):
printf
函数处理完格式化字符串后,通过系统调用write
将数据写入文件描述符指向的设备。
字符设备接口(crw_table[]):
系统调用
write
通过字符设备接口数组crw_table[]
找到对应的设备处理函数。
tty设备写(tty_write):
对于终端设备,
tty_write
函数负责将数据写入write_q
队列。
write_q队列:
write_q
队列用于暂存要写入设备的数据,直到设备准备好接收数据。
显示器写(con_write):
con_write
函数负责将write_q
队列中的数据实际写入显存。