Linux 编程中的API函数和系统调用的关系【转】

简介: 转自:http://blog.chinaunix.net/uid-25968088-id-3426027.html 原文地址:Linux 编程中的API函数和系统调用的关系 作者:up哥小号   API:(Application Programming Interface,应用程序编程接口)  指的是我们用户程序编程调用的如read(),write(),malloc(),free()之类的调用的是glibc库提供的库函数。
API:(Application Programming Interface,应用程序编程接口)
   指的是我们用户程序编程调用的如read(),write(),malloc(),free()之类的调用的是glibc库提供的库函数。API直接提供给用户编程使用,运行在用户态。
  我们经常说到的POSIX(Portable Operating System Interface of Unix)是针对API的标准,即针对API的函数名,返回值,参数类型等。POSIX兼容也就指定这些接口函数兼容,但是并不管API具体如何实现。
系统调用
   通过软中断或系统调用指令向内核发出一个明确的请求,内核将调用内核相关函数来实现(如sys_read(),sys_write(),sys_fork())。用户程序不能直接调用这些Sys_read,sys_write等函数。这些函数运行在内核态。
两者关系:
   通常API函数库(如glibc)中的函数会调用封装例程,封装例程负责发起系统调用(通过发软中断或系统调用指令),这些都运行在用户态。内核开始接收系统调用后,cpu从用户态切换到内核态(cpu处于什么状态,程序就叫处于什么状态,所以很多地方也说程序从用户态切换到内核态,实际是cpu运行级别的切换,通常cpu 运行在3级表示用户态,cpu 运行在0级表示内核态),内核调用相关的内核函数来处理再逐步返回给封装例程,cpu进行一次内核态到用户态的切换,API函数从封装例程拿到结果,再处理完后返回给用户。
 但是API函数不一定需要进行系统调用,如某些数学函数,没有必要进行系统调用,直接glibc里面就给处理了,整个过程运行在用户态。
  所以作为我们编写linux用户程序的时候,是不能直接调用内核里面的函数的,内核里面的函数位于进程虚拟地址空间里面的内核空间,用户空间函数及函数库都处于进程虚拟地址空间里面的用户空间,用户空间调用内核空间的函数只有一个通道,这个通道就是系统调用指令,所以通常要调用glibc等库的接口函数,glibc也是用户空间的,但glibc自己实现了调用特殊的宏汇编系统调用指令进行cpu运行状态的切换,把进程从用户空间切换到内核空间。
用户态函数执行全过程(这里只讲需要进行系统调用的函数)
   用户态xyz()函数,内核最终一般会调用形如sys_xyz()的服务例程来处理(不过也有一些例外,这里暂时不考虑)
  函数xyz()是直接提供给用户编程使用的。图中“SYSCALL”,“SY***IT”表示真正的汇编指令(汇编指令具体调用的是哪个暂时不关心,我们只需在此关注发起和退出了一个系统调用)。
 发起系统调用:xyz()函数执行的过程中会执行SYSCALL汇编指令,此指令将会把cpu从用户态切换到内核态。SYACALL汇编指令中会包含将要调用的内核函数的系统调用号和参数,内核在上图系统调用处理程序中去查一个sys_call_talbe数组来找到这个系统调用号对应的服务例程(如sys_xyz())函数的地址,然后调用这个地址的函数执行。(这里glibc里面的系统调用号和内核里面的系统调用号必须完全相等,当然,这是约定好的)
  系统调用返回:服务例程(如sys_xyz())函数返回值一般返回正数和0表示系统调用成功结束,而负数表示一个出错条件。紧接着SY***IT退出系统调用,此指令将cpu从内核态切换到用户态,glibc针对系统调用返回值如果出错则需要设置好errno(通常在c库头文件/usr/include/errno.h中),然后返回一个值做为glibc封装例程的返回值(如xyz()的返回值)。这里errno是libc自己用来定义的出错码,不一定是最后gblic封装例程的返回值
 

这里涉及到几个概念需要好好讲讲:

1.系统调用号

为了把系统调用号和相应的服务例程关联起来,如64位系统中
cat /usr/include/asm/unistd_64.h
#ifndef __SYSCALL
#define __SYSCALL(a, b)
#define __NR_read                               0
__SYSCALL(__NR_read, sys_read)
#define __NR_write                              1
等等
__SYSCALL(__NR_write, sys_write)
#define __NR_dup                                32
__SYSCALL(__NR_dup, sys_dup)
#define __NR_dup2                               33
__SYSCALL(__NR_dup2, sys_dup2)
等等
Glibc和内核里面的这个系统调用号是一致的,所以glibc调用汇编之类把系统调用号传给内核的时候,内核通过自己的系统调用分派表sys_call_table(可以理解为一个系统调用号,对应一个函数入口地址)找到这个具体的系统调用服务例程对应的函数入口地址,如上面sys_read,sys_write等

2.参数传递

    在发起系统调用前,eax寄存器里面存储了系统调用号。如用户程序fork()函数,glibc 发出int 0x80或sysenter指令前,eax寄存器就会设置好内核的sys_fork函数对应的系统调用号,这是glibc里面的封装例程会自动设置好的,程序员无需关心。
    有些系统调用可能调用很多参数(除了系统调用号之外),普通c函数的参数传递是通过把参数值写入活动的程序栈(用户态栈或者内核态栈)实现的。因为系统调用是一种跨用户态和内核态的特殊函数,所以这两个栈都不能用。在发出系统调用之前,系统调用的参数写入了cpu的寄存器(如glibc去写好这些寄存器),然后发出系统调用之后,而在内核调用服务例程(如sys_fork()服务例程)之前,内核再把存放在cpu中的参数拷贝的内核态的堆栈中(因为sys_fork只是普通的c函数,前面说过普通c函数的参数传递是通过把参数值写入活动的程序栈(用户态栈或者内核态栈)实现的)。内核为什么不直接把用户态的栈拷贝到内核态的栈而要去通过寄存器来传呢?首先,同事操作两个栈是比较复杂的,其次,寄存器的使用使得系统调用处理程序的结构与其它异常处理程序的结构类似。
    使用寄存器传递参数,必须满足两个条件:
    每个参数的长度不能超过寄存器的长度(比如寄存器长度32位,那参数长度就不能超过32位);
    参数的个数不能超过6个(除了eax中传递的系统调用号),因为80x86处理器的寄存器的数量是有限的。
    第一个条件总能成立,因为POSIX标准规定,如果寄存器里面装不下那个长度的参数,那么必须改用参数的地址来传递。
    第二个条件有的系统调用参数大于6个,这种情况下,必须用一个单独的寄存器执行进程地址空间的这些参数所在的一个内存区。
    存放系统调用号和系统调用参数的寄存器是eax(存放系统调用号),ebx,ecx,edx,esi,edi,ebp

3.SYSCALL,SY***IT进入退出系统调用

    这里SYSCALL,SY***IT只是个代号,具体汇编指令如下
    进入系统调用:内核2.6以前通过int $0x80汇编指令;内核2.6以后sysenter汇编语言指令。
    退出系统调用:旧的iret汇编指令,新的sy***it指令

    现在内核同时支持这两类新旧指令
    向量128(0x80)对应于内核入口点,在内核初始化期间调用的函数trap_init(0,用以下方式建立对应于向量128的中断描述符表项set_system_gate(0x80,&system_call).
    当用户态进程发出int $0x80指令时,cpu切换到内核态并开始从地址system_call处开始执行指令。System_call()函数首先把系统调用号和这个异常处理程序可以用到的所有cpu寄存器保存到相应的内核栈中,不包括由控制单元已自动保存的eflags,cs,eip,ss,esp寄存器。随后,在ebx中存放当前进程的thread_info数据结构的地址,这是通过获得内核栈指针的值并把它取整到4kb或8kb的倍数而完成的。然后检查thread_info结构flag字段的TIF_SYSCALL_TRACE和TIF_SYSCALL_AUDIT标识之一是否被设置为1,也就是检查是否有某一调试程序正在跟踪执行程序对系统调用的调用。如果被置1,那么system_call()函数两次调用do_syscall_trace()函数:一次正好在这个系统调用服务例程执行之前,一次在其之后。Do_syscall_trace函数停止current,并因此允许调试进程收集关于current的信息。

    系统调用退出:
    (1)用户态的寄存器刚进来到系统调用的时候被保存到了内核栈中,错误的返回值会被写的刚开始传递系统调用号的那个eax寄存器所在栈的位置。(那么将来当用户态恢复执行的时候,eax寄存器里面的内容就是系统调用的返回码了。)
    (2)禁止本地中断,并检查current的thread_info结构中的标志。如果有任何标志被设置,那么在返回到用户态之前还需要完成一些工作。

用source insight在linux内核中查找sys_open函数
#define __NR_open                               2
__SYSCALL(__NR_open, sys_open)
刚开始我搜索“sys_open”,苦逼的找了几遍没找到具体实现的地方,都是调用这个函数的地方。。后经伯松提醒,可能被宏给替换了。。后想起代码中出现过的SYSCALL_DEFINE宏,就进行了给name加上”sys_”前缀,所以找到SYSCALL_DEFINE中含有open的这句

点击(此处)折叠或打开

  1. SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
  2. {
  3.  long ret;
  4.  if (force_o_largefile())
  5.   flags |= O_LARGEFILE;
  6.  ret = do_sys_open(AT_FDCWD, filename, flags, mode);
  7.  /* avoid REGPARM breakage on x86: */
  8.  asmlinkage_protect(3, ret, filename, flags, mode);
  9.  return ret;
  10. }
深刻怀疑SYSCALL_DEFINE,其定义如下(在include/linux/syscalls.h中)
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)  //注意,这里name已经变成了”_name”,加上了下划线了,所以”open”变成了“_open”了

点击(此处)折叠或打开

  1. #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
  2. #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
  3. #define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
  4. #define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
  5. #define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
进一步
#define SYSCALL_DEFINEx(x, sname, ...)    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)//前面name已经加上“_open”了,记住
进一步
#define __SYSCALL_DEFINEx(x, name, ...)   asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))//这里,变成了“sys_open”了
所以,这里变成了 asmlinkage long sys_open(__SC_DECL3(__VA_ARGS__))
而再进一步
#define __SC_DECL1(t1, a1) t1 a1
#define __SC_DECL2(t2, a2, ...) t2 a2, __SC_DECL1(__VA_ARGS__)
#define __SC_DECL3(t3, a3, ...) t3 a3, __SC_DECL2(__VA_ARGS__),这里变成了asmlinkage long sys_open(const char __user*  filename,__SC_DECL2(__VA_ARGS__))
进一步变成了
asmlinkage long sys_open(const char __user*  fliename,int flags,__SC_DECL1(t1,a1))
进一步变成了
asmlinkage long sys_open(const char __user*  filename,int flags,umode_t mode)了!
所以,最终变成了

点击(此处)折叠或打开

  1. asmlinkage long sys_open(const char __user* filename,int flags,umode_t mode){
  2. long ret;
  3. if (force_o_largefile())
  4. flags |= O_LARGEFILE;
  5. ret = do_sys_open(AT_FDCWD, filename, flags, mode);
  6. /* avoid REGPARM breakage on x86: */
  7. asmlinkage_protect(3, ret, filename, flags, mode);
  8. return ret;
  9. }
补充说明:宏定义中出现的#,##,...和__VA_ARGS_的特殊说明
#  
这个是一个字符串替换,如#define S(x)   “a”#x
S(1),则变成了字符串“a1”了
##,这个是个简单的替换
如#define T(x)  a##x
T(1)则变成了 a1 了,比如你前面定义了int a1=2; 就可以printf”%d”,T(1)),即等价于printf(“%d”,a1);如果你用来上面的S(1)替换T(1)那就变成了printf(“%d”,“a1”)肯定就不对了,或者你#define S(x) a#x,用S(1)替换T(1)那就变成了printf(“%d”,a”1”)了,肯定也不对了,所以,用“##”有的时候也是必须 的

宏定义的参数,如#define X(...) printf(“%s %s”,__VA_ARGS__);
X(“a”,”b”)就变成了printf(“%s %s”,a,b);了
__VA_ARGS__就表示吧...参数给完整替换掉,”__VA_ARGS__”这个字符串缺一个字符都不可以。。
另外“#,##,...和__VA_ARGS_”也可参看
http://www.cnblogs.com/zhujudah/archive/2012/03/22/2411240.html
的一些讲解。
测试例程:

点击(此处)折叠或打开

  1. #include
  2. #define S(x) "a"#x
  3. #define T(x) a##x
  4. #define M(x) x
  5. #define N(x) (x)
  6. int main(){
  7. int a1=2;
  8. printf("%d\n",T(1));
  9. printf("%s\n",S(1));
  10. printf("%d\n",M(1));
  11. printf("%d\n",N(1));
  12. }
输出
2
a1
1
1
可以看到宏中带括号和不带括号是一样的效果
 
参考
1.《深入理解linux内核(第三版)》,
2.  kernel-3.6.7源码
3.http://www.cnblogs.com/zhujudah/archive/2012/03/22/2411240.html
【作者】 张昺华
【新浪微博】 张昺华--sky
【twitter】 @sky2030_
【facebook】 张昺华 zhangbinghua
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
目录
相关文章
|
Linux API 程序员
《Linux系统编程(第2版)》——1.2 API和ABI
程序员都希望自己实现的程序能够一直运行在其声明支持的所有系统上。他们希望能在自己的Linux版本上运行的程序也能够运行于其他Linux版本,同时还可以运行在其他支持Linux体系结构的更新(或更老)的Linux版本上。
2406 0
|
2月前
|
Linux 网络安全 数据安全/隐私保护
Linux 超级强大的十六进制 dump 工具:XXD 命令,我教你应该如何使用!
在 Linux 系统中,xxd 命令是一个强大的十六进制 dump 工具,可以将文件或数据以十六进制和 ASCII 字符形式显示,帮助用户深入了解和分析数据。本文详细介绍了 xxd 命令的基本用法、高级功能及实际应用案例,包括查看文件内容、指定输出格式、写入文件、数据比较、数据提取、数据转换和数据加密解密等。通过掌握这些技巧,用户可以更高效地处理各种数据问题。
206 8
|
2月前
|
监控 Linux
如何检查 Linux 内存使用量是否耗尽?这 5 个命令堪称绝了!
本文介绍了在Linux系统中检查内存使用情况的5个常用命令:`free`、`top`、`vmstat`、`pidstat` 和 `/proc/meminfo` 文件,帮助用户准确监控内存状态,确保系统稳定运行。
848 6
|
2月前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
134 3
|
2月前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
107 2
|
1月前
|
Linux Shell
Linux 10 个“who”命令示例
Linux 10 个“who”命令示例
80 14
Linux 10 个“who”命令示例
|
1月前
|
Ubuntu Linux
Linux 各发行版安装 ping 命令指南
如何在不同 Linux 发行版(Ubuntu/Debian、CentOS/RHEL/Fedora、Arch Linux、openSUSE、Alpine Linux)上安装 `ping` 命令,详细列出各发行版的安装步骤和验证方法,帮助系统管理员和网络工程师快速排查网络问题。
141 20
|
22天前
|
Linux
linux查看目录下的文件夹命令,find查找某个目录,但是不包括这个目录本身?
通过本文的介绍,您应该对如何在 Linux 系统中查看目录下的文件夹以及使用 `find` 命令查找特定目录内容并排除该目录本身有了清晰的理解。掌握这些命令和技巧,可以大大提高日常文件管理和查找操作的效率。 在实际应用中,灵活使用这些命令和参数,可以帮助您快速定位和管理文件和目录,满足各种复杂的文件系统操作需求。
59 8
|
1月前
|
网络协议 Linux 应用服务中间件
kali的常用命令汇总Linux
kali的常用命令汇总linux
66 7
|
2月前
|
Linux 数据库
Linux中第一次使用locate命令报错?????
在Linux CentOS7系统中,使用`locate`命令时出现“command not found”错误,原因是缺少`mlocate`包。解决方法是通过`yum install mlocate -y`或`apt-get install mlocate`安装该包,并执行`updatedb`更新数据库以解决后续的“can not stat”错误。
48 9