系统调用

来源:互联网 发布:激战2母夏尔捏脸数据 编辑:程序博客网 时间:2024/06/06 04:55

系统调用:

1.0x80中断--->系统调用引发的中断

2.系统调用表:函数指针的数组

3.系统调用的函数名转换:每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。在进行系统调用是只要指定对应的系统调用号,就可以明确的要调用哪个系统调用,这就完成了系统调用的函数名称的转换

4.系统调用的参数传递:通过寄存器实现的。

5.具体操作:在用户态的时候将系统调用号存到寄存器中,通过中断,切换到内核态的时候,操作系统先将系统调用号从寄存器eax中读进来(读到内核中),然后通过其去系统调用表(sys_call_table)中去找相应的函数实现,然后将结果返回,将其写入寄存器eax中,然后返回。

6.以fork为例:(不带参数的)

源码中系统调用表sys_call_table是在entry.c文件中。

先根据函数参数个数选择其中的某一个syscall

其中_syscall0()是unistd.h中的内嵌宏代码,它以嵌入汇编的形式调用Linux的系统调用中断int 0x80。

eg

// 以下定义系统调用嵌入式汇编宏函数。

// 不带参数的系统调用宏函数。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" \     //调用系统中断

: "=a" (__res) \               //返回值---->eax(__res)。

: "0" (__NR_##name)); \        //输入为系统调用号

__syscall_return(type,__res); \

}

 

//如果返回值大于0,则直接返回该值,否则置出错号,并返回-1

#define __syscall_return(type, res) \

do { \

if ((unsigned long)(res) >= (unsigned long)(-(128 + 1))) { \

errno = -(res); \

res = -1; \

} \

return (type) (res); \

} while (0)


问题:(__NR_##name)如果我调用 _syscall0(int,fork)替代进去不是变成__NR_##fork了吗?但系统调用常数符号的定义是#define __NR_fork 2啊,其中##是干嘛的?

答:##的意思就是宏中的字符直接替换,
如果name = fork,那么在宏中__NR_##name就替换成了__NR_fork了。

__NR_##name是系统调用号,##指的是两次宏展开。即用实际的系统调用名字代替"name",然后再把__NR_...展开.如name == fork,则为__NR_fork

 

具体实现
fork(),vfork(),_clone()库函数都根据各自需要的参数标志去调用do_fork() 


do_fork()完成了创建中的大部分工作,它定义在kernel/fork.c中。该函数调用copy_process(),接下来copy_process()实现的工作如下 
1.调用dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct结构中,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。

dup_task_struct(struct task_struct *orig)函数:

(1)orig是旧的结构体,也就是关于父进程的信息在里面。
2tsk是子进程的结构体,开始几行还未给其赋值。
3同理ti是新的thread_info,现在也没有给他赋值。
4tsk = alloc_task_struct(); 开辟新的内核栈,并且创建新的子进程结构体
5ti = alloc_thread_info(tsk); 同理,创建新的thread_info。
6arch_dup_task_struct(tsk, orig);简化版本就是:*tsk=*orig,为整个task_struct结构复制tsk。(thread_info里面有一个指向task_struct的指针 ,子进程指向子进程的,父进程指向父进程的现在,这两个thread_info中的某个指针,都指向了父进程的task_struct,所以还要使得子进程thread_info的指针指向tsk的task_struct,而不是父进程的task_struct:task_thread_info(p)->task = p;

 

 

2.检查新创建的这个子进程后,当前用户所拥有的进程数目没有超出给他分配的资源的限制。

3.现在,子进程着手使自己与父进程区别拷来。进程描述符内的许多成员都要被清0或设为初始值。进程描述符中大多数的数据都是共享的。

4.接下来,子进程的状态被设置为TASK_UNINTERRUPTIBLE(不可中断)以保证它不会投入运行。

5.copy_process()调用copy_flags()以更新task_struct的flags成员。

1更新从父进程复制到tsk_flags字段中的一些标志。

2首先清除PF_SUPERPRIV。该标志表示进程是否使用了某种超级用户权限

3然后设置PF_FORKNOEXEC标志。它表示子进程还没有发出execve系统调用。

 

6.调用get_pid()为新进程获取一个有效的PID

7.根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。

8.让父进程和子进程平分剩余的时间片。(若子进程在其第一个时间片前终止或执行新的程序,则剩余的时间片会归还给父进程)

9.最后copy_process()做扫尾工作并返回一个指向子进程的指针

再回到do_fork()函数,如果copy_process函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。

1如果父子进程运行在同一个CPU上,并且不能共享同一组页表(CLONE_VM标志被清0).那么,就把子进程插入父进程运行队列.

2并且子进程插在父进程之前.这样做的目的是:如果子进程在创建之后执行新程序,就可以避免写时复制机制执行不必要时页面复制.

3否则,如果运行在不同的CPU上,或者父子进程共享同一组页表.就把子进程插入父进程运行队列的队尾.

 

 

系统调用

描述

fork

fork创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容task_struct内容

vfork

vfork创建的子进程与父进程共享数据段,而且由vfork()创建的子进程将先于父进程运行

clone

Linux上创建线程一般使用的是pthread库 实际上linux也给我们提供了创建线程的系统调用,就是clone

 

先介绍下进程必须的4要点:

a.要有一段程序供该进程运行,就像一场戏剧要有一个剧本一样。该程序是可以被多个进程共享的,多场戏剧用一个剧本一样。

b.有起码的私有财产,就是进程专用的系统堆栈空间。

c.户口,既操作系统所说的进程控制块,在linux中具体实现是task_struct

d.有独立的存储空间。

当一个进程缺少d条件时候,我们称其为线程。

 

1.fork 创造的子进程复制了父亲进程的资源,包括内存的内容task_struct内容(2个进程的pid不同)。这里是资源的复制不是指针的复制。

2.vfork创建出来的不是真正意义上的进程,而是一个线程,因为它缺少了我们上面提到的进程的四要素的第4项,独立的内存资源

3.clone函数功能强大,带了众多参数,因此由他创建的进程要比前面2种方法要复杂。clone可以让你有选择性的继承父进程的资源,你可以选择想vfork一样和父进程共享一个虚存空间,从而使创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。

原创粉丝点击