日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

Linux系统调用相关概念

發布時間:2024/1/23 linux 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Linux系统调用相关概念 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄:

1. Linux系統調用原理

2. 系統調用的實現

3. Linux系統調用分類及列表

4.系統調用、用戶編程接口(API)、系統命令和內核函數的關系

5. Linux系統調用實例

6. Linux自定義系統調用

1.系統調用原理

系統調用,顧名思義,說的是操作系統提供給用戶程序調用的一組“特殊”接口。用戶程序可以通過這組“特殊”接口來獲得操作系統內核提供的服務,比如用戶可以通過文件系統相關的調用請求系統打開文件、關閉文件或讀寫文件,可以通過時鐘相關的系統調用獲得系統時間或設置定時器等。

從邏輯上來說,系統調用可被看成是一個內核與用戶空間程序交互的接口——它好比一個中間人,把用戶進程的請求傳達給內核,待內核把請求處理完畢后再將處理結果送回給用戶空間。

系統服務之所以需要通過系統調用來提供給用戶空間的根本原因是為了對系統進行“保護”,因為我們知道Linux的運行空間分為內核空間與用戶空間,它們各自運行在不同的級別中,邏輯上相互隔離。所以用戶進程在通常情況下不允許訪問內核數據,也無法使用內核函數,它們只能在用戶空間操作用戶數據,調用用戶空間函數。比如我們熟悉的“hello world”程序(執行時)就是標準的用戶空間進程,它使用的打印函數printf就屬于用戶空間函數,打印的字符“hello word”字符串也屬于用戶空間數據。

但是很多情況下,用戶進程需要獲得系統服務(調用系統程序),這時就必須利用系統提供給用戶的“特殊接口”——系統調用了,它的特殊性主要在于規定了用戶進程進入內核的具體位置;換句話說,用戶訪問內核的路徑是事先規定好的,只能從規定位置進入內核,而不準許肆意跳入內核。有了這樣的陷入內核的統一訪問路徑限制才能保證內核安全無虞。我們可以形象地描述這種機制:作為一個游客,你可以買票要求進入野生動物園,但你必須老老實實地坐在觀光車上,按照規定的路線觀光游覽。當然,不準下車,因為那樣太危險,不是讓你丟掉小命,就是讓你嚇壞了野生動物。

備注:

  • 在一些嵌入式操作系統中,操作系統往往通過API的形式提供給用戶一些接口,然后通過靜態鏈接的方式實現對系統的調用,因此這種模式系統態和用戶態不明顯,即用戶可以在其線程中直接調用系統的函數,并沒有切換到內核態。

2.系統調用的實現

Linux中實現系統調用利用了0x86體系結構中的軟件中斷。軟件中斷和我們常說的中斷(硬件中斷)不同之處在于,它是通過軟件指令觸發而并非外設引發的中斷,也就是說,又是編程人員開發出的一種異常(該異常為正常的異常),具體的講就是調用int $0x80匯編指令,這條匯編指令將產生向量為0x80的編程異常。

之所以系統調用需要借助異常來實現,是因為當用戶態的進程調用一個系統調用時,CPU便被切換到內核態執行內核函數,而我們在i386體系結構部分已經講述過了進入內核——進入高特權級別——必須經過系統的門機制,這里的異常實際上就是通過系統門陷入內核(除了int 0x80外用戶空間還可以通過int3——向量3、into——向量4 、bound——向量5等異常指令進入內核,而其他異常無法被用戶空間程序利用,都是由系統使用的)。

我們更詳細地解釋一下這個過程。int $0x80指令的目的是產生一個編號為0x80的編程異常,這個編程異常對應的是中斷描述符表IDT中的第128項——也就是對應的系統門描述符。門描述符中含有一個預設的內核空間地址,它指向了系統調用處理程序:system_call()(別和系統調用服務程序混淆,這個程序在entry.S文件中用匯編語言編寫)。

很顯然,所有的系統調用都會統一地轉到這個地址,但Linux一共有2、3百個系統調用都從這里進入內核后又該如何派發到它們到各自的服務程序去呢?別發昏,解決這個問題的方法非常簡單:首先Linux為每個系統調用都進行了編號(0—NR_syscall),同時在內核中保存了一張系統調用表,該表中保存了系統調用編號和其對應的服務例程,因此在系統調入通過系統門陷入內核前,需要把系統調用號一并傳入內核,在x86上,這個傳遞動作是通過在執行int0x80前把調用號裝入eax寄存器實現的。這樣系統調用處理程序一旦運行,就可以從eax中得到數據,然后再去系統調用表中尋找相應服務例程了。

除了需要傳遞系統調用號以外,許多系統調用還需要傳遞一些參數到內核,比如sys_write(unsigned int fd, const char * buf, size_t count)調用就需要傳遞文件描述符fd、要寫入的內容buf、以及寫入字節數count等幾個內容到內核。碰到這種情況,Linux會有6個寄存器可被用來傳遞這些參數:eax (存放系統調用號)、 ebx、ecx、edx、esi及edi來存放這些額外的參數(以字母遞增的順序)。具體做法是在system_call( )中使用SAVE_ALL宏把這些寄存器的值保存在內核態堆棧中.

備注:

  • 系統調用其實很簡單,就是所以操作系統的API都是通過軟件的中斷動態的調用,通過調用int $0x80 觸發軟件中斷,然后通過一些寄存器將參數傳入,實現對操作系統API的調用。
  • 在嵌入式操作系統中有軟中斷的概念,該軟中斷是指將硬中斷中次優先級的任務交給軟中斷處理,其運行于系統棧中,優先級高于任務,和本章所提及的軟件中斷有很大的區別,軟件中斷處理和硬中斷處理流程相同,只是該中斷由軟件觸發。

3.系統調用、用戶編程接口(API)、系統命令和內核函數的關系

系統調用并非直接和程序員或系統管理員打交道,它僅僅是一個通過軟中斷機制(我們后面講述)向內核提交請求,獲取內核服務的接口。而在實際使用中程序員調用的多是用戶編程接口——API,而管理員使用的則多是系統命令。

用戶編程接口其實是一個函數定義,說明了如何獲得一個給定的服務,比如read( )、malloc( )、free( )、abs( )等。它有可能和系統調用形式上一致,比如read()接口就和read系統調用對應,但這種對應并非一一對應,往往會出現幾種不同的API內部用到同一個系統調用,比如malloc( )、free( )內部利用brk( )系統調用來擴大或縮小進程的堆;或一個API利用了好幾個系統調用組合完成服務。更有些API甚至不需要任何系統調用——因為它并不是必需要使用內核服務,如計算整數絕對值的abs()接口。

另外要補充的是Linux的用戶編程接口遵循了在Unix世界中最流行的應用編程界面標準——POSIX標準,這套標準定義了一系列API。在Linux中(Unix也如此),這些API主要是通過C庫(libc)實現的,它除了定義的一些標準的C函數外,一個很重要的任務就是提供了一套封裝例程(wrapper routine)將系統調用在用戶空間包裝后供用戶編程使用。

下一個需要解釋一下的問題是內核函數和系統調用的關系。大家不要把內核函數想像的過于復雜,其實它們和普通函數很像,只不過在內核實現,因此要滿足一些內核編程的要求。系統調用是一層用戶進入內核的接口,它本身并非內核函數,進入內核后,不同的系統調用會找到對應到各自的內核函數——換個專業說法就叫:系統調用服務例程。實際上針對請求提供服務的是內核函數而非調用接口。

比如系統調用 getpid實際上就是調用內核函數sys_getpid。

asmlinkage long sys_getpid(void)

{

??? return current->tpid;

}

Linux系統中存在許多內核函數,有些是內核文件中自己使用的,有些則是可以export出來供內核其他部分共同使用的,具體情況自己決定。

內核公開的內核函數——export出來的——可以使用命令ksyms 或 cat /proc/ksyms來查看。另外,網上還有一本歸納分類內核函數的書叫作《The Linux Kernel API Book》,有興趣的讀者可以去看看。

??? 總而言之,從用戶角度向內核看,依次是系統命令、編程接口、系統調用和內核函數。在講述了系統調用實現后,我們會回過頭來看看整個執行路徑。

備注:

  • 內核函數是操作系統自己使用的一些函數,它不對外展現,不提供給用戶使用,因此接口可以變化。
  • 用戶編程接口API是直接呈現給用戶的接口,它可以使用多個系統調用構造出一個API,也可以一個系統調用被多個API使用,同時API也不可以使用系統調用,Linux的API有別于ucos操作系統的API,后者直接調用API函數進行靜態連接,系統代碼也連接到API中。
  • 命令在我看來應該是可執行的程序,它單獨將API編譯成可執行的文件進行處理。

4. Linux系統調用分類及列表

以下是Linux系統調用的一個列表,包含了大部分常用系統調用和由系統調用派生出的的函數。這可能是你在互聯網上所能看到的唯一一篇中文注釋的Linux系統調用列表,即使是簡單的字母序英文列表,能做到這么完全也是很罕見的。

按照慣例,這個列表以manpages第2節,即系統調用節為藍本。按照筆者的理解,對其作了大致的分類,同時也作了一些小小的修改,刪去了幾個僅供內核使用,不允許用戶調用的系統調用,對個別本人稍覺不妥的地方作了一些小的修改,并對所有列出的系統調用附上簡要注釋。

其中有一些函數的作用完全相同,只是參數不同。(可能很多熟悉C++朋友馬上就能聯想起函數重載,但是別忘了Linux核心是用C語言寫的,所以只能取成不同的函數名)。還有一些函數已經過時,被新的更好的函數所代替了(gcc在鏈接這些函數時會發出警告),但因為兼容的原因還保留著,這些函數我會在前面標上“*”號以示區別。

Linux系統調用很多地方繼承了Unix的系統調用,但Linux相比傳統Unix的系統調用做了很多揚棄,它省去了許多Unix系統冗余的系統調用,僅僅保留了最基本和最有用的系統調用,所以Linux全部系統調用只有250個左右(而有些操作系統系統調用多達1000個以上)。

系統調用主要分為以下幾類:

  • 控制硬件——系統調用往往作為硬件資源和用戶空間的抽象接口,比如讀寫文件時用到的write/read調用。
  • 設置系統狀態或讀取內核數據——因為系統調用是用戶空間和內核的唯一通訊手段,所以用戶設置系統狀態,比如開/關某項內核服務(設置某個內核變量),或讀取內核數據都必須通過系統調用。比如getpgid、getpriority、setpriority、sethostname
  • 進程管理——一系統調用接口是用來保證系統中進程能以多任務在虛擬內存環境下得以運行。比如 fork、clone、execve、exit等


2.1進程控制:

fork創建一個新進程
clone按指定條件創建子進程
execve運行可執行文件
exit中止進程
_exit立即中止當前進程
getdtablesize進程所能打開的最大文件數
getpgid獲取指定進程組標識號
setpgid設置指定進程組標志號
getpgrp獲取當前進程組標識號
setpgrp設置當前進程組標志號
getpid獲取進程標識號
getppid獲取父進程標識號
getpriority獲取調度優先級
setpriority設置調度優先級
modify_ldt讀寫進程的本地描述表
nanosleep使進程睡眠指定的時間
nice改變分時進程的優先級
pause掛起進程,等待信號
personality設置進程運行域
prctl對進程進行特定操作
ptrace進程跟蹤
sched_get_priority_max取得靜態優先級的上限
sched_get_priority_min取得靜態優先級的下限
sched_getparam取得進程的調度參數
sched_getscheduler取得指定進程的調度策略
sched_rr_get_interval取得按RR算法調度的實時進程的時間片長度
sched_setparam設置進程的調度參數
sched_setscheduler設置指定進程的調度策略和參數
sched_yield進程主動讓出處理器,并將自己等候調度隊列隊尾
vfork創建一個子進程,以供執行新程序,常與execve等同時使用
wait等待子進程終止
wait3參見wait
waitpid等待指定子進程終止
wait4參見waitpid
capget獲取進程權限
capset設置進程權限
getsid獲取會晤標識號
setsid設置會晤標識號

1.2文件操作

fcntl文件控制
open打開文件
creat創建新文件
close關閉文件描述字
read讀文件
write寫文件
readv從文件讀入數據到緩沖數組中
writev將緩沖數組里的數據寫入文件
pread對文件隨機讀
pwrite對文件隨機寫
lseek移動文件指針
_llseek在64位地址空間里移動文件指針
dup復制已打開的文件描述字
dup2按指定條件復制文件描述字
flock文件加/解鎖
pollI/O多路轉換
truncate截斷文件
ftruncate參見truncate
umask設置文件權限掩碼
fsync把文件在內存中的部分寫回磁盤


1.3文件系統操作
access確定文件的可存取性
chdir改變當前工作目錄
fchdir參見chdir
chmod改變文件方式
fchmod參見chmod
chown改變文件的屬主或用戶組
fchown參見chown
lchown參見chown
chroot改變根目錄
stat取文件狀態信息
lstat參見stat
fstat參見stat
statfs取文件系統信息
fstatfs參見statfs
readdir讀取目錄項
getdents讀取目錄項
mkdir創建目錄
mknod創建索引節點
rmdir刪除目錄
rename文件改名
link創建鏈接
symlink創建符號鏈接
unlink刪除鏈接
readlink讀符號鏈接的值
mount安裝文件系統
umount卸下文件系統
ustat取文件系統信息
utime改變文件的訪問修改時間
utimes參見utime
quotactl控制磁盤配額


1.4系統控制
ioctlI/O總控制函數
_sysctl讀/寫系統參數
acct啟用或禁止進程記賬
getrlimit獲取系統資源上限
setrlimit設置系統資源上限
getrusage獲取系統資源使用情況
uselib選擇要使用的二進制函數庫
ioperm設置端口I/O權限
iopl改變進程I/O權限級別
outb低級端口操作
reboot重新啟動
swapon打開交換文件和設備
swapoff關閉交換文件和設備
bdflush控制bdflush守護進程
sysfs取核心支持的文件系統類型
sysinfo取得系統信息
adjtimex調整系統時鐘
alarm設置進程的鬧鐘
getitimer獲取計時器值
setitimer設置計時器值
gettimeofday取時間和時區
settimeofday設置時間和時區
stime設置系統日期和時間
time取得系統時間
times取進程運行時間
uname獲取當前UNIX系統的名稱、版本和主機等信息
vhangup掛起當前終端
nfsservctl對NFS守護進程進行控制
vm86進入模擬8086模式
create_module創建可裝載的模塊項
delete_module刪除可裝載的模塊項
init_module初始化模塊
query_module查詢模塊信息
*get_kernel_syms取得核心符號,已被query_module代替

1.5 內存管理

brk改變數據段空間的分配
sbrk參見brk
mlock內存頁面加鎖
munlock內存頁面解鎖
mlockall調用進程所有內存頁面加鎖
munlockall調用進程所有內存頁面解鎖
mmap映射虛擬內存頁
munmap去除內存頁映射
mremap重新映射虛擬內存地址
msync將映射內存中的數據寫回磁盤
mprotect設置內存映像保護
getpagesize獲取頁面大小
sync將內存緩沖區數據寫回硬盤
cacheflush將指定緩沖區中的內容寫回磁盤

1.6網絡管理
getdomainname取域名
setdomainname設置域名
gethostid獲取主機標識號
sethostid設置主機標識號
gethostname獲取本主機名稱
sethostname設置主機名稱

socketcallsocket系統調用
socket建立socket
bind綁定socket到端口
connect連接遠程主機
accept響應socket連接請求
send通過socket發送信息
sendto發送UDP信息
sendmsg參見send
recv通過socket接收信息
recvfrom接收UDP信息
recvmsg參見recv
listen監聽socket端口
select對多路同步I/O進行輪詢
shutdown關閉socket上的連接
getsockname取得本地socket名字
getpeername獲取通信對方的socket名字
getsockopt取端口設置
setsockopt設置端口參數
sendfile在文件或端口間傳輸數據
socketpair創建一對已聯接的無名socket

1.7 用戶管理
getuid獲取用戶標識號
setuid設置用戶標志號
getgid獲取組標識號
setgid設置組標志號
getegid獲取有效組標識號
setegid設置有效組標識號
geteuid獲取有效用戶標識號
seteuid設置有效用戶標識號
setregid分別設置真實和有效的的組標識號
setreuid分別設置真實和有效的用戶標識號
getresgid分別獲取真實的,有效的和保存過的組標識號
setresgid分別設置真實的,有效的和保存過的組標識號
getresuid分別獲取真實的,有效的和保存過的用戶標識號
setresuid分別設置真實的,有效的和保存過的用戶標識號
setfsgid設置文件系統檢查時使用的組標識號
setfsuid設置文件系統檢查時使用的用戶標識號
getgroups獲取后補組標志清單
setgroups設置后補組標志清單


八、進程間通信

ipc進程間通信總控制調用



1、信號
sigaction設置對指定信號的處理方法
sigprocmask根據參數對信號集中的信號執行阻塞/解除阻塞等操作
sigpending為指定的被阻塞信號設置隊列
sigsuspend掛起進程等待特定信號
signal參見signal
kill向進程或進程組發信號
*sigblock向被阻塞信號掩碼中添加信號,已被sigprocmask代替
*siggetmask取得現有阻塞信號掩碼,已被sigprocmask代替
*sigsetmask用給定信號掩碼替換現有阻塞信號掩碼,已被sigprocmask代替
*sigmask將給定的信號轉化為掩碼,已被sigprocmask代替
*sigpause作用同sigsuspend,已被sigsuspend代替
sigvec為兼容BSD而設的信號處理函數,作用類似sigaction
ssetmaskANSI C的信號處理函數,作用類似sigaction



2、消息
msgctl消息控制操作
msgget獲取消息隊列
msgsnd發消息
msgrcv取消息



3、管道
pipe創建管道



4、信號量
semctl信號量控制
semget獲取一組信號量
semop信號量操作



5、共享內存
shmctl控制共享內存
shmget獲取共享內存
shmat連接共享內存
shmdt拆卸共享內存

5.Linux系統調用實例

在前面的文章中,我們已經了解了父進程和子進程的概念,并已經掌握了系統調用exit的用法,但可能很少有人意識到,在一個進程調用了 exit之后,該進程并非馬上就消失掉,而是留下一個稱為僵尸進程(Zombie)的數據結構。在Linux進程的5種狀態中,僵尸進程是非常特殊的一 種,它已經放棄了幾乎所有內存空間,沒有任何可執行代碼,也不能被調度,僅僅在進程列表中保留一個位置,記載該進程的退出狀態等信息供其他進程收集,除此 之外,僵尸進程不再占有任何內存空間。從這點來看,僵尸進程雖然有一個很酷的名字,但它的影響力遠遠抵不上那些真正的僵尸兄弟,真正的僵尸總能令人感到恐 怖,而僵尸進程卻除了留下一些供人憑吊的信息,對系統毫無作用。

也許讀者們還對這個新概念比較好奇,那就讓我們來看一眼Linux里的僵尸進程究竟長什么樣子。

備注:僵尸進程就是被刪除了任務,它釋放了任務棧空間,不再被任務調用,然而它只占用幾十個字節的任務控制塊內存空間。

當一個進程已退出,但其父進程還沒有調用系統調用wait(稍后介紹)對其進行收集之前的這段時間里,它會一直保持僵尸狀態,利用這個特點,我們來寫一個簡單的小程序:

/* zombie.c */ #include <sys/types.h> #include <unistd.h> main() {pid_t pid;pid = fork();if(pid < 0) /* 如果出錯 */printf("error occurred!\n");else if(pid == 0) /* 如果是子進程 */exit(0);else /* 如果是父進程 */sleep(60); /* 休眠60秒,這段時間里,父進程什么也干不了 */wait(NULL); /* 收集僵尸進程 */ }

sleep的作用是讓進程休眠指定的秒數,在這60秒內,子進程已經退出,而父進程正忙著睡覺,不可能對它進行收集,這樣,我們就能保持子進程60秒的僵尸狀態。

編譯這個程序:

$ cc zombie.c -o zombie

后臺運行程序,以使我們能夠執行下一條命令

$ ./zombie & [1] 1577

列一下系統內的進程

$ ps -ax... ...1177 pts/0 S 0:00 -bash1577 pts/0 S 0:00 ./zombie1578 pts/0 Z 0:00 [zombie <defunct>]1579 pts/0 R 0:00 ps -ax

看到中間的"Z"了嗎?那就是僵尸進程的標志,它表示1578(任務PID)號進程現在就是一個僵尸進程。

我們已經學習了系統調用exit,它的作用是使進程退出,但也僅僅限于將一個正常的進程變成一個僵尸進程,并不能將其完全銷毀。僵尸進 程雖然對其他進程幾乎沒有什么影響,不占用CPU時間,消耗的內存也幾乎可以忽略不計,但有它在那里呆著,還是讓人覺得心里很不舒服。而且Linux系統 中進程數目是有限制的,在一些特殊的情況下,如果存在太多的僵尸進程,也會影響到新進程的產生。那么,我們該如何來消滅這些僵尸進程呢?

先來了解一下僵尸進程的來由,我們知道,Linux和UNIX總有著剪不斷理還亂的親緣關系,僵尸進程的概念也是從UNIX上繼承來 的,而UNIX的先驅們設計這個東西并非是因為閑來無聊想煩煩其他的程序員。僵尸進程中保存著很多對程序員和系統管理員非常重要的信息,首先,這個進程是 怎么死亡的?是正常退出呢,還是出現了錯誤,還是被其它進程強迫退出的?其次,這個進程占用的總系統CPU時間和總用戶CPU時間分別是多少?發生頁錯誤 的數目和收到信號的數目。這些信息都被存儲在僵尸進程中,試想如果沒有僵尸進程,進程一退出,所有與之相關的信息都立刻歸于無形,而此時程序員或系統管理 員需要用到,就只好干瞪眼了。

那么,我們如何收集這些信息,并終結這些僵尸進程呢?就要靠我們下面要講到的waitpid調用和wait調用。這兩者的作用都是收集僵尸進程留下的信息,同時使這個進程徹底消失。下面就對這兩個調用分別作詳細介紹。

wait系統調用介紹

wait的函數原型是:

#include <sys/types.h> /* 提供類型pid_t的定義 */ #include <sys/wait.h> pid_t wait(int *status)

進程一旦調用了wait,就立即阻塞自己,由wait自動分析是否當前進程的某個子進程已經退出,如果讓它找到了這樣一個已經變成僵尸 的子進程,wait就會收集這個子進程的信息,并把它徹底銷毀后返回;如果沒有找到這樣一個子進程,wait就會一直阻塞在這里,直到有一個出現為止。

參數status用來保存被收集進程退出時的一些狀態,它是一個指向int類型的指針。但如果我們對這個子進程是如何死掉的毫不在意,只想把這個僵尸進程消滅掉,(事實上絕大多數情況下,我們都會這樣想),我們就可以設定這個參數為NULL,就象下面這樣:

pid = wait(NULL);

如果成功,wait會返回被收集的子進程的進程ID,如果調用進程沒有子進程,調用就會失敗,此時wait返回-1,同時errno被置為ECHILD。

1.8.2 實戰

下面就讓我們用一個例子來實戰應用一下wait調用,程序中用到了系統調用fork,如果你對此不大熟悉或已經忘記了,請參考上一篇文章《進程管理相關的系統調用(一)》。

/* wait1.c */ #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> main() {pid_t pc,pr;pc=fork();if(pc<0) /* 如果出錯 */printf("error ocurred!\n");else if(pc==0){ /* 如果是子進程 */ printf("This is child process with pid of %d\n",getpid());sleep(10); /* 睡眠10秒鐘 */}else{ /* 如果是父進程 */pr=wait(NULL); /* 在這里等待 */printf("I catched a child process with pid of %d\n"),pr);} exit(0); }

編譯并運行:

$ cc wait1.c -o wait1 $ ./wait1 This is child process with pid of 1508 I catched a child process with pid of 1508

可以明顯注意到,在第2行結果打印出來前有10秒鐘的等待時間,這就是我們設定的讓子進程睡眠的時間,只有子進程從睡眠中蘇醒過來,它 才能正常退出,也就才能被父進程捕捉到。其實這里我們不管設定子進程睡眠的時間有多長,父進程都會一直等待下去,讀者如果有興趣的話,可以試著自己修改一 下這個數值,看看會出現怎樣的結果。

1.8.3 參數status

如果參數status的值不是NULL,wait就會把子進程退出時的狀態取出并存入其中,這是一個整數值(int),指出了子進程是 正常退出還是被非正常結束的(一個進程也可以被其他進程用信號結束,我們將在以后的文章中介紹),以及正常結束時的返回值,或被哪一個信號結束的等信息。 由于這些信息被存放在一個整數的不同二進制位中,所以用常規的方法讀取會非常麻煩,人們就設計了一套專門的宏(macro)來完成這項工作,下面我們來學 習一下其中最常用的兩個:

1,WIFEXITED(status) 這個宏用來指出子進程是否為正常退出的,如果是,它會返回一個非零值。

(請注意,雖然名字一樣,這里的參數status并不同于wait唯一的參數--指向整數的指針status,而是那個指針所指向的整數,切記不要搞混了。)

2,WEXITSTATUS(status) 當WIFEXITED返回非零值時,我們可以用這個宏來提取子進程的返回值,如果子進程調用exit(5)退出,WEXITSTATUS(status) 就會返回5;如果子進程調用exit(7),WEXITSTATUS(status)就會返回7。請注意,如果進程不是正常退出的,也就是 說,WIFEXITED返回0,這個值就毫無意義。

下面通過例子來實戰一下我們剛剛學到的內容:

/* wait2.c */ #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> main() {int status;pid_t pc,pr;pc=fork();if(pc<0) /* 如果出錯 */printf("error ocurred!\n");else if(pc==0){ /* 子進程 */printf("This is child process with pid of %d.\n",getpid());exit(3); /* 子進程返回3 */}else{ /* 父進程 */pr=wait(&status);if(WIFEXITED(status)){ /* 如果WIFEXITED返回非零值 */printf("the child process %d exit normally.\n",pr);printf("the return code is %d.\n",WEXITSTATUS(status));}else /* 如果WIFEXITED返回零 */printf("the child process %d exit abnormally.\n",pr);} }

編譯并運行:

$ cc wait2.c -o wait2 $ ./wait2 This is child process with pid of 1538. the child process 1538 exit normally. the return code is 3.

父進程準確捕捉到了子進程的返回值3,并把它打印了出來。

當然,處理進程退出狀態的宏并不止這兩個,但它們當中的絕大部分在平時的編程中很少用到,就也不在這里浪費篇幅介紹了,有興趣的讀者可以自己參閱Linux man pages去了解它們的用法。

1.8.4 進程同步

有時候,父進程要求子進程的運算結果進行下一步的運算,或者子進程的功能是為父進程提供了下一步執行的先決條件(如:子進程建立文件, 而父進程寫入數據),此時父進程就必須在某一個位置停下來,等待子進程運行結束,而如果父進程不等待而直接執行下去的話,可以想見,會出現極大的混亂。這 種情況稱為進程之間的同步,更準確地說,這是進程同步的一種特例。進程同步就是要協調好2個以上的進程,使之以安排好地次序依次執行。解決進程同步問題有 更通用的方法,我們將在以后介紹,但對于我們假設的這種情況,則完全可以用wait系統調用簡單的予以解決。請看下面這段程序:

#include <sys/types.h> #include <sys/wait.h> main() {pid_t pc, pr;int status;pc=fork();if(pc<0)printf("Error occured on forking.\n");else if(pc==0){/* 子進程的工作 */exit(0);}else{/* 父進程的工作 */pr=wait(&status);/* 利用子進程的結果 */} }

這段程序只是個例子,不能真正拿來執行,但它卻說明了一些問題,首先,當fork調用成功后,父子進程各做各的事情,但當父進程的工作 告一段落,需要用到子進程的結果時,它就停下來調用wait,一直等到子進程運行結束,然后利用子進程的結果繼續執行,這樣就圓滿地解決了我們提出的進程 同步問題。

1.9 waitpid

1.9.1 簡介

waitpid系統調用在Linux函數庫中的原型是:

#include <sys/types.h> /* 提供類型pid_t的定義 */#include <sys/wait.h>pid_t waitpid(pid_t pid,int *status,int options)

從本質上講,系統調用waitpid和wait的作用是完全相同的,但waitpid多出了兩個可由用戶控制的參數pid和options,從而為我們編程提供了另一種更靈活的方式。下面我們就來詳細介紹一下這兩個參數:

pid

從參數的名字pid和類型pid_t中就可以看出,這里需要的是一個進程ID。但當pid取不同的值時,在這里有不同的意義。

  • pid>0時,只等待進程ID等于pid的子進程,不管其它已經有多少子進程運行結束退出了,只要指定的子進程還沒有結束,waitpid就會一直等下去。
  • pid=-1時,等待任何一個子進程退出,沒有任何限制,此時waitpid和wait的作用一模一樣。
  • pid=0時,等待同一個進程組中的任何子進程,如果子進程已經加入了別的進程組,waitpid不會對它做任何理睬。
  • pid<-1時,等待一個指定進程組中的任何子進程,這個進程組的ID等于pid的絕對值。
  • options

    options提供了一些額外的選項來控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED兩個選項,這是兩個常數,可以用"|"運算符把它們連接起來使用,比如:

    ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);

    如果我們不想使用它們,也可以把options設為0,如:

    ret=waitpid(-1,NULL,0);

    如果使用了WNOHANG參數調用waitpid,即使沒有子進程退出,它也會立即返回,不會像wait那樣永遠等下去。

    而WUNTRACED參數,由于涉及到一些跟蹤調試方面的知識,加之極少用到,這里就不多費筆墨了,有興趣的讀者可以自行查閱相關材料。

    看到這里,聰明的讀者可能已經看出端倪了--wait不就是經過包裝的waitpid嗎?沒錯,察看<內核源碼目錄>/include/unistd.h文件349-352行就會發現以下程序段:

    static inline pid_t wait(int * wait_stat) {return waitpid(-1,wait_stat,0); }

    1.9.2 返回值和錯誤

    waitpid的返回值比wait稍微復雜一些,一共有3種情況:

  • 當正常返回的時候,waitpid返回收集到的子進程的進程ID;
  • 如果設置了選項WNOHANG,而調用中waitpid發現沒有已退出的子進程可收集,則返回0;
  • 如果調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在;
  • 當pid所指示的子進程不存在,或此進程存在,但不是調用進程的子進程,waitpid就會出錯返回,這時errno被設置為ECHILD;

    /* waitpid.c */ #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> main() {pid_t pc, pr;pc=fork();if(pc<0) /* 如果fork出錯 */printf("Error occured on forking.\n");else if(pc==0){ /* 如果是子進程 */sleep(10); /* 睡眠10秒 */exit(0);}/* 如果是父進程 */do{pr=waitpid(pc, NULL, WNOHANG); /* 使用了WNOHANG參數,waitpid不會在這里等待 */if(pr==0){ /* 如果沒有收集到子進程 */printf("No child exited\n");sleep(1);}}while(pr==0); /* 沒有收集到子進程,就回去繼續嘗試 */if(pr==pc)printf("successfully get child %d\n", pr);elseprintf("some error occured\n"); }

    編譯并運行:

    $ cc waitpid.c -o waitpid $ ./waitpid No child exited No child exited No child exited No child exited No child exited No child exited No child exited No child exited No child exited No child exited successfully get child 1526

    父進程經過10次失敗的嘗試之后,終于收集到了退出的子進程。

    因為這只是一個例子程序,不便寫得太復雜,所以我們就讓父進程和子進程分別睡眠了10秒鐘和1秒鐘,代表它們分別作了10秒鐘和1秒鐘的工作。父子進程都有工作要做,父進程利用工作的簡短間歇察看子進程的是否退出,如退出就收集它。

    1.10 exec

    也許有不少讀者從本系列文章一推出就開始讀,一直到這里還有一個很大的疑惑:既然所有新進程都是由fork產生的,而且由fork產生 的子進程和父進程幾乎完全一樣,那豈不是意味著系統中所有的進程都應該一模一樣了嗎?而且,就我們的常識來說,當我們執行一個程序的時候,新產生的進程的 內容應就是程序的內容才對。是我們理解錯了嗎?顯然不是,要解決這些疑惑,就必須提到我們下面要介紹的exec系統調用。

    1.10.1 簡介

    說是exec系統調用,實際上在Linux中,并不存在一個exec()的函數形式,exec指的是一組函數,一共有6個,分別是:

    #include <unistd.h> int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);

    其中只有execve是真正意義上的系統調用,其它都是在此基礎上經過包裝的庫函數。

    exec函數族的作用是根據指定的文件名找到可執行文件,并用它來取代調用進程的內容,換句話說,就是在調用進程內部執行一個可執行文件。這里的可執行文件既可以是二進制文件,也可以是任何Linux下可執行的腳本文件。

    與一般情況不同,exec函數族的函數執行成功后不會返回,因為調用進程的實體,包括代碼段,數據段和堆棧等都已經被新的內容取代,只 留下進程ID等一些表面上的信息仍保持原樣,頗有些神似"三十六計"中的"金蟬脫殼"。看上去還是舊的軀殼,卻已經注入了新的靈魂。只有調用失敗了,它們 才會返回一個-1,從原程序的調用點接著往下執行。

    現在我們應該明白了,Linux下是如何執行新程序的,每當有進程認為自己不能為系統和擁護做出任何貢獻了,他就可以發揮最后一點余 熱,調用任何一個exec,讓自己以新的面貌重生;或者,更普遍的情況是,如果一個進程想執行另一個程序,它就可以fork出一個新進程,然后調用任何一 個exec,這樣看起來就好像通過執行應用程序而產生了一個新進程一樣。

    事實上第二種情況被應用得如此普遍,以至于Linux專門為其作了優化,我們已經知道,fork會將調用進程的所有內容原封不動的拷貝 到新產生的子進程中去,這些拷貝的動作很消耗時間,而如果fork完之后我們馬上就調用exec,這些辛辛苦苦拷貝來的東西又會被立刻抹掉,這看起來非常 不劃算,于是人們設計了一種"寫時拷貝(copy-on-write)"技術,使得fork結束后并不立刻復制父進程的內容,而是到了真正實用的時候才復 制,這樣如果下一條語句是exec,它就不會白白作無用功了,也就提高了效率。

    1.10.2 稍稍深入

    上面6條函數看起來似乎很復雜,但實際上無論是作用還是用法都非常相似,只有很微小的差別。在學習它們之前,先來了解一下我們習以為常的main函數。

    下面這個main函數的形式可能有些出乎我們的意料:

    int main(int argc, char *argv[], char *envp[])

    它可能與絕大多數教科書上描述的都不一樣,但實際上,這才是main函數真正完整的形式。

    參數argc指出了運行該程序時命令行參數的個數,數組argv存放了所有的命令行參數,數組envp存放了所有的環境變量。環境變量 指的是一組值,從用戶登錄后就一直存在,很多應用程序需要依靠它來確定系統的一些細節,我們最常見的環境變量是PATH,它指出了應到哪里去搜索應用程 序,如/bin;HOME也是比較常見的環境變量,它指出了我們在系統中的個人目錄。環境變量一般以字符串"XXX=xxx"的形式存在,XXX表示變量 名,xxx表示變量的值。

    值得一提的是,argv數組和envp數組存放的都是指向字符串的指針,這兩個數組都以一個NULL元素表示數組的結尾。

    我們可以通過以下這個程序來觀看傳到argc、argv和envp里的都是什么東西:

    /* main.c */ int main(int argc, char *argv[], char *envp[]) {printf("\n### ARGC ###\n%d\n", argc);printf("\n### ARGV ###\n");while(*argv)printf("%s\n", *(argv++));printf("\n### ENVP ###\n");while(*envp)printf("%s\n", *(envp++));return 0; }

    編譯它:

    $ cc main.c -o main

    運行時,我們故意加幾個沒有任何作用的命令行參數:

    $ ./main -xx 000 ### ARGC ### 3 ### ARGV ### ./main -xx 000 ### ENVP ### PWD=/home/lei REMOTEHOST=dt.laser.com HOSTNAME=localhost.localdomain QTDIR=/usr/lib/qt-2.3.1 LESSOPEN=|/usr/bin/lesspipe.sh %s KDEDIR=/usr USER=lei LS_COLORS= MACHTYPE=i386-redhat-linux-gnu MAIL=/var/spool/mail/lei INPUTRC=/etc/inputrc LANG=en_US LOGNAME=lei SHLVL=1 SHELL=/bin/bash HOSTTYPE=i386 OSTYPE=linux-gnu HISTSIZE=1000 TERM=ansi HOME=/home/lei PATH=/usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/home/lei/bin _=./main

    我們看到,程序將"./main"作為第1個命令行參數,所以我們一共有3個命令行參數。這可能與大家平時習慣的說法有些不同,小心不要搞錯了。

    現在回過頭來看一下exec函數族,先把注意力集中在execve上:

    int execve(const char *path, char *const argv[], char *const envp[]);

    對比一下main函數的完整形式,看出問題了嗎?是的,這兩個函數里的argv和envp是完全一一對應的關系。execve第1個參 數path是被執行應用程序的完整路徑,第2個參數argv就是傳給被執行應用程序的命令行參數,第3個參數envp是傳給被執行應用程序的環境變量。

    留心看一下這6個函數還可以發現,前3個函數都是以execl開頭的,后3個都是以execv開頭的,它們的區別在于,execv開頭 的函數是以"char *argv[]"這樣的形式傳遞命令行參數,而execl開頭的函數采用了我們更容易習慣的方式,把參數一個一個列出來,然后以一個NULL表示結束。這 里的NULL的作用和argv數組里的NULL作用是一樣的。

    在全部6個函數中,只有execle和execve使用了char *envp[]傳遞環境變量,其它的4個函數都沒有這個參數,這并不意味著它們不傳遞環境變量,這4個函數將把默認的環境變量不做任何修改地傳給被執行的 應用程序。而execle和execve會用指定的環境變量去替代默認的那些。

    還有2個以p結尾的函數execlp和execvp,咋看起來,它們和execl與execv的差別很小,事實也確是如此,除 execlp和execvp之外的4個函數都要求,它們的第1個參數path必須是一個完整的路徑,如"/bin/ls";而execlp和execvp 的第1個參數file可以簡單到僅僅是一個文件名,如"ls",這兩個函數可以自動到環境變量PATH制定的目錄里去尋找。

    1.10.3 實戰

    知識介紹得差不多了,接下來我們看看實際的應用:

    /* exec.c */ #include <unistd.h> main() {char *envp[]={"PATH=/tmp", "USER=lei", "STATUS=testing", NULL};char *argv_execv[]={"echo", "excuted by execv", NULL};char *argv_execvp[]={"echo", "executed by execvp", NULL};char *argv_execve[]={"env", NULL};if(fork() == 0){ if(execl("/bin/echo", "echo", "executed by execl", NULL) < 0)perror("Err on execl"); } if(fork() == 0)
    {
    if(execlp("echo", "echo", "executed by execlp", NULL) < 0)perror("Err on execlp"); } if(fork() == 0)
    {
    if(execle("/usr/bin/env", "env", NULL, envp) < 0)perror("Err on execle"); } if(fork() == 0)
    {
    if(execv("/bin/echo", argv_execv) < 0)perror("Err on execv"); } if(fork() == 0)
    {
    if(execvp("echo", argv_execvp) < 0)perror("Err on execvp"); } if(fork() == 0)
    {
    if(execve("/usr/bin/env", argv_execve, envp) < 0)perror("Err on execve"); } }

    程序里調用了2個Linux常用的系統命令,echo和env。echo會把后面跟的命令行參數原封不動的打印出來,env用來列出所有環境變量。

    由于各個子進程執行的順序無法控制,所以有可能出現一個比較混亂的輸出--各子進程打印的結果交雜在一起,而不是嚴格按照程序中列出的次序。

    編譯并運行:

    $ cc exec.c -o exec $ ./exec executed by execl PATH=/tmp USER=lei STATUS=testing executed by execlp excuted by execv executed by execvp PATH=/tmp USER=lei STATUS=testing

    果然不出所料,execle輸出的結果跑到了execlp前面。

    大家在平時的編程中,如果用到了exec函數族,一定記得要加錯誤判斷語句。因為與其他系統調用比起來,exec很容易受傷,被執行文件的位置,權限等很多因素都能導致該調用的失敗。最常見的錯誤是:

  • 找不到文件或路徑,此時errno被設置為ENOENT;
  • 數組argv和envp忘記用NULL結束,此時errno被設置為EFAULT;
  • 沒有對要執行文件的運行權限,此時errno被設置為EACCES。
  • 1.11 進程的一生

    下面就讓我用一些形象的比喻,來對進程短暫的一生作一個小小的總結:

    隨著一句fork,一個新進程呱呱落地,但它這時只是老進程的一個克隆。

    然后隨著exec,新進程脫胎換骨,離家獨立,開始了為人民服務的職業生涯。

    人有生老病死,進程也一樣,它可以是自然死亡,即運行到main函數的最后一個"}",從容地離我們而去;也可以是自殺,自殺有2種方 式,一種是調用exit函數,一種是在main函數內使用return,無論哪一種方式,它都可以留下遺書,放在返回值里保留下來;它還甚至能可被謀殺, 被其它進程通過另外一些方式結束他的生命。

    進程死掉以后,會留下一具僵尸,wait和waitpid充當了殮尸工,把僵尸推去火化,使其最終歸于無形。

    這就是進程完整的一生。

    1.12 小結

    本文重點介紹了系統調用wait、waitpid和exec函數族,對與進程管理相關的系統調用的介紹就在這里告一段落,在下一篇文章,也是與進程管理相關的系統調用的最后一篇文章中,我們會通過兩個很酷的實際例子,來重溫一下最近學過的知識。


    5.Linux自定義系統調用

    如果用戶在Linux中添加新的系統調用,應該遵循幾個步驟才能添加成功,下面幾個步驟詳細說明了添加系統調用的相關內容。

    2.1添加源代碼

    第一個任務是編寫加到內核中的源程序,即將要加到一個內核文件中去的一個函數,該函數的名稱應該是新的系統調用名稱前面加上sys_標志。假設新加的系統調用為mycall(int number),在/usr/src/linux/kernel/sys.c文件中添加源代碼,如下所示:

    asmlinkage int sys_mycall(int number)

    {

     return number;

    }

    作為一個最簡單的例子,我們新加的系統調用僅僅返回一個整型值。

    2.2 連接新的系統調用

    添加新的系統調用后,下一個任務是使Linux內核的其余部分知道該程序的存在。為了從已有的內核程序中增加到新的函數的連接,需要編輯兩個文件。

    在我們所用的Linux內核版本(RedHat 6.0,內核為2.2.5-15)中,第一個要修改的文件是:

    /usr/src/linux/include/asm-i386/unistd.h

    該文件中包含了系統調用清單,用來給每個系統調用分配一個唯一的號碼。文件中每一行的格式如下:

    #define __NR_name NNN

    其中,name用系統調用名稱代替,而NNN則是該系統調用對應的號碼。應該將新的系統調用名稱加到清單的最后,并給它分配號碼序列中下一個可用的系統調用號。我們的系統調用如下:

    #define __NR_mycall 191

    系統調用號為191,之所以系統調用號是191,是因為Linux-2.2內核自身的系統調用號碼已經用到190。

    第二個要修改的文件是:

    /usr/src/linux/arch/i386/kernel/entry.S

    該文件中有類似如下的清單:

    .long SYMBOL_NAME()

    該清單用來對sys_call_table[]數組進行初始化。該數組包含指向內核中每個系統調用的指針。這樣就在數組中增加了新的內核函數的指針。我們在清單最后添加一行:

    .long SYMBOL_NAME(sys_mycall)


    2.3 重建新的Linux內核

    為使新的系統調用生效,需要重建Linux的內核。這需要以超級用戶身份登錄。

    #pwd

    /usr/src/linux

    #

    超級用戶在當前工作目錄(/usr/src/linux)下,才可以重建內核。

    #make config

    #make dep

    #make clearn

    #make bzImage

    編譯完畢后,系統生成一可用于安裝的、壓縮的內核映象文件:

    /usr/src/linux/arch/i386/boot/bzImage 

    2.4用新的內核啟動系統

    要使用新的系統調用,需要用重建的新內核重新引導系統。為此,需要修改/etc/lilo.conf文件,在我們的系統中,該文件內容如下:

      boot=/dev/hda

      map=/boot/map

      install=/boot/boot.b

      prompt

      timeout=50

      image=/boot/vmlinuz-2.2.5-15

      label=linux

      root=/dev/hdb1

      read-only

      other=/dev/hda1

      label=dos

      table=/dev/had

      首先編輯該文件,添加新的引導內核:

      image=/boot/bzImage-new

      label=linux-new

      root=/dev/hdb1

      read-only

      添加完畢,該文件內容如下所示:

      boot=/dev/hda

      map=/boot/map

      install=/boot/boot.b

      prompt

      timeout=50

      image=/boot/bzImage-new

      label=linux-new

      root=/dev/hdb1

      read-only

      image=/boot/vmlinuz-2.2.5-15

      label=linux

      root=/dev/hdb1

      read-only

      other=/dev/hda1

      label=dos

      table=/dev/hda

      這樣,新的內核映象bzImage-new成為缺省的引導內核。

      為了使用新的lilo.conf配置文件,還應執行下面的命令:

      #cp /usr/src/linux/arch/i386/boot/zImage /boot/bzImage-new

      其次配置lilo:

      # /sbin/lilo

      現在,當重新引導系統時,在boot:提示符后面有三種選擇:linux-new 、 linux、dos,新內核成為缺省的引導內核。

      至此,新的Linux內核已經建立,新添加的系統調用已成為操作系統的一部分,重新啟動Linux,用戶就可以在應用程序中使用該系統調用了。

    2.5使用新的系統調用

    在應用程序中使用新添加的系統調用mycall。同樣為實驗目的,我們寫了一個簡單的例子xtdy.c。

    ?????? /* xtdy.c */

      #include $#@60;linux/unistd.h$#@62;

      _syscall1(int,mycall,int,ret)

      main()

      {

      printf("%d \n",mycall(100));

      }

      編譯該程序:

      # cc -o xtdy xtdy.c

      執行:

      # xtdy

      結果:

      # 100

    注意,由于使用了系統調用,編譯和執行程序時,用戶都應該是超級用戶身份。


    總結

    以上是生活随笔為你收集整理的Linux系统调用相关概念的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。