Linux 内核学习——系统调用(fork)

    昨天花了一天的时间在看Linux0.11的内核,当看到main.c中的int()函数的时候被一个系统调用难倒了(其实笔者的c,汇编等基础并不是很厉害。但是处于某些原因,就入了这个内核的坑,被浇冷水是经常的事情)。于是各种资料各种搜索,但是大家给出的资料都是千篇一律,不是一开始就介绍什么是系统调用,就是没有切实的去一句一句的剖析系统调用的原理,反正就是巴拉巴拉说的基本都是一样,看下来还是什么都不懂……  ps:可能是笔者真的太渣,没办法理解。

    于是我便自己看是研究了,首先是从init哪那里开始,针对fork()这个系统调用开始了深入的研究(其实也不是很深入了,哈哈哈)。跟踪它实现的过程,我想现在或者以后也会有人遇到跟我类似的问题,再加上想更加巩固一下掌握的知识,就着手写了这篇博了,那么开始正题吧!

    首先要说的是,我们现在针对的是Linux0.11版本内核中的fork()系统调用来对系统调用做分析。

    当调用fork()函数时,在main.c中的init()函数中就出现了fork()的调用:

  // 下面fork()用于创建一个子进程(子任务)。对于被创建的子进程,fork()将返回0 值,
  // 对于原(父进程)将返回子进程的进程号。所以180-184 句是子进程执行的内容。该子进程
  // 关闭了句柄0(stdin),以只读方式打开/etc/rc 文件,并执行/bin/sh 程序,所带参数和
  // 环境变量分别由argv_rc 和envp_rc 数组给出。参见后面的描述。
  if (!(pid = fork ()))
    {
      close (0);
      if (open ("/etc/rc", O_RDONLY, 0))
	_exit (1);		// 如果打开文件失败,则退出(/lib/_exit.c,10)。
      execve ("/bin/sh", argv_rc, envp_rc);	// 装入/bin/sh 程序并执行。
      _exit (2);		// 若execve()执行失败则退出(出错码2,“文件或目录不存在”)。
    }

    大家应该都知道这个函数,确切的说应该是系统调用是用来创建一个新进程的。但是大家会发现内核里面根本找不到这个函数的具体原型,那它到底是怎么实现的呢?我们可以用编辑器查看的定义,我用的是source Insight 3 ,发现在unistd中有它的函数原型定义:

      int fork (void);// 对应各系统调用的函数原型定义。在文件下的第235行
但是我们只能找到这个函数原型定义,却找不到它的源。还是在这个文件中有这样一些函数定义:

// 以下定义系统调用嵌入式汇编宏函数。
// 不带参数的系统调用宏函数。type name(void)。
// %0 - eax(__res),%1 - eax(__NR_##name)。其中name 是系统调用的名称,与 __NR_ 组合形成上面
// 的系统调用符号常数,从而用来对系统调用表中函数指针寻址。
// 返回:如果返回值大于等于0,则返回该值,否则置出错号errno,并返回-1。
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ( "int $0x80" \	// 调用系统中断0x80。
:"=a" (__res) \		// 返回值??eax(__res)。
:"" (__NR_
##name)); \			// 输入为系统中断调用号__NR_name。
      if (__res >= 0) \		// 如果返回值>=0,则直接返回该值。
      return (type) __res; errno = -__res; \	// 否则置出错号,并返回-1。
      return -1;}
刚看到这个嵌入汇编语句的c程序的时候也是一头雾水,不知所云,但是根据注释发现这可能跟系统调用有很大的关系,但是就是不清楚这个syscall(后面的0,1,2……表示的是所带的参数,这里暂且统称为syscall吧)函数和fork函数的关系。后来通过一番死缠烂打的无赖询问和翻阅书籍发现是这样一回事:

    在使用这些系统调用的文件中会有这样一些声明,比如在main中:

/*
 * 我们需要下面这些内嵌语句 - 从内核空间创建进程(forking)将导致没有写时复制(COPY ON WRITE)!!!
 * 直到一个执行execve 调用。这对堆栈可能带来问题。处理的方法是在fork()调用之后不让main()使用
 * 任何堆栈。因此就不能有函数调用 - 这意味着fork 也要使用内嵌的代码,否则我们在从fork()退出
 * 时就要使用堆栈了。
 * 实际上只有pause 和fork 需要使用内嵌方式,以保证从main()中不会弄乱堆栈,但是我们同时还
 * 定义了其它一些函数。
 */
static inline
_syscall0 (int, fork)		// 是unistd.h 中的内嵌宏代码。以嵌入汇编的形式调用
	// Linux 的系统调用中断0x80。该中断是所有系统调用的
	// 入口。该条语句实际上是int fork()创建进程系统调用。
	// syscall0 名称中最后的0 表示无参数,1 表示1 个参数。
     static inline _syscall0 (int, pause)	// int pause()系统调用:暂停进程的执行,直到
	// 收到一个信号。
     static inline _syscall1 (int, setup, void *, BIOS)	// int setup(void * BIOS)系统调用,仅用于
	// linux 初始化(仅在这个程序中被调用)。
     static inline _syscall0 (int, sync)	// int sync()系统调用:更新文件系统。
可以看到,这里有一个static inline _syscall0(int,fork),这样的声明,很明显这就是fork()和syscall()之间的联系了。如果还是不明显可以这样,把声明,宏函数全部按照顺序进行展开,把 staticinline_syscall0(int,fork)带到syscall0函数中去,然后把宏替换掉就会变成这样的一个函数:

void fork(void) 
{ 
	long __res; 
	__asm__ volatile ( "int $0x80" :"=a" (__res) :"" (__NR_fork)); 
      if (__res >= 0) 
      return (type) __res; errno = -__res; 
      return -1;
}
哈哈,没错这就是fork()函数了,在调用fork之前我们要在当前文件下先进行上面main中的那样声明,这就是创建和调用了fork函数(可能这样做的原因是减少系统代码量吧,如果每个系统调用都要像函数那样去声明的话,如有60个系统调用,那么就要有60个这样的函数,如果用这样的方法的话,那就只有短短几个函数加上一些函数声明了。可能这只是其中之一)。

    那这个函数是如何工作的呢?

    在上面的内嵌汇编语句中我们可以看到这样的一些语句,其实上面也有详细的说明我就不多解释。系统调用要通过0x80中断来实现,int $0x80就是调用0x80中断的意思,返回值是__res,函系统中断调用号是__NR_和name的链接,如果nam是fork的话,那系统调用号就是__NR_fork啦。系统调用号是在sys.h头文件中(截取):

extern int sys_setup ();	// 系统启动初始化设置函数。 (kernel/blk_drv/hd.c,71)
extern int sys_exit ();		// 程序退出。 (kernel/exit.c, 137)
extern int sys_fork ();		// 创建进程。 (kernel/system_call.s, 208)
extern int sys_read ();		// 读文件。 (fs/read_write.c, 55)
extern int sys_write ();	// 写文件。 (fs/read_write.c, 83)
extern int sys_open ();		// 打开文件。 (fs/open.c, 138)

// 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
  sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
  sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
  sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
  sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
  sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
  sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
  sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
  sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
  sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
  sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
  sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
  sys_setreuid, sys_setregid
};
其实__NR_fork对应的宏定义是2,也就对应的是fn_ptr sys_call_table[]中的2号——sys_fork,这样一来fork系统调用的具体实现也就转到了sys_fork这个函数中来了。这个函数的位置就是 址 = _sys_call_table + %eax * 4。(未查证,其他博文中看到的)。接下来就是在内核状态下的系统调用具体实现了。

其实最关键的还是在理解:

__asm__ volatile ( "int $0x80" :"=a" (__res) :"" (__NR_fork)); 
这句话上面,而这句话中,最关键的就是0x80号中断,由于涉及到中断处理和汇编程序较多,但是笔者现在也只是刚刚开始了解内核,所以不好多做解释,大家可以根据以下几篇博文进行深入了解:

ps:我擦,突然发现哈没吃午饭,哈哈,那就先到这里吧,如果有跟深入的理解,会出续篇,同时也欢迎大家指正,既然都看到这里了,给笔者一个赞鼓励下吧,哈哈


Linux系统中断处理程序int 0x80实现原理

GCC在C语言中内嵌汇编 call _volasile_
Linux内核——fork()函数创建进程
Linux系统调用的实现技术
系统调用的实现原理

在此非常各位感谢作者的无私奉献

你可能感兴趣的:(操作系统学习笔记)