用戶態(User Mode)
權限級別:較低,限制應用程序直接訪問硬件或關鍵系統資源。
適用場景:普通應用程序的運行環境。
限制:無法執行特權指令(如操作I/O端口、修改內存管理單元配置等)。
內核態(Kernel Mode)
權限級別:最高,允許完全訪問硬件和系統資源。
適用場景:操作系統內核代碼的執行環境。
能力:可執行所有CPU指令,管理進程、內存、設備驅動等核心功能。
1. 操作系統的運行方式
文章開頭給出的概念是站在用戶的角度上來講的,對于加深我們對二者的理解來說,無異于杯水車薪。要搞清楚用戶態與內核態的概念與意義,我們首先要了解操作系統是如何運行的。
如下是Linux0.11版本中,任務0(init)的主函數部分代碼:
void main_rename(void)
{...... // 這里省略前面的初始化代碼
/** 注意!! 對于任何其它的任務,'pause()'將意味著我們必須等待收到一個信號才會返* 回就緒運行態,但任務0(task0)是唯一的意外情況(參見'schedule()'),因為任* 務0 在任何空閑時間里都會被激活(當沒有其它任務在運行時),* 因此對于任務0'pause()'僅意味著我們返回來查看是否有其它任務可以運行,如果沒* 有的話我們就回到這里,一直循環執行'pause()'。*/for(;;) pause();
}
任務0是內核啟動后創建的第一個用戶態任務(注意任務0不等同于操作系統本身),它的主要作用是在系統空閑時讓出CPU,執行for(;;) pause();循環,通過調用pause()系統調用觸發schedule(),主動讓出CPU給其他任務。
pause()
的作用
常規行為:對于普通任務(非任務 0),
pause()
會使任務進入可中斷睡眠狀態,直到收到信號后被喚醒。任務 0 的特殊性:
任務 0 是內核初始化后創建的第一個任務(空閑任務),它的pause()
行為被刻意修改為:
不進入睡眠:直接調用
schedule()
主動觸發調度。輪詢機制:若沒有其他任務可運行,調度器會再次選中任務 0,繼續執行循環。
簡單來說,這個死循環的作用就是:不斷觸發主動調度,并作為沒有其他進程正在運行時的默認動作。?
注意:操作系統內核并不是以一個進程的形式存在的,也就是說內核不是依靠進程調度來獲得CPU的(畢竟調度本身就是操作系統的任務)。內核是操作系統的核心部分,它是一段運行在特權級別的代碼,直接與硬件交互,對系統進行全面的控制和管理。
那么,操作系統的內核在何時能得到運行呢?
在任務0作為一個進程存在的同時,操作系統的內核在不斷地等待著別人請求自己的服務,而請求操作系統服務的方式就是引發中斷,喚醒處于等待狀態之中的操作系統。?
可以說,除了初始化以外,在沒有中斷的時候,操作系統的代碼是不執行的;又或者說操作系統就是初始化程序 + 中斷處理程序。
包括schedule()主動觸發調度,本質上也是依靠觸發軟中斷來請求操作系統的進程調度服務的。
接下來,我們再來介紹一下中斷到底是什么,以及在操作系統中有哪幾類中斷。
2. 中斷
中斷(Interrupt) 是計算機系統中一種核心的事件驅動機制,允許CPU暫停當前執行的任務,轉而處理優先級更高或需要即時響應的外部或內部事件。其本質是通過硬件或軟件觸發的信號,強制CPU切換執行流程,以保障系統的實時性、資源利用率和錯誤處理能力。
中斷的核心特性
- 異步性:中斷的發生與CPU當前執行的指令無關,隨時可能被觸發(如鍵盤輸入、定時器到期)。
- 優先級分層:不同中斷源(如硬件故障、磁盤I/O完成)有不同的優先級,高優先級中斷可搶占低優先級中斷。
- 上下文保存:CPU在響應中斷前,會自動保存當前任務的狀態(如寄存器值、程序計數器PC),以便處理完成后恢復原任務。
中斷的分類
1. 硬件中斷(Hardware Interrupt)
定義:由外部硬件設備通過中斷請求線(IRQ)主動觸發。
特點:
異步性:與CPU當前執行的指令無關。
可屏蔽性:可通過中斷屏蔽位(如x86的
IF
標志位)控制是否響應。子類型:
可屏蔽中斷(Maskable Interrupt)
例如:鍵盤輸入、磁盤I/O完成、定時器中斷等。
通過中斷控制器(如APIC或8259A)管理優先級。不可屏蔽中斷(Non-Maskable Interrupt, NMI)
例如:硬件故障(內存校驗錯誤)、系統看門狗超時。
CPU必須立即處理,無法通過軟件屏蔽。
2. ???????軟件中斷(Software Interrupt)
定義:由程序執行特定指令(如
int
、syscall
)主動觸發。特點:
同步性:由程序顯式調用,與指令流同步。
不可屏蔽性:無法通過硬件屏蔽。
子類型:
系統調用(System Call)
用戶程序通過int 0x80
(x86)或syscall
(x86_64)切換到內核態。調試中斷(Breakpoint)
例如:int 3
指令觸發調試器斷點。異常(Exception)
CPU執行指令時檢測到錯誤(如除零、頁錯誤),自動觸發。
2.1 硬件中斷
通過外部硬件中斷,操作系統就不需要對外設進行任何周期性的檢測或者輪詢。
由外部設備觸發的,中斷系統運行流程,叫做硬件中斷。
其中,中斷向量表在內存中的位置與格式由 CPU 架構硬性規定,中斷號實際上就是相對于中斷向量表起始地址的偏移量。當CPU獲得中斷號之后,就可以在中斷向量表當中查詢到對應的中斷服務程序的起始地址,進而轉向執行中斷服務程序。
而中斷服務程序則是由操作系統負責注冊到中斷向量表當中的:
// 下面是異常(陷阱)中斷程序初始化子程序。設置它們的中斷調用門(中斷向量)。
// set_trap_gate()與set_system_gate()的主要區別在于前者設置的特權級為0,后者是3。因此
// 斷點陷阱中斷int3、溢出中斷overflow 和邊界出錯中斷bounds 可以由任何程序產生。
// 這兩個函數均是嵌入式匯編宏程序(include/asm/system.h,第36 行、39 行)。
void trap_init(void)
{int i;set_trap_gate(0,÷_error);// 設置除操作出錯的中斷向量值。以下雷同。set_trap_gate(1,&debug);set_trap_gate(2,&nmi);set_system_gate(3,&int3); /* int3-5 can be called from all */set_system_gate(4,&overflow);set_system_gate(5,&bounds);set_trap_gate(6,&invalid_op);set_trap_gate(7,&device_not_available);set_trap_gate(8,&double_fault);set_trap_gate(9,&coprocessor_segment_overrun);set_trap_gate(10,&invalid_TSS);set_trap_gate(11,&segment_not_present);set_trap_gate(12,&stack_segment);set_trap_gate(13,&general_protection);set_trap_gate(14,&page_fault);set_trap_gate(15,&reserved);set_trap_gate(16,&coprocessor_error);
// 下面將int17-48 的陷阱門先均設置為reserved,以后每個硬件初始化時會重新設置自己的陷阱門。for (i=17;i<48;i++)set_trap_gate(i,&reserved);set_trap_gate(45,&irq13);// 設置協處理器的陷阱門。outb_p(inb_p(0x21)&0xfb,0x21);// 允許主8259A 芯片的IRQ2 中斷請求。outb(inb_p(0xA1)&0xdf,0xA1);// 允許從8259A 芯片的IRQ13 中斷請求。set_trap_gate(39,¶llel_interrupt);// 設置并行口的陷阱門。
}初始化串行中斷程序和串行接口。
void
rs_init (void)
{set_intr_gate (0x24, rs1_interrupt); // 設置串行口1 的中斷門向量(硬件IRQ4 信號)。set_intr_gate (0x23, rs2_interrupt); // 設置串行口2 的中斷門向量(硬件IRQ3 信號)。init (tty_table[1].read_q.data); // 初始化串行口1(.data 是端口號)。init (tty_table[2].read_q.data); // 初始化串行口2。outb (inb_p (0x21) & 0xE7, 0x21); // 允許主8259A 芯片的IRQ3,IRQ4 中斷信號請求。
}
2.2?時鐘中斷
我們曾經使用過sleep,alarm等函數,也了解了時間片的概念,那么操作系統是如何計時的呢?
實際上,操作系統運行的一大依據就是時鐘中斷,顧名思義,這種中斷就是用來幫助系統計時的。
時鐘中斷本質上也是一種硬件中斷,這種中斷依靠一種特定的硬件設備來觸發:時鐘源。
早期的時候時鐘源就是上圖中的外設之一,但是鑒于其重要程度以及時鐘中斷的時效性,現在大多數的CPU都已經集成了中斷源,使中斷源能直接向CPU發送信號。
時鐘源發送時鐘中斷的時間間隔是固定的,由其頻率。時鐘源的頻率也叫CPU或者系統的主頻:
時鐘中斷的核心作用
系統計時
通過全局變量jiffies
記錄自系統啟動以來的節拍數(每個節拍對應一次時鐘中斷),結合HZ
(每秒節拍數,默認100~1000)實現時間計算。例如,jiffies/HZ
可轉換為系統運行時間(秒)。進程調度
每次時鐘中斷會調用do_timer()函數更新進程時間片,觸發搶占式調度。進程的時間片本質上就是一個計數器,每次時鐘中斷都會使其減1,直到為0。動態定時器管理
內核通過紅黑樹或時間輪管理動態定時器(timer_list
結構),時鐘中斷觸發時檢查并執行到期定時器的回調函數。資源統計
統計進程消耗的用戶態和內核態時間,更新系統負載平均值。
?2.3 軟中斷
系統調用本質上就是請求操作系統的服務,這就要求我們能從軟件層面上觸發中斷,即軟中斷。
為了讓操作系統支持進行系統調用,CPU也設計了對應的匯編指令(int 或者 syscall),可以讓CPU內部觸發中斷邏輯。
本質上,我們使用的系統調用函數內部的邏輯就是使用int 0x80指令請求操作系統提供指定的服務,其本身并不具備任務對系統進行操作的能力。
打開文件函數。
// 打開并有可能創建一個文件。
// 參數:filename - 文件名;flag - 文件打開標志;...
// 返回:文件描述符,若出錯則置出錯碼,并返回-1。
int open(const char * filename, int flag, ...)
{register int res;va_list arg;// 利用va_start()宏函數,取得flag 后面參數的指針,然后調用系統中斷int 0x80,功能open 進行
// 文件打開操作。
// %0 - eax(返回的描述符或出錯碼);%1 - eax(系統中斷調用功能號__NR_open);
// %2 - ebx(文件名filename);%3 - ecx(打開文件標志flag);%4 - edx(后隨參數文件屬性mode)。va_start(arg,flag);res = va_arg(arg,int);_asm{mov eax,__NR_openmov ebx,filenamemov ecx,flagmov edx,resint 0x80mov res,eax}
/* __asm__("int $0x80":"=a" (res):"0" (__NR_open),"b" (filename),"c" (flag),"d" (va_arg(arg,int)));*/
// 系統中斷調用返回值大于或等于0,表示是一個文件描述符,則直接返回之。if (res>=0)return res;
// 否則說明返回值小于0,則代表一個出錯碼。設置該出錯碼并返回-1。errno = -res;return -1;
}
可以看到open函數內部是使用嵌入式匯編的方式傳遞參數,并使用int 0x80觸發軟中斷。
軟中斷處理程序 _system_call 根據第一個參數 __NR_open 就能索引到需要調用的函數:
正真有能力對系統進行操作的,操作系統內部的系統調用函數被注冊到了一張表當中,這張表就是上面匯編程序索引到所需函數的依據:
?3. 內核態與用戶態的本質
經歷了上面的學習,相信大家對于內核態與用戶態的本質已經有了一定的想法。
即,執行用戶代碼時,系統就處于用戶態;因中斷而轉向執行中斷處理程序時(操作系統內核代碼),系統就處于內核態。
內核態的代碼是全局的,不受約束的(或者說只受到硬件約束);而用戶態的代碼是以進程的形式,在虛擬地址空間上運行的,受到操作系統的約束。
問題是,CPU在運行代碼時,如何區分其是內核代碼(特權級)還是用戶代碼(受限制)呢?
Linux內核態與用戶態的本質區別在于CPU特權級隔離和內存訪問權限控制,這種設計通過硬件機制與軟件協同實現系統資源的保護:
特權級劃分
x86架構通過Ring等級劃分權限(0-3級),Linux僅使用Ring0(內核態)和Ring3(用戶態)。關鍵寄存器CS
(代碼段寄存器)的低2位存儲當前特權級(CPL):// 內核代碼段選擇子(CPL=0) #define __KERNEL_CS 0x10 // 用戶代碼段選擇子(CPL=3) #define __USER_CS 0x1B
內存隔離機制
頁表隔離:用戶態進程只能訪問用戶空間頁表項(
VM_READ
/VM_WRITE
),內核態通過全局頁表swapper_pg_dir
直接映射物理內存。地址空間劃分:x86_64下用戶空間為0x0000_0000_0000_0000 - 0x0000_7FFF_FFFF_FFFF,內核空間為0xFFFF_8000_0000_0000以上。也就是說,用戶代碼中地址的范圍與內核代碼中地址的范圍是互斥的,用戶態下無法使用內核地址,在內核態下無法使用用戶地址。