Linux系统调用

来源:互联网 发布:js时间戳转换成 小时前 编辑:程序博客网 时间:2024/06/05 16:51

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

以下是Linux系统调用的一个列表,包含了大部分常用系统调用和由系统调用派生出的的函数。这可能是你在互联网上所能看到的唯一一篇中文注释的Linux系统调用列表,即使是简单的字母序英文列表,能做到这么完全也是很罕见的。

按照惯例,这个列表以manpages第2节,即系统调用节为蓝本。按照笔者的理解,对其作了大致的分类,同时也作了一些小小的修改,删去了几个仅供内核使用,不允许用户调用的系统调用,对个别本人稍觉不妥的地方作了一些小的修改,并对所有列出的系统调用附上简要注释。

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

一、进程控制:

fork创建一个新进程clone按指定条件创建子进程execve运行可执行文件exit中止进程_exit立即中止当前进程getdtablesize进程所能打开的最大文件数getpgid获取指定进程组标识号setpgid设置指定进程组标志号getpgrp获取当前进程组标识号setpgrp设置当前进程组标志号getpid获取进程标识号getppid获取父进程标识号getpriority获取调度优先级setpriority设置调度优先级modify_ldt读写进程的本地描述表nanosleep使进程睡眠指定的时间nice改变分时进程的优先级pause挂起进程,等待信号personality设置进程运行域prctl对进程进行特定操作ptrace进程跟踪sched_get_priority_max取得静态优先级的上限sched_get_priority_min取得静态优先级的下限sched_getparam取得进程的调度参数sched_getscheduler取得指定进程的调度策略sched_rr_get_interval取得按RR算法调度的实时进程的时间片长度sched_setparam设置进程的调度参数sched_setscheduler设置指定进程的调度策略和参数sched_yield进程主动让出处理器,并将自己等候调度队列队尾vfork创建一个子进程,以供执行新程序,常与execve等同时使用wait等待子进程终止wait3参见waitwaitpid等待指定子进程终止wait4参见waitpidcapget获取进程权限capset设置进程权限getsid获取会晤标识号setsid设置会晤标识号

回页首

二、文件系统控制


1、文件读写操作
fcntl文件控制open打开文件creat创建新文件close关闭文件描述字read读文件write写文件readv从文件读入数据到缓冲数组中writev将缓冲数组里的数据写入文件pread对文件随机读pwrite对文件随机写lseek移动文件指针_llseek在64位地址空间里移动文件指针dup复制已打开的文件描述字dup2按指定条件复制文件描述字flock文件加/解锁pollI/O多路转换truncate截断文件ftruncate参见truncateumask设置文件权限掩码fsync把文件在内存中的部分写回磁盘


2、文件系统操作
access确定文件的可存取性chdir改变当前工作目录fchdir参见chdirchmod改变文件方式fchmod参见chmodchown改变文件的属主或用户组fchown参见chownlchown参见chownchroot改变根目录stat取文件状态信息lstat参见statfstat参见statstatfs取文件系统信息fstatfs参见statfsreaddir读取目录项getdents读取目录项mkdir创建目录mknod创建索引节点rmdir删除目录rename文件改名link创建链接symlink创建符号链接unlink删除链接readlink读符号链接的值mount安装文件系统umount卸下文件系统ustat取文件系统信息utime改变文件的访问修改时间utimes参见utimequotactl控制磁盘配额

回页首

三、系统控制

ioctlI/O总控制函数_sysctl读/写系统参数acct启用或禁止进程记账getrlimit获取系统资源上限setrlimit设置系统资源上限getrusage获取系统资源使用情况uselib选择要使用的二进制函数库ioperm设置端口I/O权限iopl改变进程I/O权限级别outb低级端口操作reboot重新启动swapon打开交换文件和设备swapoff关闭交换文件和设备bdflush控制bdflush守护进程sysfs取核心支持的文件系统类型sysinfo取得系统信息adjtimex调整系统时钟alarm设置进程的闹钟getitimer获取计时器值setitimer设置计时器值gettimeofday取时间和时区settimeofday设置时间和时区stime设置系统日期和时间time取得系统时间times取进程运行时间uname获取当前UNIX系统的名称、版本和主机等信息vhangup挂起当前终端nfsservctl对NFS守护进程进行控制vm86进入模拟8086模式create_module创建可装载的模块项delete_module删除可装载的模块项init_module初始化模块query_module查询模块信息*get_kernel_syms取得核心符号,已被query_module代替

回页首

四、内存管理

brk改变数据段空间的分配sbrk参见brkmlock内存页面加锁munlock内存页面解锁mlockall调用进程所有内存页面加锁munlockall调用进程所有内存页面解锁mmap映射虚拟内存页munmap去除内存页映射mremap重新映射虚拟内存地址msync将映射内存中的数据写回磁盘mprotect设置内存映像保护getpagesize获取页面大小sync将内存缓冲区数据写回硬盘cacheflush将指定缓冲区中的内容写回磁盘

回页首

五、网络管理

getdomainname取域名setdomainname设置域名gethostid获取主机标识号sethostid设置主机标识号gethostname获取本主机名称sethostname设置主机名称

回页首

六、socket控制

socketcallsocket系统调用socket建立socketbind绑定socket到端口connect连接远程主机accept响应socket连接请求send通过socket发送信息sendto发送UDP信息sendmsg参见sendrecv通过socket接收信息recvfrom接收UDP信息recvmsg参见recvlisten监听socket端口select对多路同步I/O进行轮询shutdown关闭socket上的连接getsockname取得本地socket名字getpeername获取通信对方的socket名字getsockopt取端口设置setsockopt设置端口参数sendfile在文件或端口间传输数据socketpair创建一对已联接的无名socket

回页首

七、用户管理

getuid获取用户标识号setuid设置用户标志号getgid获取组标识号setgid设置组标志号getegid获取有效组标识号setegid设置有效组标识号geteuid获取有效用户标识号seteuid设置有效用户标识号setregid分别设置真实和有效的的组标识号setreuid分别设置真实和有效的用户标识号getresgid分别获取真实的,有效的和保存过的组标识号setresgid分别设置真实的,有效的和保存过的组标识号getresuid分别获取真实的,有效的和保存过的用户标识号setresuid分别设置真实的,有效的和保存过的用户标识号setfsgid设置文件系统检查时使用的组标识号setfsuid设置文件系统检查时使用的用户标识号getgroups获取后补组标志清单setgroups设置后补组标志清单

回页首

八、进程间通信

ipc进程间通信总控制调用


1、信号
sigaction设置对指定信号的处理方法sigprocmask根据参数对信号集中的信号执行阻塞/解除阻塞等操作sigpending为指定的被阻塞信号设置队列sigsuspend挂起进程等待特定信号signal参见signalkill向进程或进程组发信号*sigblock向被阻塞信号掩码中添加信号,已被sigprocmask代替*siggetmask取得现有阻塞信号掩码,已被sigprocmask代替*sigsetmask用给定信号掩码替换现有阻塞信号掩码,已被sigprocmask代替*sigmask将给定的信号转化为掩码,已被sigprocmask代替*sigpause作用同sigsuspend,已被sigsuspend代替sigvec为兼容BSD而设的信号处理函数,作用类似sigactionssetmaskANSI C的信号处理函数,作用类似sigaction


2、消息
msgctl消息控制操作msgget获取消息队列msgsnd发消息msgrcv取消息


3、管道
pipe创建管道


4、信号量
semctl信号量控制semget获取一组信号量semop信号量操作


5、共享内存
shmctl控制共享内存shmget获取共享内存shmat连接共享内存shmdt拆卸共享内存

参考资料

  • Linux>Linux 2.6内核标准教程  第7章 系统调用

    使用 Linux 系统调用的内核命令

    深入理解Linux的系统调用

    Linux系统调用接口、系统调用例程和内核服务例程之间的关系

    向linux内核中添加三个系统调用(Ubuntu9.10)

    ++++++++++++++++++++++++++++++++++

    使用 Linux 系统调用的内核命令



    作者:M. Tim Jones    转贴自:本站原创

     

    Linux® 系统调用 —— 我们每天都在使用它们。不过您清楚系统调用是如何在用户空间和内核之间执行的吗?本文将探究 Linux 系统调用接口(SCI),学习如何添加新的系统调用(以及实现这种功能的其他方法),并介绍与 SCI 有关的一些工具。

    系统调用就是用户空间应用程序和内核提供的服务之间的一个接口。由于服务是在内核中提供的,因此无法执行直接调用;相反,您必须使用一个进程来跨越用户空间与内核之间的界限。在特定架构中实现此功能的方法会有所不同。因此,本文将着眼于最通用的架构 —— i386。

    在本文中,我将探究 Linux SCI,演示如何向 2.6.20 内核添加一个系统调用,然后从用户空间来使用这个函数。我们还将研究在进行系统调用开发时非常有用的一些函数,以及系统调用的其他选择。最后,我们将介绍与系统调用有关的一些辅助机制,比如在某个进程中跟踪系统调用的使用情况。

    SCI

    Linux 中系统调用的实现会根据不同的架构而有所变化,而且即使在某种给定的体架构上也会不同。例如,早期的 x86 处理器使用了中断机制从用户空间迁移到内核空间中,不过新的 IA-32 处理器则提供了一些指令对这种转换进行优化(使用>

    SCI 的核心是系统调用多路分解表。这个表如图 2 所示,使用> 

    添加一个 Linux 系统调用

    系统调用多路分解

    有些系统调用会由内核进一步进行多路分解。例如,BSD(Berkeley Software Distribution)socket 调用(socket、bind、> asmlinkage> asmlinkage>  long result;
     >
     >    err = put_user( result, presult );

      }

      return err ? -EFAULT : 0;
    }


    内核 jiffies

    Linux 内核具有一个名为 jiffies 的全局变量,它代表从机器启动时算起的时间滴答数。这个变量最初被初始化为 0,每次时钟中断时都会加 1。您可以使用> 

    #define __NR_getcpu        318
    #define __NR_epoll_pwait    319
    #define __NR_getjiffies        320
    #define __NR_diffjiffies    321
    #define __NR_pdiffjiffies    322
    #define NR_syscalls    323


    现在已经有了自己的内核系统调用,以及表示这些系统调用的编号。接下来需要做的是要在这些编号(表索引)和函数本身之间建立一种对等关系。这就是第 3 个步骤,更新系统调用表。如清单 4 所示,我将为这个新函数更新> .long>对用户内存进行读写

    Linux 内核提供了几个函数,可以用来将系统调用参数移动到用户空间中,或从中移出。方法包括一些基本类型的简单函数(例如>int access_ok( type, address, size );

    要在内核和用户空间移动一些简单类型(例如 int 或 long 类型),可以使用 get_user 和 put_user 轻松地实现。这两个宏都包含一个值以及一个指向变量的指针。get_user 函数将用户空间地址(ptr)指定的值移动到所指定的内核变量(var)中。 put_user 函数则将内核变量(var)指定的值移动到用户空间地址(ptr)。 如果成功,这两个函数都返回 0:

    int get_user( var, ptr );
    int put_user( var, ptr );


    要移动更大的对象,例如结构或数组,您可以使用 copy_from_user 和 copy_to_user 函数。这些函数将在用户空间和内核之间移动完整的数据块。 copy_from_user 函数会将一块数据从用户空间移动到内核空间,copy_to_user 则会将一块数据从内核空间移动到用户空间:

    unsigned long copy_from_user( void *to, const void __user *from, unsigned long n );
    unsigned long copy_to_user( void *to, const void __user *from, unsigned long n );

    最后,您可以使用 strncpy_from_user 函数将一个以 NULL 结尾的字符串从用户空间移动到内核空间中。在调用这个函数之前,您可以通过调用 strlen_user 宏来获得用户空间字符串的大小:

    long strncpy_from_user( char *dst, const char __user *src, long count );
    strlen_user( str );

    这些函数为内核和用户空间之间的内存移动提供了基本功能。实际上还可以使用另外一些函数(例如减少执行检查数量的函数)。您可以在 uaccess.h 中找到这些函数。

    使用系统调用

    现在内核已经使用新系统调用完成更新了,接下来看一下从用户空间应用程序中使用这些系统调用需要执行的操作。使用新的内核系统调用有两种方法。第一种方法非常方便(但是在产品代码中您可能并不希望使用),第二种方法是传统方法,需要多做一些工作。

    使用第一种方法,您可以通过>
     >syscall( SYS_getpid )

    syscall 函数特定于架构,使用一种机制将控制权交给内核。其参数是基于 __NR 索引与 /usr/include/bits/syscall.h 提供的 SYS_ 符号之间的映射(在编译 libc 时定义)。永远都不要直接引用这个文件;而是要使用 /usr/include/sys/syscall.h 文件。

    传统的方法要求我们创建函数调用,这些函数调用必须匹配内核中的系统调用索引(这样就可以调用正确的内核服务),而且参数也必须匹配。Linux 提供了一组宏来提供这种功能。_syscallN 宏是在 /usr/include/linux/unistd.h 中定义的,格式如下:

    _syscall0( ret-type, func-name )
    _syscall1( ret-type, func-name, arg1-type, arg1-name )
    _syscall2( ret-type, func-name, arg1-type, arg1-name, arg2-type, arg2-name )

    用户空间和 __NR 常量

    注意清单 6 中提供了 __NR 符号常量。您可以在 /usr/include/asm/unistd.h 中找到它们(对于标准系统调用来说)。

    _syscall 宏最多可定义 6 个参数(不过此处只显示了 3 个)。

    现在,让我们来看一下如何使用 _syscall 宏来使新系统调用对于用户空间可见。清单 6 显示的应用程序使用了 _syscall 宏定义的所有系统调用。


    清单 6. 将 _syscall 宏 用于用户空间应用程序开发
    #include <stdio.h>
    #include <linux/unistd.h>
    #include <sys/syscall.h>

    #define __NR_getjiffies        320
    #define __NR_diffjiffies    321
    #define __NR_pdiffjiffies    322

    _syscall0(>
     >
      if (!err) {
        printf( "difference is %lx\n", result );
      } else {
        printf( "error\n" );
      }

      return 0;
    }

    注意 __NR 索引在这个应用程序中是必需的,因为 _syscall 宏使用了 func-name 来构造 __NR 索引(getjiffies -> __NR_getjiffies)。其结果是您可以使用它们的名字来调用内核函数,就像其他任何系统调用一样。

    用户/内核交互的其他选择

    系统调用是请求内核中服务的一种有效方法。使用这种方法的最大问题就是它是一个标准接口,很难将新的系统调用增加到内核中,因此可以通过其他方法来实现类似服务。如果您无意将自己的系统调用加入公共的 Linux 内核中,那么系统调用就是将内核服务提供给用户空间的一种方便而且有效的方法。

    让您的服务对用户空间可见的另外一种方法是通过 /proc 文件系统。/proc 文件系统是一个虚拟文件系统,您可以通过它来向用户提供一个目录和文件,然后通过文件系统接口(读、写等)在内核中为新服务提供一个接口。

    使用 strace 跟踪系统调用

    Linux 内核提供了一种非常有用的方法来跟踪某个进程所调用的系统调用(以及该进程所接收到的信号)。这个工具就是>strace date

    结果会产生大量信息,显示在执行>munmap(0xb747a000, 4096)    = 0
    exit_group(0)            = ?
    $

    当当前系统调用请求具有一个名为 syscall_trace 的特定字段集(它导致 do_syscall_trace 函数的调用)时,将在内核中完成跟踪。您还可以看到跟踪调用是 ./linux/arch/i386/kernel/entry.S 中系统调用请求的一部分(请参看 syscall_trace_entry)。

    结束语

    系统调用是穿越用户空间和内核空间,请求内核空间服务的一种有效方法。不过对这种方法的控制也很严格,更简单的方式是增加一个新的 /proc 文件系统项来提供用户/内核间的交互。不过当速度因素非常重要时,系统调用则是使应用程序获得最佳性能的理想方法。请参看 参考资料 的内容进一步了解 SCI。


    ++++++++++++++++++++++++++++++++++

    深入理解Linux的系统调用 发布时间:2005.06.30 13:18    来源:赛迪网    作者:技术应用

    一、 什么是系统调用

    在Linux的世界里,我们经常会遇到系统调用这一术语,所谓系统调用,就是内核提供的、功能十分强大的一系列的函数。这些系统调用是在内核中实现的,再通过一定的方式把系统调用给用户,一般都通过门(gate)陷入(trap)实现。系统调用是用户程序和内核交互的接口。

    二、 系统调用的作用

    系统调用在Linux系统中发挥着巨大的作用,如果没有系统调用,那么应用程序就失去了内核的支持。

    我们在编程时用到的很多函数,如fork、open等这些函数最终都是在系统调用里实现的,比如说我们有这样一个程序:


    这里我们用到了两个函数,即fork和exit,这两函数都是glibc中的函数,但是如果我们跟踪函数的执行过程,看看glibc对fork和exit函数的实现就可以发现在glibc的实现代码里都是采用软中断的方式陷入到内核中再通过系统调用实现函数的功能的。具体过程我们在系统调用的实现过程会详细的讲到。

    由此可见,系统调用是用户接口在内核中的实现,如果没有系统调用,用户就不能利用内核。

    三、 系统调用的现实及调用过程

    详细讲述系统调用的之前也讲一下Linux系统的一些保护机制。

    Linux系统在CPU的保护模式下提供了四个特权级别,目前内核都只用到了其中的两个特权级别,分别为“特权级0”和“特权级3”,级别0也就是我们通常所讲的内核模式,级别3也就是我们通常所讲的用户模式。划分这两个级别主要是对系统提供保护。内核模式可以执行一些特权指令和进入用户模式,而用户模式则不能。

    这里特别提出的是,内核模式与用户模式分别使用自己的堆栈,当发生模式切换的时候同时要进行堆栈的切换。

    每个进程都有自己的地址空间(也称为进程空间),进程的地址空间也分为两部分:用户空间和系统空间,在用户模式下只能访问进程的用户空间,在内核模式下则可以访问进程的全部地址空间,这个地址空间里的地址是一个逻辑地址,通过系统段面式的管理机制,访问的实际内存要做二级地址转换,即:逻辑地址?线性地址?物理地址。

    系统调用对于内核来说就相当于函数,我们是关键问题是从用户模式到内核模式的转换、堆栈的切换以及参数的传递。

    下面将结合内核源代码对这些过程进行分析,以下分析环境为FC2,kernel 2.6.5

    下面是内核源代码里arch/i386/kernel/entry.S的一段代码。


    以上这段代码里定义了两个非常重要的宏,即SAVE_ALL和RESTORE_ALL

    SAVE_ALL先保存用户模式的寄存器和堆栈信息,然后切换到内核模式,宏__SWITCH_KERNELSPACE实现地址空间的转换RESTORE_ALL的过程过SAVE_ALL的过程正好相反。

    在内核原代码里有一个系统调用表:(entry.S的文件里)


    在2.6.5的内核里,有280多个系统调用,这些系统调用的名称全部在这个系统调用表里。

    在这个原文件里,还有非常重要的一段。


    这一段完成系统调用的执行。

    system_call函数根据用户传来的系统调用号,在系统调用表里找到对应的系统调用再执行。

    从glibc的函数到系统调用还有一个很重要的环节就是系统调用号。

    系统调用号的定义在include/asm-i386/unistd.h里


    每一个系统调用号都对应有一个系统调用

    接下来就是系统调用宏的展开

    没有参数的系统调用的宏展开

    !!!代码6::

    带一个参数的系统调用的宏展开

    !!!代码7::

    两个参数

    代码8::

    #define _syscall2(type,name,type1,arg1,type2,arg2) \

    三个参数的

    代码9::

    #define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \

    四个参数的

    代码10::

    #define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \

    五个参数的

    代码11::

    #define _syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \

    type5,arg5) \

    六个参数的

    代码12::

    #define _syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \

    type5,arg5,type6,arg6) \

    _res); \

    从这段代码我们可以看出int $0x80通过软中断开触发系统调用,当发生调用时,函数中的name会被系统系统调用名所代替。然后调用前面所讲的system_call。这个过程里包含了系统调用的初始化,系统调用的初始化原代码在:

    arch/i386/kernel/traps.c中每当用户执行int 0x80时,系统进行中断处理,把控制权交给内核的system_call。

    整个系统调用的过程可以总结如下:

    1. 执行用户程序(如:fork)

    2. 根据glibc中的函数实现,取得系统调用号并执行int $0x80产生中断。

    3. 进行地址空间的转换和堆栈的切换,执行SAVE_ALL。(进行内核模式)

    4. 进行中断处理,根据系统调用表调用内核函数。

    5. 执行内核函数。

    6. 执行RESTORE_ALL并返回用户模式

    解了系统调用的实现及调用过程,我们可以根据自己的需要来对内核的系统调用作修改或添加。

    ++++++++++++++++++++++++++++++++++

    Linux系统调用接口、系统调用例程和内核服务例程之间的关系
    信息来源: 维库开发网 发布时间:2009年2月17日

      系统调用接口的主要任务是把进程从用户态切换到内核态。在具有保护机制的计算机系 统中,用户必须通过软件中断或陷阱,才能使进程从用户态切换为内核态。

      在i386体系中,Linux的系统调用接口是通过调用软中断指令“int 0x80”使进程从用户态进入内核态的,这个过程也叫做“陷入”。当系统调用接口调用软中断指令“int0x80”时,这个指令会发生一个中断向量码为128的中断请求,并在中断响应过程中将进程由用户态切换为内核态。

      因为Linux只允许系统调用接口使用128这一个软中断向量,这也就意味着所有的系统调用接口必须共享这一个中断通道,并在同一个中断服务例程中调用不同的内核服务例程,所以,系统调用接口除了要引发“int0x80”软中断之外,为了进人内核后能调用不同的内核服务例程,还要提供识别内核服务例程的参数,这个参数叫做“系统调用号”。也就是说,所有可为进程提供服务的内核服务例程都应具有一个唯一的系统调用号。当然,系统调用接口还应为内核服务例程准各必要的参数。

      综上所述,系统调用接口需要完成以下几个任务:

      ●用软中断指令“int 0x80”发生一个中断向量码为128的中断请求,以使进程进入内核态。

      ●要保护用户态的现场,即把处理器的用户态运行环境保护到进程的内核堆栈。

      ●为内核服务例程准备参数,并定义返回值的存储位置。

      ●跳转到系统调用例程。

      ●系统调用例程结束后返回。

      系统调用例程是系统提供的一个通用的汇编语言程序.其实它是一个中断向量为128的中断服务程序,其入口为system_call。它应完成的任务有:

      ●接受系统调用接口的参数。

      ●根据系统调用号,转向对应的内核服务例程,并将相关参数传遴给内核服务例程。

      ●在内核服务例程结束后,自中断返田到系统凋甩接口.

      系统调用的过程如图所示。

      从图中可以看到,系统调用接口是用高级语言来编写的,而通过调用中断指令陷入内核后的系统调用例程(即图中的系统调用处理程序)则是用汇编语言编写的。

      为了通过系统调用号来调用不同的内核服务例程,系统必须维护一个系统调用表,这个表实质上就是系统调用号与内核服务函数的对照表。Linux是用数组sys_call_tabl来作为这个表的,在这个表的每个表项中存放着对应内核服务例程的指针,而该表项的下标就是该内核服务例程的系统调用号。Linux规定,在1386体系中,系统调用号由处理器的寄存器eax来传递。



      图 系统调用的处理过程

      系统调用表Sys_call_table的部分内容列举如下:


      欢迎转载,信息来自维库电子市场网(www.dzsc.com)


    ++++++++++++++++++++++++++++++++++

    向linux内核中添加三个系统调用(Ubuntu9.10)

              系统调用是操作系统提供给软件开发人员的唯一接口,开发人员可利用它使用系统功能。OS核心中都有一组实现系统功能的过程(子程序),系统调用就是对上述过程的调用。因此,系统调用像一个黑箱子那样,对用户屏蔽了操作系统的具体动作而只提供有关的功能。
        
        系统调用在os中发挥着巨大的作用,如果没有系统调用那么应用程序就是失去了内核的支持。在系统中真正被所有进程都使用的内核通信方式是系统调用。例如当进程请求内核服务时,就使用的是系统调用。一般情况下,进程是不能够存取系统内核的。它不能存取内核使用的内存段,也不能调用内核函数,CPU的硬件结构保证了这一点。只有系统调用是一个例外。进程使用寄存器中适当的值跳转到内核中事先定义好的代码中执行,(当然,这些代码是只读的)。在Intel结构的计算机中,这是由中断0x80实现的。

        进程可以跳转到的内核中的位置叫做system_call。在此位置的过程检查系统调用号,它将告诉内核进程请求的服务是什么。然后,它再查找系统调用表sys_call_table,找到希望调用的内核函数的地址,并调用此函数,最后返回。
        

        所以,如果希望改变一个系统调用的函数,需要做的是编写一个自己的函数,然后改变sys_call_table中的指针指向该函数,最后再使用cleanup_module将系统调用表恢复到原来的状态。我们现在向内核中添加的三个系统调用就是属于向内核中添加新的函数,且这些函数是可以直接操作系统内核的。下面是系统调用的基本处理过程:

      

       
        

        Linux的系统调用机制
         
         在Linux系统中,系统调用是作为一种异常类型实现的。它将执行相应的机器代码指令来产生异常信号。产生中断或异常的重要效果是系统自动将用户态切换为核心态来对它进行处理。这就是说,执行系统调用异常指令时,自动地将系统切换为核心态,并安排异常处理程序的执行。Linux用来实现系统调用异常的实际指令是:
                     Int $0x80

    这一指令使用中断/异常向量号128(即16进制的80)将控制权转移给内核。为达到在使用系统调用时不必用机器指令编程,在标准的C语言库中为每一系统调用提供了一段短的子程序,完成机器代码的编程工作。事实上,机器代码段非常简短。它所要做的工作只是将送给系统调用的参数加载到CPU寄存器中,接着执行int $0x80指令。然后运行系统调用,系统调用的返回值将送入CPU的一个寄存器中,标准的库子程序取得这一返回值,并将它送回用户程序。

      为使系统调用的执行成为一项简单的任务,Linux提供了一组预处理宏指令。它们可以用在程序中。这些宏指令取一定的参数,然后扩展为调用指定的系统调用的函数。

      这些宏指令具有类似下面的名称格式:

      _syscallN(parameters)

      其中N是系统调用所需的参数数目,而parameters则用一组参数代替。这些参数使宏指令完成适合于特定的系统调用的扩展。例如,为了建立调用setuid()系统调用的函数,应该使用:

      _syscall1( int, setuid, uid_t, uid )

      syscallN( )宏指令的第1个参数int说明产生的函数的返回值的类型是整型,第2个参数setuid说明产生的函数的名称。后面是系统调用所需要的每个参数。这一宏指令后面还有两个参数uid_t和uid分别用来指定参数的类型和名称。

      另外,用作系统调用的参数的数据类型有一个限制,它们的容量不能超过四个字节。这是因为执行int $0x80指令进行系统调用时,所有的参数值都存在32位的CPU寄存器中。使用CPU寄存器传递参数带来的另一个限制是可以传送给系统调用的参数的数目。这个限制是最多可以传递5个参数。所以Linux一共定义了6个不同的_syscallN()宏指令,从_syscall0()、 _syscall1()直到_syscall5()。

        一旦_syscallN()宏指令用特定系统调用的相应参数进行了扩展,得到的结果是一个与系统调用同名的函数,它可以在用户程序中执行这一系统调用。
        
    整个系统调用的过程可以总结如下:
    1,  执行用户程序;
        2, 根据glibc(GNU实现的一套标准C的库函数)中的函数实现,取得系统调    用号并执行 int $0x80产生中断;
        3, 进行地址空间的转换和堆栈的切换,执行SAVE_ALL。(进入内核模式)
        4, 进行中断处理,根据系统调用表调用内核函数;
        5, 执行内核函数;
        6, 执行RESTORE_ALL并返回用户模式;
        
        

       系统实现:

     

          这里以具体的例子来说明如何向系统中添加新的系统调用。具体实现所用的文件等可能与上面所述有点不一致,但原理是相同的。
        1、实验环境:
           实验的环境为Ubuntu9.10系统,内核版本为2.6.31-21-generic。添加完系统调用后的内核版本命名为2.6.31-12。
         2,实验步骤:
       1)下载Linux内核:在终端中输入命令$sudo apt-get install   linux-source。下载后的文件默认放在目录/usr/src下。
       2) 将内核代码解压缩:例如下载的内核文件为linux-source-2.6.31.tar.bz2,运行解压命令tar –jxvf linux-source-2.6.31.tar.bz2。解压出的文件夹为/usr/src/linux-source-2.6.31。如下图:
        
      3)  修改/usr/src/linux-source-2.6.31/kernel/sys.c文件,在文件末尾增加三个系统响应函数。函数实现如下:
    asmlinkage int sys_mycall(int number)
     {
        printk("这是我添加的第一个系统调用");
        return number;
     }
     asmlinkage int sys_addtotal(int number)
     {
         int i=0,enddate=0;
         printk("这是我添加的第二个系统调用");
         while(i<=number)
           enddate+=i++;
         return enddate;
     }
     asmlinkage int sys_three()
     {
         printk("这是我添加的第三个系统调用");
         return 0;
     }

        4)在/usr/src/linux-source-2.6.31/arch/x86/kernel/syscall_table_32.S 中添加:.long sys_mycall。

     

        
           
            
          5)在/usr/src/linux-2.6.31/arch/x86/include/asm/unistd_32.h中添加:#define __NR_mycall 序号(例如337),添加系统调用的入口参数(注意:其中会顺序定义入口参数的序号,添加的序号是在原有最大值的基础上+1);实现如下:
          
        编译内核,命令依次如下:
       首先切换到解压的内核目录下。

       第一步:make mrproper //清除内核中不稳定的目标文件,附属文件及内核配置文件
       第二步:make clean //清除以前生成的目标文件和其他文件
       第三步:make oldconfig// 采用默认的内核配置(使用make menuconfig可以自己配置编译选项)
       第四步:make bzImage //编译内核
       第五步:make modules //编译模块
       第六步:make modules_install// 安装模块

        编译完成后,设置采用新内核启动。

        我编译成功的内核版本号命名为2.6.31.12
        运行命令:
        cp /usr/src/linux-source-2.6.31/arch/i386/boot/
        bzImage   /boot/vmlinuz-2.6.31.12-mykernel(注意:2.6.31.12为你编译的内核版本。)
        
        mkinitramfs  -o initrd.img-2.6.31.12 2.6.31.12
           //执行目录/usr/src/linux-source-2.6.31/下
        
        cp /usr/src/linux-source-2.6.31/initrd.img-2.6.31.12 /boot/    initrd.img-2.6.31.12   
        
        
        
        增加引导菜单项,配置启动项文件/boot/grub/grub.cfg。添加的配置如下:
         
    完成后执行终端命令sudo update-grub2,之后重启,终端输入uname -a检查你的内核版本是否是你编译的版本2.6.31.12 。


        编写测试函数:我的测试函数如下:
        /*~~~~~~~~~~~~~~~test1.c~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
        #include<stdio.h>
        
        int main()
        {
          int tmp;
          tmp=syscall(337,1);
          printf("\n");
          if(tmp==1)
          {
            printf("第1次系统调用成功!\n");
          }
        
          tmp=syscall(338,5);
          printf("\n");
          if(tmp==15)
          {
            printf("第2次系统调用成功!\n");
          }
          tmp=syscall(339);
          printf("\n");
          if(tmp==0)
          {
            printf("第3次系统调用成功!\n");
          }
        }
        
    编译,运行。在终端输入dmesg -c可显示函数的输出内容。



        总结:
    由于使用了系统调用,编译和执行程序时,用户都应该获得超级用户权限。而且grub.cfg默认是没有写权限的,需要修改是有写权限,为了系统安全,在配置完文件后不要忘了再将权限改回来。

    编译的时间会很长,我成功的一次编译用了接近2.5个小时。需要点耐心。

    如果用虚拟机安装的话,需要注意磁盘空间的大小,我第一次用了个5G个虚拟系统,结果编译到中途就没空间了。10G左右的系统大小估计差不多。

    最后的测试函数可能看不到预想的输出系统,因为printk不会直接打印出来,而是需要命令:dmesg,直接执行会打印出很多东西,但它有一个参数:-c,清除缓存中的系统信息。于是每次用dmesg时,都加上这个参数,结果就只打印我们需要的信息。

    ++++++++++++++++++++++++++++++++++


阅读(3850)>
0

上一篇:Linux 的虚拟文件系统--各结构之间的联系

下一篇:file_operations -- file -- inode



0 0
原创粉丝点击