Linux是如何创建进程的

Posted by 听雨coding on Monday, November 4, 2024

Linux的进程

首先我们看下进程的定义:进程就是处于执行期的程序

我们知道,进程是操作系统资源分配的基本单位,每个进程有独属于自己的虚拟空间。

在Linux中,内核把进程存放在一个双向循环链表中,称为任务队列。链表中的每一项都是task_struct,称为进程描述符。

Linux中的每个进程都有一个独一无二的pid,用于唯一标识这个进程,同时每个进程都有一个父进程,init进程是全局的父进程,它没有父进程。

那么Linux进程是如何知道它的task_struct在哪里呢?实际上每个进程都会有自己的内核栈,而在栈顶存储了一个名为thread_info的结构,thread_info中就存储着所对应task_struct的地址。

接下来我们介绍Linux内核创建进程的方式:fork()

使用fork()创建进程

在探索fork()执行流程之前,我们先来看看进程有哪些状态,也就是task_struct中的state字段:

  • TASK_RUNNING:进程是可执行的。这表示它正在执行或者正在运行队列中等待执行。
  • TASK_INTERRUPTIBLE:进程正在睡眠(被阻塞),等待某些条件的达成。一旦条件达成,那么进程就会立即运行。处于此状态的进程可以被立即唤醒并变为TASK_RUNNING状态。
  • TASK_UNINTERRUPTIBLE:进程不可被唤醒,只能等待条件达成才可以变成TASK_RUNNING状态。
  • TASK_TRACED:被其他线程跟踪。
  • TASK_STOPPED:进程停止执行。

当使用fork()函数创建一个进程,此进程立马进入TASK_RUNNING状态(准备就绪但还未投入运行),接下来通过schedule()函数将进程投入运行,此时的进程即处于运行状态。

![[Pasted image 20241125203947.png]]

创建进程

首先我们明确一点,Linux调用fork()函数只是拷贝当前进程去创建一个子进程。子进程与当前进程的区别仅是PID、PPID(父进程的pid)和某些资源和统计量(例如:挂起的信号)。而exec()函数则是读取一段代码去执行这个子进程,也就是说,想要让子进程执行与父进程不同的逻辑,就必须调用exec()函数。

以下是一个创建进程的简单代码实例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid;

    // 创建子进程
    pid = fork();

    if (pid < 0) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        printf("Child process (PID: %d), executing new program...\n", getpid());
        char *args[] = {"ls", "-l", NULL};  // 参数数组
        execvp(args[0], args);
        perror("exec failed");
        exit(EXIT_FAILURE);
    } else {
        printf("Parent process (PID: %d), waiting for child process (PID: %d)...\n", getpid(), pid);
        int status;
        wait(&status); 
        if (WIFEXITED(status)) {
            printf("Child process exited with status %d\n", WEXITSTATUS(status));
        } else {
            printf("Child process did not terminate normally\n");
        }
    }
    return 0;
}
  • fork():
    • 创建一个子进程。
    • 返回值:
      • 父进程中返回子进程的 PID。
      • 子进程中返回 0。
      • 失败时返回 -1。
  • execvp():
    • 用新的程序替换当前进程的代码段。
    • 参数:
      • 第一个参数是要执行的程序名。
      • 第二个参数是参数数组,最后一个元素必须为 NULL
  • wait():
    • 父进程等待子进程完成并获取其退出状态。

写时复制

Linux的写时复制(copy-on-write)是一种可以推迟甚至可以免除拷贝的技术。

在执行fork()函数时,Linux并不会将父进程的所有资源进行拷贝,而是让子进程和父进程共享空间。 只有在需要写入的时候,父进程的资源才会被复制。如果fork()后立即调用exec(),也就代表父进程的页不会被写入,所以父进程的资源就不会被复制了。 fork()的实际开销就是复制父进程的页表和给子进程创建一个task_struct,但是一般情况下,进程创建后都会立即执行exec()函数,也就是运行一个可执行文件,所以也就避免了拷贝大量的数据。

停止进程

僵尸进程

进程一般调用exit()系统调用去终结,或者接收到特定信号或异常时去终结,而这些终结进程的方式最后都会调用do_exit()方法。

在进程终结后会释放它所占用的资源,但是会保留它的进程描述符task_struct,处于这个状态的进程也叫僵尸进程

在进程释放资源后,需要通知它的父进程来删除它的task_struct,父进程使用wati()函数(wait4()系统调用)去删除子进程的task_struct;

孤儿进程

当父进程在子进程之前终结,这时如果子进程找不到合适的父进程,子进程就成了一个孤儿进程,当孤儿进程终结时,无法找到父进程删除它的task_struct,那么这个进程就会永远变成僵尸进程。

解决方案就是当子进程找不到父进程时,就在当前线程组内找一个进程作为父进程,如果不行,就会让全局父进程init进程进行托管。

总结

  • 进程的本质:是操作系统中管理和分配资源的基本单位。
  • 创建与执行
    • fork() 创建子进程,初始与父进程共享内存。
    • exec() 使子进程运行独立的逻辑。
  • 僵尸进程孤儿进程 是进程管理中常见问题,需通过 waitinit 进程来处理。
  • 写时复制 提高了资源利用效率,是 fork() 的关键优化。

comments powered by Disqus