购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.5 进程创建

在内核启动期间,会创建一个名为init的内核线程,该内核线程接着又被配置为初始化第一个用户模式进程(具有相同的名称)。然后init(pid 1)进程执行通过配置文件指定的各种初始化操作,创建一系列进程。每个进一步创建的子进程(可能会接着创建自己的子进程)都是init进程的后代。因此,创建的进程最终形成了类似于树状结构或单一层次的模型。Shell就是这样的一个进程,当程序被调用执行时,它成为用户创建用户进程的接口。

fork、vfork、exec、clone、wait和exit是创建和控制新进程的核心内核接口。这些操作是通过相应的用户模式API调用的。

1.5.1 fork()

自从传统的UNIX版本发布以来,fork()就是*nix系统中可用的核心“UNIX线程API”之一。正如其名字一样,它从正在运行的进程中分出一个新进程。当fork()执行成功时,通过复制调用者的地址空间和任务结构体来创建新进程(称为子进程)。从fork()返回时,调用者(父)和新进程(子)会继续执行来自同一代码段的指令,该指令是通过写时复制的方式复制而来的。fork()可能是唯一一个在调用者进程上下文中进入内核模式的API,并且在执行成功后,会在调用者和子进程(新进程)的上下文中返回到用户模式。

除了少数一些属性,如内存锁、挂起的信号、活跃的定时器和文件记录锁(有关例外的完整列表,请参阅fork(2)帮助文档)之外,父进程task structure的大多数资源条目(如内存描述符、文件描述符表、信号描述符和调度属性)都由子进程继承。子进程被赋予一个唯一的pid,并通过其task structure的ppid字段引用其父进程的pid;而子进程的资源利用和处理器使用条目会被重置为零。

父进程可以通过使用 wait()系统调用更新自己关于子进程的状态,并且通常等待子进程的终止。假如未能调用wait(),子进程可能会终止并且进入僵尸状态。

1.5.2 写时复制(COW)

在通过复制父进程来创建子进程时,需要为子进程克隆父进程的用户模式地址空间(栈、数据、代码和堆段)和任务结构体;而这会导致执行开销,从而导致创建进程时间的不确定性。更糟糕的是,如果父进程和子进程都没有对克隆资源进行任何状态更改操作,这个克隆过程将变得毫无用处。

根据写时复制(Copy-On-Write,COW),当创建一个子进程时,会为其分配一个唯一的task structure,其中包含引用父进程task structure的所有资源条目(包括页表),并且对父进程和子进程有只读访问权限。当两个进程中的任意一个启动状态更改操作时,资源才会被真正复制,因此称为写时复制。COW中的Write就意味着状态更改。COW通过将复制进程数据的需求延迟到直到写入时才完成,并且在只发生读取的时候完全避免复制,使效率和优化凸显出来。这种按需复制还可以减少所需交换页的数量,缩短花费在交换页上的时间,并有助于减少分页请求。

1.5.3 exec

有时候,创建一个子进程可能用处不大,除非它完全运行一个新的程序,exec系列函数正是为此目的而服务的。exec通过在现有的进程中执行一个新的可执行二进制文件来替代现有的程序:

#include <unistd.h>
int execve(const char *filename, char *const argv[],
char *const envp[]);

execve是一个系统调用,它会将第一个传给它的参数作为路径,用来执行二进制文件程序。第二个和第三个参数是以null结尾的数组参数和字符串环境变量,它们将作为命令行参数传递给一个新程序。这个系统调用也可以通过各种glibc(库)封装器来调用,这样会更加方便和灵活:

include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execv(const char *path, char *constargv[]);
int execvp(const char *file, char *constargv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);

命令行用户界面程序(如shell)使用exec接口来启动用户请求的程序二进制文件。

1.5.4 vfork()

与fork()不同,vfork()创建子进程并会阻塞父进程,这意味着子进程会作为一个单独的线程运行并且不允许与父进程并发;换句话说,父进程暂时被挂起,直到子进程退出或调用exec()。子进程共享父进程的数据。

1.5.5 Linux线程支持

一个进程中的执行流被称为线程(thread),这意味着每个进程至少会有一个执行线程。多线程意味着在一个进程中存在多个执行上下文流。使用现代的多核体系结构,一个进程中的多个执行流可以真正并发,实现公平的多任务处理。

在计划执行的进程中,线程通常被枚举为纯用户级实体;它们共享父进程的虚拟地址空间和系统资源。每个线程维护其自身代码、堆栈和线程本地存储。线程由线程库调度和管理,线程库使用称为线程对象的结构体来保存唯一的线程标识符,用于调度属性和保存线程上下文。用户级线程应用程序在内存上通常比较轻量化,并且是事件驱动型应用程序的首选并发性模型。另一方面,这样的用户级线程模型不适合并行计算,因为它们被绑定在与父进程绑定的同一个处理器核上执行。

Linux不直接支持用户级线程,它提出了一个替代API枚举并称为轻量级进程(Light Weight Process,LWP)的特殊进程,该进程可以与父进程共享一组配置资源,例如动态内存分配、全局数据、打开文件、信号处理程序和其他广泛的资源。每个LWP由一个唯一的PID和任务结构体来标识,并被内核视为一个独立的执行上下文。在Linux中,术语“线程”总是指LWP,因为由线程库(Pthreads)初始化的每个线程都被内核枚举为LWP。

clone()

clone()是Linux特有的一个系统调用,用来创建一个新的进程;它被认为是fork()系统调用的通用版本,通过flags参数提供更精细的控制来自定义其功能:

int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);

它提供了超过20种不同的CLONE_*标志来控制 clone操作的各个方面,包括父进程和子进程是否共享资源,如虚拟内存、打开文件描述符和信号处理。使用适当的内存地址(作为第二个参数传递)创建子进程,以用作堆栈(用于存储子进程的本地数据)使用。子进程以其启动函数(作为第一个参数传递给clone调用)开始执行。

当进程尝试通过pthread库创建线程时,会使用以下标志(见表1-1)调用clone():

/*clone flags for creating threads*/
flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|
CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID;

表1-1

clone()也可以用来创建一个常规子进程,但通常是使用fork()和vfork()生成的: IVJ4zy36wPBwsdzuTIZd2Q51xK05/KCowsjbnxjX3d+zAWevQL/n6zP5eVouD3mr

/* clone flags for forking child */
flags = SIGCHLD;
/* clone flags for vfork child */
flags = CLONE_VFORK | CLONE_VM | SIGCHLD;
点击中间区域
呼出菜单
上一章
目录
下一章
×