Linux 系统调用的执行过程
什么是系统调用系统调用 (在 Linux 中常称为 syscalls ) 是应用程序访问硬件设备之间的桥梁。系统调用层为用户空间提供一种硬件的抽象接口,使得用户不用关注设备的具体信息,同时系统调用保证了系统的稳定和安全。在 Linux 中,除了异常和陷入外,系统调用是用户空间访问内核的唯一手段。实际上,其他的像设备文件和 /proc 之类的方式,最终也还是要通过系统调用的方式进行访问。系统调用号在
什么是系统调用
系统调用 (在 Linux 中常称为 syscalls ) 是应用程序访问硬件设备之间的桥梁。
系统调用层为用户空间提供一种硬件的抽象接口,使得用户不用关注设备的具体信息,同时系统调用保证了系统的稳定和安全。
在 Linux 中,除了异常和陷入外,系统调用是用户空间访问内核的唯一手段。
实际上,其他的像设备文件和 /proc 之类的方式,最终也还是要通过系统调用的方式进行访问。
系统调用号
在 Linux 中,每个系统调用被赋予一个系统调用号。通过这个独一无二的调用号就可以关联具体的系统调用。
在用户空间执行一个系统调用时候,这个系统调用号就被用来指明到底是要执行哪个系统调用,进程不会提及系统调用的名称。
系统调用号一旦分配就不能再有任何改变,否则编译好的应用程序就会崩溃。
在内核中通过系统调用表来记录所有已注册过的系统调用的列表,存储在 sys_call_table 中。它与体系结构有关,一般在 entry.s 中定义。这个表中为每一个有效的系统调用制定了一个唯一的系统调用号。
rch\x86\kernel\syscall_table_32.S
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_mknod
.long sys_chmod / 15 */
...
应用程序依靠软中断的方式通知内核要进行系统调用。
该通知内核的机制通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。
在 x86 系统上软中断由 int $0X80 指令产生(int 指令是程序用来显式声明软中断的,故而所谓的“基于int指令的系统调用”便是来源于此)。这条指令会触发一个异常导致系统切换到内核态并执行第 128 号异常处理程序,而该程序正是系统调用的处理程序,名为 system_call()。它与硬件体系结构紧密相关。
在 x86 上,系统调用号是通过 eax 寄存器传递给内核的。在陷入内核之前,用户空间就把相应的系统调用号放到 eax 中了。这样系统调用程序一旦运行,就可以从 eax 获取系统调用号。
system_call() 通过将给定的系统调用号与 NR_syscalls 做比较来检查其有效性。若它大于或等于 NR_syscalls,该函数就返回 -ENOSYS。否则,就执行相应的系统调用。
call *sys_call_table(, %eax, 4)
由于系统调用表中的表项是以 32 位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以 4,然后用所得的结果在该表中查询其位置。
如以 read()调用过程如下:
参数传递
由于用户空间和内核空间使用不同的栈空间,因此系统调用的参数需要使用寄存器进行传递。在 x86 系统上,ebx、ecx、edx、esi 和 edi 按照顺序存放前5个参数。若参数大于或等于6个,需要用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。
给用户空间发的返回值也通过寄存器传递。在 x86 系统上,它存放在 eax 寄存器中。若系统调用产生大量的数据不能通过返回机制传递给用户进程,那必须通过指定的内存区交换该数据。当然,该内存区必须在用户空间中,使得用户应用层序能够访问。
在内核访问自身的内存区时,虚拟地址和物理内存页之间的映射总是存在的。但用户空间中的情况有所不同,页可能被换出,甚至可能尚未分配物理内存页。
因而内核不能简单的反引用用户空间的指针,而必须采用特定的函数,确保目标内存区已经在物理内存中,为确保这种约定,用户空间指针通过_user属性标记,以支持 C check tools 对源代码的自动化检查。
大多数情况下,用户在用户空间和内核空间之间复制数据的函数使用copy_to_user() 和 copy_from_user(),但还有更多的变体。
注意 copy_to_user() 和 copy_from_user() 都有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。
初始化
Linux 内核在启动过程中会对向量中断进行初始化,该初始化 trap_init 中会对系统调用设置中断号,SYSCALL_VECTOR 就是 0x80 中断号。
void __init trap_init(void)
{
int i;
...
set_system_gate(SYSCALL_VECTOR,&system_call);
...
cpu_init();
trap_init_hook();
}
而 system_call 具体实现在 arch\x86\kernel\entry_32.S 中。
系统调用过程
当用户进程调用一个系统调用时,用户进程会触发一个中断向量号为 0x80 的软中断,最终会执行 system_call 函数。
在实际执行中断向量表中的第 0x80 号所对应的 system_call 函数前,CPU 首先还要进行栈的切换。在 Linux 中,用户态和内核态使用的是不同的栈,两者各自负责各自的函数调用,互不干扰。
在int指令中,CPU 除了切入内核态之外,还要找到当前进程的内核栈,在内核栈中依次压入当前进程用户态的寄存器 SS(Stack Segment,堆栈段寄存器)、ESP、EFLAGS、CS(Code Segment,代码段寄存器 )、EIP。这些中断指令自动地由硬件完成。
当然,当内核从系统调用中返回的时候,需要调用 iret 指令来回到用户态,iret 指令则从内核栈中弹出 SS、ESP、EFLAGS、CS、EIP 的值,使得栈恢复到用户态的状态。
当 CPU 在 int 指令中切换了栈后,程序通过 0x80 从中断向量表中获取中断处理程序,也即是 system_call(),该函数在 arch\x86\kernel\entry_32.S 中。
# system call handler stub
ENTRY(system_call) #执行int 0x80的下一条指令
RING0_INT_FRAME # can’t unwind into user space anyway
pushl %eax # save orig_eax
CFI_ADJUST_CFA_OFFSET 4
SAVE_ALL #将所有寄存器的值在内核态栈上保存,也就是所谓的保存现场
# 通过宏获取当前进程的thread_info 结构地址 #define GET_THREAD_INFO(reg) movl $-THREAD_SIZE, reg; andl %esp, reg
GET_THREAD_INFO(%ebp) # ebp用于存放当前进程thread_info结构的地址
# system call tracing in operation / emulation
/* Note, _TIF_SECCOMP is bit number 8, and so it needs testw and not testb */
#检测当前进程是否被跟踪,也即是_TIF_SYSCALL_TRACE、_TIF_SYSCALL_AUDIT 被置1,若发生被跟踪情况则转向相应的处理命令处
testw $(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
jnz syscall_trace_entry
# 对用户进程传递过来的系统调用号进行合法性检查,若不合法,则跳到syscall_badsys处
cmpl $(nr_syscalls), %eax
jae syscall_badsys # 不合法,跳入到异常处理
#若系统调用号合法,则跳入到相应系统调用号所对应的服务历程当中,也即是从 sys_call_table表中找到相应的入口函数。
syscall_call:
call sys_call_table(,%eax,4) #由于表中的表项占4个字节,因此获取服务历程的方法为:sys_call_table表基地址+%eax系统调用号4
# %eax保存的是当前系统调用返回值,把该返回值保存在曾保存用户态eax寄存器值的那个栈单元位置上。用户态就可以从eax寄存器中获取系统调用的返回码了。
movl %eax,PT_EAX(%esp) # store the return value
syscall_exit:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don’t miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
TRACE_IRQS_OFF
testl $TF_MASK,PT_EFLAGS(%esp) # If tracing set singlestep flag on exit
jz no_singlestep
orl $_TIF_SINGLESTEP,TI_flags(%ebp)
no_singlestep:
movl TI_flags(%ebp), %ecx
testw $_TIF_ALLWORK_MASK, %cx # 检查当前进程是否还有工作没有完成,若有,跳到 syscall_exit_work
jne syscall_exit_work #进程调度时机
restore_all:
movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
# Warning: PT_OLDSS(%esp) contains the wrong/random values if we
# are returning to the kernel.
# See comments in process.c:copy_thread() for details.
movb PT_OLDSS(%esp), %ah
movb PT_CS(%esp), %al
andl $(VM_MASK | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
CFI_REMEMBER_STATE
je ldt_ss # returning to user-space with LDT SS
restore_nocheck:
TRACE_IRQS_IRET
restore_nocheck_notrace:
RESTORE_REGS # 恢复了save_all保存的所有寄存器的值
addl $4, %esp # skip orig_eax/error_code
CFI_ADJUST_CFA_OFFSET -4
1: INTERRUPT_RETURN #中断返回 相当于iret,程序将回到用户态继续执行
.section .fixup,“ax”
iret_exc:
pushl $0 # no error code
pushl $do_iret_error
jmp error_code
.previous
.section __ex_table,“a”
.align 4
.long 1b,iret_exc
.previous
CFI_RESTORE_STATE
ldt_ss:
larl PT_OLDSS(%esp), %eax
jnz restore_nocheck
testl $0x00400000, %eax # returning to 32bit stack?
jnz restore_nocheck # allright, normal return
#ifdef CONFIG_PARAVIRT
/*
* The kernel can’t run on a non-flat stack if paravirt mode
* is active. Rather than try to fixup the high bits of
* ESP, bypass this code entirely. This may break DOSemu
* and/or Wine support in a paravirt VM, although the option
* is still available to implement the setting of the high
* 16-bits in the INTERRUPT_RETURN paravirt-op.
*/
cmpl $0, pv_info+PARAVIRT_enabled
jne restore_nocheck
#endif
/* If returning to userspace with 16bit stack,
* try to fix the higher word of ESP, as the CPU
* won't restore it.
* This is an "official" bug of all the x86-compatible
* CPUs, which we can try to work around to make
* dosemu and wine happy. */
movl PT_OLDESP(%esp), %eax
movl %esp, %edx
call patch_espfix_desc
pushl $__ESPFIX_SS
CFI_ADJUST_CFA_OFFSET 4
pushl %eax
CFI_ADJUST_CFA_OFFSET 4
DISABLE_INTERRUPTS(CLBR_EAX)
TRACE_IRQS_OFF
lss (%esp), %esp
CFI_ADJUST_CFA_OFFSET -8
jmp restore_nocheck
CFI_ENDPROC
ENDPROC(system_call)
# perform work that needs to be done immediately before resumption
ALIGN
RING0_PTREGS_FRAME # can't unwind into user space anyway
work_pending:
testb $_TIF_NEED_RESCHED, %cl #判断是否需要进程调度
jz work_notifysig
work_resched:
call schedule #执行进程调度
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don’t miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
TRACE_IRQS_OFF # 关闭中断跟踪
movl TI_flags(%ebp), %ecx # 检测是否还有其他任务
andl $_TIF_WORK_MASK, %ecx # is there any work to be done other
# than syscall tracing?
jz restore_all # 返回restore_all
testb $_TIF_NEED_RESCHED, %cl
jnz work_resched
work_notifysig: # 处理未决信号集 # deal with pending signals and
# notify-resume requests
#ifdef CONFIG_VM86
testl $VM_MASK, PT_EFLAGS(%esp)
movl %esp, %eax
jne work_notifysig_v86 # returning to kernel-space or
# vm86-space
xorl %edx, %edx
call do_notify_resume
jmp resume_userspace_sig
ALIGN
work_notifysig_v86:
pushl %ecx # save ti_flags for do_notify_resume
CFI_ADJUST_CFA_OFFSET 4
call save_v86_state # %eax contains pt_regs pointer
popl %ecx
CFI_ADJUST_CFA_OFFSET -4
movl %eax, %esp
#else
movl %esp, %eax
#endif
xorl %edx, %edx
call do_notify_resume # 将信号传递到进程
jmp resume_userspace_sig
END(work_pending)
# perform syscall exit tracing
ALIGN
syscall_trace_entry:
movl $-ENOSYS,PT_EAX(%esp)
movl %esp, %eax
xorl %edx,%edx
call do_syscall_trace
cmpl $0, %eax
jne resume_userspace # ret != 0 -> running under PTRACE_SYSEMU,
# so must skip actual syscall
movl PT_ORIG_EAX(%esp), %eax
cmpl $(nr_syscalls), %eax
jnae syscall_call
jmp syscall_exit
END(syscall_trace_entry)
# perform syscall exit tracing
ALIGN
syscall_exit_work:
testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|_TIF_SINGLESTEP), %cl
jz work_pending
TRACE_IRQS_ON
ENABLE_INTERRUPTS(CLBR_ANY) # could let do_syscall_trace() call
# schedule() instead
movl %esp, %eax
movl $1, %edx
call do_syscall_trace
jmp resume_userspace # 恢复用户空间
END(syscall_exit_work)
CFI_ENDPROC
RING0_INT_FRAME # can't unwind into user space anyway
syscall_fault:
pushl %eax # save orig_eax
CFI_ADJUST_CFA_OFFSET 4
SAVE_ALL
GET_THREAD_INFO(%ebp)
movl $-EFAULT,PT_EAX(%esp)
jmp resume_userspace
END(syscall_fault)
在执行系统调用前会把寄存器中保存的用户态信息保存到内核栈中,然后通过系统调用号找到从 sys_call_table 中找到具体的系统调用入口,执行系统调用。系统调用执行完后,把返回值保存到 eax% 中,用户态程序可以从 eax% 中获取系统调用结果。
对于宏 SAVE_ALL 来说,会把将寄存器的值压入堆栈当中,压入顺序对应struct pt_regs ,出栈时调用 RESTORE_REGS 恢复 SAVE_ALL 压入的寄存器的值。
#define SAVE_ALL
cld;
pushl %fs;
pushl %es;
pushl %ds;
pushl %eax;
pushl %ebp;
pushl %edi;
pushl %esi;
pushl %edx;
pushl %ecx;
pushl %ebx;
movl $(__USER_DS), %edx;
movl %edx, %ds;
movl %edx, %es;
movl $(__KERNEL_PERCPU), %edx;
movl %edx, %fs
struct pt_regs {
long ebx;
long ecx;
long edx;
long esi;
long edi;
long ebp;
long eax;
int xds;
int xes;
int xfs;
/* int xgs; */
long orig_eax;
long eip;
int xcs;
long eflags;
long esp;
int xss;
};
恢复现场的宏 RESTORE_REGS ,中断返回时,恢复相关寄存器的内容是通过RESTORE_REGS 宏完成的,同时也可以看出,SAVE_ALL 和 RESTORE_REGS 遥相呼应,当执行 iret指令时,内核栈又恢复了进入中断前的状态,并使 CPU 从中断中返回。
#define RESTORE_INT_REGS
popl %ebx;
popl %ecx;
popl %edx;
popl %esi;
popl %edi;
popl %ebp;
popl %eax; \
#define RESTORE_REGS
RESTORE_INT_REGS;
1: popl %ds; \
2: popl %es; \
3: popl %fs; \
…
具体系统调用流程如下:
附:
SS(Stack Segment)为堆栈段寄存器,存放栈顶的段地址
SP(Stack Pointer) 为堆栈指针寄存器, 存放栈顶的偏移地址
任意时刻,SS:SP指向栈顶元素。
关系如下图
原文链接
Linux 系统调用的执行过程
公众号 Linux码农 推荐阅读
Linux ‘网络配置’ 和 ‘故障排除’ 命令总结
Linux 进程管理之基础知识
你需要了解的55个网络概念
Centos7 开启 iptables 日志
服务端 TCP 连接的 TIME_WAIT 过多问题的分析与解决
一文讲懂什么是vlan、三层交换机、网关、DNS、子网掩码、MAC地址
关注公众号 Linux码农 获取更多干货
更多推荐
所有评论(0)