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

转自:http://blog.chinaunix.net/uid-25968088-id-3426027.html

原文地址:Linux 编程中的API函数和系统调用的关系 作者:up哥小号

 

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

时间: 2024-10-28 16:46:12

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

网络编程-socket编程中的accept函数

问题描述 socket编程中的accept函数 一个简单的客户/服务器的实现中,connect成功了,但是accept失败,它的返回值为0,这是怎么回事? 解决方案 已解决,原来是其中 = 的运算级别问题,加了个括号就行了,多谢楼上各位! 解决方案二: 名称 accept() 接收一个套接字中已建立的连接 使用格式 #include <sys/types.h> #include <sys/socket.h> int accept(int sockfd,struct sockaddr

详解Linux内核中的container_of函数_unix linux

前言 在linux 内核中,container_of 函数使用非常广,例如 linux内核链表 list_head.工作队列work_struct中. 在linux内核中大名鼎鼎的宏container_of() ,其实它的语法很简单,只是一些指针的灵活应用,它分两步:       第一步,首先定义一个临时的数据类型(通过typeof( ((type *)0)->member )获得)与ptr相同的指针变量__mptr,然后用它来保存ptr的值.       第二步,用(char *)__mptr

在Visual C#中运用API函数获取系统信息

visual|函数 API函数是构筑Windows应用程序的基石,是Windows编程的必备利器.每一种Windows应用程序开发工具都提供了间接或直接调用了Windows API函数的方法,或者是调用Windows API函数的接口,也就是说具备调用动态连接库的能力.Visual C#和其它开发工具一样也能够调用动态链接库的API函数.本文中笔者就结合实例向大家介绍在Visual C#中如何调用各种返回值的API,该实例就是一个通过API函数调用获取系统信息的程序. 在Visual C#中调用

C#中调用API函数RegisterHotKey注册多个系统热键

函数 要设置快捷键必须使用user32.dll下面的两个方法. BOOL RegisterHotKey( //注册系统热键的API函数 HWND hWnd, int id, UINT fsModifiers, UINT vk );  BOOL UnregisterHotKey( //删除系统热键的API函数 HWND hWnd, int id );  在C#中引用命名空间System.Runtime.InteropServices;来加载非托管类user32.dllusing System;us

DELPHI中利用API函数实现多态FORM

实现异型FORM并不是一件难事,本文将向您介绍如何利用API函数实现圆角矩 形和椭圆形FORM,并在此基础之上探讨实现TWINcontrol类的后裔的异型的实现 . 欲改变FORM的形状,也就是实现对区域(region)的控制.在Win32 API程序 参考手册有关区域(region)的定义是这样描述的:它可以是一个矩形,多边形 ,椭圆形(或者是两者的复合,或者是更多的形状),这些都可以被填充,画图 ,翻转,结构化并可以得到焦点执行. 由定义得出结论:区域(region)是可以被改变和操纵的,依

Linux编程中的while循环问题

我们先要理解while循环的意义,使用while循环的目的就是多次循环! while循环是根据while关键字后面指定的条件决定是否退出的循环的. 如果你希望执行一次命令就退出,那么无需用while循环,使用while就表示你需要多次循环,好了,我猜测下,你的问题是希望true的条件下,如何退出循环吧? 如果是的话,那么大概有如下几个方法. 1.while后的条件不是为true永远为真,而是指定一个条件,那么条件不满足就退出了. 例子如下: i=0 while ((i<10)) do echo

实例讲解C++编程中的虚函数与虚基类_C 语言

虚函数① #include "stdafx.h" #include <iostream> using namespace std; class B0//基类B0声明 { public: void display(){cout<<"B0::display()"<<endl;}//公有成员函数 }; class B1: public B0//公有派生类B1声明 { public: void display(){cout<<

详解C++编程中的虚函数_C 语言

我们知道,在同一类中是不能定义两个名字相同.参数个数和类型都相同的函数的,否则就是"重复定义".但是在类的继承层次结构中,在不同的层次中可以出现名字相同.参数个数和类型都相同而功能不同的函数. 人们提出这样的设想,能否用同一个调用形式,既能调用派生类又能调用基类的同名函数.在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们.例如,用同一个语句"pt->display( );"可以调用不同派生层次中的display函数,只需在调用前

深入解析C++编程中的静态成员函数_C 语言

C++静态成员函数 与数据成员类似,成员函数也可以定义为静态的,在类中声明函数的前面加static就成了静态成员函数.如 static int volume( ); 和静态数据成员一样,静态成员函数是类的一部分,而不是对象的一部分. 如果要在类外调用公用的静态成员函数,要用类名和域运算符"::".如 Box::volume( ); 实际上也允许通过对象名调用静态成员函数,如 a.volume( ); 但这并不意味着此函数是属于对象a的,而只是用a的类型而已. 与静态数据成员不同,静态成