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应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
高可用应用架构
欢迎来到“高可用应用架构”课程,本课程是“弹性计算Clouder系列认证“中的阶段四课程。本课程重点向您阐述了云服务器ECS的高可用部署方案,包含了弹性公网IP和负载均衡的概念及操作,通过本课程的学习您将了解在平时工作中,如何利用负载均衡和多台云服务器组建高可用应用架构,并通过弹性公网IP的方式对外提供稳定的互联网接入,使得您的网站更加稳定的同时可以接受更多人访问,掌握在阿里云上构建企业级大流量网站场景的方法。 学习完本课程后,您将能够: 理解高可用架构的含义并掌握基本实现方法 理解弹性公网IP的概念、功能以及应用场景 理解负载均衡的概念、功能以及应用场景 掌握网站高并发时如何处理的基本思路 完成多台Web服务器的负载均衡,从而实现高可用、高并发流量架构
目录
相关文章
|
6天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
25 1
|
1天前
|
SQL 运维 监控
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
|
9天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
1月前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
119 4
linux进程管理万字详解!!!
|
14天前
|
缓存 算法 Linux
Linux内核的心脏:深入理解进程调度器
本文探讨了Linux操作系统中至关重要的组成部分——进程调度器。通过分析其工作原理、调度算法以及在不同场景下的表现,揭示它是如何高效管理CPU资源,确保系统响应性和公平性的。本文旨在为读者提供一个清晰的视图,了解在多任务环境下,Linux是如何智能地分配处理器时间给各个进程的。
|
25天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
64 8
|
22天前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
41 1
|
22天前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
1月前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
69 4
|
1月前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####