Linux操作系统分析学习心得

127 阅读13分钟

Linux操作系统分析学习心得

学习收获

通过课程对linux操作系统分析的学习,我收获了很多针对linux系统分析的知识。课程从用户的角度对linux的环境及其使用进行了简单的介绍,然后通过linux操作系统源代码分析了linux操作系统与底层硬件、上层应用之间的结构关系、系统调用关系,熟悉了Linux操作系统的配置、安装过程,掌握了linux操作系统的内核关键技术,例如中断和异常处理、地址空间管理、内存分配技术、进程管理、进程切换关键代码、进程间通信机制、文件系统管理等内容,最后还介绍了虚拟机和容器等前沿技术与发展情况,开阔了视野。

学习课程的过程有一种见微知著的感觉,课程资料以小见大地让人领略了linux源码的分析与学习的方法,为以后的学习工作中更深入地学习linux操作系统提供了工具和手段。

linux中的进程管理

通过课程由浅入深的教学,以及参考了教学书籍和课件的资料,下面简要总结进程的描述和创建原理。

进程的描述

操作系统内核中最核心的功能是进程管理,谈到进程管理就要涉及到进程的描述。进程的描述有提纲挈领的作用,它可以把内存管理、文件系统、进程间通信等内容串起来。Linux内核中的进程是非常复杂的,在操作系统原理中可知,我们通过进程控制块PCB描述进程,为了管理进程,内核要描述进程的结构,我们也称其为进程描述符,进程描述符直接或间接提供了进程相关的所有信息。

在Linux内核中是struct task_struct来描述进程,以下代码摘录了struct task_struct 数据结构的一部分,具体见 include/linux/sched.h 文件。

struct task_struct {
    #ifdef CONFIG_THREAD_INFO_IN_TASK
    /*
    * For reasons of header soup (see current_thread_info()), this
    * must be the first element of task_struct.
    */
    struct thread_info thread_info;
    #endif
    /* -1 unrunnable, 0 runnable, >0 stopped: */
    volatile long state;
    /*
    * This begins the randomizable portion of task_struct. Only
    * scheduling-critical items should be added above here.
    */
    randomized_struct_fields_start
    void *stack;
    ...
    /* CPU-specific state of this task: */
    struct thread_struct thread;
    /*
    * WARNING: on x86, 'thread_struct' contains a variable-sized
    * structure. It *MUST* be at the end of 'task_struct'.
    *
    * Do not put anything below here!
    */

完整的代码中的一些变量含义如下:其中state是进程状态,stack是堆栈等,还有比如进程的状态、进程双向链表的管理,以及控制台tty、文件系统fs的描述、进程打开文件的文件描述符files、内存管理的描述mm,还有进程间通信的信号signal的描述等。

进程的状态

操作系统原理中的进程有就绪态、运行态、阻塞态这3种基本状态,实际的Linux内核管理进程状态与三种状态并不一致。

  • 使用fork()系统调用来创建一个新进程时状态为 TASK_RUNNING

  • 调度器选择新创建的进程运行时,切换到运行态同样为 TASK_RUNNING

  • 在Linux内核中,当进程是TASK_RUNNING状态时,它是可运行的,也就是就绪态,是否在运行取决于它有没有获得CPU的控制权

    • 如果在CPU中实际执行着,进程状态就是运行态
    • 如果被内核调度出去了,在等待队列里就是就绪态
  • 正在运行的进程,调用用户态库函数 exit() 会陷入内核执行内核函数 do_exit() 终止进程,会进入 tsk->exit_state 进程的终止状态

  • tsk->exit_state状态的进程一般叫作僵尸进程,Linux内核会在适当的时候把僵尸进程给处理掉,处理掉之后进程描述符被释放了,该进程才从Linux系统里消失

  • 阻塞态也有两种:TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE

    • 如果事件发生或者资源可用,进程被唤醒并被放到运行队列上
    • 如果阻塞的条件没有了就进入就绪态,调度器选择到它时就进入运行态
进程链表

Linux内核系统用一个双向链表的数据结构 struct list_head tasks 来管理进程,struct list_head tasks把所有的进程用双向链表链起来,而且还会头尾相连把所有的进程用双向循环链表链起来,数据结构具体内容如下,详见 include/linux/types.h

struct list_head tasks;//task_struct中定义的
// struct list_head数据结构具体
struct list_head {
    struct list_head *next, *prev;
};
进程的父子兄弟关系

进程的描述符 struct task_struct 数据结构中的如下代码展示进程间父子关系的记录方式:

/* Real parent process: */
struct task_struct __rcu *real_parent;
/* Recipient of SIGCHLD, wait4() reports: */
struct task_struct __rcu *parent;
/*
* Children/sibling form the list of natural child
*/
struct list_head children;
struct list_head sibling;
struct task_struct *group_lead
  • real_parent、parent:记录当前进程的父进程
  • structlist_head children:记录当前进程的子进程的双向链表
  • structlist_head sibling:记录当前进程的兄弟进程的双向链表
进程描述符总结

进程描述符中还有文件系统相关的数据结构、打开的文件描述符,有和信号处理相关以及和pipe管道相关的内容;其中,进程状态、堆栈、保存进程上下文CPU状态的thread(ip和sp等)是比较关键的,另外还有文件系统、信号、内存、进程空间等,这些在进程描述符里面有相应的结构体变量或指针,包含或指向其中的具体内容

要研究Linux内核的某一部分的特定内容,进程描述符可以起到提纲挈领的作用。进程描述符为我们进一步深入研究Linux内核提供了基础,下面可以进一步了解系统的某一方面。比如进程是怎么创建起来的,在系统中可以按相同的方式创建好多个进程,这就需要理解进程之间如何调度切换等,逐渐理解整个系统的工作机制,最终就能从整体上准确把握Linux内核的运作机制。

进程的创建

通过对进程描述符的介绍,我们了解了进程描述符的内容、包括进程状态转换、双向循环链表、thread等,下面可以了解进程创建的源头和过程。

Linux内核中进程的初始化

Linux内核0号进程的初始化,见 init/main.c 代码的 set_task_stack_end_magic(&init_task); ,init_task为第一个进程(0号进程)的进程描述符结构体变量:

struct task_struct init_task
= {
#ifdef CONFIG_THREAD_INFO_IN_TA
.thread_info = INIT_THREAD_INFO
.stack_refcount = REFCOUNT_IN
#endif
.state = 0,
.stack = init_stack,
.usage = REFCOUNT_INIT(2),
.flags = PF_KTHREAD,
.prio = MAX_PRIO - 20,

它的初始化是通过硬编码方式固定下来的。除此之外,所有其他进程的初始化都是通过do_fork复制父进程的方式初始化的。

1号和2号进程的创建是 start_kernel 初始化到最后,由 rest_ init 通过 kernel_thread 创建两个内核线程:

  • kernel_init :负责用户态的进程 init 启动,是所有用户进程的祖先
  • kthreadd:内核线程,是所有内核线程的祖先,负责管理所有内核线程
  •   noinline void __ref rest_init(void)
      {…
          pid = kernel_thread(kernel_init, NULL, CLONE_FS);
          …
          pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
      …}
    

在系统启动时,除了0号进程的初始化过程是代码显式创建的,1号init进程的创建实际上是复制0号进程,根据1号进程的需要修改了进程pid等,然后再加载一个init可执行程序,后续会具体介绍加载可执行程序的过程;同样2号kthreadd内核线程也是通过复制0号进程来创建的。

通过如下 kernel/fork.c 中kernel_thread代码可以看到1号进程和2号进程最终都是通过_do_fork 创建的,用户态通过系统调用fork创建一个进程最终也是通过 _do_fork来完成的:

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

_do_fork 创建的进程是复制产生一个子进程,并对复制的进程描述符做一些修改。在进程调度时,新创建的子进程处于就绪状态有机会被调度执行。为了讲解子进程从哪里执行,我们首先了解用户态创建进程,可以参考如下示例代码:

int main(int argc, char * argv[])
{
    int pid;
        /* fork another process */
    pid = fork();
    if (pid < 0)
    {
        /* error occurred */
    }
    else if (pid == 0)
    {
        /* child process */
    }
    else
    {
        /* parent process */
    }
}

代码中用到了库函数的 fork(),是用户态创建一个子进程的系统调用API接口。fork系统调用把当前进程又复制了一个子进程,也就一个进程变成了两个进程,两个进程执行相同的代码,只是fork系统调用在父进程和子进程中的返回值不同。fork之后,父子进程的执行顺序和调度算法密切相关,多次执行可以看到父子进程的执行顺序并不是确定的。

进程创建的主要过程

在 Linux 系统中,进程创建的核心目标是复制当前进程(父进程)的信息以创建一个新进程(子进程)。这一过程主要通过 _do_fork 函数实现。尽管父子进程的大部分信息是相同的,但某些关键信息必须保持独立,以确保两个进程能够正确运行。以下是创建进程的框架分析:

  1. 复制进程描述符 task_struct

    父进程的 task_struct 结构体被复制到子进程中。task_struct 包含了进程的所有关键信息,如进程状态、资源、文件描述符等。

    复制后的子进程 task_struct 需要进行修改,以确保父子进程具有独立的特性,例如:

    • 进程标识符(PID) :子进程必须有唯一的 PID。
    • 内核堆栈:子进程需要独立的内核堆栈,以避免父子进程之间的堆栈冲突。
    • 执行上下文thread 数据结构记录了进程执行上下文的关键信息,如指令指针和栈顶寄存器。子进程的 thread 数据结构需要独立设置,以确保子进程从正确的位置开始执行。
  2. 修改子进程的 task_struct

    子进程的 task_struct 在多个地方被修改,以反映其独立的特性。这包括但不限于:

    • 设置子进程的 PID。
    • 初始化子进程的内核堆栈。
    • 配置子进程的 thread 数据结构,确保其指令指针和栈顶寄存器指向正确的地址。
  3. 链接到系统链表

    子进程的 task_struct 需要被链接到内核的进程管理链表中,以便内核能够管理和调度新进程。

  4. 返回到用户态

    fork 系统调用在父子进程中分别返回到用户态。尽管父子进程的大部分信息是相同的,但内核堆栈中的某些信息(如返回地址)可能不同,以确保父子进程能够正确返回到用户态。

在分析 fork 创建子进程的过程中,特别需要注意的是,复制父进程资源时采用了写时复制(Copy-On-Write, COW)技术。在创建子进程时,只有当父子进程尝试修改共享资源时,才会实际复制这些资源。因此父子进程在未修改资源之前可以共享相同的内存存储空间,这大大提高了效率。

/*
* Create a kernel thread.
*/
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
    return _do_fork(&args);
}
SYSCALL_DEFINE0(fork)
{
    return _do_fork(&args);
}
SYSCALL_DEFINE0(vfork)
{
    return _do_fork(&args);
}
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
{
    return _do_fork(&args)
}

从代码中可以看出,forkvforkclone 这三个系统调用,以及 do_forkkernel_thread 内核函数都可以创建新进程,且它们最终都通过 _do_fork 函数实现,只是传递的参数不同。为了避免重复,我们直接从 _do_fork 函数入手进行分析,具体代码见 kernel/fork.c

long _do_fork(struct kernel_clone_args *args)
{
    //复制进程描述符和执行时所需的其他数据结构
    p = copy_process(NULL, trace, NUMA_NO_NODE, args);
    wake_up_new_task(p);//将子进程添加到就绪队列
    return nr;//返回子进程pid(父进程中fork返回值为子进程的pid)
}

_do_fork 函数主要完成了调用 copy_process() 复制父进程、获得pid、调用 wake_up_new_task 将子进程加入就绪队列等待调度执行等 。

copy_process() 函数是创建进程的核心代码,主要完成了以下任务:

  1. 调用 dup_task_struct 复制父进程的 task_struct 描述符。
  2. 进行信息检查和初始化。
  3. 将进程状态设置为 TASK_RUNNING(子进程进入就绪态)。
  4. 采用写时复制技术逐一复制其他进程资源。
  5. 调用 copy_thread_tls 初始化子进程的内核栈。
  6. 设置子进程的 PID 等。

其中,dup_task_structcopy_thread_tls 是最关键的部分,完整代码见 kernel/fork.c

static __latent_entropy struct task_struct *copy_process(
struct pid *pid,
int trace,
int node,
struct kernel_clone_args *args)
{
    //复制进程描述符task_struct、创建内核堆栈等
    p = dup_task_struct(current, node);
    /* copy all the process information */
    shm_init_task(p);
    …
    // 初始化子进程内核栈和thread
    retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p,
    args->tls);
    …
    return p;//返回被创建的子进程描述符指针
}

进程创建的原理还有进程关键上下文和系统调用的内核堆栈等,子进程创建好了进程描述符、内核堆栈等,就可以通过 wake_up_new_task(p) 将子进程添加到就绪队列,使之有机会被调度执行,进程的创建工作就完成了,子进程就可以等待调度执行,子进程的执行从设定的 ret_from_fork 开始。

总结来说,进程的创建过程大致是父进程通过fork系统调用进入内核_do_fork函数,如下图所示:

QQ图片20250520233241.png

复制进程描述符及相关进程资源(采用写时复制技术)、分配子进程的内核堆栈并对内核堆栈和thread等进程关键上下文进行初始化,最后将子进程放入就绪队列,fork系统调用返回;而子进程则在被调度执行时根据设置的内核堆栈和thread等进程关键上下文开始执行。

参考资料

  1. 《庖丁解牛Linux操作系统分析》 gitee.com/mengning997…
  2. 《linux操作系统分析课程资料》 gitee.com/mengning997…