深入理解Linux的系统调用【转】
一、 什么是系统调用
在Linux的世界里,我们经常会遇到系统调用这一术语,所谓系统调用,就是内核提供的、功能十分强大的一系列的函数。这些系统调用是在内核中实现的,再通过一定的方式把系统调用给用户,一般都通过门(gate)陷入(trap)实现。系统调用是用户程序和内核交互的接口。
二、 系统调用的作用
系统调用在Linux系统中发挥着巨大的作用,如果没有系统调用,那么应用程序就失去了内核的支持。
我们在编程时用到的很多函数,如fork、open等这些函数最终都是在系统调用里实现的,比如说我们有这样一个程序:
#include <unistd.h>
#include <stdlib.c>
int main()
{
fork();
exit();
}
这里我们用到了两个函数,即fork和exit,这两函数都是glibc中的函数,但是如果我们跟踪函数的执行过程,看看glibc对fork和exit函数的实现就可以发现在glibc的实现代码里都是采用软中断的方式陷入到内核中再通过系统调用实现函数的功能的。具体过程我们在系统调用的实现过程会详细的讲到。
由此可见,系统调用是用户接口在内核中的实现,如果没有系统调用,用户就不能利用内核。
三、 系统调用的现实及调用过程
详细讲述系统调用的之前也讲一下Linux系统的一些保护机制。
Linux系统在CPU的保护模式下提供了四个特权级别,目前内核都只用到了其中的两个特权级别,分别为“特权级0”和“特权级3”,级别0也就是我们通常所讲的内核模式,级别3也就是我们通常所讲的用户模式。划分这两个级别主要是对系统提供保护。内核模式可以执行一些特权指令和进入用户模式,而用户模式则不能。
这里特别提出的是,内核模式与用户模式分别使用自己的堆栈,当发生模式切换的时候同时要进行堆栈的切换。
每个进程都有自己的地址空间(也称为进程空间),进程的地址空间也分为两部分:用户空间和系统空间,在用户模式下只能访问进程的用户空间,在内核模式下则可以访问进程的全部地址空间,这个地址空间里的地址是一个逻辑地址,通过系统段面式的管理机制,访问的实际内存要做二级地址转换,即:逻辑地址线性地址物理地址。
系统调用对于内核来说就相当于函数,我们是关键问题是从用户模式到内核模式的转换、堆栈的切换以及参数的传递。
下面将结合内核源代码对这些过程进行分析,以下分析环境为FC2,kernel 2.6.5
下面是内核源代码里arch/i386/kernel/entry.S的一段代码
/* clobbers ebx, edx and ebp */ #define __SWITCH_KERNELSPACE \ cmpl $0xff000000, %esp; \ jb 1f; \ \ /* \ * switch pagetables and load the real stack, \ * keep the stack offset: \ */ \ \ movl $swapper_pg_dir-__PAGE_OFFSET, %edx; \ \ /* GET_THREAD_INFO(%ebp) intermixed */ \ : \ ……………………………………. \ : #endif #define __SWITCH_USERSPACE \ /* interrupted any of the user return paths? */ \ \ movl EIP(%esp), %eax; \ ……………………………………….. \ jb 22f; /* yes - switch to virtual stack */ \ /* return to userspace? */ \ : \ movl EFLAGS(%esp),%ecx; \ movb CS(%esp),%cl; \ testl $(VM_MASK ),%ecx; \ jz 2f; \ : \ /* \ * switch to the virtual stack, then switch to \ * the userspace pagetables. \ */ \ \ GET_THREAD_INFO(%ebp); \ movl TI_virtual_stack(%ebp), %edx; \ movl TI_user_pgd(%ebp), %ecx; \ \ movl %esp, %ebx; \ andl $(THREAD_SIZE-), %ebx; \ orl %ebx, %edx; \ int80_ret_start_marker: \ movl %edx, %esp; \ movl %ecx, %cr3; \ \ __RESTORE_ALL; \ int80_ret_end_marker: \ : #else /* !CONFIG_X86_HIGH_ENTRY */ #define __SWITCH_KERNELSPACE #define __SWITCH_USERSPACE #endif #define __SAVE_ALL \ …………………………………….. #define __RESTORE_INT_REGS \ …………………………. #define __RESTORE_REGS \ __RESTORE_INT_REGS; \ : popl %ds; \ : popl %es; \ .section .fixup,"ax"; \ : movl $,(%esp); \ jmp 111b; \ : movl $,(%esp); \ jmp 222b; \ .previous; \ .section __ex_table,"a";\ .align ; \ .long 111b,444b;\ .long 222b,555b;\ .previous #define __RESTORE_ALL \ __RESTORE_REGS \ addl $, %esp; \ : iret; \ .section .fixup,"ax"; \ : sti; \ movl $(__USER_DS), %edx; \ movl %edx, %ds; \ movl %edx, %es; \ pushl $; \ call do_exit; \ .previous; \ .section __ex_table,"a";\ .align ; \ .long 333b,666b;\ .previous #define SAVE_ALL \ __SAVE_ALL; \ __SWITCH_KERNELSPACE; #define RESTORE_ALL \ __SWITCH_USERSPACE; \ __RESTORE_ALL;
以上这段代码里定义了两个非常重要的宏,即SAVE_ALL和RESTORE_ALL
SAVE_ALL先保存用户模式的寄存器和堆栈信息,然后切换到内核模式,宏__SWITCH_KERNELSPACE实现地址空间的转换RESTORE_ALL的过程过SAVE_ALL的过程正好相反。
在内核原代码里有一个系统调用表:(entry.S的文件里)
ENTRY(sys_call_table) .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */ .long sys_exit .long sys_fork .long sys_read .long sys_write .long sys_open /* 5 */ ……………….. .long sys_mq_timedreceive /* 280 */ .long sys_mq_notify .long sys_mq_getsetattr syscall_table_size=(.-sys_call_table)
在2.6.5的内核里,有280多个系统调用,这些系统调用的名称全部在这个系统调用表里。
在这个原文件里,还有非常重要的一段
ENTRY(system_call) pushl %eax # save orig_eax SAVE_ALL GET_THREAD_INFO(%ebp) cmpl $(nr_syscalls), %eax jae syscall_badsys # system call tracing in operation testb $(_TIF_SYSCALL_TRACE _TIF_SYSCALL_AUDIT),TI_flags(%ebp) jnz syscall_trace_entry syscall_call: call *sys_call_table(,%eax,) movl %eax,EAX(%esp) # store the return value syscall_exit: cli # make sure we don't miss an interrupt # setting need_resched or sigpending # between sampling and the iret movl TI_flags(%ebp), %ecx testw $_TIF_ALLWORK_MASK, %cx # current->work jne syscall_exit_work restore_all: RESTORE_ALL
这一段完成系统调用的执行。
system_call函数根据用户传来的系统调用号,在系统调用表里找到对应的系统调用再执行。
从glibc的函数到系统调用还有一个很重要的环节就是系统调用号。
系统调用号的定义在include/asm-i386/unistd.h里
#define __NR_restart_syscall 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 …………………………………..
每一个系统调用号都对应有一个系统调用
接下来就是系统调用宏的展开
//没有参数的系统调用的宏展开
#define _syscall0(type,name) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "" (__NR_##name)); \ __syscall_return(type,__res); \ } // 带一个参数的系统调用的宏展开
#define _syscall1(type,name,type1,arg1) \ type name(type1 arg1) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "" (__NR_##name),"b" ((long)(arg1))); \ __syscall_return(type,__res); \ } // 两个参数 #define _syscall2(type,name,type1,arg1,type2,arg2) \ // 三个参数的 #define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \ // 四个参数的 #define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \ // 五个参数的 #define _syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \ type5,arg5) \
// 六个参数的 #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的系统调用【转】的更多相关文章
- 理解Linux系统调用
目录 1.什么是系统调用 2.linux的系统调用 3.linux系统调用实现 1.什么是系统调用 系统调用,指的是操作系统提供给用户程序调用的一组特殊接口,用户程序可以根据这组接口获得操作系统内核的 ...
- 深入理解linux网络技术内幕读书笔记(三)--用户空间与内核的接口
Table of Contents 1 概论 1.1 procfs (/proc 文件系统) 1.1.1 编程接口 1.2 sysctl (/proc/sys目录) 1.2.1 编程接口 1.3 sy ...
- 理解 Linux 配置文件分类和使用
理解 Linux 配置文件分类和使用 本文说明了 Linux 系统的配置文件,在多用户.多任务环境中,配置文件控制用户权限.系统应用程序.守护进程.服务和其它管理任务.这些任务包括管理用户帐号.分配磁 ...
- 深入理解Linux内存分配
深入理解Linux内存分配 为了写一个用户层程序,你也许会声明一个全局变量,这个全局变量可能是一个int类型也可能是一个数组,而声明之后你有可能会先初始化它,也有可能放在之后用到它的时候再初始化.除此 ...
- 深入理解linux系统下proc文件系统内容
深入理解linux系统下proc文件系统内容 内容摘要:Linux系统上的/proc目录是一种文件系统,即proc文件系统. Linux系统上的/proc目录是一种文件系统,即proc文件系统.与其它 ...
- 深入理解linux关闭文件和删除文件
背景介绍 最近看了linux系统编程(linux system programming)一书,结合深入理解linux内核(understanding the linux kernel)一书,深入理解了 ...
- 读书笔记之Linux系统编程与深入理解Linux内核
前言 本人再看深入理解Linux内核的时候发现比较难懂,看了Linux系统编程一说后,觉得Linux系统编程还是简单易懂些,并且两本书都是讲Linux比较底层的东西,只不过侧重点不同,本文就以Linu ...
- 理解Linux的进程,线程,PID,LWP,TID,TGID
在Linux的top和ps命令中,默认看到最多的是pid (process ID),也许你也能看到lwp (thread ID)和tgid (thread group ID for the threa ...
- 深入理解Solaris X64系统调用
理解系统调用的关键在于洞悉系统调用号是联系用户模式与内核模式的纽带.而在Solaris x64平台上,系统调用号被保存在寄存器RAX中,从用户模式传递到内核模式.一旦进入内核模式,内核的sys_sys ...
随机推荐
- html css 如何将表头固定(转)
html css 如何将表头固定 position属性取值为fixed时,则元素的位置将不受滚动条的影响,而是直接依据窗口定位,这就是将表头固定的最直接方法,网上其他途径感觉都是在走弯路.但是与此同时 ...
- maven添加jar包依赖
maven的东西使用了一段时间,但是每次使用都多少有点含糊,所以总结一下. 目前主要使用是在jar包的控制上 原理: 在本地,指定一个文件夹,便是maven的仓库,maven会从远程的中央仓库中下载你 ...
- 利用钩子函数来捕捉键盘响应的windows应用程序
一:引言: 你也许一直对金山词霸的屏幕抓词的实现原理感到困惑,你也许希望将你的键盘,鼠标的活动适时的记录下来,甚至你想知道木马在windows操作系统是怎样进行木马dll的加载的…..其实这些都是用到 ...
- uva 1377
比较不错的一个题,关键是理解状态转移 #include<algorithm> #include<cstdio> #include<cstring> #include ...
- spoj 297
就是对距离进行二分找最大值 .... #include <cstring> #include <cstdio> #include <algorithm> #incl ...
- 关于.net中的脚本语言使用
基于.net中drl框架的脚本现在有很多,最近也由于工作的需要,目前有lua.python.ruby.javascript的.net实现,对ruby不怎么了解,python.lua.js就成了试验的对 ...
- jmeter 启用gzip压缩——解决测试中web服务器上行流量过大的问题
最近测了几个前端的项目,发现它们都有一个共同点:应用所在服务器的网卡上行(trans)非常大——经常是 117 MB/S,这已经逼近了千兆网卡的极限了.下面记录下排查和解决过程: 一. jmeter ...
- UIcollectionView的使用(首页的搭建2)
2.2 直接购买的UIcollectionCell 2.2.1创建CFPromptBuyCell,继承自UICollectionViewCell,定义了标题和图片两个属性 2.2.2 在.m文件中定义 ...
- png24是支持Alpha透明的。。。。。。
这个可能跟每个人使用切图软件有关. 1.Photoshop 1)只能导出布尔透明(全透明或者全不透明)的PNG8. 2)能导出alpha透明(全透明,全不透明,半透明)的PNG24. ...
- (转)Android系统自带样式(@android:style/)
在AndroidManifest.xml文件的activity中配置 1.android:theme="@android:style/Theme" 默认状态,即如果theme这里不 ...