系统调用

目 录

  1. 系统调用
    1. 系统调用简述
      1. 系统调用表
      2. 系统调用入口函数
    2. 系统调用实现过程
      1. 函数名约定
      2. 系统调用号
      3. 系统调用表
      4. 从ptrace系统调用命令到INT 0X80中断请求的转换
      5. 系统调用功能模块的初始化
      6. 内核服务
    3. 代码分析:mlock()
      1. 主要数据结构
      2. 重要常量
      3. 代码函数功能分析
    4. 添加新调用
      1. 例子一
      2. 例子二


系统调用

    在系统中真正被所有进程都使用的内核通信方式是系统调用。例如当进程请求内核服务时,就使用的是系统调用。一般情况下,进程是不能够存取系统内核的。它不能存取内核使用的内存段,也不能调用内核函数,CPU的硬件结构保证了这一点。只有系统调用是一个例外。进程使用寄存器中适当的值跳转到内核中事先定义好的代码中执行,(当然,这些代码是只读的)。在Intel结构的计算机中,这是由中断0x80实现的。

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

    所以,如果希望改变一个系统调用的函数,需要做的是编写一个自己的函数,然后改变sys_call_table中的指针指向该函数,最后再使用cleanup_module将系统调用表恢复到原来的状态

[目录]


系统调用简述

    linux里面的每个系统调用是靠一些宏,,一张系统调用表,一个系统调用入口来完成的。
[目录]



    宏就是_syscallN(type,name,x...),N是系统调用所需的参数数目,type是返回类型,name即面向用户的系统调用函数名,x...是调用参数,个数即为N。
    例如:
#define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
                  "d" ((long)(arg3))); \
if (__res>=0) \
        return (type) __res; \
errno=-__res; \
return -1; \
}
(这是2.0.33版本)
    这些宏定义于include\asm\Unistd.h,这就是为什么你在程序中要包含这个头文件的原因。该文件中还以__NR_name的形式定义了164个常数,这些常数就是系统调用函数name的函数指针在系统调用表中的偏移量。

[目录]


系统调用表

    系统调用表定义于entry.s的最后。
    这个表按系统调用号(即前面提到的__NR_name)排列了所有系统调用函数的指针,以供系统调用入口函数查找。从这张表看得出,linux给它所支持的系统调用函数取名叫sys_name。


[目录]


系统调用入口函数

    系统调用入口函数定义于entry.s:

ENTRY(system_call)
        pushl %eax                      # save orig_eax
        SAVE_ALL
#ifdef __SMP__
        ENTER_KERNEL
#endif
        movl $-ENOSYS,EAX(%esp)
        cmpl $(NR_syscalls),%eax
        jae ret_from_sys_call
        movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax
        testl %eax,%eax
        je ret_from_sys_call
#ifdef __SMP__
        GET_PROCESSOR_OFFSET(%edx)
        movl SYMBOL_NAME(current_set)(,%edx),%ebx
#else
        movl SYMBOL_NAME(current_set),%ebx
#endif
        andl $~CF_MASK,EFLAGS(%esp)
        movl %db6,%edx
        movl %edx,dbgreg6(%ebx)
        testb $0x20,flags(%ebx)
        jne 1f
        call *%eax
        movl %eax,EAX(%esp)
        jmp ret_from_sys_call
    这段代码现保存所有的寄存器值,然后检查调用号(__NR_name)是否合法(在系统调用表中查找),找到正确的函数指针后,就调用该函数(即你真正希望内核帮你运行的函数)。运行返回后,将调用ret_from_sys_call,这里就是著名的进程调度时机之一。
    当在程序代码中用到系统调用时,编译器会将上面提到的宏展开,展开后的代码实际上是将系统调用号放入ax后移用int 0x80使处理器转向系统调用入口,然后查找系统调用表,进而由内核调用真正的功能函数。
    自己添加过系统调用的人可能知道,要在程序中使用自己的系统调用,必须显示地应用宏_syscallN。
    而对于linux预定义的系统调用,编译器在预处理时自动加入宏_syscall3(int,ioctl,arg1,arg2,arg3)并将其展开。所以,并不是ioctl本身是宏替换符,而是编译器自动用宏声明了ioctl这个函数。


[目录]


系统调用实现过程

[目录]


函数名约定

系统调用响应函数的函数名约定
    函数名以“sys_”开头,后跟该系统调用的名字,由此构成164个形似sys_name()的函数名。因此,系统调用ptrace()的响应函数是sys_ptrace() (kernel/ptrace.c)。


[目录]


系统调用号

系统调用号
    文件include/asm/unistd.h为每个系统调用规定了唯一的编号:

#define __NR_setup                  0
#define __NR_exit                            1
#define __NR_fork                  2
…        …
#define __NR_ptrace                  26

    以系统调用号__NR_name作为下标,找出系统调用表sys_call_table (arch/i386/kernel/entry.S)中对应表项的内容,正好就是该系统调用的响应函数sys_name的入口地址。

[目录]


系统调用表

系统调用表
    系统调用表sys_call_table (arch/i386/kernel/entry.S)形如:

ENTRY(sys_call_table)
        .long SYMBOL_NAME(sys_setup)                /* 0 */
        .long SYMBOL_NAME(sys_exit)
        .long SYMBOL_NAME(sys_fork)
                …        …
                .long SYMBOL_NAME(sys_stime)                /* 25 */
        .long SYMBOL_NAME(sys_ptrace)
                …        …

    sys_call_table记录了各sys_name函数(共166项,其中2项无效)在表中的位子。有了这张表,很容易根据特定系统调用在表中的偏移量,找到对应的系统调用响应函数的入口地址。NR_syscalls(即256)表示最多可容纳的系统调用个数。这样,余下的90项就是可供用户自己添加的系统调用空间。

[目录]


从ptrace系统调用命令到INT 0X80中断请求的转换

从ptrace系统调用命令到INT  0X80中断请求的转换
    宏定义syscallN()(include/asm/unistd.h)用于系统调用的格式转换和参数的传递。

#define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \
type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
          "d" ((long)(arg3)),"S" ((long)(arg4))); \
__syscall_return(type,__res); \
}

    N取0与5之间任意整数。参数个数为N的系统调用由syscallN负责格式转换和参数传递。例如,ptrace()有四个参数,它对应的格式转换宏就是syscall4()。

    syscallN()第一个参数说明响应函数返回值的类型,第二个参数为系统调用的名称(即name),其余的参数依次为系统调用参数的类型和名称。例如,

_syscall4(int, ptrace, long request, long pid, long addr, long data)

    说明了系统调用命令

int sys_ptrace(long request, long pid, long addr, long data)

    宏定义的余下部分描述了启动INT 0X80和接收、判断返回值的过程。也就是说,以系统调用号对EAX寄存器赋值,启动INT 0X80。规定返回值送EAX寄存器。函数的参数压栈,压栈顺序见下表:
参数        参数在堆栈的位置        传递参数的寄存器

arg1        00(%esp)        ebx
arg2        04(%esp)        ecx
arg3        08(%esp)        edx
arg4        0c(%esp)        esi
arg5        10(%esp)        edi

    若INT 0X80的返回值非负,则直接按类型type返回;否则,将INT 0X80的返回值取绝对值,保留在errno变量中,返回-1。

[目录]


系统调用功能模块的初始化

系统调用功能模块的初始化
    对系统调用的初始化也即对INT 0X80的初始化。系统启动时,汇编子程序setup_idt(arch/i386/kernel/head.S)准备了张256项的idt 表,由start_kernel()(init/main.c)、trap_init()(arch/i386/kernel/traps.c)调用的C语言宏定义set_system_gate(0x80, &system_call)(include/asm/system.h)设置0X80号软中断的服务程序为system_call。system_call(arch/i386/kernel/entry.S)就是所有系统调用的总入口。


[目录]


内核服务

LINUX内部是如何分别为各种系统调用服务的
    当进程需要进行系统调用时,必须以C语言函数的形式写一句系统调用命令。当进程执行到用户程序的系统调用命令时,实际上执行了由宏命令_syscallN()展开的函数。系统调用的参数由各通用寄存器传递。然后执行INT 0X80,以核心态进入入口地址system_call。
ENTRY(system_call)
        pushl %eax                        # save orig_eax
        SAVE_ALL
#ifdef __SMP__
        ENTER_KERNEL
#endif
        movl $-ENOSYS,EAX(%esp)
        cmpl $(NR_syscalls),%eax
        jae ret_from_sys_call
        movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax
        testl %eax,%eax
        je ret_from_sys_call
#ifdef __SMP__
        GET_PROCESSOR_OFFSET(%edx)
        movl SYMBOL_NAME(current_set)(,%edx),%ebx
#else
        movl SYMBOL_NAME(current_set),%ebx
#endif
        andl $~CF_MASK,EFLAGS(%esp)        # clear carry - assume no errors
        movl %db6,%edx
        movl %edx,dbgreg6(%ebx)  # save current hardware debugging status
        testb $0x20,flags(%ebx)                # PF_TRACESYS
        jne 1f
        call *%eax
        movl %eax,EAX(%esp)                # save the return value
        jmp ret_from_sys_call

    从system_call入口的汇编程序的主要功能是:
    ·保存寄存器当前值(SAVE_ALL);
    ·检验是否为合法的系统调用;
    ·根据系统调用表_sys_call_table和EAX持有的系统调用号找出并转入系统调用响应函数;
    ·从该响应函数返回后,让EAX寄存器保存函数返回值,跳转至ret_from_sys_call(arch/i386/kernel/entry.S)。
    ·最后,在执行位于用户程序中系统调用命令后面余下的指令之前,若INT 0X80的返回值非负,则直接按类型type返回;否则,将INT 0X80的返回值取绝对值,保留在errno变量中,返回-1。


[目录]


代码分析:mlock()

    系统调用mlock的作用是屏蔽内存中某些用户进程所要求的页。

    mlock调用的语法为:
        int sys_mlock(unsigned long start, size_t len);
    初始化为:

len=(len+(start &~PAGE_MASK)+ ~PAGE_MASK)&PAGE_MASK;
start &=PAGE_MASK;

    其中mlock又调用do_mlock(),语法为:

int do_mlock(unsigned long start, size_t len,int on);

    初始化为:

len=(len+~PAGE_MASK)&PAGE_MASK;

    由mlock的参数可看出,mlock对由start所在页的起始地址开始,长度为len(注:len=(len+(start&~PAGE_MASK)+ ~PAGE_MASK)&PAGE_MASK)的内存区域的页进行加锁。
    sys_mlock如果调用成功返回,这其中所有的包含具体内存区域的页必须是常驻内存的,或者说在调用munlock 或 munlockall之前这部分被锁住的页面必须保留在内存。当然,如果调用mlock的进程终止或者调用exec执行其他程序,则这部分被锁住的页面被释放。通过fork()调用所创建的子进程不能够继承由父进程调用mlock锁住的页面。
    内存屏蔽主要有两个方面的应用:实时算法和高度机密数据的处理。实时应用要求严格的分时,比如调度,调度页面是程序执行延时的一个主要因素。保密安全软件经常处理关键字节,比如密码或者密钥等数据结构。页面调度的结果是有可能将这些重要字节写到外存(如硬盘)中去。这样一些黑客就有可能在这些安全软件删除这些在内存中的数据后还能访问部分在硬盘中的数据。        而对内存进行加锁完全可以解决上述难题。
    内存加锁不使用压栈技术,即那些通过调用mlock或者mlockall被锁住多次的页面可以通过调用一次munlock或者munlockall释放相应的页面
    mlock的返回值分析:若调用mlock成功,则返回0;若不成功,则返回-1,并且errno被置位,进程的地址空间保持原来的状态。返回错误代码分析如下:
    ·ENOMEM:部分具体地址区域没有相应的进程地址空间与之对应或者超出了进程所允许的最大可锁页面。
    ·EPERM:调用mlock的进程没有正确的优先权。只有root进程才允许锁住要求的页面。
    ·EINVAL:输入参数len不是个合法的正数。


[目录]


主要数据结构

1.mm_struct
struct mm_struct {
        int count;
        pgd_t * pgd; /* 进程页目录的起始地址*/
        unsigned long context;
        unsigned long start_code, end_code, start_data, end_data;
        unsigned long start_brk, brk, start_stack, start_mmap;
        unsigned long arg_start, arg_end, env_start, env_end;
        unsigned long rss, total_vm, locked_vm;
        unsigned long def_flags;
        struct vm_area_struct * mmap;     /* 指向vma双向链表的指针 */
        struct vm_area_struct * mmap_avl; /* 指向vma AVL树的指针 */
        struct semaphore mmap_sem;
}
·start_code、end_code:进程代码段的起始地址和结束地址。
·start_data、end_data:进程数据段的起始地址和结束地址。
·arg_start、arg_end:调用参数区的起始地址和结束地址。
·env_start、env_end:进程环境区的起始地址和结束地址。
·rss:进程内容驻留在物理内存的页面总数。


2. 虚存段(vma)数据结构:vm_area_atruct
    虚存段vma由数据结构vm_area_atruct(include/linux/mm.h)描述:

struct vm_area_struct {
        struct mm_struct * vm_mm;        /* VM area parameters */
        unsigned long vm_start;
        unsigned long vm_end;
        pgprot_t vm_page_prot;
        unsigned short vm_flags;
/* AVL tree of VM areas per task, sorted by address */
        short vm_avl_height;
        struct vm_area_struct * vm_avl_left;
        struct vm_area_struct * vm_avl_right;
/* linked list of VM areas per task, sorted by address */
        struct vm_area_struct * vm_next;
/* for areas with inode, the circular list inode->i_mmap */
/* for shm areas, the circular list of attaches */
/* otherwise unused */
        struct vm_area_struct * vm_next_share;
        struct vm_area_struct * vm_prev_share;
/* more */
        struct vm_operations_struct * vm_ops;
        unsigned long vm_offset;
        struct inode * vm_inode;
        unsigned long vm_pte;                        /* shared mem */
};

vm_start;//所对应内存区域的开始地址
vm_end; //所对应内存区域的结束地址
vm_flags; //进程对所对应内存区域的访问权限
vm_avl_height;//avl树的高度
vm_avl_left; //avl树的左儿子
vm_avl_right; //avl树的右儿子
vm_next;// 进程所使用的按地址排序的vm_area链表指针
vm_ops;//一组对内存的操作
    这些对内存的操作是当对虚存进行操作的时候Linux系统必须使用的一组方法。比如说,当进程准备访问某一虚存区域但是发现此区域在物理内存不存在时(缺页中断),就激发某种对内存的操作执行正确的行为。这种操作是空页(nopage)操作。当Linux系统按需调度可执行的页面映象进入内存时就使用这种空页(nopage)操作。
    当一个可执行的页面映象映射到进程的虚存地址时,一组vm_area_struct结构的数据结构(vma)就会生成。每一个vm_area_struct的数据结构(vma)代表可执行的页面映象的一部分:可执行代码,初始化数据(变量),非初始化数据等等。Linux系统可以支持大量的标准虚存操作,当vm_area_struct数据结构(vma)一被创建,它就对应于一组正确的虚存操作。
    属于同一进程的vma段通过vm_next指针连接,组成链表。如图2-3所示,struct mm_struct结构的成员struct vm_area_struct * mmap  表示进程的vma链表的表头。
    为了提高对vma段 查询、插入、删除操作的速度,LINUX同时维护了一个AVL(Adelson-Velskii and Landis)树。在树中,所有的vm_area_struct虚存段均有左指针vm_avl_left指向相邻的低地址虚存段,右指针vm_avl_right指向相邻的高地址虚存段,如图2-5。struct mm_struct结构的成员struct vm_area_struct * mmap_avl表示进程的AVL树的根,vm_avl_height表示AVL树的高度。
    对平衡树mmap_avl的任何操作必须满足平衡树的一些规则:
Consistency and balancing rulesJ(一致性和平衡规则):

tree->vm_avl_height==1+max(heightof(tree->vm_avl_left),heightof(
tree->vm_avl_right))
abs( heightof(tree->vm_avl_left) - heightof(tree->vm_avl_right) ) <= 1
foreach node in tree->vm_avl_left: node->vm_avl_key <= tree->vm_avl_key,        foreach node in tree->vm_avl_right: node->vm_avl_key >= tree->vm_avl_key.
        注:其中node->vm_avl_key= node->vm_end

对vma可以进行加锁、加保护、共享和动态扩展等操作。


[目录]


重要常量

    mlock系统调用所用到的重要常量有:PAGE_MASK、PAGE_SIZE、PAGE_SHIFT、RLIMIT_MEMLOCK、VM_LOCKED、 PF_SUPERPRIV等。它们的值分别如下:

        PAGE_SHIFT                        12                                // PAGE_SHIFT determines the page size
        PAGE_SIZE                        0x1000                        //1UL<<PAGE_SHIFT
        PAGE_MASK                        ~(PAGE_SIZE-1)        //a very useful constant variable
        RLIMIT_MEMLOCK                8                                //max locked-in-memory address space
        VM_LOCKED                        0x2000                        //8*1024=8192, vm_flags的标志之一。
        PF_SUPERPRIV                0x00000100                //512

[目录]


代码函数功能分析

mlock系统调用代码函数功能分析

    下面对各个函数的功能作详细的分析((1)和(2)在前面简介mlock时已介绍过,并在后面有详细的程序流程):

suser():如果用户有效(即current->euid == 0        ),则设置进程标志为root优先权(current->flags |= PF_SUPERPRIV),并返回1;否则返回0。

find_vma(struct mm_struct * mm, unsigned long addr):输入参数为当前进程的mm、需要加锁的开始内存地址addr。find_vma的功能是在mm的mmap_avl树中寻找第一个满足mm->mmap_avl->vm_start<=addr< mm->mmap_avl->vm_end的vma,如果成功则返回此vma;否则返回空null。

mlock_fixup(struct vm_area_struct * vma, unsigned long start, unsigned long end, unsigned int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址和结束地址、需要修改的标志(0:加锁,1:释放锁)。

merge_segments(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr):输入参数为当前进程的mm、需要加锁的开始内存地址start_addr和结束地址end_addr。merge_segments的功能的是尽最大可能归并相邻(即内存地址偏移量连续)并有相同属性(包括vm_inode,vm_pte,vm_ops,vm_flags)的内存段,在这过程中冗余的vm_area_structs被释放,这就要求vm_mmap链按地址大小排序(我们不需要遍历整个表,而只需要遍历那些交叉或者相隔一定连续区域的邻接vm_area_structs)。当然在缺省的情况下,merge_segments是对vm_mmap_avl树进行循环处理,有多少可以合并的段就合并多少。

mlock_fixup_all(struct vm_area_struct * vma, int newflags):输入参数为vm_mmap链中的某个vma、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_all的功能是根据输入参数newflags修改此vma的vm_flags。

mlock_fixup_start(struct vm_area_struct * vma,unsigned long end, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域结束地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_start的功能是根据输入参数end,在内存中分配一个新的new_vma,把原来的vma分成两个部分: new_vma和vma,其中new_vma的vm_flags被设置成输入参数newflags;并且按地址(new_vma->start和new_vma->end)大小序列把新生成的new->vma插入到当前进程mm的mmap链或mmap_avl树中(缺省情况下是插入到mmap_avl树中)。
        注:vma->vm_offset+= vma->vm_start-new_vma->vm_start;
mlock_fixup_end(struct vm_area_struct * vma,unsigned long start, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_end的功能是根据输入参数start,在内存中分配一个新的new_vma,把原来的vma分成两个部分:vma和new_vma,其中new_vma的vm_flags被设置成输入参数newflags;并且按地址大小序列把new->vma插入到当前进程mm的mmap链或mmap_avl树中。
        注:new_vma->vm_offset= vma->vm_offset+(new_vma->vm_start-vma->vm_start);
mlock_fixup_middle(struct vm_area_struct * vma,unsigned long start, unsigned long end, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址和结束地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_middle的功能是根据输入参数start、end,在内存中分配两个新vma,把原来的vma分成三个部分:left_vma、vma和right_vma,其中vma的vm_flags被设置成输入参数newflags;并且按地址大小序列把left->vma和right->vma插入到当前进程mm的mmap链或mmap_avl树中。
        注:vma->vm_offset += vma->vm_start-left_vma->vm_start;
                right_vma->vm_offset += right_vma->vm_start-left_vma->vm_start;

kmalloc():常用的一个内核函数

insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp):输入参数为当前进程的mm、需要插入的vmp。insert_vm_struct的功能是按地址大小序列把vmp插入到当前进程mm的mmap链或mmap_avl树中,并且把vmp插入到vmp->inode的i_mmap环(循环共享链)中。

avl_insert_neighbours(struct vm_area_struct * new_node,** ptree,** to_the_left,** to_the_right):输入参数为当前需要插入的新vma结点new_node、目标mmap_avl树ptree、新结点插入ptree后它左边的结点以及它右边的结点(左右边结点按mmap_avl中各vma->vma_end大小排序)。avl_insert_neighbours的功能是插入新vma结点new_node到目标mmap_avl树ptree中,并且调用avl_rebalance以保持ptree的平衡树特性,最后返回new_node左边的结点以及它右边的结点。

avl_rebalance(struct vm_area_struct *** nodeplaces_ptr, int count):输入参数为指向vm_area_struct指针结构的指针数据nodeplaces_ptr[](每个元素表示需要平衡的mmap_avl子树)、数据元素个数count。avl_rebalance的功能是从nodeplaces_ptr[--count]开始直到nodeplaces_ptr[0]循环平衡各个mmap_avl子树,最终使整个mmap_avl树平衡。

down(struct semaphore * sem):输入参数为同步(进入临界区)信号量sem。down的功能根据当前信号量的设置情况加锁(阻止别的进程进入临界区)并继续执行或进入等待状态(等待别的进程执行完成退出临界区并释放锁)。
        down定义在/include/linux/sched.h中:
extern inline void down(struct semaphore * sem)
{
        if (sem->count <= 0)
                __down(sem);
        sem->count--;
}

up(struct semaphore * sem)输入参数为同步(进入临界区)信号量sem。up的功能根据当前信号量的设置情况(当信号量的值为负数:表示有某个进程在等待使用此临界区 )释放锁。
        up定义在/include/linux/sched.h中:
extern inline void up(struct semaphore * sem)
{
        sem->count++;
        wake_up(&sem->wait);
        }

kfree_s(a,b):kfree_s定义在/include/linux/malloc.h中:#define kfree_s(a,b) kfree(a)。而kfree()将在后面3.3中详细讨论。

avl_neighbours(struct vm_area_struct * node,* tree,** to_the_left,** to_the_right):输入参数为作为查找条件的vma结点node、目标mmap_avl树tree、node左边的结点以及它右边的结点(左右边结点按mmap_avl中各vma->vma_end大小排序)。avl_ neighbours的功能是根据查找条件node在目标mmap_avl树ptree中找到node左边的结点以及它右边的结点,并返回。

avl_remove(struct vm_area_struct * node_to_delete, ** ptree):输入参数为需要删除的结点node_to_delete和目标mmap_avl树ptree。avl_remove的功能是在目标mmap_avl树ptree中找到结点node_to_delete并把它从平衡树中删除,并且调用avl_rebalance以保持ptree的平衡树特性。

remove_shared_vm_struct(struct vm_area_struct *mpnt):输入参数为需要从inode->immap环中删除的vma结点mpnt。remove_shared_vm_struct的功能是从拥有vma结点mpnt 的inode->immap环中删除的该结点。


[目录]


添加新调用

[目录]


例子一

深入LINUX内核:为你的LINUX增加一条系统调用
  充分利用LINUX开放源码的特性,我们可以轻易地对它进行修改,使我们能够随心所欲驾驭LINUX,完成一个真正属于自己的操作系统,这种感觉使无与伦比的,下面通过为LINUX增加一个系统调用来展示LINUX作为一个开放源码操作系统的强大魅力。
  首先,让我们简单地分析一下LINUX中与系统调用的相关的部分:
  LINUX的系统调用的总控程序是system_call,它是LINUX系统中所有系统调用的总入口,这个system_call是作为一个中断服务程序挂在中断0x80上,系统初始化时通过void init trap_init(void)调用一个宏set_system_ gate(SYSCALL_VERCTOR,&system_call)来对IDT表进行初始化,在0x80对应的中断描述符处填入system_call函数的地址,其中宏SYSCALL_VERCTOR就是0x80。
  当发生一条系统调用时,由中断总控程序保存处理机状态,检查调用参数的合法性,然后根据系统调用向量在sys_call_table中找到相应的系统服务例程的地址,然后执行该服务例程,完成后恢复中断总控程序所保存的处理机状态,返回用户程序。
  系统服务例程一般定义于kernel/sys.c中,系统调用向量定义在include/asm-386/unistd.h中,而sys_call _table表则定义在arch/i386/kernel/entry.S文件里。
  现在我们知道增加一条系统调用我们首先要添加服务例程实现代码,然后在进行对应向量的申明,最后当然还要在sys_call_table表中增加一项以指明服务例程的入口地址。
  OK,有了以上简单的分析,现在我们可以开始进行源码的修改,假设我们需要添加一条系统调用计算两个整数的平方和,系统调用名为add2,我们需要修改三个文件:kernel/sys.c , arch/i386/kernel/entry.S 和 include/asm-386/unistd.h。
  1、修改kernel/sys.c ,增加服务例程代码:
  asmlinkage int sys_add2(int a , int b)
    {
      int c=0;
      c=a*a+b*b;
      return c;
    }
  2、修改include/asm-386/unistd.h ,对我们刚才增加的系统调用申明向量,以使用户或系统进程能够找到这条系统调用,修改后文件如下所示:
  .... .....
  #define _NR_sendfile   187
  #define _NR_getpmsg    188
  #define _NR_putmsg    189
  #define _NR_vfork     190
  #define _NR_add2     191   /* 这是我们添加的部分,191即向量 */
  3、修改include/asm-386/unistd.h , 将服务函数入口地址加入 sys_call_table,首先找到这么一段:
  .... .....
  .long SYMBOL_NAME(sys_sendfile)
  .long SYMBOL_NAME(sys_ni_syscall) /* streams 1 */
  .long SYMBOL_NAME(sys_ni_syscall) /* streams 2 */
  .long SYMBOL_NAME(sys_vfork) /*190 */
  .rept NR_syscalls-190
  修改为如下:
  .... .....
  .long SYMBOL_NAME(sys_sendfile)
  .long SYMBOL_NAME(sys_ni_syscall) /* streams 1 */
  .long SYMBOL_NAME(sys_ni_syscall) /* streams 2 */
  .long SYMBOL_NAME(sys_vfork) /*190 */
  .long SYMBOL_NAME(sys_add2) <=我们的系统调用
  .rept NR_syscalls-191 <=将190改为191
  OK,大功告成,现在只需要重新编译你的LINUX内核,然后你的LINUX就有了一条新的系统调用int add2(int a, int b)。

[目录]


例子二

如何在Linux中添加新的系统调用

  系统调用是应用程序和操作系统内核之间的功能接口。其主要目的是使得用户可以使用操作系统提供的有关设备管理、输入/输入系统、文件系统和进程控制、通信以及存储管理等方面的功能,而不必了解系统程序的内部结构和有关硬件细节,从而起到减轻用户负担和保护系统以及提高资源利用率的作用。

1 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()宏指令用特定系统调用的相应参数进行了扩展,得到的结果是一个与系统调用同名的函数,它可以在用户程序中执行这一系统调用。

2 添加新的系统调用
  如果用户在Linux中添加新的系统调用,应该遵循几个步骤才能添加成功,下面几个步骤详细说明了添加系统调用的相关内容。
(1) 添加源代码
  第一个任务是编写加到内核中的源程序,即将要加到一个内核文件中去的一个函数,该函数的名称应该是新的系统调用名称前面加上sys_标志。假设新加的系统调用为mycall(int number),在/usr/src/linux/kernel/sys.c文件中添加源代码,如下所示:
  asmlinkage int sys_mycall(int number)
  {
  return number;
  }
  作为一个最简单的例子,我们新加的系统调用仅仅返回一个整型值。
(2) 连接新的系统调用
  添加新的系统调用后,下一个任务是使Linux内核的其余部分知道该程序的存在。为了从已有的内核程序中增加到新的函数的连接,需要编辑两个文件。
  在我们所用的Linux内核版本(RedHat 6.0,内核为2.2.5-15)中,第一个要修改的文件是:
  /usr/src/linux/include/asm-i386/unistd.h
  该文件中包含了系统调用清单,用来给每个系统调用分配一个唯一的号码。文件中每一行的格式如下:
  #define __NR_name NNN
  其中,name用系统调用名称代替,而NNN则是该系统调用对应的号码。应该将新的系统调用名称加到清单的最后,并给它分配号码序列中下一个可用的系统调用号。我们的系统调用如下:
  #define __NR_mycall 191
  系统调用号为191,之所以系统调用号是191,是因为Linux-2.2内核自身的系统调用号码已经用到190。
  第二个要修改的文件是:
  /usr/src/linux/arch/i386/kernel/entry.S
  该文件中有类似如下的清单:
  .long SYMBOL_NAME()
  该清单用来对sys_call_table[]数组进行初始化。该数组包含指向内核中每个系统调用的指针。这样就在数组中增加了新的内核函数的指针。我们在清单最后添加一行:

  .long SYMBOL_NAME(sys_mycall)

(3) 重建新的Linux内核
  为使新的系统调用生效,需要重建Linux的内核。这需要以超级用户身份登录。
  #pwd
  /usr/src/linux
  #
  超级用户在当前工作目录(/usr/src/linux)下,才可以重建内核。
  #make config
  #make dep
  #make clearn
  #make bzImage
  编译完毕后,系统生成一可用于安装的、压缩的内核映象文件:
  /usr/src/linux/arch/i386/boot/bzImage
(4) 用新的内核启动系统
  要使用新的系统调用,需要用重建的新内核重新引导系统。为此,需要修改/etc/lilo.conf文件,在我们的系统中,该文件内容如下:
  boot=/dev/hda
  map=/boot/map
  install=/boot/boot.b
  prompt
  timeout=50
  image=/boot/vmlinuz-2.2.5-15
  label=linux
  root=/dev/hdb1
  read-only
  other=/dev/hda1
  label=dos
  table=/dev/had
  首先编辑该文件,添加新的引导内核:
  image=/boot/bzImage-new
  label=linux-new
  root=/dev/hdb1
  read-only
  添加完毕,该文件内容如下所示:
  boot=/dev/hda
  map=/boot/map
  install=/boot/boot.b
  prompt
  timeout=50
  image=/boot/bzImage-new
  label=linux-new
  root=/dev/hdb1
  read-only
  image=/boot/vmlinuz-2.2.5-15
  label=linux
  root=/dev/hdb1
  read-only
  other=/dev/hda1
  label=dos
  table=/dev/hda
  这样,新的内核映象bzImage-new成为缺省的引导内核。
  为了使用新的lilo.conf配置文件,还应执行下面的命令:
  #cp /usr/src/linux/arch/i386/boot/zImage /boot/bzImage-new
  其次配置lilo:
  # /sbin/lilo
  现在,当重新引导系统时,在boot:提示符后面有三种选择:linux-new 、 linux、dos,新内核成为缺省的引导内核。
  至此,新的Linux内核已经建立,新添加的系统调用已成为操作系统的一部分,重新启动Linux,用户就可以在应用程序中使用该系统调用了。
(5)使用新的系统调用
  在应用程序中使用新添加的系统调用mycall。同样为实验目的,我们写了一个简单的例子xtdy.c。
  /* xtdy.c */
  #include <linux/unistd.h>
  _syscall1(int,mycall,int,ret)
  main()
  {
  printf("%d \n",mycall(100));
  }
  编译该程序:
  # cc -o xtdy xtdy.c
  执行:

  # xtdy
  结果:
  # 100
  注意,由于使用了系统调用,编译和执行程序时,用户都应该是超级用户身份。
(文/程仁田)


[目录]


[ 本文件由良友·收藏家自动生成 ]