Linux内核学习笔记知识点杂烩(三)——内核源码解析

init目录下的main.c文件的start_kerne函数相当于普通C程序的main函数,是内核的初始化的起点。

Linux内核学习笔记知识点杂烩(三)——内核源码解析_第1张图片

Linux内核的核心代码在kernel目录中:

  • ipc目录:进程间通信
  • mm:内存管理
  • net:网络相关
  • ...


    Linux内核学习笔记知识点杂烩(三)——内核源码解析_第2张图片

rest_init从start_kernel一启动的时候便一直存在,称为0号进程。0号进程即最终的idle进程(init task即手工创建的PCB)。当系统没有进程需要执行时就调度到idle进程。不管分析内核的哪一部分都会涉及到 start_kernel,所有的模块都需要调用start_kernel来完成初始化。0号进程创建了1号进程kernel_thread,是第一个用户态进程,即 init 进程,是用户态所有进程的祖先,然后,新建 kthreadd 进程,是内核态所有进程的祖先。最后,通过 cpu_startup_entry 函数启动 0 号进程。

一般现代CPU都有几种不同的指令执行级别:

在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态,而在相应的低级别执行状态下,代码的掌控范围会受到限制。只能在对应级别允许的范围内活动。

举例:
intel x86 CPU有四种不同的执行级别0-3(ring0-ring3),Linux只使用了其中的0级和3级分别来表示内核态和用户态。

Q:为什么有权限级别的划分?
A:为了保护系统,为了程序员把系统搞崩溃。资深程序员写的更健壮。

  • cs寄存器的最低两位表明了当前代码的特权级
  • CPU每条指令的读取都是通过cs:eip这两个寄存器:其中cs是代码段选择寄存器,eip是偏移量寄存器。
  • 上述判断由硬件完成,一般来说在Linux中,地址空间是一个显著的标志:
    0xc0000000以上的地址空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都可以访问
  • 注意:这里所说的地址空间是逻辑地址而不是物理地址

中断处理是用户态进入内核态的主要方式。
系统调用时一种特殊的中断。中断发生后的第一件事就是保存现场。处理完后的第一件事就是恢复现场。

系统调用和API(应用编程接口)的关系:

  • API只是一个函数的定义
  • 系统调用通过软中断(trap)向内核发出一个明确的请求(是一个中断)

不是每个API都对应一个特定的系统调用:

  • API可能直接提供用户态的服务
    如,一些数学函数

  • 一个单独的API可能调用几个系统调用

  • 不同的API可能调用了同一个系统调用

系统调用返回值:大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用,-1在多数情况下表示内核不能满足进程的请求。

系统调用的三层皮:

  1. API(函数接口,e.g:xyz())
  2. system_call(系统调用)
  3. sys_xyz(中断服务程序)

当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。 在Linux中是通过执行int $ 0x80来执行系统调用的, 这条汇编指令产生向量为128的编程异常。

传参: 内核实现了很多不同的系统调用, 进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数——使用eax寄存器。

寄存器传递参数具有如下限制:

  • 每个参数的长度不能超过寄存器的长度,即32位
  • 在系统调用号(eax)之外,参数的个数不能超过6个(ebx, ecx,edx,esi,edi,ebp)

超过6个怎么办?会将某一个寄存器作为一个指针,指向一块内存,进入内核态之后就可以访问所有的地址空间,将通过这块内存来传递。

Linux内核学习笔记知识点杂烩(三)——内核源码解析_第3张图片

Linux内核学习笔记知识点杂烩(三)——内核源码解析_第4张图片

初始化的时候就把 int 0x80与system_call绑定起来啦,通过中断向量来匹配起来的。

系统调用机制的初始化:


Linux内核学习笔记知识点杂烩(三)——内核源码解析_第5张图片

Linux内核学习笔记知识点杂烩(三)——内核源码解析_第6张图片

SAVE_ALL:保存现场

call *sys_call_table(, %eax,4)作用为调用系统调用处理函数 ,eax传递系统调用号

irq_return:系统调用过程结束

call schedule:进程调度的代码

restore_all:恢复现场

OS的三大功能:

  • 进程管理(核心)
  • 内存管理
  • 文件系统

进程控制块 :PCB——task_struct

为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。

  • struct task_struct数据结构很庞大

  • Linux进程的状态与操作系统原理中的描述的进程状态似乎有所不同,比如就绪状态和运行状态都是TASK_RUNNING,为什么呢?

Linux内核学习笔记知识点杂烩(三)——内核源码解析_第7张图片
  • 进程的标示pid

  • 所有进程链表struct list_head tasks; (双向链表)


    Linux内核学习笔记知识点杂烩(三)——内核源码解析_第8张图片
  • 内核的双向循环链表的实现方法

  • 程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系


    Linux内核学习笔记知识点杂烩(三)——内核源码解析_第9张图片
  • Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_info和进程的内核堆栈

  • 进程处于内核态时使用,不同于用户态堆栈,即PCB中指定了内核栈,那为什么PCB中没有用户态堆栈?用户态堆栈是怎么设定的?

  • 内核控制路径所用的堆栈很少,因此对栈和Thread_info来说,8KB足够了struct thread_struct thread; //CPU-specific state of this task

  • 文件系统和文件描述符

  • 内存管理——进程的地址空间

再谈系统调用:


Linux内核学习笔记知识点杂烩(三)——内核源码解析_第10张图片

Linux内核学习笔记知识点杂烩(三)——内核源码解析_第11张图片

Linux内核学习笔记知识点杂烩(三)——内核源码解析_第12张图片

fork()是在用户态创建子进程的一个系统调用。 fork系统调用在父进程和子进程各返回一次。 所以下面的else if和else可能都会被执行,这并不是不遵循if-else结构,而是对应了多个进程。


创建新进程是通过复制当前进程来实现的(父进程的部分信息),复制完父进程之后再进行必要的修改(修改PCB,堆栈等),为子进程初始化。初始化时拷贝内核堆栈数据和指定新进程的第一条指令地址(修改ip和sp)。系统调用内核函数sys_fork, sys_clone(fork使用的系统调用),sys_vfork来创建新的进程,他们都是调用的do_fork。do_fork里面有一个copy_process,里面就是创建进程内容的主要代码。

fork出的子进程是从哪里开始执行的?

fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。

一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

由fork函数创建的新进程被称为子进程。fork函数被调用一次,但是返回两次。父进程返回的值是新进程的进程ID,而子进程返回的值是0。

子进程执行代码开始位置

fork确实创建可一个子进程并完全复制父进程,但是子进程是从fork后面到那个指令开始执行。如果子进程也从main开头到尾执行所有指令,那么它执行到fork指令时也必定会创建一个个子子进程,子子孙孙无穷尽。

常见的两种应用场景

  • 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务中是常见的——父进程等待客户端的服务请求,当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求的到达
  • 一个进程要执行一个不同的程序。这是shell中常见的情况,子进程从fork返回后立即调用exec

fork函数返回值的三种情况

  • 返回子进程Id给父进程

    因为一个进程的子进程可能有多个,并且没有一个函数可以获得一个进程的所有子进程ID。

  • 返回给子进程值为0

    一个进程只会有一个父进程,所以子进程总是可以调用getpid以获得当前进程Id以及调用getppid获得父进程Id.

  • 出现错误,返回负值

    • 当前进程数已经达到系统规定的上限,这时errno的值被设置为EAGAIN
    • 系统内存不足,这时errno的值被设置为ENOMEM

创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略

子进程运行之后且返回用户态之前会发生进程调度吗?

当程序执行完成,子进程使用exit()系统调用终止。exit()会释放进程的大部分数据结构,并且把这个终止的消息通知给父进程。这时候,子进程被称为zombie process(僵尸进程)。直到父进程通过wait()系统调用知悉子进程终止之前,子进程都不会被完全的清除。一旦父进程知道子进程终止,它会清除子进程的所有数据结构和进程描述符。

僵尸进程和孤儿进程有什么区别、如何处理?

区别:僵尸进程占用一个进程ID号,占用资源,危害系统。但孤儿进程与僵尸进程不同的是,由于父进程已经死亡,子系统会帮助父进程回收处理孤儿进程。所以孤儿进程实际上是不占用资源的,因为它最终是被系统回收了,不会像僵尸进程那样占用ID,损害运行系统。
1)僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程没有调用wait或者waitpid获取子进程的状态,那么子进程的进程描述符仍然保存在系统中,这种进程称为僵尸进程。
2)孤儿进程:一个父进程退出,而他的一个或者多个子进程还在运行,那么那些子进程称为孤儿进程。孤儿进程将被init(进程号为1)收养,并由init进程对它们完成状态收集的工作。子进程的死亡需要父进程来处理,所以正常的进程应该是子进程先于父进程死亡,当父进程先于子进程死亡时,子进程死亡没有父进程处理,这个死亡的子进程就是孤儿进程。

简单来说。
僵尸进程:父进程没死,子进程死了,但是父进程不帮他收尸(通过wait,waitpid获取其状态),所以变成僵尸。
孤儿进程:父进程死了,子进程没死,子进程成了孤儿,只能被孤儿院(init)收养。

怎样避免僵尸进程呢?

单独一个线程wait子进程,或者,有两个信号,一个SIGCHLD、一个SIGCLD,设置这两个信号的处理方式为忽略,它们告诉内核,不关心子进程结束的状态所以当子进程终止的时候直接释放所有资源就行。它们的区别是SIGCLD在安装完信号处理函数的时候还会检查是否已经存在结束的子进程,如果有就调用信号处理函数,而SIGCHLD不会,也就是可能会丢掉已经有子进程已经结束这个事实

你可能感兴趣的:(Linux内核学习笔记知识点杂烩(三)——内核源码解析)