外設工作起來
操作系統讓外設工作的基本原理和過程,具體來說,它概括了以下幾個關鍵步驟:
-
發出指令:操作系統通過向控制器中的寄存器發送指令來啟動外設的工作。這些指令通常是通過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; // 第一個空閑的fdf->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
隊列中的數據實際寫入顯存。
-