與內(nèi)核通信
為了和用戶空間上運(yùn)行的進(jìn)程進(jìn)行交互,內(nèi)核提供了一組接口。透過該接口丛楚,應(yīng)用程序可以訪問硬件設(shè)備和其他操作系統(tǒng)資源。這組接口在應(yīng)用程序和內(nèi)核之間扮演了使者的角色魂挂,應(yīng)用程序發(fā)送各種請求,而內(nèi)核負(fù)責(zé)滿足這些需求(或者讓應(yīng)用程序暫時擱置)。實(shí)際上提供這組接口主要是為了保證系統(tǒng)穩(wěn)定可靠,避免應(yīng)用程序恣意妄行顽决,惹出大麻煩短条。
系統(tǒng)調(diào)用在用戶空間進(jìn)程和硬件設(shè)備之間添加了一個中間層,該層的主要作用有三個才菠。第一慌烧,它為用戶空間提供了一種硬件的抽象接口。第二鸠儿,系統(tǒng)調(diào)用保證了系統(tǒng)的穩(wěn)定和安全。作為硬件設(shè)備和應(yīng)用程序之間的中間人厕氨,內(nèi)核可以基于權(quán)限和其他一些規(guī)則對需要的訪問進(jìn)行裁決进每。第三,每個進(jìn)程都運(yùn)行在虛擬系統(tǒng)中命斧,而在用戶空間和系統(tǒng)的其余部分提歐共這樣的一層公共接口田晚,也是出于這種考慮,如果應(yīng)用程序可以隨意訪問硬件爾內(nèi)核又對此一無所知的話国葬,幾乎就沒法實(shí)現(xiàn)多任務(wù)和虛擬內(nèi)存贤徒,當(dāng)然也不可能實(shí)現(xiàn)良好的穩(wěn)定性和安全性。
API汇四、POSIX和C庫
1接奈、一般情況下,應(yīng)用程序通過在用戶空間實(shí)現(xiàn)的應(yīng)用編程接口(API)而不是直接通過系統(tǒng)調(diào)用來編程通孽。一個API定義了一組應(yīng)用程序使用的編程接口序宦。它可以實(shí)現(xiàn)成一個系統(tǒng)調(diào)用,也可以通過調(diào)用多個系統(tǒng)調(diào)用來實(shí)現(xiàn)背苦,而完全不使用任何系統(tǒng)調(diào)用也不存在任何問題互捌。
2、在Unix系統(tǒng)中行剂,最流行的應(yīng)用程序編程接口是基于POSIX標(biāo)準(zhǔn)的秕噪。
3、Linux的系統(tǒng)調(diào)用作為C庫的一部分提供厚宰。C庫實(shí)現(xiàn)了Unix系統(tǒng)主要API腌巾,包括標(biāo)準(zhǔn)C庫函數(shù)和系統(tǒng)調(diào)用接口。
4固阁、應(yīng)用編程與系統(tǒng)調(diào)用無關(guān)緊要壤躲,但內(nèi)核只跟系統(tǒng)調(diào)用打交道;庫函數(shù)及應(yīng)用程序是怎么使用系統(tǒng)調(diào)用的备燃,不是內(nèi)核所關(guān)心的碉克。
5、Unix接口設(shè)計(jì)有一句格言:“提供機(jī)制而不提供策略”并齐,換句話說漏麦,Unix系統(tǒng)調(diào)用抽象出了用于完成某種確定目的的函數(shù)客税。至于這些函數(shù)怎么使用完全不用內(nèi)核關(guān)心。
系統(tǒng)調(diào)用
系統(tǒng)調(diào)用(在Linux種常稱作syscalls)通常通過函數(shù)進(jìn)行調(diào)用撕贞。它們通常都需要定義一個或者多個參數(shù)更耻,而且可能產(chǎn)生一些副作用,例如寫某個文件或向給定的指針拷貝數(shù)據(jù)等等捏膨。系統(tǒng)調(diào)用還會通過一個long類型的返回值來表示成功或者錯誤秧均。通常,用一個負(fù)的返回值來表示錯誤号涯。返回一個0值表示成功目胡。Unix系統(tǒng)調(diào)用在出現(xiàn)錯誤的時候,C庫會把錯誤碼寫入errno全局變量链快,通過調(diào)用perror()庫函數(shù)誉己,可以把變量翻譯成用戶可以理解的錯誤字符串。
當(dāng)然域蜗,系統(tǒng)調(diào)用最終具有一種明確的操作:例如getpid() 系統(tǒng)調(diào)用巨双,根據(jù)定義它會返回當(dāng)前進(jìn)程的PID,內(nèi)核中他的實(shí)現(xiàn)非常簡單:
/**
* sys_getpid - return the thread group id of the current process
*
* Note, despite the name, this returns the tgid not the pid. The tgid and
* the pid are identical unless CLONE_THREAD was specified on clone() in
* which case the tgid is the same in all threads of the same group.
*
* This is SMP safe as current->tgid does not change.
*/
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);
}
SYSCALL_DEFINE0只是一個宏霉祸,它定義了一個無參數(shù)的系統(tǒng)調(diào)用(這里數(shù)字為0)筑累,展開后的代碼如下:
asmlinkage long sys_getpid(void);
首先,必須在聲明中使用asmlinkage限定詞脉执,這是一個編譯指令疼阔,通知編譯器僅從棧中提取該函數(shù)的參數(shù),所有的系統(tǒng)調(diào)用都需要這個詞半夷。
其次婆廊,函數(shù)返回值。為了保證32位和64位系統(tǒng)的兼容巫橄,系統(tǒng)調(diào)用在用戶空間和內(nèi)核空間有不同的返回值類型淘邻,用戶空間為int,內(nèi)核空間為long湘换。
最后宾舅,系統(tǒng)調(diào)用應(yīng)該被定義與sys_XX的形式。這是Linux種所有系統(tǒng)調(diào)用都應(yīng)該遵守的命名規(guī)則彩倚。
(1)系統(tǒng)調(diào)用號
1筹我、在Linux中,每個系統(tǒng)調(diào)用被賦予一個系統(tǒng)調(diào)用號帆离。通過這個系統(tǒng)調(diào)用號可以關(guān)聯(lián)系統(tǒng)調(diào)用蔬蕊。
2、系統(tǒng)調(diào)用號非常重要哥谷,一旦分配就不能再有任何變更岸夯,否則編譯好的應(yīng)用程序就會崩潰麻献。
3、如果一個系統(tǒng)調(diào)用被刪除猜扮,它所占用的系統(tǒng)調(diào)用號也不允許被回收利用勉吻,否則,以前編譯過的代碼會調(diào)用此系統(tǒng)調(diào)用旅赢,但事實(shí)上卻調(diào)用另一個系統(tǒng)調(diào)用齿桃。
4、Linux中有 一個“未實(shí)現(xiàn)”系統(tǒng)調(diào)用 sys_ni_syscall()煮盼,它除了返回-ENOSYS外不做任何事源譬,此錯誤號就是專門針對無效的系統(tǒng)調(diào)用而設(shè)的。
5孕似、內(nèi)核記錄了系統(tǒng)調(diào)用表中的所有已注冊過的系統(tǒng)調(diào)用的列表,存儲在sys_call_tabe中刮刑。
(2)系統(tǒng)調(diào)用性能
Linux系統(tǒng)調(diào)用比其他許多操作系統(tǒng)執(zhí)行的要快喉祭。
系統(tǒng)調(diào)用處理函數(shù)
1、應(yīng)用程序通知內(nèi)核的機(jī)制是靠軟中斷實(shí)現(xiàn)的:通過引發(fā)一個異常來促使系統(tǒng)切換到內(nèi)核態(tài)去執(zhí)行異常處理程序雷绢。此時的異常處理程序?qū)嶋H上就是系統(tǒng)調(diào)用處理程序泛烙。
2、指定恰當(dāng)?shù)南到y(tǒng)調(diào)用
1)翘紊、僅僅陷入內(nèi)核空間是不夠的蔽氨。必須把系統(tǒng)調(diào)用號一并傳給內(nèi)核。
2)帆疟、在X86上鹉究,系統(tǒng)調(diào)用號是通過eax寄存器傳遞給內(nèi)核的。在陷入內(nèi)核之前踪宠,用戶空間就把相應(yīng)系統(tǒng)調(diào)用所對應(yīng)的號放入eax中自赔。
3)、system_call函數(shù)通過將給定的系統(tǒng)調(diào)用號與NR_syscalls作比較來檢查其有效性柳琢。如果它大于或等于NR_syscalls绍妨,該函數(shù)就返回-ENOSYS。否則柬脸,就執(zhí)行相應(yīng)的系統(tǒng)調(diào)用他去。
call *sys_call_table( , %rax, 8);
3、參數(shù)傳遞
1)倒堕、除了系統(tǒng)調(diào)用號之外灾测,大部分系統(tǒng)調(diào)用還需要一些外部參數(shù)的輸入。再發(fā)生陷入的時候涩馆,應(yīng)該把這些參數(shù)從用戶空間中傳給內(nèi)核行施。
2)允坚、用寄存器傳遞系統(tǒng)調(diào)用。在X86系統(tǒng)上蛾号,ebx稠项、ecx、edx鲜结、esi和edi按順序存放前五個參數(shù)展运。留個或留個以上參數(shù)不常見。此外精刷,應(yīng)該用一個單獨(dú)的寄存器存放指向所有這些參數(shù)在用戶空間地址的指針拗胜。
系統(tǒng)調(diào)用的實(shí)現(xiàn)
1)、實(shí)現(xiàn)一個新的系統(tǒng)調(diào)用的第一步是決定它的用途怒允,它要做些什么埂软?每個系統(tǒng)調(diào)用應(yīng)該有一個明確的用途,Linux中不提倡多用途的系統(tǒng)調(diào)用(一個系統(tǒng)調(diào)用通過傳遞不同的參數(shù)值來完成不同的工作)纫事,ioctl 就是一個典型的反例勘畔。
2)、系統(tǒng)調(diào)用必須仔細(xì)檢查它們所有的參數(shù)是否合法有效丽惶。最重要的一種檢查就是檢查用戶提供的指針是否有效炫七,在接收一個用戶空間的指針之前,內(nèi)核必須保證:
I钾唬、指針指向的內(nèi)存區(qū)域?qū)儆谟脩艨臻g万哪。
II、指針指向的內(nèi)存區(qū)域在進(jìn)程的地址空間里抡秆。
III奕巍、如果是讀,改內(nèi)核應(yīng)被標(biāo)記為可讀儒士;如果是寫伍绳,改內(nèi)核應(yīng)被標(biāo)記為可寫;如果是可執(zhí)行乍桂,改內(nèi)核應(yīng)被標(biāo)記為可執(zhí)行冲杀。
3)、內(nèi)核提供了兩個方法來完成必須的檢查和內(nèi)核空間與用戶空間之間數(shù)據(jù)的來回拷貝睹酌。
I权谁、copy_to_user();
II、copy_from_user();
III憋沿、如果執(zhí)行失敗旺芽,這兩個函數(shù)返回的都是沒能完成拷貝的數(shù)據(jù)的字節(jié)數(shù)。如果成功,則返回0.當(dāng)出現(xiàn)上述錯誤時采章,系統(tǒng)調(diào)用返回標(biāo)準(zhǔn)-EEAULT运嗜。
4)、檢查針對是否有合法權(quán)限悯舟。
系統(tǒng)允許檢查針對特定資源的特殊權(quán)限担租,調(diào)用者可以使用ns_capable()函數(shù)來檢查是否有權(quán)能對特定的資源進(jìn)行操作。
例如:下面reboot的系統(tǒng)調(diào)用抵怎,第一步是判斷是否具有CAP_SYS_BOOT的權(quán)能奋救?
SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
void __user *, arg)
{
struct pid_namespace *pid_ns = task_active_pid_ns(current);
char buffer[256];
int ret = 0;
/* We only trust the superuser with rebooting the system. */
if (!ns_capable(pid_ns->user_ns, CAP_SYS_BOOT))
return -EPERM;
/* For safety, we require "magic" arguments. */
if (magic1 != LINUX_REBOOT_MAGIC1 ||
(magic2 != LINUX_REBOOT_MAGIC2 &&
magic2 != LINUX_REBOOT_MAGIC2A &&
magic2 != LINUX_REBOOT_MAGIC2B &&
magic2 != LINUX_REBOOT_MAGIC2C))
return -EINVAL;
/*
* If pid namespaces are enabled and the current task is in a child
* pid_namespace, the command is handled by reboot_pid_ns() which will
* call do_exit().
*/
ret = reboot_pid_ns(pid_ns, cmd);
if (ret)
return ret;
/* Instead of trying to make the power_off code look like
* halt when pm_power_off is not set do it the easy way.
*/
if ((cmd == LINUX_REBOOT_CMD_POWER_OFF) && !pm_power_off)
cmd = LINUX_REBOOT_CMD_HALT;
mutex_lock(&reboot_mutex);
switch (cmd) {
case LINUX_REBOOT_CMD_RESTART:
kernel_restart(NULL);
break;
case LINUX_REBOOT_CMD_CAD_ON:
C_A_D = 1;
break;
case LINUX_REBOOT_CMD_CAD_OFF:
C_A_D = 0;
break;
case LINUX_REBOOT_CMD_HALT:
kernel_halt();
do_exit(0);
panic("cannot halt");
case LINUX_REBOOT_CMD_POWER_OFF:
kernel_power_off();
do_exit(0);
break;
case LINUX_REBOOT_CMD_RESTART2:
ret = strncpy_from_user(&buffer[0], arg, sizeof(buffer) - 1);
if (ret < 0) {
ret = -EFAULT;
break;
}
buffer[sizeof(buffer) - 1] = '\0';
kernel_restart(buffer);
break;
#ifdef CONFIG_KEXEC
case LINUX_REBOOT_CMD_KEXEC:
ret = kernel_kexec();
break;
#endif
#ifdef CONFIG_HIBERNATION
case LINUX_REBOOT_CMD_SW_SUSPEND:
ret = hibernate();
break;
#endif
default:
ret = -EINVAL;
break;
}
mutex_unlock(&reboot_mutex);
return ret;
}
系統(tǒng)調(diào)用上下文
系統(tǒng)調(diào)用運(yùn)行在進(jìn)程上下文,所以可以休眠反惕,可以被搶占尝艘,所以要保證該系統(tǒng)調(diào)用時可重入的。
1姿染、綁定一個系統(tǒng)調(diào)用的最后步驟
1)背亥、把系統(tǒng)調(diào)用注冊成一個正式的系統(tǒng)調(diào)用:
I、首先悬赏,在系統(tǒng)調(diào)用表的最后加入一個表項(xiàng)隘梨。
II、對于所支持的各種體系結(jié)構(gòu)舷嗡,系統(tǒng)調(diào)用號都必須定義于<asm/unistd.h>中。
III嵌莉、系統(tǒng)調(diào)用必須被編譯進(jìn)內(nèi)核映像(不能編譯成模塊)进萄。
2、從用戶空間訪問系統(tǒng)調(diào)用
1)锐峭、用戶程序通過包含標(biāo)準(zhǔn)頭文件和C庫連接中鼠,就可以使用系統(tǒng)調(diào)用。
2)沿癞、Linux本身提供一個宏援雇,用于直接對系統(tǒng)調(diào)用進(jìn)行訪問。
以Android系統(tǒng)一個reboot系統(tǒng)調(diào)用為例椎扬,應(yīng)用程序調(diào)用reboot系統(tǒng)調(diào)用的方法如下:
ret = syscall(__NR_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2,
LINUX_REBOOT_CMD_RESTART2, arg);
系統(tǒng)調(diào)用號的宏定義:位于文件/bionic/libc/kernel/uapi/asm-arm/asm/unistd.h
其中:
#define __NR_reboot 142
匯編定義相關(guān)函數(shù)的中斷調(diào)用過程:syscall位于文件/bionic/libc/arch-arm64/bionic/syscall.S惫搏,內(nèi)容如下:
ENTRY(syscall)
/* Move syscall No. from x0 to x8 */
mov x8, x0
/* Move syscall parameters from x1 thru x6 to x0 thru x5 */
mov x0, x1
mov x1, x2
mov x2, x3
mov x3, x4
mov x4, x5
mov x5, x6
svc #0
/* check if syscall returned successfully */
cmn x0, #(MAX_ERRNO + 1)
cneg x0, x0, hi
b.hi __set_errno_internal
ret
END(syscall)
3、不通過系統(tǒng)調(diào)用的方式實(shí)現(xiàn)的原因蚕涤。
盡量不要自己添加系統(tǒng)調(diào)用筐赔,有很多其他方法可以替代:
1)實(shí)現(xiàn)一個設(shè)備節(jié)點(diǎn),對設(shè)備進(jìn)行read,write操作揖铜,使用ioctl對特定的設(shè)置進(jìn)行操作茴丰。
2)把增加的信息作為一個文件放在sysfs中。