Linux0.11 系统调用进程创建与执行(九)(下)

本文涉及的产品
网络型负载均衡 NLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
公网NAT网关,每月750个小时 15CU
简介: Linux0.11 系统调用进程创建与执行(九)

Linux0.11 系统调用进程创建与执行(九)(上):https://developer.aliyun.com/article/1597307

三、调用 fork 创建进程 1(init)

    fork 函数是个系统调用,此处由进程 0 在用户态调用 fork 函数来创建进程 1fork 函数触发的中断,由 kernel/system_call.ssystem_call 函数响应。fork 函数定义如下(可参考 三、fork 函数定义 ):

static inline int fork(void) { 
  long __res; 
  __asm__ volatile ("int $0x80"     // 调用系统中断 0x80
      : "=a" (__res)          // 返回值 => eax(__res)
      : "0" (2)); 
  if (__res >= 0) 
      return (type) __res; 
  errno = -__res; 
  return -1; 
}


1、中断描述符

   中断描述符表位于 0x000054b8 处,系统调用中断号为 0x80(128),idt[128] 其值为:EF00:87632。

IDT 对应的描述符说明如下(可参考 中断描述符表):

   由上可以看出,这是一个陷阱门。其段选择符为:0x08,其偏移值为:0x7632(30258)。

结合下图的调用说明,

可知:段选择符为 0x08 指向内核代码(参见上面 内核CS段),其段基址为 0x00。因此其指向 0x7632 处。从 System.map 文件可以看到此处正是 system_call 函数。

2、 system_call 函数

其位于 kernel/system_call.s 中。

系统调用 0x80 会导致 CPU 硬件自动将 ssespeflagscseip 的值压栈。系统调用进入可参考 系统调用进入

# 错误的系统调用号
.align 2        # 内存 4 字节对齐
bad_sys_call:
  movl $-1,%eax   # eax 中置 -1,退出中断
  iret
# 重新执行调度程序入口。调度程序 schedule 在(kernel/sched.c,104)
# 当调度程序 schedule() 返回时就从 ret_from_sys_call 处(101行)继续执行  
.align 2
reschedule:
  pushl $ret_from_sys_call  # 将 ret_from_sys_call 的地址入栈
  jmp schedule
#### int 0x80 --linux 系统调用入口点(调用中断 int 0x80,eax 中是调用号)。
.align 2
system_call:
  cmpl $nr_system_calls-1,%eax  # 调用号如果超出范围的话就在 eax 中置-1 并退出。
  ja bad_sys_call
  push %ds            # 保存原段寄存器值。
  push %es
  push %fs
# 一个系统调用最多可带有 3 个参数,也可以不带参数。下面入栈的 ebx、ecx 和 edx 中放着系统
# 调用相应 C 语言函数(见第 94 行)的调用参数。这几个寄存器入栈的顺序是由 GNU GCC 规定的,
# ebx 中可存放第 1 个参数,ecx 中存放第 2 个参数,edx 中存放第 3 个参数。
# 系统调用语句可参见头文件 include/unistd.h 中第 133 至 183 行的系统调用宏。
  pushl %edx
  pushl %ecx      # push %ebx, %ecx,%edx as parameters
  pushl %ebx      # to the system call
  movl $0x10,%edx   # set up ds,es to kernel space
  mov %dx,%ds     # ds,es 指向内核数据段(全局描述符表中数据段描述符)。
  mov %dx,%es
# fs 指向局部数据段(局部描述符表中数据段描述符),即指向执行本次系统调用的用户程序的数据段。
#注意,在 Linux 0.11 中内核给任务分配的代码和数据内存段是重叠的,它们的段基址和段限长相同。
# 参见 fork.c 程序中 copy_mem() 函数。
  mov1 $0x17, %edx  # fs points to local data space
  mov %dx,%fs
# 下面这句操作数的含义是:调用地址=[_sys_call_table + %eax * 4]。参见程序后的说明。
# sys_call_table[]是一个指针数组,定义在 include/linux/sys.h 中。该指针数组中设置了
# 所有 72 个系统调用 C 处理函数的地址。
  call *sys_call_table(,%eax,4)   # 间接调用指定功能 C 函数。
95: 
  pushl %eax            # 把系统调用返回值入栈。
# 下面 96-100 行查看当前任务的运行状态。如果不在就结状态(state 不等于 0)就去执行调度
#程序。如果该任务在就绪状态,但其时间片已用完(counter = 0),则也去执行调度程序。
#例如当后台进程组中的进程执行控制终端读写操作时,那么默认条件下该后台进程组所有进程。
# 会收到 SIGTTIN 或 SIGTTOU 信号,导致进程组中所有进程处于停止状态。而当前进程则会立刻。
# 返回。
  movl _current, %eax     #取当前任务(进程)数据结构地址到 eax。
  cmpl $0,state(%eax)     # state
  jne reschedule
  cmpl $0,counter(%eax)   # counter
  je reschedule
# 以下这段代码执行从系统调用 C 函数返回后,对信号进行识别处理。其他中断服务程序退出时也。
# 将跳转到这里进行处理后才退出中断过程,例如后面 131 行上的处理器出错中断 int 16。
101:
ret_from_sys_call:
# 首先判别当前任务是否是初始任务 task0,如果是则不必对其进行信号量方面的处理,直接返回。
# 103 行上的_task 对应 C 程序中的 task 数组,直接引用 task 相当于引用 task[0]。
  movl current,%eax   # task[0] cannot have signals
  cmpl task,%eax
  je 3f         # 向前(forward)跳转到标号 3 处退出中断处理。
# 通过对原调用程序代码选择符的检查来判断调用程序是否是用户任务。如果不是则直接退出中断。
# 这是因为任务在内核态执行时不可抢占。否则对任务进行信号量的识别处理。这里比较选择符是否,
# 为用户代码段的选择符 0x000f(RPL=3,局部表,第 1 个段(代码段))来判断是否为用户任务。如
# 果不是则说明是某个中断服务程序跳转到第 101 行的,于是跳转退出中断程序。如果原堆栈段选择。
#符不为 0x17(即原堆栈不在用户段中),也说明本次系统调用的调用者不是用户任务,则也退出。
  cmpw $0x0f,CS(%esp)   # was old code segment supervisor ?
  jne 3f
  cmpw $0x17,OLDSS(%esp)    # was stack segment = 0x17 ?
  jne 3f
# 下面这段代码( 109-120 )用于处理当前任务中的信号。首先取当前任务结构中的信号位图(32 位,
# 每位代表 1 种信号),然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值
# 最小的信号值,再把原信号位图中该信号对应的位复位(置 0),最后将该信号值作为参数之一调
# 用 do_signal()。 do_signal() 在(kernel/signal.c,82)中,其参数包括 13 个入栈的信息。
  movl signal(%eax),%ebx    #取信号位图放入ebx中, 每 1 位代表 1 种信号,共 32 个信号。
  movl blocked(%eax),%ecx   #取阻塞(屏蔽)信号位图放入ecx中。
  notl %ecx         # 每位取反。
  andl %ebx,%ecx        #获得许可的信号位图。
  bsfl %ecx,%ecx        # 从低位(位 0)开始扫描位图,看是否有 1 的位,
                # 若有,则 ecx 保留该位的偏移值(即第几位 0-31)。
  je 3f           # 如果没有信号则向前跳转退出。
  btrl %ecx,%ebx        # 复位该信号(ebx 含有原 signal 位图)。
  movl %ebx,signal(%eax)    # 重新保存 signal 位图信息到 current->signal 中。
  incl %ecx         # 将信号调整为从 1 开始的数(1-32)。
  pushl %ecx          # 信号值入栈作为调用 do_signal 的参数之一
  call do_signal        # 调用 C 函数信号处理程序(kernel/signal.c,82)
  popl %eax         # 弹出入栈的信号值。
3:  popl %eax         # eax 中含有第 95 行入栈的系统调用返回值。
  popl %ebx
  popl %ecx
  popl %edx
  pop %fs
  pop %es
  pop %ds
  iret

函数中 call *sys_call_table(,%eax,4) 实际调用的就是 sys_fork 函数。其也定义在当前文件中:

.align 2
sys_fork:
  call find_empty_process
  testl %eax,%eax   # 在eax中返回进程号pid。若返回负数则退出
  js 1f
  push %gs
  pushl %esi
  pushl %edi
  pushl %ebp
  pushl %eax
  call copy_process
  addl $20,%esp   # 丢弃这里所有压栈内容,即上面压入的 gs、esi、edi、ebp、eax
1:  ret

   sys_fork 函数会调用 find_empty_process 函数找到一个空闲的任务,并返回进程号。然后调用 copy_process 拷贝父进程信息。

2.1 copy_process 函数

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
    long ebx,long ecx,long edx,
    long fs,long es,long ds,
    long eip,long cs,long eflags,long esp,long ss)
{
  // ...
}

   GCC 中函数调用的参数逆次压入栈中(参考C 与汇编程序的相互调用3.2 copy_process 函数 ),即最后压入变量 nrcopy_process 函数参数依次对应 %eax%ebp%edi%esi%gsnone(调用 sys_fork 函数时压入栈的返回地址),%ebx%ecx%edx%fs%es%dseipcseflagsespss

2.2 设置进程 1 的分页管理

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
    long ebx,long ecx,long edx,
    long fs,long es,long ds,
    long eip,long cs,long eflags,long esp,long ss)
{
  // ...
  if (copy_mem(nr,p)) {   // 设置子进程的代码段、数据段及创建、复制子进程的第一个页表
    task[nr] = NULL;
    free_page((long) p);
    return -EAGAIN;
  }
  // ...
}

copy_men 函数在 kernel/fork.c 中。参考 3.2.1 copy_mem 函数

int copy_mem(int nr,struct task_struct * p)
{
  unsigned long old_data_base,new_data_base,data_limit;
  unsigned long old_code_base,new_code_base,code_limit;
  
  // 取子进程的代码、数据段限长,跟踪两者都为:655360
  code_limit=get_limit(0x0f);
  data_limit=get_limit(0x17);
  // 获取父进程(现在为进程 0)的代码段、数据段基址,
  // 跟踪两者都为 0
  old_code_base = get_base(current->ldt[1]);
  old_data_base = get_base(current->ldt[2]);
  if (old_data_base != old_code_base)
    panic("We don't support separate I&D");
  if (data_limit < code_limit)
    panic("Bad data_limit");
  new_data_base = new_code_base = nr * 0x4000000;
  p->start_code = new_code_base;
  set_base(p->ldt[1],new_code_base);  // 设置子进程代码段基址
  set_base(p->ldt[2],new_data_base);  // 设置子进程数据段基址
  if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
    printk("free_page_tables: from copy_mem\n");
    free_page_tables(new_data_base,data_limit);
    return -ENOMEM;
  }
  return 0;
}

copy_page_tables 函数请参考 3.2.1.1 copy_page_tables 函数

3、系统调用返回

sys_fork 返回后,其栈中数据如下:

%ebx%ecx%edx%fs%es%dseipcseflagsespss

# kernel/system_call.s

95: 
  pushl %eax            # 把系统调用返回值入栈。
# 下面 96-100 行查看当前任务的运行状态。如果不在就结状态(state 不等于 0)就去执行调度
#程序。如果该任务在就绪状态,但其时间片已用完(counter = 0),则也去执行调度程序。
#例如当后台进程组中的进程执行控制终端读写操作时,那么默认条件下该后台进程组所有进程。
# 会收到 SIGTTIN 或 SIGTTOU 信号,导致进程组中所有进程处于停止状态。而当前进程则会立刻。
# 返回。
  movl _current, %eax     #取当前任务(进程)数据结构地址到 eax。
  cmpl $0,state(%eax)     # state
  jne reschedule
  cmpl $0,counter(%eax)   # counter
  je reschedule
# 以下这段代码执行从系统调用 C 函数返回后,对信号进行识别处理。其他中断服务程序退出时也。
# 将跳转到这里进行处理后才退出中断过程,例如后面 131 行上的处理器出错中断 int 16。
101:
ret_from_sys_call:
# 首先判别当前任务是否是初始任务 task0,如果是则不必对其进行信号量方面的处理,直接返回。
# 103 行上的_task 对应 C 程序中的 task 数组,直接引用 task 相当于引用 task[0]。
  movl current,%eax   # task[0] cannot have signals
  cmpl task,%eax
  je 3f         # 向前(forward)跳转到标号 3 处退出中断处理。
# 通过对原调用程序代码选择符的检查来判断调用程序是否是用户任务。如果不是则直接退出中断。
# 这是因为任务在内核态执行时不可抢占。否则对任务进行信号量的识别处理。这里比较选择符是否,
# 为用户代码段的选择符 0x000f(RPL=3,局部表,第 1 个段(代码段))来判断是否为用户任务。如
# 果不是则说明是某个中断服务程序跳转到第 101 行的,于是跳转退出中断程序。如果原堆栈段选择。
#符不为 0x17(即原堆栈不在用户段中),也说明本次系统调用的调用者不是用户任务,则也退出。
  cmpw $0x0f,CS(%esp)   # was old code segment supervisor ?
  jne 3f
  cmpw $0x17,OLDSS(%esp)    # was stack segment = 0x17 ?
  jne 3f
# 下面这段代码( 109-120 )用于处理当前任务中的信号。首先取当前任务结构中的信号位图(32 位,
# 每位代表 1 种信号),然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值
# 最小的信号值,再把原信号位图中该信号对应的位复位(置 0),最后将该信号值作为参数之一调
# 用 do_signal()。 do_signal() 在(kernel/signal.c,82)中,其参数包括 13 个入栈的信息。
  movl signal(%eax),%ebx    #取信号位图放入ebx中, 每 1 位代表 1 种信号,共 32 个信号。
  movl blocked(%eax),%ecx   #取阻塞(屏蔽)信号位图放入ecx中。
  notl %ecx         # 每位取反。
  andl %ebx,%ecx        #获得许可的信号位图。
  bsfl %ecx,%ecx        # 从低位(位 0)开始扫描位图,看是否有 1 的位,
                # 若有,则 ecx 保留该位的偏移值(即第几位 0-31)。
  je 3f           # 如果没有信号则向前跳转退出。
  btrl %ecx,%ebx        # 复位该信号(ebx 含有原 signal 位图)。
  movl %ebx,signal(%eax)    # 重新保存 signal 位图信息到 current->signal 中。
  incl %ecx         # 将信号调整为从 1 开始的数(1-32)。
  pushl %ecx          # 信号值入栈作为调用 do_signal 的参数之一
  call do_signal        # 调用 C 函数信号处理程序(kernel/signal.c,82)
  popl %eax         # 弹出入栈的信号值。
3:  popl %eax         # eax 中含有第 95 行入栈的系统调用返回值。
  popl %ebx
  popl %ecx
  popl %edx
  pop %fs
  pop %es
  pop %ds
  iret

4、寄存器信息

4.1 进程 0

其 ldt ,tss 不变。

4.1.1 内核态信息

4.1.2 用户态信息

  从用户态切换到内核态,处理器从当前执行任务的 TSS 段中得到异常处理过程(内核态)使用的堆栈的段选择符和栈指针(例如 tss.ss0、tss.esp0)。然后处理器会把被中断程序(或任务)的 栈选择符 和 栈指针 压入新栈中。接着处理器会把 EFLAGS、CS 和 EIP 寄存器的当前值也压入新栈中。

  结合上面可知,其 cs,ss,ds,es 发生了改变。除了系统调用时处理器会自动改变 ss、es 的值。其它值是在 kernel/system_call.s 文件中 system_call 函数中改变的,其改变了ds,es,以及 fs 的值 。

  movl $0x10,%edx   # set up ds,es to kernel space
  mov %dx,%ds     # ds,es 指向内核数据段(全局描述符表中数据段描述符)。
  mov %dx,%es
# fs 指向局部数据段(局部描述符表中数据段描述符),即指向执行本次系统调用的用户程序的数据段。
#注意,在 Linux 0.11 中内核给任务分配的代码和数据内存段是重叠的,它们的段基址和段限长相同。
# 参见 fork.c 程序中 copy_mem() 函数。
  mov1 $0x17, %edx  # fs points to local data space
  mov %dx,%fs

系统调用进入可参考 系统调用进入,系统调用返回可参考 系统调用返回

4.1.3 task_struct 中其它字段

4.2 进程 1

4.2.1 初始化时信息:
ldt

tss

4.2.2 用户态信息
通用寄存器和段寄存器

ldttss 不变。

4.2.3 内核态信息
通用寄存器和段寄存器

ldttss 不变。

4.2.4 task_struct 中其它字段

四、进程 2

1、调用 fork 创建进程 2

void init(void) {
  // ...
  
  if (!(pid = fork())) {
    close(0);
    if (open("/etc/rc", O_RDONLY, 0))
      _exit(1);
    execve("/bin/sh", argv_rc, envp_rc);
    _exit(2);
  }
  // ...  

通用寄存器和段寄存器

ldt

tss

task_struct 中其它字段


除了 start_code 其它字段与进程 0 和 进程 1 类似。其中的 ldttss 可以参看上图。

2、进程 2 执行 execve 函数

execve 是个系统调用,最终会调用 fs/exec.c 文件中的 do_execve 函数。此时程序处在进程 2 的内核态。其通用寄存器和段寄存器未发生改变。详细可以参考:Linux0.11 execve函数(六)

ldt

ldt[2] 值为:0x08c0f300:3fff,此 LDT 描述符图释如下:详细可参考:4、段描述符

由上图可知:其段基址为 128MB,处在用户态,段限长为 64MB

tss

task_struct 中其它字段

进程 2 相比进程 01,其现在 end_codeend_databrkexcutable 都有值了。这些值都是在 do_execve 函数中赋予的。

中断

task0

信息被压入到 tss 中。

task1

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
目录
相关文章
|
6月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
249 67
|
5月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
135 16
|
5月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
107 20
|
4月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
83 0
|
4月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
114 0
|
4月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
80 0
|
4月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
81 0
|
7月前
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
239 4
|
7月前
|
Linux Shell
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
346 5
|
7月前
|
Linux 应用服务中间件 nginx
Linux 进程管理基础
Linux 进程是操作系统中运行程序的实例,彼此隔离以确保安全性和稳定性。常用命令查看和管理进程:`ps` 显示当前终端会话相关进程;`ps aux` 和 `ps -ef` 显示所有进程信息;`ps -u username` 查看特定用户进程;`ps -e | grep &lt;进程名&gt;` 查找特定进程;`ps -p &lt;PID&gt;` 查看指定 PID 的进程详情。终止进程可用 `kill &lt;PID&gt;` 或 `pkill &lt;进程名&gt;`,强制终止加 `-9` 选项。
100 3