與內核通信
為了和用戶空間上運行的進程進行交互,內核提供了一組接口。透過該接口,應用程序可以訪問硬件設備和其他操作系統資源。這組接口在應用程序和內核之間扮演了使者的角色,應用程序發送各種請求,而內核負責滿足這些需求(或者讓應用程序暫時擱置)。實際上提供這組接口主要是為了保證系統穩定可靠,避免應用程序恣意妄行,惹出大麻煩。
系統調用在用戶空間進程和硬件設備之間添加了一個中間層,該層的主要作用有三個。第一,它為用戶空間提供了一種硬件的抽象接口。第二,系統調用保證了系統的穩定和安全。作為硬件設備和應用程序之間的中間人,內核可以基于權限和其他一些規則對需要的訪問進行裁決。第三,每個進程都運行在虛擬系統中,而在用戶空間和系統的其余部分提歐共這樣的一層公共接口,也是出于這種考慮,如果應用程序可以隨意訪問硬件爾內核又對此一無所知的話,幾乎就沒法實現多任務和虛擬內存,當然也不可能實現良好的穩定性和安全性。
API、POSIX和C庫
1、一般情況下,應用程序通過在用戶空間實現的應用編程接口(API)而不是直接通過系統調用來編程。一個API定義了一組應用程序使用的編程接口。它可以實現成一個系統調用,也可以通過調用多個系統調用來實現,而完全不使用任何系統調用也不存在任何問題。
2、在Unix系統中,最流行的應用程序編程接口是基于POSIX標準的。
3、Linux的系統調用作為C庫的一部分提供。C庫實現了Unix系統主要API,包括標準C庫函數和系統調用接口。
4、應用編程與系統調用無關緊要,但內核只跟系統調用打交道;庫函數及應用程序是怎么使用系統調用的,不是內核所關心的。
5、Unix接口設計有一句格言:“提供機制而不提供策略”,換句話說,Unix系統調用抽象出了用于完成某種確定目的的函數。至于這些函數怎么使用完全不用內核關心。
系統調用
系統調用(在Linux種常稱作syscalls)通常通過函數進行調用。它們通常都需要定義一個或者多個參數,而且可能產生一些副作用,例如寫某個文件或向給定的指針拷貝數據等等。系統調用還會通過一個long類型的返回值來表示成功或者錯誤。通常,用一個負的返回值來表示錯誤。返回一個0值表示成功。Unix系統調用在出現錯誤的時候,C庫會把錯誤碼寫入errno全局變量,通過調用perror()庫函數,可以把變量翻譯成用戶可以理解的錯誤字符串。
當然,系統調用最終具有一種明確的操作:例如getpid() 系統調用,根據定義它會返回當前進程的PID,內核中他的實現非常簡單:
/**
* 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只是一個宏,它定義了一個無參數的系統調用(這里數字為0),展開后的代碼如下:
asmlinkage long sys_getpid(void);
首先,必須在聲明中使用asmlinkage限定詞,這是一個編譯指令,通知編譯器僅從棧中提取該函數的參數,所有的系統調用都需要這個詞。
其次,函數返回值。為了保證32位和64位系統的兼容,系統調用在用戶空間和內核空間有不同的返回值類型,用戶空間為int,內核空間為long。
最后,系統調用應該被定義與sys_XX的形式。這是Linux種所有系統調用都應該遵守的命名規則。
(1)系統調用號
1、在Linux中,每個系統調用被賦予一個系統調用號。通過這個系統調用號可以關聯系統調用。
2、系統調用號非常重要,一旦分配就不能再有任何變更,否則編譯好的應用程序就會崩潰。
3、如果一個系統調用被刪除,它所占用的系統調用號也不允許被回收利用,否則,以前編譯過的代碼會調用此系統調用,但事實上卻調用另一個系統調用。
4、Linux中有 一個“未實現”系統調用 sys_ni_syscall(),它除了返回-ENOSYS外不做任何事,此錯誤號就是專門針對無效的系統調用而設的。
5、內核記錄了系統調用表中的所有已注冊過的系統調用的列表,存儲在sys_call_tabe中。
(2)系統調用性能
Linux系統調用比其他許多操作系統執行的要快。
系統調用處理函數
1、應用程序通知內核的機制是靠軟中斷實現的:通過引發一個異常來促使系統切換到內核態去執行異常處理程序。此時的異常處理程序實際上就是系統調用處理程序。
2、指定恰當的系統調用
1)、僅僅陷入內核空間是不夠的。必須把系統調用號一并傳給內核。
2)、在X86上,系統調用號是通過eax寄存器傳遞給內核的。在陷入內核之前,用戶空間就把相應系統調用所對應的號放入eax中。
3)、system_call函數通過將給定的系統調用號與NR_syscalls作比較來檢查其有效性。如果它大于或等于NR_syscalls,該函數就返回-ENOSYS。否則,就執行相應的系統調用。
call *sys_call_table( , %rax, 8);
3、參數傳遞
1)、除了系統調用號之外,大部分系統調用還需要一些外部參數的輸入。再發生陷入的時候,應該把這些參數從用戶空間中傳給內核。
2)、用寄存器傳遞系統調用。在X86系統上,ebx、ecx、edx、esi和edi按順序存放前五個參數。留個或留個以上參數不常見。此外,應該用一個單獨的寄存器存放指向所有這些參數在用戶空間地址的指針。
系統調用的實現
1)、實現一個新的系統調用的第一步是決定它的用途,它要做些什么?每個系統調用應該有一個明確的用途,Linux中不提倡多用途的系統調用(一個系統調用通過傳遞不同的參數值來完成不同的工作),ioctl 就是一個典型的反例。
2)、系統調用必須仔細檢查它們所有的參數是否合法有效。最重要的一種檢查就是檢查用戶提供的指針是否有效,在接收一個用戶空間的指針之前,內核必須保證:
I、指針指向的內存區域屬于用戶空間。
II、指針指向的內存區域在進程的地址空間里。
III、如果是讀,改內核應被標記為可讀;如果是寫,改內核應被標記為可寫;如果是可執行,改內核應被標記為可執行。
3)、內核提供了兩個方法來完成必須的檢查和內核空間與用戶空間之間數據的來回拷貝。
I、copy_to_user();
II、copy_from_user();
III、如果執行失敗,這兩個函數返回的都是沒能完成拷貝的數據的字節數。如果成功,則返回0.當出現上述錯誤時,系統調用返回標準-EEAULT。
4)、檢查針對是否有合法權限。
系統允許檢查針對特定資源的特殊權限,調用者可以使用ns_capable()函數來檢查是否有權能對特定的資源進行操作。
例如:下面reboot的系統調用,第一步是判斷是否具有CAP_SYS_BOOT的權能?
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;
}
系統調用上下文
系統調用運行在進程上下文,所以可以休眠,可以被搶占,所以要保證該系統調用時可重入的。
1、綁定一個系統調用的最后步驟
1)、把系統調用注冊成一個正式的系統調用:
I、首先,在系統調用表的最后加入一個表項。
II、對于所支持的各種體系結構,系統調用號都必須定義于<asm/unistd.h>中。
III、系統調用必須被編譯進內核映像(不能編譯成模塊)。
2、從用戶空間訪問系統調用
1)、用戶程序通過包含標準頭文件和C庫連接,就可以使用系統調用。
2)、Linux本身提供一個宏,用于直接對系統調用進行訪問。
以Android系統一個reboot系統調用為例,應用程序調用reboot系統調用的方法如下:
ret = syscall(__NR_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2,
LINUX_REBOOT_CMD_RESTART2, arg);
系統調用號的宏定義:位于文件/bionic/libc/kernel/uapi/asm-arm/asm/unistd.h
其中:
#define __NR_reboot 142
匯編定義相關函數的中斷調用過程:syscall位于文件/bionic/libc/arch-arm64/bionic/syscall.S,內容如下:
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、不通過系統調用的方式實現的原因。
盡量不要自己添加系統調用,有很多其他方法可以替代:
1)實現一個設備節點,對設備進行read,write操作,使用ioctl對特定的設置進行操作。
2)把增加的信息作為一個文件放在sysfs中。