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

本文涉及的产品
应用型负载均衡 ALB,每月750个小时 15LCU
网络型负载均衡 NLB,每月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服务器的负载均衡,从而实现高可用、高并发流量架构
目录
相关文章
|
16天前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
93 2
|
16天前
|
Linux Python
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
32 2
|
14天前
|
项目管理 敏捷开发 开发框架
敏捷与瀑布的对决:解析Xamarin项目管理中如何运用敏捷方法提升开发效率并应对市场变化
【8月更文挑战第31天】在数字化时代,项目管理对软件开发至关重要,尤其是在跨平台框架 Xamarin 中。本文《Xamarin 项目管理:敏捷方法的应用》通过对比传统瀑布方法与敏捷方法,揭示敏捷在 Xamarin 项目中的优势。瀑布方法按线性顺序推进,适用于需求固定的小型项目;而敏捷方法如 Scrum 则强调迭代和增量开发,更适合需求多变、竞争激烈的环境。通过详细分析两种方法在 Xamarin 项目中的实际应用,本文展示了敏捷方法如何提高灵活性、适应性和开发效率,使其成为 Xamarin 项目成功的利器。
34 1
|
14天前
|
Linux
揭秘Linux心脏:那些让你的编程事半功倍的主要系统调用
【8月更文挑战第31天】Linux中的系统调用是操作系统提供给应用程序的接口,用于请求内核服务,如文件操作、进程控制等。本文列举了22种主要系统调用,包括fork()、exec()、exit()、wait()、open()、close()、read()、write()等,并通过示例代码展示了如何使用fork()创建新进程及使用open()、write()、close()操作文件。这些系统调用是Linux中最基本的接口,帮助应用程序与内核交互。
27 1
|
18天前
|
消息中间件 Linux
Linux进程间通信
Linux进程间通信
31 1
|
3天前
|
存储 Linux 程序员
Linux中的主要系统调用
【9月更文挑战第11天】在Linux操作系统中,通过系统调用`fork`创建新进程,子进程继承父进程的数据结构与代码,但可通过`execve`执行不同程序。`fork`返回值区分父子进程,`waitpid`让父进程等待子进程结束。
|
4天前
|
Linux 开发者 Python
从Windows到Linux,Python系统调用如何让代码飞翔🚀
【9月更文挑战第10天】在编程领域,跨越不同操作系统的障碍是常见挑战。Python凭借其“编写一次,到处运行”的理念,显著简化了这一过程。通过os、subprocess、shutil等标准库模块,Python提供了统一的接口,自动处理底层差异,使代码在Windows和Linux上无缝运行。例如,`open`函数在不同系统中以相同方式操作文件,而`subprocess`模块则能一致地执行系统命令。此外,第三方库如psutil进一步增强了跨平台能力,使开发者能够轻松编写高效且易维护的代码。借助Python的强大系统调用功能,跨平台编程变得简单高效。
11 0
|
13天前
|
Unix Linux
linux中在进程之间传递文件描述符的实现方式
linux中在进程之间传递文件描述符的实现方式
|
14天前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
38 0
|
19天前
|
存储 Linux 调度
Linux0.11 进程切换(十)
Linux0.11 进程切换(十)
13 0