首页 > 代码库 > Linux系统调用

Linux系统调用

    1. 方法之四:以功能为中心,各个击破 

        从功能上看,整个Linux系统可看作有一下几个部分组成:
      1. 进程管理机制部分;
      2. 内存管理机制部分; 
      3. 文件系统部分; 
      4. 硬件驱动部分; 
      5. 系统调用部分等;

在这五个功能部件中,系统调用是用户程序或操作调用核心所提供的功能的接口;也是分析Linux内核源码几个很好的入口点之一。

 http://www.yesky.com/20010813/192117_3.shtml

 

与系统调用相关的内容主要有:系统调用总控程序,系统调用向量表sys_call_table,以及各系统调用服务程序。

    1. 保护模式下的初始化过程中,设置并初始化idt,共256个入口,服务程序均为ignore_int, 该服务程序仅打印“Unknown interruptn”。(源码参见/Arch/i386/KERNEL/head.S文件;相关内容可参见 保护模式下的初始化部分)
    2. 在系统初始化完成后运行的第一个内核程序start_kernel中,通过调用 trap_init函数,把各自陷和中断服务程序的入口地址设置到 idt 表中;同时,此函数还通过调用函数set_system_gate 把系统调用总控程序的入口地址挂在中断0x80上。其中:
  • start_kernel的原型为void __init start_kernel(void) ,其源码在文件 init/main.c中;
  • trap_init函数的原型为void __init trap_init(void),定义在arch/i386/kernel/traps.c 中
  • 函数set_system_gate同样定义在arch/i386/kernel/traps.c 中,调用原型为set_system_gate(SYSCALL_VECTOR,&system_call);
  • 其中,SYSCALL_VECTOR是定义在 linux/arch/i386/kernel/irq.h中的一个常量0x80;
  • 而 system_call 即为系统调用总控程序的入口地址;中断总控程序用汇编语言定义在arch/i386/kernel/entry.S中。

 

  • 系统调用向量表sys_call_table, 是一个含有NR_syscalls=256个单元的数组。它的每个单元存放着一个系统调用服务程序的入口地址。该数组定义在/arch/i386/kernel/entry.S中;而NR_syscalls则是一个等于256的宏,定义在include/linux/sys.h中。
  • 各系统调用服务程序则分别定义在各个模块的相应文件中;例如asmlinkage int sys_time(int * tloc)就定义在kerneltime.c中;另外,在kernelsys.c中也有不少服务程序;

 

系统调用个数:

<style>p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 13.0px Arial; color: #323333 } span.s1 { }</style>

从2.4的190个到2.6的300多个

 

 

技术分享

eax是寄存器

 

技术分享

 

 说一下我的理解。Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。库子程序(除了其中系统调用的部分)是回到了用户态的,只有系统调用是内核态的。内核态、用户态的知识,在另一篇文章中再详细讨论。

技术分享

 

技术分享

 

 

技术分享

 

技术分享

 

注意标准C库的作用:

技术分享

 

注意:

技术分享

所以,一般不会直接调用系统调用的,都是调用标准C库,或者更加抽象的库子程序。

 

虽然理论上,

<style>p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px "Helvetica Neue" } span.s1 { }</style>

对于简单的操作,我们可以直接调用系统调用来访问资源,如“人”,对于复杂操作,我们借助于库函数来实现,如“仁”。显然,这样的库函数依据不同的标准也可以有不同的实现版本,如ISO C 标准库,POSIX标准库等。

但是实际上,

 

谁也不想去写复杂的系统调用映射,处理寄存器操作,等等。

 

如果真的想调用,有下面三种方法:

http://www.linuxidc.com/Linux/2014-12/110238.htm

 

<style>p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; text-align: justify; font: 14.0px Tahoma; color: #323333; background-color: #fafafc } span.s1 { }</style>

系统调用(System Call)是操作系统为在用户态运行的进程与硬件设备(如CPU、磁盘、打印机等)进行交互提供的一组接口。当用户进程需要发生系统调用时,CPU 通过软中断切换到内核态开始执行内核系统调用函数。下面介绍Linux 下三种发生系统调用的方法:

 

1. 通过 glibc 提供的库函数

glibc 是 Linux 下使用的开源的标准 C 库,它是 GNU 发布的 libc 库,即运行时库。glibc 为程序员提供丰富的 API(Application Programming Interface),除了例如字符串处理、数学运算等用户态服务之外,最重要的是封装了操作系统提供的系统服务,即系统调用的封装。

 

那么glibc提供的系统调用API与内核特定的系统调用之间的关系是什么呢?

通常情况,每个特定的系统调用对应了至少一个 glibc 封装的库函数,如系统提供的打开文件系统调用 sys_open 对应的是 glibc 中的 open 函数;

其次,glibc 一个单独的 API 可能调用多个系统调用,如 glibc 提供的 printf 函数就会调用如 sys_open、sys_mmap、sys_write、sys_close 等等系统调用;

另外,多个 API 也可能只对应同一个系统调用,如glibc 下实现的 malloccallocfree 等函数用来分配和释放内存,都利用了内核的 sys_brk 的系统调用。

代码如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdio.h>

int main()
{
        int rc;

        rc = chmod("/etc/passwd", 0444);
        if (rc == -1)
                fprintf(stderr, "chmod failed, errno = %d\n", errno);
        else
                printf("chmod success!\n");
        return 0;
}

 

 

2. 使用 syscall 直接调用

有点不足是,如果 glibc 没有封装某个内核提供的系统调用时,我就没办法通过上面的方法来调用该系统调用。如我自己通过编译内核增加了一个系统调用,这时 glibc 不可能有你新增系统调用的封装 API,此时我们可以利用 glibc 提供的syscall 函数直接调用。该函数定义在 unistd.h 头文件中,函数原型如下:

long int syscall (long int sysno, ...)

sysno 是系统调用号,每个系统调用都有唯一的系统调用号来标识。在 sys/syscall.h 中有所有可能的系统调用号的宏定义。
... 为剩余可变长的参数,为系统调用所带的参数,根据系统调用的不同,可带0
~5个不等的参数,如果超过特定系统调用能带的参数,多余的参数被忽略。
返回值 该函数返回值为特定系统调用的返回值,在系统调用成功之后你可以将该返回值转化为特定的类型,如果系统调用失败则返回
-1,错误代码存放在 errno 中。

代码如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>

int main()
{
        int rc;
        rc = syscall(SYS_chmod, "/etc/passwd", 0444);

        if (rc == -1)
                fprintf(stderr, "chmod failed, errno = %d\n", errno);
        else
                printf("chmod succeess!\n");
        return 0;
}

 

 

3. 通过 int 指令陷入

如果我们知道系统调用的整个过程的话,应该就能知道用户态程序通过软中断指令int 0x80 来陷入内核态(在Intel Pentium II 又引入了sysenter指令),参数的传递是通过寄存器,eax 传递的是系统调用号,ebx、ecx、edx、esi和edi 来依次传递最多五个参数,当系统调用返回时,返回值存放在 eax 中

仍然以上面的修改文件属性为例,将调用系统调用那段写成内联汇编代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <errno.h>

int main()
{
        long rc;
        char *file_name = "/etc/passwd";
        unsigned short mode = 0444;

        asm(
                "int $0x80"
                : "=a" (rc)
                : "0" (SYS_chmod), "b" ((long)file_name), "c" ((long)mode)
        );

        if ((unsigned long)rc >= (unsigned long)-132) {
                errno = -rc;
                rc = -1;
        }

        if (rc == -1)
                fprintf(stderr, "chmode failed, errno = %d\n", errno);
        else
                printf("success!\n");

        return 0;
}

如果 eax 寄存器存放的返回值(存放在变量 rc 中)在 -1~-132 之间,就必须要解释为出错码(在/usr/include/asm-generic/errno.h 文件中定义的最大出错码为 132),这时,将错误码写入 errno 中,置系统调用返回值为 -1;否则返回的是 eax 中的值。

上面程序在 32位Linux下以普通用户权限编译运行结果与前面两个相同!

 

Linux系统调用列表(其实指的是glibc库函数的列表)

http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part1/appendix.html

这个列表以man pages第2节,即系统调用节为蓝本

 

<style>p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 15.0px Arial; color: #232323 } span.s1 { }</style>

其中有一些函数的作用完全相同,只是参数不同。(可能很多熟悉C++朋友马上就能联想起函数重载,但是别忘了Linux核心是用C语言写的,所以只能取成不同的函数名)。还有一些函数已经过时,被新的更好的函数所代替了(gcc在链接这些函数时会发出警告),但因为兼容的原因还保留着,这些函数我会在前面标上“*”号以示区别。

 

(各给一个示例,具体的看原文吧,对加强对Linux系统的理解,很有好处!)

一、进程控制:

fork 创建一个新进程

 

二、文件系统控制

1、文件读写操作

fcntl 文件控制

open 打开文件

 

2、文件系统操作

access 确定文件的可存取性

 

三、系统控制

ioctl I/O总控制函数

 

四、内存管理

mmap 映射虚拟内存页

 

Linux系统调用