最近看了关于容器时间的文章,里面涉及到了系统调用gettimeofday,突然问了自己什么是系统调用?系统调用这个名字经常会出现在我们的耳边,也经常会听到别人说“***系统调用返回错误”。那到底什么是系统调用?系统调用到底是怎么调用的呢?接下来就详细阐述下系统调用的过程,如有不足,请指正。(注:本文的涉及到glibc是2.28. 9000版本,linux内核版本4.20.0)
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/resource.h>
int
main(int argc, char *argv[])
{
extern int errno;
int prio = getpriority(PRIO_PROCESS,0);
if(prio == -1)
{
printf("%s\n",strerror(errno));
exit(EXIT_FAILURE);
}
else
{
printf("%d\n", prio);
}
exit(EXIT_SUCCESS);
}
gcc -g -o test test.c
生成对应的可执行文件test之后,使用GDB进行单步跟踪
gdb –q ./test
可以看到调用getpriority函数的时候,实际是调用./sysdeps/unix/sysv/linux/getpriority.c第39行处代码。找到getpriority.c的第39行代码:
调用了INLINE_SYSCALL(getpriority, 2, (int)which, who)。INLINE_SYSCALL到底是什么呢?主要实现什么功能?其实从字面意思我们可以大致猜测出,INLINE_SYSCALL括号中的getpriority是系统调用名,which和who是传入的参数,2可能是表示的参数个数。
那么,INLINE_SYSCALL到底是什么呢?其实INLINE_SYSCALL是个macro(宏),具体位置在glibc的sysdeps/unix/sysv/linux/x86_64/sysdep.h文件定义【注:如果大家用vim的ctags跳转时可能会出现多个针对不同平台的INLINE_SYSCALL,本文以x86_64为例】。
从INLINE_SYSCALL,我们发现又调用了INTERNAL_SYSCALL(也是宏),具体位置在sysdeps/unix/sysv/linux/x86_64/sysdep.h,和INLINE_SYSCALL在同一个文件
interSYS_ify又是做什么的呢?同样也是宏,并且在同一个文件中。系统调用getpriority对应SYS_ify(syscall_name)展开就是__NR_getpriority(所在位置./usr/include/asm/unistd_64.h,编译内核产生的文件)
那internal_syscall##nr(SYS_ify(name), err, args)又是什么呢?
Internal_syscall##nr也是宏,但为啥找不到具体的定义呢?其实是有的,internal_syscall##nr,##是连接符,nr表示参数个数。因此本文例子系统调用getpriority展开后就是
internal_syscall2 (SYS_ify(name), err,argc)
internal_syscall2也是宏,也在sysdeps/unix/sysv/linux/x86_64/sysdep.h
Internal_syscall2里重点看 asmvolatile(”syscall\n\t” :),这是段内联汇编,主要实现用户态切换到内核态的过程(详细过程还没研究)。大致过程是***一段复杂的前期准备工作(没细分析),紧接着执行汇编程序arch/x86/entry/entry_64.S(内核源码)文件的entry_SYSCALL_64。
重点是do_syscall_64函数
do_syscall_64主要逻辑是什么?从代码可以看到,do_syscall_64里有一行sys_call_tablenr,这行是核心逻辑。主要实现根据系统调用表,找到对应的内核接口函数。
可以看出,sys_call_table是个数组,那存储的是什么数据?数据从哪来?
继续跳转发现sys_call_table在arch/x86/entry/syscall_64.c定义。数组元素主要是#include<asm/syscalls_64.h>。找了半天没找到这个文件在哪里,不得google一下才知道这个文件是编译内核时生成的。
grep一把发现果然在arch/x86/entry/syscalls/Makefile中有out。
顺便看下arch/x86/entry/syscalls/Makefile内容,生成syscalls_64.h依赖于syscall64和systbl。这两个在Makefile也有定义。
其中,arch/x86/entry/syscalls/syscall_64.tbl作为arch/x86/entry/syscalls/syscalltbl.sh脚本的参数,然后生成syscall_64.h文件。具体生成规则请阅读syscalltbl.sh,比较简单。
arch/x86/entry/syscalls/syscall_64.tbl文件内容如下:
本文也尝试编译了内核,生成/arch/x86/include/generated/asm/syscalls_64.h文件,内容如下:
__SYSCALL_COMMON是宏,在arch/x86/entry/syscall_64.c。关系是:
__SYSCALL_COMMON->__SYSCALL_64->sym,从__SYSCALL_COMMON(140,sys_getpriority, sys_getpriority)可知,sym就是sys_getpriority
sys_getpriority就是内核实际要执行的函数。那sys_getpriority定义在哪里呢?grep所有代码没有发现定义sys_getpriority,又google了一把才知道原来sys_*也是通过宏展开调用的。
SYSCALL_DEFINE2->SYSCALL_DEFINE2,那么getpriority对应的就是SYSCALL_DEFINE2(2,_getpriority, __VA_ARGS)。从__SYSCALL_DEFINE2展开可知调用了sys_getpriority,并且__se_sys_getpriority和sys_getpriority等价(别名)。而__se_sys_getpriority又调用了__do_sys_getpriority函数
/kernel/sys.c里面定义了getpriority的底层实现。
注:getpriority获取进程nice值时,内核定义的是[-20, 19],系统调用会转换成[1,40],即将实际nice+20返回。
总结一下:
1、 经过跟踪getpriority的系统调用,对整个linux的系统调用过程有了大概的了解(由于本人水平有限,对用户态转内核态的过程:包括中断处理、寄存器存取参数以及汇编等没有做详细介绍,还需要继续学习)
2、 Getpriority返回给用户引用程序的是转换后的正整数(内核真实值是[-20,19])