Linux kernel: basics

前言

在阅读Linux源码前,你需要了解下面的内容。

操作系统基本概念

操作系统的两个主要目标

  • 与硬件交互,为硬件平台上的底层编程单元提供服务

  • 为运行在计算机系统上的程序提供运行环境

用户态与内核态

现代的操作系统依靠特殊的硬件特性来禁止用户直接与底层硬件交互,或者禁止直接访问任意的内存地址。

在Intel/AMD CPU上,内核态运行在ring0级别上,可以获得最高的权限;用户态运行在ring3级别上,权限最低。

  • 实模式,保护模式与长模式:操作系统在BIOS引导时进入权限最高的16位的实模式,然后告知CPU进入32位保护模式或者64位长模式,此时可以对资源进行保护(ring protection),保护模式与长模式的区别不大,后者可以运行64位的程序也可以在兼容子模式下运行32位程序。

  • 虚拟化:最近的X86 CPU所支持的虚拟化技术,会创建在ring1下,这样可以让虚拟操作系统运行在ring0上而不影响宿主。

多用户操作系统

特点:

  • 核实用户身份的认证机制

  • 防止有错误的用户程序妨碍其它用户程序的保护机制

  • 防止恶意的用户程序干涉或者窥视其它用户程序的保护机制

  • 限制每个用户资源的记账机制

用户与组

  • 用户标识符(UID):所有的用户由一个唯一的数字来标识

  • 用户组标识符(user group ID),与其它用户有选择地分享资料

  • 超级用户(root),系统管理员

进程

进程:程序执行地一个实例

程序与进程:几个进程可以并发地同时执行同一程序,同一个进程能顺序执行几个程序

抢占式进程: 多用户系统中地进程是抢占式的,操作系统记录进程占用的CPU时间,并周期性地激活调度程序。

抢占式内核:抢占式内核允许高优先级的进程挂起处于内核态的调用,并抢先执行该高优先级的进程。

进程/内核模式:每个进程都自以为是系统中唯一的进程

内核体系结构

使用模块达到微内核理论上的很多优点而又不影响性能。

  • 模块化方法:任何模块都可以在运行时被链接或者解除链接

  • 平台无关性:即使模块依赖于某些特殊的硬件特点但不会依赖某个固定的硬件平台。

  • 节省内存:需要模块时链接,否则解除链接

  • 无性能损失: 模块在调用时无需显式地进行消息传递,只有在链接或者解除链接时有性能下降。

Unix 文件系统概述

文件

Unix文件是以字节序列组成的信息载体。

硬链接与软连接

硬链接的限制:

  • 不允许给目录创建硬链接

  • 同一文件系统中的文件之间才能创建硬链接

软连接(符号链接):

可以指向位于任何一个文件系统的任意文件或目录,甚至可以指向一个不存在的文件。

文件类型

  • 普通文件

  • 目录

  • 符号链接

  • 面向块的设备文件

  • 面向字节的设备文件

  • 管道和命名管道

  • 套接字

文件描述符与索引节点

Unix的文件系统必须至少提供POSIX标准中指定的如下属性:

  • 文件类型

  • 与文件相关的硬链接个数

  • 以字节为单位的文件长度

  • 设备标识符

  • 在文件系统中标识文件的索引节点号

  • 文件拥有者的UID

  • 文件的用户组ID

  • 时间戳:索引节点状态改变的时间、最后访问时间、最后修改时间

  • 访问权限和文件模式

访问权限和文件模式

文件的潜在用户:

  • 文件所有者

  • 不包括所有者的同组用户

  • 其他用户

三种类型的访问权限(粘滞位): 读、写及执行。

三种附加的标记:

  • suid:使进程从进程拥有者的UID切换为文件拥有者的UID

  • sgid:使进程从进程组的用户组的ID切换为该文件用户组的ID

  • sticky:向内核发出请求,当程序结束以后仍保留在内存

文件操作的系统调用

打开文件

open()系统调用:fd = open(path, flag, mode)

path

表示文件的绝对或相对路径

flag

文件打开的方式:读、写、读/写、追加

mode

指定新创建文件的访问权限

访问打开的文件

普通Unix文件,可以顺序访问,也可以随机访问

设备文件与命名管道文件,通常只能顺序访问

默认使用顺序访问,read()write()系统调用的指针都是指向文件的第一个字节。

  • lseek

lseek()系统调用:newoffset = lseek(fd, offset, whence)

fd: 打开文件的文件描述符

offset:有符号整数,计算文件指针的新位置

whence:文件指针新位置的计算方式:offset+0offset+当前位置offset+文件最后一个字节的位置

  • read

read()系统调用:nread = read(fd, buf, count)

fd: 打开文件的文件描述符

buf:进程地址空间中的缓冲区地址

count:所读取的字节数

返回的nread为实际读取的文件字节数

关闭文件

res = close(fd)

进程终止时,内核会关闭其所有仍然打开着的文件。进程需要维护进程的上下文打开的文件描述符数量,否则会触及进程能够打开的文件描述符上限。

更名以及删除文件

res = rename(oldpath, newpath)

改变文件链接的名字

res = unlink(pathname)

删除一个文件链接,当链接数为零的时候,文件才被真正删除。

Unix 内核概述

进程/内核模式

Intel和AMD的CPU实现了ring0 - ring3的四种执行状态,但是所有标准的Unix内核都仅用了内核态(Ring0)和用户态(Ring3)

程序在需要时会陷入内核态,满足需要后回到用户态

进程是动态的实体,在系统内通常只有有限的生存期,创建、撤销及同步现有进程的认为委托给内核中的一组例程

内核本身不是一个进程,而是进程的管理者。

kernel thread的特权进程:

  • 以内核态运行在内核地址空间

  • 不与用户直接交互,不需要终端设备

  • 通常在系统启动时创建,一直活跃到系统关闭

系统启动时,创建了第一个PID为0的进程,该进程完成内核的初始化,包括init/main.c中的start_kernel()及其前后的汇编代码,这个0号进程fork了一个kernel_init进程来初始化用户空间和一个kthreadd来调度进程。


noinline void __ref rest_init(void)
{
    struct task_struct *tsk;
    int pid;

    rcu_scheduler_starting();
    /*
     * We need to spawn init first so that it obtains pid 1, however
     * the init task will end up wanting to create kthreads, which, if
     * we schedule it before we create kthreadd, will OOPS.
     */
    pid = kernel_thread(kernel_init, NULL, CLONE_FS);
    /*
     * Pin init on the boot CPU. Task migration is not properly working
     * until sched_init_smp() has been run. It will set the allowed
     * CPUs for init to the non isolated CPUs.
     */
    rcu_read_lock();
    tsk = find_task_by_pid_ns(pid, &init_pid_ns);
    tsk->flags |= PF_NO_SETAFFINITY;
    set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
    rcu_read_unlock();

    numa_default_policy();
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    rcu_read_lock();
    kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
    rcu_read_unlock();

    /*
     * Enable might_sleep() and smp_processor_id() checks.
     * They cannot be enabled earlier because with CONFIG_PREEMPTION=y
     * kernel_thread() would trigger might_sleep() splats. With
     * CONFIG_PREEMPT_VOLUNTARY=y the init task might have scheduled
     * already, but it's stuck on the kthreadd_done completion.
     */
    system_state = SYSTEM_SCHEDULING;

    complete(&kthreadd_done);

    /*
     * The boot idle thread must execute schedule()
     * at least once to get things moving:
     */
    schedule_preempt_disabled();
    /* Call into cpu_idle with preempt disabled */
    cpu_startup_entry(CPUHP_ONLINE);
}

start_kernel的最后部分会调用rest_init来初始化这两个进程

/*
 * Create a kernel thread.
 */
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
    struct kernel_clone_args args = {
        .flags      = ((lower_32_bits(flags) | CLONE_VM |
                    CLONE_UNTRACED) & ~CSIGNAL),
        .exit_signal    = (lower_32_bits(flags) & CSIGNAL),
        .stack      = (unsigned long)fn,
        .stack_size = (unsigned long)arg,
    };

    return kernel_clone(&args);
}

kernel_clone则是常规的fork流程。

激活内核例程的几种方式:

  • 进程调用系统调用

  • 正在执行进程的CPU发出一个异常信号

  • 外围设备向CPU发出一个中断

  • 内核线程被执行

进程实现

管理(暂停与回复)当前进程所需要的寄存器信息:

  • 程序计数器(PC)和栈指针(SP)寄存器

  • 通用寄存器

  • 浮点寄存器

  • 包含CPU状态信息的处理器控制寄存器

  • 用来跟踪进程对RAM访问的内存管理寄存器

可重入内核

若干个进程可以同时在内核态下执行。

内核控制路径表示内核处理系统调用、异常或中断所执行的指令序列。

CPU交错执行内核控制路径的事件:

  • 用户态下的进程调用的系统调用未完成,CPU又执行其它的内核控制路径。

  • 运行内核控制路径时,CPU检测到一个异常

  • CPU正在运行一个启用了中断的内核控制路径时,一个硬件中断发生。

  • 抢占式调度的内核中,更高优先级的进程加入就绪队列,发生中断。第一个内核控制路径还没有执行完,CPU又开始执行另一个内核控制路径。

进程地址空间

每个进程运行在它的私有地址空间,每个内核控制路径都引用它自己的私有内核栈。

有些时候进程之间可以共享部分地址空间,可以由进程显示地提出,例如mmap()与共享内存,可以由内核自动完成对指令的共享,对于使用KVM的虚拟机,Linux也可以完成对内存的共享。

同步和临界区

当计算结果取决于如何调度两个或多个线程时,相关代码不正确,即存在竞态。

如何同步内核控制路径?

非抢占式内核

大多数Unix内核是非抢占式的,但是多处理器系统上是低效的

禁止中断

进入临界区之前禁用硬件中断

在多处理器系统中禁止CPU上的中断是远远不够的。

信号量(semaphore)

广泛使用,是一个与数据结构相关的计数器:

  • 一个整数变量

  • 一个等待进程的链表

  • 两个原子方法:down()up()

down()方法对信号量减一,up()方法对信号量加一

信号量小于0,把正在运行的进程加入信号量链表,然后阻塞该进程;信号量大于或等于0,则激活信号量链表中的一个或多个进程。

信号量初始值为1,大于或等于0时允许内核控制路径访问数据结构,小于0时将该内核控制路径加入该信号量的链表并阻塞该进程。

自旋锁

多处理器系统上,没有进程链表,当一个进程发现锁被另一个进程锁着,则使用一个紧凑的循环指令直至锁打开。

避免死锁

按规定的顺序请求信号量来规避死锁。

信号和进程间通信

Unix信号:把系统事件报告给进程的一种机制

两种系统事件:

  • 异步通告:Ctrl+C发出的SIGINT

  • 同步错误或异常: 进程访问非法地址,内核发出SIGSEGV

进程可以忽略,也可以异步地执行一个指定的过程。

默认的五种操作:

  • 终止进程

  • 核心转储(core dump)

  • 忽略信号

  • 挂起进程

  • 如果进程被暂停,则恢复执行

进程管理

fork()创建一个新的进程

_exit()终止一个进程

exec()装入一个新的程序

实现fork()使用的是写时复制(Copy-On_Write)的技术

僵死进程

子进程退出时,父进程没有即使执行wait4()导致不能即使释放子进程的进程描述符

解决办法是使用init这个特殊的系统进程,它监控所有的子进程,并按常规发布wait4()系统调用

然而当今的init已经逐渐转变成systemd的天下了,但是仍旧在某些环境中,只有openrc等其它脚本化的init系统才能工作

进程组和登录会话

一个登录会话包含指定终端已经开始工作会话的那个进程的所有后代进程。

内存管理

虚拟内存

抽象层

作为一种逻辑层处于应用程序的内存请求与硬件内存管理单元(MMU)之间,用途和优点:

  • 若干进程可以并发执行

  • 应用所需内存大于可用物理内存时也可以执行

  • 程序只有部分代码装入内存时可以执行

  • 允许每个进程访问可用物理内存的子集

  • 进程可以共库函数或者程序的一个单独的内存映像

  • 程序可以重定位,即可以放在物理内存的任何位置

  • 程序员可以编写与机器无关的代码

随机访问存储器(RAM)的使用

两部分:存放内存镜像(内核代码和内核静态数据结构)和交给虚拟内存管理,用于:

  • 满足内核对缓冲区、描述符及其它动态内核数据结构的请求

  • 满足进程对一般内存区的请求及文件内存映射的请求

  • 文件缓存

需要回收内存和处理内存碎片

内核内存分配器

内核内存分配器(KMA)特点:

  • 必须快

  • 将内存的浪费减少到最小

  • 努力减轻内存碎片

  • 能与其他内存管理系统合作,以便借用和释放页框

Linux的KMA在伙伴系统之上采用了SLAB分配算法:

  • SLAB:常规的slab分配器,在每个cpu和节点队列上缓存热对象

  • SLUB:slab的默认实现,最小化cache line的使用,可以有效地使用内存,适用于大型系统

  • SLOB:直接分配,适用于嵌入式系统

高速缓存

内存可以作为文件系统的缓存使用,但一个进程请求访问磁盘时,内核会首先检查进程请求的数据有无在缓存中。

sync()系统调用把所有脏缓冲区(即与磁盘块内容不同的部分)写入磁盘来同步。

设备驱动程序

内核通过设备驱动程序与I/O设备交互,特点:

  • 可以把特点设备的代码封装进入模块

  • 厂商可以在不了解内核源代码的情况下,根据接口规范就能增加新设备

  • 内核以统一的方式对待所有设备,并通过相同的接口访问设备

  • 可以把设备驱动写成模块,并且动态的装载进入内核

前言

在阅读Linux源码前,你需要了解下面的内容。

操作系统基本概念

操作系统的两个主要目标

  • 与硬件交互,为硬件平台上的底层编程单元提供服务

  • 为运行在计算机系统上的程序提供运行环境

用户态与内核态

现代的操作系统依靠特殊的硬件特性来禁止用户直接与底层硬件交互,或者禁止直接访问任意的内存地址。

在Intel/AMD CPU上,内核态运行在ring0级别上,可以获得最高的权限;用户态运行在ring3级别上,权限最低。

  • 实模式,保护模式与长模式:操作系统在BIOS引导时进入权限最高的16位的实模式,然后告知CPU进入32位保护模式或者64位长模式,此时可以对资源进行保护(ring protection),保护模式与长模式的区别不大,后者可以运行64位的程序也可以在兼容子模式下运行32位程序。

  • 虚拟化:最近的X86 CPU所支持的虚拟化技术,会创建在ring1下,这样可以让虚拟操作系统运行在ring0上而不影响宿主。

多用户操作系统

特点:

  • 核实用户身份的认证机制

  • 防止有错误的用户程序妨碍其它用户程序的保护机制

  • 防止恶意的用户程序干涉或者窥视其它用户程序的保护机制

  • 限制每个用户资源的记账机制

用户与组

  • 用户标识符(UID):所有的用户由一个唯一的数字来标识

  • 用户组标识符(user group ID),与其它用户有选择地分享资料

  • 超级用户(root),系统管理员

进程

进程:程序执行地一个实例

程序与进程:几个进程可以并发地同时执行同一程序,同一个进程能顺序执行几个程序

抢占式进程: 多用户系统中地进程是抢占式的,操作系统记录进程占用的CPU时间,并周期性地激活调度程序。

抢占式内核:抢占式内核允许高优先级的进程挂起处于内核态的调用,并抢先执行该高优先级的进程。

进程/内核模式:每个进程都自以为是系统中唯一的进程

内核体系结构

使用模块达到微内核理论上的很多优点而又不影响性能。

  • 模块化方法:任何模块都可以在运行时被链接或者解除链接

  • 平台无关性:即使模块依赖于某些特殊的硬件特点但不会依赖某个固定的硬件平台。

  • 节省内存:需要模块时链接,否则解除链接

  • 无性能损失: 模块在调用时无需显式地进行消息传递,只有在链接或者解除链接时有性能下降。

Unix 文件系统概述

文件

Unix文件是以字节序列组成的信息载体。

硬链接与软连接

硬链接的限制:

  • 不允许给目录创建硬链接

  • 同一文件系统中的文件之间才能创建硬链接

软连接(符号链接):

可以指向位于任何一个文件系统的任意文件或目录,甚至可以指向一个不存在的文件。

文件类型

  • 普通文件

  • 目录

  • 符号链接

  • 面向块的设备文件

  • 面向字节的设备文件

  • 管道和命名管道

  • 套接字

文件描述符与索引节点

Unix的文件系统必须至少提供POSIX标准中指定的如下属性:

  • 文件类型

  • 与文件相关的硬链接个数

  • 以字节为单位的文件长度

  • 设备标识符

  • 在文件系统中标识文件的索引节点号

  • 文件拥有者的UID

  • 文件的用户组ID

  • 时间戳:索引节点状态改变的时间、最后访问时间、最后修改时间

  • 访问权限和文件模式

访问权限和文件模式

文件的潜在用户:

  • 文件所有者

  • 不包括所有者的同组用户

  • 其他用户

三种类型的访问权限(粘滞位): 读、写及执行。

三种附加的标记:

  • suid:使进程从进程拥有者的UID切换为文件拥有者的UID

  • sgid:使进程从进程组的用户组的ID切换为该文件用户组的ID

  • sticky:向内核发出请求,当程序结束以后仍保留在内存

文件操作的系统调用

打开文件

open()系统调用:fd = open(path, flag, mode)

path

表示文件的绝对或相对路径

flag

文件打开的方式:读、写、读/写、追加

mode

指定新创建文件的访问权限

访问打开的文件

普通Unix文件,可以顺序访问,也可以随机访问

设备文件与命名管道文件,通常只能顺序访问

默认使用顺序访问,read()write()系统调用的指针都是指向文件的第一个字节。

  • lseek

lseek()系统调用:newoffset = lseek(fd, offset, whence)

fd: 打开文件的文件描述符

offset:有符号整数,计算文件指针的新位置

whence:文件指针新位置的计算方式:offset+0offset+当前位置offset+文件最后一个字节的位置

  • read

read()系统调用:nread = read(fd, buf, count)

fd: 打开文件的文件描述符

buf:进程地址空间中的缓冲区地址

count:所读取的字节数

返回的nread为实际读取的文件字节数

关闭文件

res = close(fd)

进程终止时,内核会关闭其所有仍然打开着的文件。进程需要维护进程的上下文打开的文件描述符数量,否则会触及进程能够打开的文件描述符上限。

更名以及删除文件

res = rename(oldpath, newpath)

改变文件链接的名字

res = unlink(pathname)

删除一个文件链接,当链接数为零的时候,文件才被真正删除。

Unix 内核概述

进程/内核模式

Intel和AMD的CPU实现了ring0 - ring3的四种执行状态,但是所有标准的Unix内核都仅用了内核态(Ring0)和用户态(Ring3)

程序在需要时会陷入内核态,满足需要后回到用户态

进程是动态的实体,在系统内通常只有有限的生存期,创建、撤销及同步现有进程的认为委托给内核中的一组例程

内核本身不是一个进程,而是进程的管理者。

kernel thread的特权进程:

  • 以内核态运行在内核地址空间

  • 不与用户直接交互,不需要终端设备

  • 通常在系统启动时创建,一直活跃到系统关闭

系统启动时,创建了第一个PID为0的进程,该进程完成内核的初始化,包括init/main.c中的start_kernel()及其前后的汇编代码,这个0号进程fork了一个kernel_init进程来初始化用户空间和一个kthreadd来调度进程。


noinline void __ref rest_init(void)
{
    struct task_struct *tsk;
    int pid;

    rcu_scheduler_starting();
    /*
     * We need to spawn init first so that it obtains pid 1, however
     * the init task will end up wanting to create kthreads, which, if
     * we schedule it before we create kthreadd, will OOPS.
     */
    pid = kernel_thread(kernel_init, NULL, CLONE_FS);
    /*
     * Pin init on the boot CPU. Task migration is not properly working
     * until sched_init_smp() has been run. It will set the allowed
     * CPUs for init to the non isolated CPUs.
     */
    rcu_read_lock();
    tsk = find_task_by_pid_ns(pid, &init_pid_ns);
    tsk->flags |= PF_NO_SETAFFINITY;
    set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
    rcu_read_unlock();

    numa_default_policy();
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    rcu_read_lock();
    kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
    rcu_read_unlock();

    /*
     * Enable might_sleep() and smp_processor_id() checks.
     * They cannot be enabled earlier because with CONFIG_PREEMPTION=y
     * kernel_thread() would trigger might_sleep() splats. With
     * CONFIG_PREEMPT_VOLUNTARY=y the init task might have scheduled
     * already, but it's stuck on the kthreadd_done completion.
     */
    system_state = SYSTEM_SCHEDULING;

    complete(&kthreadd_done);

    /*
     * The boot idle thread must execute schedule()
     * at least once to get things moving:
     */
    schedule_preempt_disabled();
    /* Call into cpu_idle with preempt disabled */
    cpu_startup_entry(CPUHP_ONLINE);
}

start_kernel的最后部分会调用rest_init来初始化这两个进程

/*
 * Create a kernel thread.
 */
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
    struct kernel_clone_args args = {
        .flags      = ((lower_32_bits(flags) | CLONE_VM |
                    CLONE_UNTRACED) & ~CSIGNAL),
        .exit_signal    = (lower_32_bits(flags) & CSIGNAL),
        .stack      = (unsigned long)fn,
        .stack_size = (unsigned long)arg,
    };

    return kernel_clone(&args);
}

kernel_clone则是常规的fork流程。

激活内核例程的几种方式:

  • 进程调用系统调用

  • 正在执行进程的CPU发出一个异常信号

  • 外围设备向CPU发出一个中断

  • 内核线程被执行

进程实现

管理(暂停与回复)当前进程所需要的寄存器信息:

  • 程序计数器(PC)和栈指针(SP)寄存器

  • 通用寄存器

  • 浮点寄存器

  • 包含CPU状态信息的处理器控制寄存器

  • 用来跟踪进程对RAM访问的内存管理寄存器

可重入内核

若干个进程可以同时在内核态下执行。

内核控制路径表示内核处理系统调用、异常或中断所执行的指令序列。

CPU交错执行内核控制路径的事件:

  • 用户态下的进程调用的系统调用未完成,CPU又执行其它的内核控制路径。

  • 运行内核控制路径时,CPU检测到一个异常

  • CPU正在运行一个启用了中断的内核控制路径时,一个硬件中断发生。

  • 抢占式调度的内核中,更高优先级的进程加入就绪队列,发生中断。第一个内核控制路径还没有执行完,CPU又开始执行另一个内核控制路径。

进程地址空间

每个进程运行在它的私有地址空间,每个内核控制路径都引用它自己的私有内核栈。

有些时候进程之间可以共享部分地址空间,可以由进程显示地提出,例如mmap()与共享内存,可以由内核自动完成对指令的共享,对于使用KVM的虚拟机,Linux也可以完成对内存的共享。

同步和临界区

当计算结果取决于如何调度两个或多个线程时,相关代码不正确,即存在竞态。

如何同步内核控制路径?

非抢占式内核

大多数Unix内核是非抢占式的,但是多处理器系统上是低效的

禁止中断

进入临界区之前禁用硬件中断

在多处理器系统中禁止CPU上的中断是远远不够的。

信号量(semaphore)

广泛使用,是一个与数据结构相关的计数器:

  • 一个整数变量

  • 一个等待进程的链表

  • 两个原子方法:down()up()

down()方法对信号量减一,up()方法对信号量加一

信号量小于0,把正在运行的进程加入信号量链表,然后阻塞该进程;信号量大于或等于0,则激活信号量链表中的一个或多个进程。

信号量初始值为1,大于或等于0时允许内核控制路径访问数据结构,小于0时将该内核控制路径加入该信号量的链表并阻塞该进程。

自旋锁

多处理器系统上,没有进程链表,当一个进程发现锁被另一个进程锁着,则使用一个紧凑的循环指令直至锁打开。

避免死锁

按规定的顺序请求信号量来规避死锁。

信号和进程间通信

Unix信号:把系统事件报告给进程的一种机制

两种系统事件:

  • 异步通告:Ctrl+C发出的SIGINT

  • 同步错误或异常: 进程访问非法地址,内核发出SIGSEGV

进程可以忽略,也可以异步地执行一个指定的过程。

默认的五种操作:

  • 终止进程

  • 核心转储(core dump)

  • 忽略信号

  • 挂起进程

  • 如果进程被暂停,则恢复执行

进程管理

fork()创建一个新的进程

_exit()终止一个进程

exec()装入一个新的程序

实现fork()使用的是写时复制(Copy-On_Write)的技术

僵死进程

子进程退出时,父进程没有即使执行wait4()导致不能即使释放子进程的进程描述符

解决办法是使用init这个特殊的系统进程,它监控所有的子进程,并按常规发布wait4()系统调用

然而当今的init已经逐渐转变成systemd的天下了,但是仍旧在某些环境中,只有openrc等其它脚本化的init系统才能工作

进程组和登录会话

一个登录会话包含指定终端已经开始工作会话的那个进程的所有后代进程。

内存管理

虚拟内存

抽象层

作为一种逻辑层处于应用程序的内存请求与硬件内存管理单元(MMU)之间,用途和优点:

  • 若干进程可以并发执行

  • 应用所需内存大于可用物理内存时也可以执行

  • 程序只有部分代码装入内存时可以执行

  • 允许每个进程访问可用物理内存的子集

  • 进程可以共库函数或者程序的一个单独的内存映像

  • 程序可以重定位,即可以放在物理内存的任何位置

  • 程序员可以编写与机器无关的代码

随机访问存储器(RAM)的使用

两部分:存放内存镜像(内核代码和内核静态数据结构)和交给虚拟内存管理,用于:

  • 满足内核对缓冲区、描述符及其它动态内核数据结构的请求

  • 满足进程对一般内存区的请求及文件内存映射的请求

  • 文件缓存

需要回收内存和处理内存碎片

内核内存分配器

内核内存分配器(KMA)特点:

  • 必须快

  • 将内存的浪费减少到最小

  • 努力减轻内存碎片

  • 能与其他内存管理系统合作,以便借用和释放页框

Linux的KMA在伙伴系统之上采用了SLAB分配算法:

  • SLAB:常规的slab分配器,在每个cpu和节点队列上缓存热对象

  • SLUB:slab的默认实现,最小化cache line的使用,可以有效地使用内存,适用于大型系统

  • SLOB:直接分配,适用于嵌入式系统

高速缓存

内存可以作为文件系统的缓存使用,但一个进程请求访问磁盘时,内核会首先检查进程请求的数据有无在缓存中。

sync()系统调用把所有脏缓冲区(即与磁盘块内容不同的部分)写入磁盘来同步。

设备驱动程序

内核通过设备驱动程序与I/O设备交互,特点:

  • 可以把特点设备的代码封装进入模块

  • 厂商可以在不了解内核源代码的情况下,根据接口规范就能增加新设备

  • 内核以统一的方式对待所有设备,并通过相同的接口访问设备

  • 可以把设备驱动写成模块,并且动态的装载进入内核

graph TB
    dev0_p --- scall
    dev1_p --- scall
    dev2_p --- scall
    dev3_p --- scall
    scall --- VFS
    subgraph 程序
        dev0_p(P)
        dev1_p(P)
        dev2_p(P)
        dev3_p(P)
    end
    scall["System call interface"]
    subgraph 内核
        VFS(虚拟文件系统) --- cdev(字符设备)
        VFS(虚拟文件系统) --- bdev(块设备)
            cdev --- tty驱动
            cdev --- 声音驱动
            bdev --- 磁盘驱动
        subgraph 驱动
            tty驱动
            声音驱动
            磁盘驱动
        end
    end
    subgraph 设备
        tty_0[tty] --- tty驱动
        tty_1[tty] --- tty驱动
        Mic[Mic.] --- 声音驱动
        speaker[Speaker] --- 声音驱动
        Disk[Disk] --- 磁盘驱动
        Disk[Disk] --- 磁盘驱动
    end
知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
上一篇
下一篇