LINUX下的系统调用
注: 我这里看到源码是linux 2.6.11版本的源码;
系统调用是什么东西,这个才能开始后续工作。
系统调用举个例子更容易理解,假如你想找个“铁饭碗”,你是不是得托人给领导送礼呀。你个平头百姓怎么会平时接触到那些大领导呢,你肯定是费尽心机地找到了一个“铁关系”,这个“铁关系”作为中间人,将你的意思传达给大领导,大领导帮你开个后门,你是不是就有了一个”铁饭碗“。
这个”铁关系“就是这里的系统调用,然后这里的你就是用户模式,而这里的大领导肯定就是内核模式。
系统调用的定义就很简单了,系统调用时发生在用户进程通过调用特殊函数,以请求内核提供服务的时候。此时用户态被暂时挂起(我替你办事 ,你肯定得等呀),内核检验用户请求(看你的要求过不过分),尝试执行。并把结果返回给用户(将好消息带回去),接着你就可以带着你的好消息启动了。
解决完概念问题,那么来研究一下,这个内核是怎么给你帮忙的;
系统调用的分类:
1.处理I/O请求:open、close、read、write、poll
2.进程fork、execve、kill
3.时间time、settimeofday
4.内存mmap、brk
开始之前先说点简单的,带回来的消息;
带回来的消息就一定是好消息吗?系统调用必须返回类型是int,你想一下你平时使用的系统函数,不是int就是int封装的类型。那么返回值0或者正数就是好消息,如果返回来的是负数就是坏消息。其实在最新的内核版本里面返回负数不一定代表错误,lseek函数即使结果正确也会返回一个很大的负数,而错误的返回码一般在-1到-4095之间。
再来点专业的术语,不然一会用到的时候不理解;
2.LINUX下是通过int 0x80来触发所有的系统调用
3.系统调用号,是通过寄存器eax传入内核的。
万事俱备只欠东风;
先来个系统调用的大致图解吧(看图简单)fork为例:
根据内核代码分析:
中断向量表------->sys_call函数
内核代码:Linux/arch/i386/kernel/traps.c
初始化中断向量表
void __init trap_init(void)
{
#ifdef CONFIG_EISA
void __iomem *p = ioremap(0x0FFFD9, 4);
if (readl(p) == 'E'+('I'<<8)+('S'<<16)+('A'<<24)) {
EISA_bus = 1;
}
iounmap(p);
#endif
#ifdef CONFIG_X86_LOCAL_APIC
init_apic_mappings();
#endif
set_trap_gate(0,÷_error);
set_intr_gate(1,&debug);
set_intr_gate(2,&nmi);
set_system_intr_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_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);
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_intr_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
#ifdef CONFIG_X86_MCE
set_trap_gate(18,&machine_check);
#endif
set_trap_gate(19,&simd_coprocessor_error);
set_system_gate(SYSCALL_VECTOR,&system_call); //SYSCALL_VECTOR是一个宏定义,
//#define SYSCALL_VECTOR 0x80 ,对应的是系统调用中断号是0x80,最终执行的sys_call函数。
/*
* Should be a barrier for any external CPU state.
*/
cpu_init();
trap_init_hook();
sys_call函数的实现:
sys_call---->sys_call_table
内核代码:Linux/arch/i386/kernel/unistd.h
#define _syscall1(type,name) \
type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \ //__asm__ 是一个GCC的额关键字,表示接下来要嵌入汇编代码
//_asm_ 函数的第一个参数是一个字符串,表示汇编代码
//volatile 告诉编译器不要对代码进行优化。
: "=a" (__res) \ //将eax寄存器的数据,存在_res上。“a”表示用eax寄存器
: "0" (__NR_##name)); \ //_NR_##name 拼接字符串,“0”表示编译器选择输入、输出用相同的寄存器来传递参数。
__syscall_return(type,__res); \
}
检查系统调用的返回值,并把它转化成相应的errno错误码。
#define __syscall_return(type, res) \
do { \
if ((unsigned long)(res) >= (unsigned long)(-(128 + 1))) { \
errno = -(res); \
res = -1; \
} \
return (type) (res); \
} while (0)
有一个参数时,参数存在ebx中。
linux下支持6个参数的传递,分别用寄存器ebx、ecx、edx、esi、edi、ebp
#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1))); \ //"b"表示EBX寄存器,
__syscall_return(type,__res); \
}
sys_call内部调用sys_call_table的实现:
内核代码:Linux/arch/i386/kernel/entry.s
sys_call函数的实现:
# system call handler stub
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL //将各种寄存器的值压入栈,以避免后续执行的代码的覆盖。
GET_THREAD_INFO(%ebp)
# system call tracing in operation
testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax //nr_syscalls表示最大的系统调用号加1.。eax存放的是用户传进来的系统调用号。
jae syscall_badsys
//系统调用号确定有效后
syscall_call:
call *sys_call_table(,%eax,4) //sys_call_table是查找中断服务程序并执行。
movl %eax,EAX(%esp) # store the return value
syscall_exit:
cli # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
movl TI_flags(%ebp), %ecx
testw $_TIF_ALLWORK_MASK, %cx # current->work
jne syscall_exit_work
restore_all:
RESTORE_ALL //RESTORE_ALL系统恢复
# perform work that needs to be done immediately before resumption
ALIGN
sys_call_table调用sys_fork实现fork函数
内核代码:Linux/arch/i386/kernel/traps.c
中断服务程序
sys_call_table(0,%eax,4) ;
/*
1.函数的第一个参数:系统调用函数的地址;
给出的例子:指的是syscall_table上偏移量为0+eax * 4 的值指向的函数 ;也是eax寄存器里面存的系统调用号所对应的系统函数;
*/
.data
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long sys_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
.long sys_close
.long sys_waitpid
.long sys_creat
.long sys_link
.long sys_unlink /* 10 */
.long sys_execve
.long sys_chdir
.long sys_time
........
.long sys_request_key
.long sys_keyctl
syscall_table_size=(.-sys_call_table)
SAVE_ALL保存寄存器
#define SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__USER_DS), %edx; \ //后面这三条mov指令,都是设置内核的数据段
movl %edx, %ds; \
movl %edx, %es;
//函数倒数6个 寄存器,刚好是系统调用传递参数的6个寄存器。
//所以系统调用的参数是通过SAVE_ALL保存在栈上的。并且在调用sys_call_table时,没有任何代码影响到栈
asmlinkage这个宏
asmlinkage int sys_fork(struct pt_regs regs);
宏定义
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
asmlinkage意思:只让函数从栈上获取参数。
部分细节代码分析结束,感觉好混乱呀,一大堆代码来一张思路图,让代码走势有线路;
简单根据代码画出总思路图(代码走向图);
补充一张堆栈切换图;
系统调用大致就是这样子,有些地方还没有解析很仔细,会继续改进的。