荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: igmp (igmp), 信区: Security
标  题: 论文集(四)
发信站: 荔园晨风BBS站 (Wed Jun 27 23:15:25 2001), 转信


Linux 系统调用与实例分析

目    录
一        系统调用概述……………………………………………………1
二        Linux系统调用流程…………………………………………… 2
  2.1  Linux系统调用的中断机制……………………………………………2
  2.2  相关的数据结构及函数………………………………………………  2
2. 3  Linux系统调用的流程…………………………………………………7
三  实例分析 - fork系统调用…………………………………… 8
  3. 1  系统调用fork简介…………………………………………………   8
  3. 2  系统调用fork的设置………………………………………………… 8
  3. 3  函数do_fork()的分析………………………………………………  9
四   讨论………………………………………………………………15



一.    系统调用概述
从一般用户的观点,操作系统是用户与计算机硬件系统之间的接口;从资源管理观
点,操作系统是计算机系统资源的管理者。用户在操作系统的帮助下能够方便、快
捷、安全、可靠的操纵计算机和运行自己的程序。用户可以通过以下的两种方式来
使用计算机:
1.命令方式。这是指由操作系统提供了一组联机命令(语言),用户可通过键盘
键入有关的命令,来直接操纵计算机系统。
2.系统调用方式。操作系统提供了一组系统调用,用户可在应用程序中通过调用
                            相应的系统调用来操纵计算机系统。系统调用是用
户程序与kernel的接口.
           以下是对系统调用的简介。系统调用命令是操作系统为满足用户所需的功能和
保证程序的正常运转事先编制好的具有特定功能的例行子程序。每当用户在程序中
需要操作系统提供某种服务时,便可利用一条系统调用命令,去调用系统过程。它
一般运行在核心态;可通过中断进入,返回时通常需要重新调度。
          Linux系统调用由0x80号中断进入系统调用入口,使系统由用户态转为核心态。
通过使用系统调用表保存系统调用服务函数的入口地址,转入特定的例行子程序去
执行,完成用户当前所需要的服务来实现。
    本文通过对Linux的一般系统调用过程分析和创建子进程的系统调用fork的分
析来阐述Linux系统调用过程.

二.    Linux系统调用流程
 2.1 Linux系统调用的中断机制
    linux系统是通过中断处理来实现系统调用的。设置中断向量时,把0x80号中
断向量和系统调用处理程序联系起来。该处理程序的功能是:做常规的现场保护后
,按系统调用功能号找到对应的系统调用函数的地址,转到该函数中去执行。在用
户程序中,安排一句系统调用命令(该命令已由_syscallN(parametres)展开,展开
部分有指令:INT $0x80),当程序执行到这条命令时,就发生中断, 系统由用户态转
为核心态,操作系统的系统调用处理程序得到控制权,它在保存所有寄存器和确定
该系统调用合法后,将根据用户提供的系统调用名,利用syscall number确定系统
调用的功能号,根据sys_call_table找到例行系统调用函数的入口地址,转入对应
的系统调用函数执行。执行完毕后,返回到用户程序的断点继续执行。
 2.2 相关的数据结构及函数
    2.2.1 设定0x80号中断
        系统启动后所进行的一系列初始化工作中较重要的一部分在start_kernel()函数
中进
行,各种中断服务程序入口在其中通过调用
trap_init()(arch/i386/kernel/traps.c中)
被设置,与系统调用相关的是:set_system_gate(0x80,&system_call)
宏set_system_gate()("include/asm-i386/system.h"中):
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)

宏_set_gate()(include/asm-i386/system.h中,作用是使addr地址值置入
gate_addr的地址值所指向的内存单元中,使中断向量表中的0x80项保存了中断服
务程序system_call的入口地址):
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ __volatile__ ("movw %%dx,%%ax\n\t" \
"movw %2,%%dx\n\t" \
"movl %%eax,%0\n\t" \
"movl %%edx,%1" \
:"=m" (*((long *) (gate_addr))), \
"=m" (*(1+(long *) (gate_addr))) \
:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \

"d" ((char *) (addr)),"a" (KERNEL_CS << 16) \
:"ax","dx"

初始化中断向量表(其中0x80项保存了中断服务程序system_call的入口地址):

0       divide_error
1       debug
2       nmi
3       int 3
4       overflow
5       bounds
6       Invalid_op
7       device_not_available
8       double_fault
9       Coprocessor_segment_overrun
10      Invalid_TSS
11      Segment_not_present
12      Stack_segment
13      General_protection
14      Page_fault
15      Spurious_interrupt_bug
16      Coprocessor_error
17      Alignment_check
18-48   reserved
128     System_call

    2.2.2 数据结构
介绍系统调用主要的两种数据结构:系统调用表和寄存器帧结构。
先分析系统调用表sys_call_table(/arch/i386/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_mremap)
        .long 0,0
                .long SYMBOL_NAME(sys_vm86)     /* 166 */
        .space (NR_syscalls-166)*4

表中保存了所有Linux基于Intel x86系列体系结构的计算机的166个系统调用入口
地址(其中3个保留,Linux开辟的系统调用表可容纳NR_syscalls(
/include/linux/sys.h中定义的值为256的宏)项),其中每项都被说明成 long型
。可根据特定系统调用在表中偏移量找到对应的系统调用代码。.
space(NR_syscalls-166)*4表示所剩的可供用户自己添加系统调用的空间。
在unistd.h中为每一种系统调用分配了一个唯一的编号(syscall number),相应于
系统调用在sys_call_table中的偏移量。如:
#define __NR_setup                0
#define __NR_exit                 1
    #define __NR_fork             2
    #define __NR_read             3
       :
                 :
                 :

        寄存器帧结构:pt_regs(include/asm-i386/ptrace.h中),该帧结构与系统调
用时压入堆栈的寄存器的顺序保持一致,用来在系统跟踪、系统调用返回时传递参
数,定义如下:
struct pt_regs {
long ebx;
long ecx;
long edx;
long esi;
long edi;
                                        :
                                        :
                                        :
};
在entry.s中压入堆栈的一帧和一个pt_regs结构体相互对应;这样,只要pt_regs
结构体的首地址是该帧的帧顶,则EAX(%esp)与 regs->eax将指向同一内存单元:

如:(entry.s中)
/*
 *       0(%esp) - %ebx        EBX=0X00
 *       4(%esp) - %ecx        ECX=0X04
 *       8(%esp) - %edx        EDX=0X08
                                        :
                                        :
                                        :
 */

    2.2.3 相关函数
1)        系统调用函数sys_NAME(parametres) (syscall.c, sys.c, time.c等文件中

系统调用时可根据sys_call_table调用这些以sys_开头的系统调用函数。
如:
  asmlinkage int sys_fork(struct pt_regs *regs) {...}
2)      一些有关的宏
  _syscallN(type,name, type1,arg1,type2,arg2……)(
include/asm-i386/unistd.h中)
 (N=0~5,表示系统调用的参数个数,相应的宏的参数为2* N+2个),在该文件中
, 共
  定义了6个宏。
  以宏_syscall2即系统调用的参数个数等于2为例:

#define _syscall2(type,name,type1,arg1,type2,arg2) \
type name(type1 arg1,type2 arg2) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \
if (__res >= 0) \
        return (type) __res; \
errno = -__res; \
return -1; \
}

    宏指令的第一个参数说明产生函数的返回值的类型,第二个参数为产生函数的
名称。参数列表中若还有参数,则第2i个参数是系统调用函数的第i个参数的类型
,第2i+1个参数是系统调用函数的第i个参数。
该宏以内联汇编形式实现,每次调用宏_syscallN (type, name, type1, arg1,
type2, arg2……),该宏扩展成返回值类型为type, 函数名为name的函数,并通过
name和NR_name(syscall numbers)建立联系, 从而确定该调用在sys_call_table
 中的偏移量,再根据sys_call_table确定系统调用函数的入口地址。其中,用户态
到核心态的转换是由宏扩展后的int $0x80指令完成的。如:
        _syscall0(int,fork)  即  int fork()
 传给系统调用的参数和系统调用的返回值是存放在CPU寄存器中的。所以要求
 系统调用的参数的数据类型不能超过4个字节;并且最多可传送5个参数,因此只

 义了_syscall0~5这6个不同的_syscallN()。
     语句 "=a" (__res) 指明返回参数(即__res)使用eax寄存器。在语句
 "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); 中:
 "0" (__NR_##name) 中将__NR_与参数name串接起来,形成的标志符存入eax
 寄存器,作为区别系统调用类型的参数。随后将参数arg1,arg2分别传给寄存
 器ebx和ecx,在"_syscallX"宏中,约定五个参数分别与五个寄存器对应:
           eax:名为name的系统调用在sys_call_table 中的偏移量
       ebx:arg1
       ecx:arg2
       edx:arg3
       esi :arg4
       edi :arg5
 在系统调用返回时,syscallN()都检验返回值是否为负,如果是(不合法)
 把_errno置为该返回值的绝对值,并返回-1,否则直接返回该返回值。
 宏中的汇编指令"int $0x80"使程序流转入"system_call"。

3)      中断入口system_call()(/arch/i386/entry.s中)
system_call是所有系统调用的入口。它的功能是:保存所有寄存器,检验是否是
合法的系统调用,根据_sys_call_table中的偏移量把控制权转给真正的系统调用
代码,系统调用完毕后调用_ret_from_sys_call(),返回到用户空间。下面解释关
于它的一些重要指令:
 调用宏过程SAVE_ALL保护现场(保存寄存器)。这样保存的一帧寄存器该
过程所要传递的pt_regs结构类型的参数结构一致。cmpl$(NR_syscalls),%eax比较
NR_syscalls与eax的大小,如果eax大于或等于NR_syscalls,表明指定的系统调用
函数错误,jae ret_from_sys_call使系统调用直接返回。再执行movl
SYMBOL_NAME(sys_call_table)(,%eax,4),%eax ,以sys_call_table为基地址,
eax寄存器中的内容(系统调用的序号)乘以4(long型字节数)为偏移量,即得到
所需调用的系统调用函数的入口地址,将其存入寄存器eax。然后判断寄存器eax值
是否为0,若是,表明出错,直接返回。语句:
#ifdef __SMP__
                GET_PROCESSOR_OFFSET(%edx)
                movl SYMBOL_NAME(current_set)(,%edx),%ebx
#else
        movl SYMBOL_NAME(current_set),%ebx
    #endif
使寄存器ebx指向当前进程。语句:
andl $~CF_MASK,EFLAGS(%esp)
movl %db6,%edx
        movl %edx,dbgreg6(%ebx)
将CF清为0,并保存当前调试信息于task_struct结构中。在entry.S中,定义了一系
列宏,用来表示当前进程的信息,它们是:
state           = 0
counter         = 4
priority                = 8
signal          = 12
blocked         = 16
flags           = 20
dbgreg6         = 52
dbgreg7         = 56
exec_domain     = 60
    语句testb $0x20,flags(%ebx)检测当前进程是否正跟踪系统调用,如果不是
,直接调用所选系统调用函数:call *%eax; 如判断当前进程正处于跟踪系统调
用状态(current->flags&PF_TRACESYS==0)就调用函数体syscall_trace()(
/arch/i386 /kernel/ptrace.c中),使当前进程状态转为TASK_STOPPED,将该进
程转入睡眠状态。然后从堆栈中弹出原来的eax值,再重新设置系统调用函数的偏
移量,调用实现相应系统调用的函数,语句为:
call SYMBOL_NAME(syscall_trace)
movl ORIG_EAX(%esp),%eax
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
    之后,就进入了系统调用服务函数,该函数返回以后进入ret_from_sys_call
, 处
理一些系统调用返回前应该处理的事情,如检测bottom half缓冲区,判断CPU是否
需要重新调度等,之后,系统调用返回。

2.3 Linux系统调用的流程
    系统启动时,经过引导和实模式下的初始化,进入保护模式下的核心初始化,
执行head.s。其中的startup_32代码段中调用 setup_idt。Setup_idt的功能是建
立一个256项的空的中断向量表。
接着,系统转入start_kernel()(/usr/src/linux/init/main.c中)模块,在该模
块中,调用trap_init()(/usr/src/linux/kernel/traps.c中)初始化中断向量表
,其中使系统调用system_call项成为0x80号中断的中断服务程序。

建立及初始化中断向量表的流程:
                           startup_32
                           call setup_idt

                           setup_idt

                           start kernel()

                           trap_init()
其中trap_init()含:
set_call_gate(&default_ldt,lcall7)
set_trap_gate(0,&divide_error)



     for(i-18;i<48;i++)
     set_trp_gate(i,&reserved)
set_system_gate(0x80,&system_call)

用户必须在程序中以系统调用命令(以调用函数的形式给出)进行系统调用,将该
命令由相应的宏_syscallN(……)展开。宏指令本身将在预处理时扩展为指定名
称的函数(宏_syscall(……)的第二个参数与系统调用命令时所用函数名一致)
。执行由宏指令扩展的函数,将系统调用参数值存入相应的CPU寄存器,然后执行
int $0x80中断处理指令,进入核心态,入口地址为&system_call。执行
system_call,保存寄存器,检查调用是否合法,如合法,根据_sys_call_table中
的偏移量转入相应的系统调用代码。系统调用结束时,调用_ret_from_sys_call。
对内核函数的返回值进行检查后返回用户态,并返回相应的值。系统调用通过可屏
蔽中断int 0x80 调用进行.

系统调用流程:
                    _syscallN (type, name, type1, arg1, type2, arg2……
);

                    type  name(type1 arg1,type2  arg2……);


三.    实例分析 - fork系统调用
 3.1系统调用fork简介
    fork系统调用的功能是创建新进程(调用fork()的进程的子进程),该子进程
是父进程的一个拷贝,只有进程号和其它少数参数不同而已。子进程将继承父进程
的实际和有效的 uid和 gid;所有父进程打开的文件传给子进程;子进程与父进程
具有相同的umask; 子进程继承当前的工作目录;一旦子进程建立,则父进程和子
进程都在fork内部继续执行。如果调用成功,fork系统调用对父进程返回新生成的
子进程的进程标识号pid,对子进程返回0;否则,将出错原因存入error变量,并向
父进程返回-1。产生的出错原因有:
1.     ENOMEM: fork为自己的存在申请内存空间失败。
2.     EAGAIN: fork为子进程的PCB的数据项分配内存空间失败。
    fork与clone类似,只是clone是创建一个与父进程完全相同的新进程,两者的
pid也相同。


3.2系统调用fork的设置
在include/asm-i386/unistd.h中进行系统调用的设置(包括fork);该系统调用
fork的设置使用的宏应为:_syscall0(int,fork),因为fork()是不带参数的,
且返回类型为int。
在include/asm-i386/unistd.h中,fork的内联宏语句为: static inline
_syscall0(int,fork)。这样,在调用fork时,系统将调用宏指令_syscall0(int,
fork),进而调用0x80号中断,此时寄存器eax中的值为__NR_fork(=2),作为参数
传给int $0x80。
调用中断int $0x80后,System_call在保存所有寄存器,检验是否是合法的系统调
用后,用eax中的值(__NR_fork)乘4得到系统调用表(sys_call_table)中的偏
移,找到入口:.long SYMBOL_NAME(sys_fork)
接着转向函数sys_fork()(arch/i386/kernel/process.c中):
asmlinkage int sys_fork(struct pt_regs regs)
{
                return do_fork(SIGCHLD, regs.esp, &regs);
}

    SIGCHLD (signal.h中)是一种信号类型,作为do_fork的第一个参数
clone_flags指明do_fork()函数应创建一子进程:
    #define SIGCHLD             17

    sys_fork()将类型为struct pt_regs(寄存器帧)的regs的地址作为参数传递给
do_fork(),
并且通过寄存器:regs.esp传递了栈顶指针。由于系统调用是通过寄存器传递参数
的,且
system_call 已将所有的CPU寄存器保存在堆栈中,根据2.2.2的讨论,只要regs首
址为当
前堆栈的栈顶指针,就可以通过regs取压入堆栈的各寄存器值(参数值)。
    实际上,fork系统调用最终是由do_fork()函数完成的(clone也是借助
do_fork完成的,
不同之处在于传入do_fork()的参数不同)。do_fork()在task数组中找到空闲位置
,继承父进
程现有资源,初始化进程时钟、信号、时间等数据。


3.3函数do_fork()的分析
int do_fork(unsigned long clone_flags, unsigned long usp, struct pt_regs
 *regs) (linux /kernel/fork.c中)
参数:clone_flages:unsigned long类型,区分sys_clone和sys_fork的标志。
Usp: unsigned long类型,用来传递sp的值。在sys_fork系统调用时,将regs.
esp       传给Usp,通过 copy_thread将其赋给子进程的esp。
 Regs:指向struct pt_regs 的指针类型。
返回值:int类型。如果调用成功,对父进程返回新生成的子进程的进程标识号
pid,对
     子进程返回0;否则,将出错原因存入error变量,并向父进程返回-1。

1.为新进程分配空间
do_fork()函数开始就将可能返回的error初始值置为-ENOMEM,假设内存已被
用完。然后,才进入主流程。
p = (struct task_struct *) kmalloc(sizeof(*p), GFP_KERNEL);
         if (!p)
                goto bad_fork;
 new_stack = alloc_kernel_stack();
       if (!new_stack)
                goto bad_fork_free_p;

       error = -EAGAIN;
          nr = find_empty_process();
       if (nr < 0)
                goto bad_fork_free_stack;


先调用kmalloc为进程申请内存空间,GFP_KERNEL表示允许申请不到内存时转入睡
眠状态。如果申请内存失败的话,将返回NULL。这时,do_fork()函数转入
bad_fork执行,直接返回内存已被用完的出错信息。
不然,do_fork()函数调用宏alloc_kernel_stack(),分配进程所需的堆栈,
如果申请失败,转入 bad_fork_free_p执行。
否则,表示ENOMEM的危险已经过去;
执行error = -EAGAIN;假设 fork为子进程的PCB的数据项分配内存空间失败。



Task数组(/kernel/sched.c中):
 struct task_struct *task[NR_TASKS];
其中, NR_TASKS的值为512,它规定了系统可运行的最大进程数,用来存放所
有进程PCB的指针。Find_empty_process()就是在不超过系统规定的最大进程数

资源限定允许的条件下在task数组中找是否有空闲的区域,有则把数组下标作为该

数的返回值,否则返回错误代码-EAGAIN。在do_fork中,把nr作为
find_empty_process
的返回值,若为正,说明find_empty_precess正常返回,且nr为task数组中空闲
PCB
的指针;若为负,说明无法增加新的进程,进入错误处理程序
bad_fork_free_stack,返
回值为-EAGAIN。


2.新进程初始化及有关参数设置
   #1.
  *p = *current;
 将父进程的内容赋给子进程,这时,子进程完全继承了父进程的特征,接下来根

   据clone_flags对子进程数据成员进行初始化。

  #2.
            if (p->exec_domain && p->exec_domain->use_count)
                (*p->exec_domain->use_count)++;
        if (p->binfmt && p->binfmt->use_count)
                (*p->binfmt->use_count)++;
分别将全局执行域结构和全局执行文件格式结构所对应的use_count加一,表示
进程数增一。

   #3.
    p->did_exec = 0;
子进程正在创建,未被执行过
        p->swappable = 0;
进程刚创建,暂不可调出内存

   #4.
        p->kernel_stack_page = new_stack;
*(unsigned long *) p->kernel_stack_page = STACK_MAGIC;
首先把核心栈所在物理页的基地址设为核心栈首址,然后把STACK_MAGIC与核
心栈所在页的unsigned long 形式联系起来。其中,STACK_MAGIC(kernel.h中)

   #define STACK_MAGIC  0xdeadbeef
*(unsigned long *) p->kernel_stack_page = STACK_MAGIC;为了在进程退出时,

验stack_page是否出错。

   #5.
p->state = TASK_UNINTERRUPTIBLE;
   设置新进程刚创建时状态为TASK_UNINTERRUPTIBLE,表示本进程将被置于等待
队列中,由于资源未分配好,因此置为不可中断,使其待资源有效、所有信息都设
置好后才被唤醒,不可由其它进程通过信号唤醒。

    #6.
        p->flags &= ~(PF_PTRACED|PF_TRACESYS|PF_SUPERPRIV);
关闭PF_PTRACED,PF_TRACESYS,PF_SUPERPRIV 信号,拒绝新建进程具有超
级用户特权或被跟踪
        p->flags |= PF_FORKNOEXEC;
开启PF_FORKNOEXEC信号,表示新建进程还没执行。

   #7.
        p->pid = get_pid(clone_flags);
得到新进程的pid。get_pid()函数先判断是否为clone系统调用(根据
clone_flages),若不是,则在1~0xffff8000中找到不等于任何进程的pid,
pgrp或session
的最小的数,并把这个最小数作为返回值。

   #8.
p->next_run = NULL;
p->prev_run = NULL;
由于新产生的进程的状态还是为TASK_UNINTERRUPTIBLE,因此不将其放入就绪队
列,其next_run和prev_run均置为NULL。
p->p_pptr = p->p_opptr = current;
p->p_cptr = NULL;
新进程的parent 和original parent置为当前进程Current,child置为空。

   #9.
        init_waitqueue(&p->wait_chldexit);
        为新进程的子进程初始化等待队列。

   #10.
        p->signal = 0;
        表示现在新建进程还没有收到信号。

   #11.
                p->it_real_value = p->it_virt_value = p->it_prof_value = 0;
        p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0;
        初始化用于进程计时的数据项,置为0,。表示现在新建进程尚未定时。其中
it_real_value, it_real_incr与jiffies保持一致,表示真实时间;
it_virt_value, it_virt_incr用于虚拟软件实时,它仅在进程运行时有效,因此
,该数据项用于进程内计时,当时间到时,发送信号  。参见do_it_virt()(
/kernel/sched.c中)。

init_timer(&p->real_timer);
        p->real_timer.data = (unsigned long) p;
初始化timer_list类型的real_timer,使其data与p相等,作为区分timer的标志。

        p->utime = p->stime = 0;
        p->cutime = p->cstime = 0;
分别将进程用户态时间总和,核心态时间总和,子进程用户态时间总和,子进程
核心态时间总和均初始化为零。
        p->start_time = jiffies;
将当前进程的建立时间置为jiffies。

   #12.
#ifdef __SMP__
        p->processor = NO_PROC_ID;
        p->lock_depth = 1;
#endif
设置多处理器信息。

   #13.
        task[nr] = p;
将p的值赋给task数组的第nr项,使新进程的PCB进入PCB_SET数组。
        SET_LINKS(p);
将新进程与初始进程相关联。
        nr_tasks++;
当前进程数加1。
        error = -ENOMEM;
假设错误为内存不够。

  #14.
        if (copy_files(clone_flags, p))
                goto bad_fork_cleanup;
        if (copy_fs(clone_flags, p))
                goto bad_fork_cleanup_files;
        if (copy_sighand(clone_flags, p))
                goto bad_fork_cleanup_fs;
        if (copy_mm(clone_flags, p))
                goto bad_fork_cleanup_sighand;
        根据clone_flages判断,进程是否由sys_clone产生,如果是,则不进行拷贝,只

是把当前进程中相关数据结构成员的count值加1;否则,拷贝所有进程信息(文件
信息(copy_files),目录信息(copy_fs),信号信息(copy_sighand)和内存
信息(copy_mm))。只有当父进程或子进程要对虚存进行写操作时,才给子进程
的mm所指向的数据结构分配内存,并将父进程的mm所指向的数据内容拷贝到子进程
的mm上。这些函数的正常返回值均为0,如果返回值非0,就转入到相应的异常处理
程序中去。

   #15.
      copy_thread(nr, clone_flags, usp, p, regs); (
/arch/i386/kernel/process.c中)
                对子进程PCB的tss(任务状态段)和ldt(进程局部描述符表的指针)进
行设
置。
其中,tss 包括各种通用寄存器。
其中:
              childregs = ((struct pt_regs *) (p->kernel_stack_page +
PAGE_SIZE)) - 1;
        p->tss.esp = (unsigned long) childregs;
    子进程任务状态段的esp指向当前栈顶。
          p->tss.eip = (unsigned long) ret_from_sys_call;
    将ret_from_sys_call赋予子进程任务状态段的eip。当子进程被唤醒时,
执行
ret_from_sys_call,由核心态返回到用户态。
          *childregs = *regs;
    使子进程的寄存器帧的内容与当前进程的同。
          childregs->eax = 0;
    子进程从fork()的返回值为0。
          childregs->esp = esp;
    把当前进程的esp赋给子进程的esp,使父子进程共用一个堆栈。


3.唤醒新进程,返回system_call
新生成的进程的参数全部设置完毕,接下来分配内存,用来保存与新进程相关的文
件系统,内存页面,信号处理程序等:
*       p->swappable = 1;
新进程已完成初始化,可以换出内存。
*   p->exit_signal = clone_flags & CSIGNAL;
设置系统强行退出时发出的信号,其中,CSIGNAL(sched.h中):
#define CSIGNAL         0x000000ff
为进程终止时须发的信息。
*       p->counter = (current->counter >>= 1);
子进程的counter为父进程的一半,即其时间片只有父进程的一半。
*       wake_up_process(p);
唤醒新进程,置其状态为TASK_RUNNING,如果它不在就绪队列中,把它加入。
*       ++total_forks;
进程数增一。
*       return p->pid;
返回至system_call,返回值为新进程的pid 。

4.其它
bad_fork_cleanup_sighand:
                exit_sighand(p);
bad_fork_cleanup_fs:
                exit_fs(p);
bad_fork_cleanup_files:
        exit_files(p);
错误处理函数    exit_sighand(p),exit_fs(p),exit_files(p)三者结构类似,分
别把子进
程的sighand,fs,files置为NULL,如果所对应的count为零,则释放该域所占据

空间。具体代码:
bad_fork_cleanup:
                if (p->exec_domain && p->exec_domain->use_count)
                        (*p->exec_domain->use_count)--;
                if (p->binfmt && p->binfmt->use_count)
                        (*p->binfmt->use_count)--;
                task[nr] = NULL;
        REMOVE_LINKS(p);
                nr_tasks--;
bad_fork_free_stack:
        free_kernel_stack(new_stack);
bad_fork_free_p:
                kfree(p);
bad_fork:
                return error;


函数do_fork()的流程:

 为新进程申请内存空间


        申请系统堆栈


将新进程的 PCB添入PCB-SET中


 拷贝当前进程内容 (copy_files,copy_fs,
copy_sighand,copy_mm,copy_thread)


 进行数据成员初始化


       返回新进程的pid

        以上只是粗略的流程,关于各步的出错处理,参见上文。


四.    讨论
系统调用表中留有空项,故用户可根据需要,添加系统调用。首先,编写调用函数
代码,以sys_XXX为名加到/usr/src/linux/kernel/sys.c中,其次,在
/usr/src/linux/include/ asm /i386/unistd.h中加上 #define __NR_XXX  N(N
为其偏移量,(167~256)),在/usr/src/linux/arch/i386/kernel/entry.s中加

   .long_sys_XXX
      .space(NR_syscall-N)*4
调用时只需在用户子程序开头添加宏_syscallN(type,name, type1,agr1...),且
include  两个文件unistd.h及entry.s,即可调用。

--

※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.43.46]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店