@TOC
📝前言
🌠 信號捕捉的流程
如果信號的處理動作是⽤⼾⾃定義函數,在信號遞達時就調⽤這個函數,這稱為捕捉信號。 由於信號處理函數的代碼是在⽤⼾空間的,處理過程⽐較複雜,舉例如下:
- ⽤⼾程序註冊了SIGQUIT 信號的處理函數sighandler
- 當前正在執⾏main 函數,這時發⽣中斷或異常切換到內核態。
- 在中斷處理完畢後要返回⽤⼾態的main 函數之前檢查到有信號SIGQUIT 遞達。
- 內核決定返回用户態後不是恢復main函數的上下文繼續執行,而是執行sighandler函數, sighandler和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關係,是兩個獨立的控制流程。
- sighandler函數返回後自動執行特殊的系統調用sigreturn再次進入內核態。
- 如果沒有新的信號要遞達,這次再返回用户態就是恢復main函數的上下文繼續執行了。
🌉 sigaction
SYNOPSIS
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
- sigaction函數可以讀取和修改與指定信號相關聯的處理動作。調⽤成功則返回0,出錯則返回-1。signo是指定信號的編號。若act指針⾮空,則根據act修改該信號的處理動作。若oact指針⾮空,則通過oact傳出該信號原來的處理動作。act和oact指sigaction結構體:
- 將sa_handler賦值為常數SIG_IGN傳給sigaction表⽰忽略信號,賦值為常數SIG_DFL表⽰執⾏系統默認動作,賦值為⼀個函數指針表⽰⽤⾃定義函數捕捉信號,或者説向內核註冊了⼀個信號處理函數,該函數返回值為void,可以帶⼀個int參數,通過參數可以得知當前信號的編號,這樣就可以⽤同⼀個函數處理多種信號。顯然,這也是⼀個回調函數,不是被main函數調⽤,⽽是被系統所調⽤。
當某個信號的處理函數被調⽤時,內核⾃動將當前信號加⼊進程的信號屏蔽字,當信號處理函數返回時⾃動恢復原來的信號屏蔽字,這樣就保證了在處理某個信號時,如果這種信號再次產⽣,那麼它會被阻塞到當前處理結束為⽌。如果在調⽤信號處理函數時,除了當前信號被⾃動屏蔽之外,還希望⾃動屏蔽另外⼀些信號,則⽤sa_mask字段説明這些需要額外屏蔽的信號,當信號處理函數返回時⾃動恢復原來的信號屏蔽字。sa_flags字段包含⼀些選項,本章的代碼都把sa_flags設為0,sa_sigaction是實時信號的處理函數,本章不詳細解釋這兩個字段,有興趣的同學可以在瞭解⼀下。
🌠穿插話題-操作系統是怎麼運⾏的
🌉 硬件中斷
- 中斷向量表就是操作系統的⼀部分,啓動就加載到內存中了
- 通過外部硬件中斷,操作系統就不需要對外設進⾏任何週期性的檢測或者輪詢
- 由外部設備觸發的,中斷系統運⾏流程,叫做硬件中斷
// Linux
內核
0.11 源碼
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,以後每個硬件初始化時會重新設置自己的陷阱門o
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中斷信號請求。
}
🌉時鐘中斷
問題: 進程可以在操作系統的指揮下,被調度,被執⾏,那麼操作系統⾃⼰被誰指揮,被誰推動執⾏呢?
外部設備可以觸發硬件中斷,但是這個是需要⽤⼾或者設備⾃⼰觸發,有沒有⾃⼰可以定期觸發的
設備?
這樣,操作系統不就在硬件的推動下,⾃動調度了麼!!!
// 調度程序的初始化子程序。
void sched_init(void)
{
... set_intr_gate(0x20, & timer_interrupt);
// 修改中斷控制器屏蔽碼,允許時鐘中斷。
outb(inb_p(0x21) & ~0x01,0x21);
// 設置系統調用中斷門。
set_system_gate(0×80, & system_call);
...
}
// system_call.s
_timer_interrupt : ...;
// do_timer(CPL)執行任務切換、計時等工作,在kernel/shched.c,305行實現。
call _do_timer; // 'do_timer(long CPL )' does everything from
// 調度入口
void do_timer(long cpl)
{
...;
schedule();
}
void schedule(void)
{
switch_to(next); // 切換到任務號為next的任務,並運行之。
}
🌉死循環
如果是這樣,操作系統不就可以躺平了嗎?對,操作系統⾃⼰不做任何事情,需要什麼功能,就向中 斷向量表⾥⾯添加⽅法即可.操作系統的本質:就是⼀個死循環!
void main(void) /* 這⾥確實是void,並沒錯。*/
{ /* 在startup 程序(head.s)中就是這樣假設的。*/
...
/*
★注意!!對於任何其它的任務,' pause()'將意味着我們必須等待收到一個信號才會返
★回就緒運行態,但任務0 (taskl)是唯一的意外情況(參見' schedule() '),因為任
★務0在任何空閒時間裏都會被激活(當沒有其它任務在運行時),
★因此對於任務0' pause()'僅意否有其它任務可以運行,如果沒
★有的話我們就回到這裏,—直循環執行' pause( ) '。
*/
for (;;)
pause();
} //end main
- 這樣,操作系統,就可以在硬件時鐘的推動下,⾃動調度了
- 所以,什麼是時間⽚?CPU為什麼會有主頻?為什麼主頻越快,CPU越快?
🌉軟中斷
- 上述外部硬件中斷,需要硬件設備觸發。
- 有沒有可能,因為軟件原因,也觸發上⾯的邏輯?有!
- 為了讓操作系統⽀持進⾏系統調⽤,CPU也設計了對應的彙編指令(int或者syscall),可以讓CPU內
- 部觸發中斷邏輯。 所以:
- 問題:
- ⽤⼾層怎麼把系統調⽤號給操作系統?-寄存器(⽐如EAX)
- 操作系統怎麼把返回值給⽤⼾?-寄存器或者⽤⼾傳⼊的緩衝區地址
- 系統調⽤的過程,其實就是先int0x80、syscall陷⼊內核,本質就是觸發軟中斷,CPU就會⾃動執⾏系統調⽤的處理⽅法,⽽這個⽅法會根據系統調⽤號,⾃動查表,執⾏對應的⽅法
- 系統調⽤號的本質:數組下標!
/ sys.h
//系統調用函數指針表。用於系統調用中斷處理程序(int 0x80),作為跳轉表。
extern int sys_setup ();//系統啓動初始化設置函數。(kernel/blk_drv/hd . c,71)
extern int sys_exit ();//程序退出。(kernel/exit.c, 137)
extern int sys_fork ();//創建進程。(kernel/system_call.s,208)extern int sys_read ();//讀文件。(fs/read_write.c,55)
extern int sys_write ();//寫文件。(fs/read_write.c, 83)
extern int sys_open ();//打開文件。(fs/open.c,138)
extern int sys_close ();//關閉文件。(fs/open.c,192)
extern int sys_waitpid ();/ /等待進程終止。(kernel/exit.c,142)
extern int sys_creat ();//創建文件。(fs/open.c,187)
extern int sys_link ();//創建一個文件的硬連接。(fs/namei. c,721,)
extern int sys_unlink ();//刪除一個文件名(或刪除文件)。(fs/namei.c,663)
extern int sys_execve ();//執行程序。(kernel/system_call.s,200)
extern int sys_chdir ();//更改當前目錄。(fs /open.c,75)
extern int sys_time ();//取當前時間。(kernel/sys.c,102)
extern int sys_mknod ();//建立塊/字符特殊文件。(fs/namei.c,412)
extern int sys_chmod ();/修改文件屬性。(fs/open.c,105)
extern int sys_chown ();//修改文件宿主和所屬組。(fs/open.c,1212
extern int sys_break ();//(-kernel/sys.c,21)
extern int sys_stat ();//使用路徑名取文件的狀態信息。(fs/stat.c,36)
extern int sys_lseek ();//重新定位讀/寫文件偏移。(fs/read_write.c,25)
extern int sys_getpid ();//取進程id。(kernel/sched.c,348)
extern int sys_mount ();//安裝文件系統。(fs/super.c,200)
extern int sys_umount ();//卸載文件系統。( fs/super.c,167)
extern int sys_setuid ();//設置進程用户id。(kernel/sys.c,143)
extern int sys_getuid ();//取進程用户id。(kernel/sched.c,358)
extern int sys_stime ();//設置系統時間日期。(-kernel/sys.c,148)
extern int sys_ptrace ();//程序調試。(-kernel/sys.c,26)
extern int sys_alarm ();/設置報警。(kernel/sched.c,338)
extern int sys_fstat ();//使用文件句柄取文件的狀態信息。(fs/stat.c47)
extern int sys_pause ();//暫停進程運行。(kernel/sched.c,144)
extern int sys_utime ();//改變文件的訪問和修改時間。(fs /open.c,24)
extern int sys_stty ();//修改終端行設置。(-kernel/sys.c,31)
extern int sys_gtty ();//取終端行設置信息。(-kernel/sys.c,36)
extern int sys_access ();//檢查用户對一個文件的訪問權限。(fs/open.c47)
extern int sys_nice ();//設置進程執行優先權。(kernel/sched.c,378)
extern int sys_ftime ();//取日期和時間。(-kernel/sys.c,16)
extern int sys_sync ();//同步高速緩衝與設備中數據。(fs/buffer.c,44)
extern int sys_kill ();//終止一個進程。(kernel/exit.c,60)
extern int sys_rename ();//更改文件名。(-kernel/sys.c,41)
extern int sys_mkdir ();//創建目錄。(fs/namei.c,463)
extern int sys_rmdir ();//刪除目錄。( fs/namei.c,587)
extern int sys_dup ();//複製文件句柄。(fs /fcntl.c,42)
extern int sys_pipe ();//創建管道。(fs/pipe.c,71)
extern int sys_times ();//取運行時間。(kernel/sys.c,156)
extern int sys_prof ();//程序執行時間區域。(-kernel/sys.c,46)
extern int sys_brk ();//修改數據段長度。(kernel/sys.c,168)
extern int sys_setgid ();//設置進程組id。(kernel/sys.c,72)
extern int sys_getgid ();//取進程組id。(kernel/sched.c,368)
extern int sys_signal ();//信號處理。(kernel/signal.c,48)
extern int sys_geteuid ();//取進程有效用户id。(kenrl/sched.c, 363)
extern int sys_getegid ();//取進程有效組id。(kenrl/sched.c,373
extern int sys_acct ();//進程記帳。(-kernel/sys.c,77)
extern int sys_phys ();//(-kernel/sys.c,82)
extern int sys_lock ();//(-kernel/sys.c,87)
extern int sys_ioctl ();//設備控制。(fs/ioctl.c, 30)
extern int sys_fcntl ();/文件句柄操作。(fs/fcntl.c,47)
extern int sys_mpx ();//(-kernel/sys.c,92)
extern int sys_setpgid ();//設置進程組id。(kernel/sys.c,181)
extern int sys_ulimit ();// (-kernel/sys.c, 97)
extern int sys_uname ();//顯示系統信息。(kernel/sys.c,216)
extern int sys_umask ();//取默認文件創建屬性碼。(kernel/sys.c, 230)
extern int sys_chroot ();//改變根系統。(fs/open.c,90)
extern int sys_ustat ();//取文件系統信息。(fs/open.c,19)
extern int sys_dup2 ();//複製文件句柄。(fs /fcntl.c,36)
extern int sys_getppid ();//取父進程id。(kernel/sched.c,353)
extern int sys_getpgrp ();//取進程組id,等於getpgid(0)。(kernel/s'sys.c,201)
extern int sys_setsid ();//在新會話中運行程序。(kernel/sys.c,206)
extern int sys_sigaction ();//改變信號處理過程。(kernel/signal.c63)
extern int sys_sgetmask ();//取信號屏蔽碼。(kernel/signal.c,15)
extern int sys_ssetmask ();//設置信號屏蔽碼。(kernel/signal.c,20
extern int sys_setreuid ();//設置真實與/或有效用户id。(kernel/sys:.c ,118)
extern int sys_setregid ();//設置真實與/或有效組id。(kernel/sys.c51)
系統調用函數指針表:
//系統調用函數指針表。用於系統調用中斷處理程序(int 0x80),作為跳轉表。
fn_ptr sys_call_table[] = { sys_setup,sys_exit,sys_fork,sys_read,sys_write,sys_open,sys_close,sys_waitpid,sys_creat,sys_link,sys_unlink,sys_execve,sys_chdir,sys_time,sys_mknod,sys_chmod ,sys_chown,sys_break,sys_stat,sys_lseek,sys_getpid,sys_mount,sys_umount,sys_setuid,sys_getuid,sys_stime,sys_ptrace,sys_alarm,sys_fstat,sys_pause,sys_utime,sys_stty,sys_gtty,sys_access
sys_nice,sys_ftime,sys_sync,sys_kill, sys_rename,sys_mkdir,
sys_rmdir,sys_dup,sys_pipe,sys_times,sys_prof,sys_brk,sys_setgid,sys_getgid,sys_signal,sys_geteuid,sys_getegid,sys_acct,sys_phys,sys_lock,sys_ioctl,sys_fcntl,sys_mpx,sys_setpgid, sys_ulimit
sys_uname,sys_umask,sys_chroot,sys_ustat,sys_dup2,sys_getppid,sys_getpgrp,sys_setsid,sys_sigaction,sys_sgetmask,sys_ssetmask,sys_setreuid,sys_setregid
};
調度程序的初始化⼦程序:
// 調度程序的初始化⼦程序。
void sched_init(void)
{
...
// 設置系統調用中斷門。
set_system_gate(0x80, & system_call);
}
_system_call :
cmp eax, nr_system_calls - 1; // 調用號如果超出範圍的話就在eax中置-1並退出。
ja bad_sys_call
push ds; // 保存原段寄存器值。
push es
push fs
push edx;// ebx, ecx, edx中放着系統調用相應的c語言函數的調用參數。
push ecx; // push %ebx , %ecx , %edx as parameters
push ebx; // to the system call
mov edx, 10h;// set up ds, es to kernel space
mov ds,dx ; // ds, es 指向內核數據段(全局描述符表中數據段描述符)。
mov es,dx
mov edx,17h ;// fs points to local data space
mov fs,dx ; // fs指向局部數據段(局部描述符表中數據段描述符)。
;//下面這句操作數的含義是:調用地址_sys_call_table + %eax * 4。參見列表後的説明。
;//對應的C程序中的sys_call_table 在include/linux/sys.h 中,其中定義了一個包括72個
;//系統調用c處理函數的地址數組表。
call [_sys_call_table+eax*4]push eax ;//把系統調用號入棧。
mov eax,_current ;//取當前任務(進程)數據結構地址??eaxo
;//下面97-100 行查看當前任務的運行狀態。如果不在就緒狀態(state 不等於0)就去執行調度程序。
;//如果該任務在就緒狀態但counter[??]值等於o,則也去執行調度程序。
cmp dword ptr [state+eax],0 ; // state
jne reschedule
cmp dword ptr [counter+eax],0 ;// counter
je reschedule
;//以下這段代碼執行從系統調用c函數返回後,對信號量進行識別處理。
ret_from_sys_call:
可是為什麼我們⽤的系統調⽤,從來沒有⻅過什麼上層的函數的啊?int 0x80 或者syscall 呢?都是直接調⽤
那是因為Linux的gnuC標準庫,給我們把⼏乎所有的系統調⽤全部封裝了。
🌠 缺⻚中斷?內存碎⽚處理?除零野指針錯誤?
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);//設置並行口的陷阱門。
}
缺⻚中斷?內存碎⽚處理?除零野指針錯誤?這些問題,全部都會被轉換成為CPU內部的軟中斷, 然後⾛中斷處理例程,完成所有處理。有的是進⾏申請內存,填充⻚表,進⾏映射的。有的是⽤來 處理內存碎⽚的,有的是⽤來給⽬標進⾏發送信號,殺掉進程等等。
所以:
- 操作系統就是躺在中斷處理例程上的代碼塊!
- CPU內部的軟中斷,比如int Ox80或者syscall,我們叫做陷阱
- CPU內部的軟中斷,比如除零/野指針等,我們叫做異常。(所以,能理解“缺頁異常”為什麼這麼叫了嗎? )
🌠 如何理解內核態和⽤⼾態
結論:
- 操作系統⽆論怎麼切換進程,都能找到同⼀個操作系統!換句話説操作系統系統調⽤⽅法的執⾏,是在進程的地址空間中執⾏的!
- 關於特權級別,涉及到段,段描述符,段選擇⼦,DPL,CPL,RPL等概念,⽽現在芯⽚為了保證兼容性,已經⾮常複雜了,進⽽導致OS也必須得照顧它的複雜性,這塊我們不做深究了
- ⽤⼾態就是執⾏⽤⼾[0,3]GB時所處的狀態
- 內核態就是執⾏內核[3,4]GB時所處的狀態
- 區分就是按照CPU內的CPL決定,CPL的全稱是CurrentPrivilegeLevel,即當前特權級別。
- ⼀般執⾏int 0x80 或者
syscall軟中斷,CPL會在校驗之後⾃動變更(怎麼校驗看學⽣反映)
這樣會不會不安全??
- 關於系統調用執行位置與安全性
- 系統調用並不是在進程的地址空間中執行的。實際上,系統調用是從用户態陷入內核態,在操作系統內核空間執行的。
- 操作系統提供系統調用接口,當進程發起系統調用(如通過
int 0x80或syscall指令)時,會觸發軟中斷,硬件會將當前的執行環境(包括寄存器狀態等)保存起來,然後將控制權轉移到內核預先設置好的中斷處理程序入口。這個過程是安全的,因為:
- 首先,從用户態進入內核態有嚴格的權限檢查。在x86架構中,CPL(Current Privilege Level)會被校驗。CPL為3表示用户態,CPL為0表示內核態。當用户態進程發起系統調用時,會檢查CPL是否有足夠的權限來執行請求的系統調用。例如,對於一些關鍵的系統資源操作,只有內核態(CPL = 0)才被允許訪問。
- 其次,內核在處理系統調用時,會使用自己獨立的地址空間(對於32位系統通常是高1GB的內存空間)。這樣可以防止用户進程直接訪問和篡改內核數據和代碼,保證了操作系統的穩定性和安全性。
- 特權級別及相關機制與安全性
- 雖然特權級別涉及到段、段描述符、段選擇子、DPL(Descriptor Privilege Level)、CPL、RPL(Requested Privilege Level)等複雜概念,但這些機制本身是為了增強系統的安全性而設計的。
- 例如,DPL用於描述段的特權級別,CPL表示當前執行代碼的特權級別,RPL是請求特權級別(在訪問段時起作用)。當訪問一個段時,會進行特權級檢查,如CPL必須小於或等於DPL才能訪問,這防止了低特權級的代碼非法訪問高特權級的數據和代碼。
- 對於從用户態(CPL = 3)到內核態(CPL = 0)的轉換,通過軟中斷(如
int 0x80或syscall)的方式,系統會進行嚴格的校驗。這個校驗過程包括檢查系統調用號是否合法、參數傳遞是否符合要求等。只有通過校驗後,CPL才會被改變,從而允許進入內核態執行相應的系統調用服務例程。這種機制確保了只有合法的、經過授權的請求才能進入內核態,保障了系統的安全性。