本系列博客基本是《openEuler 操作系统》、《现代操作系统 原理与实现》的读书笔记
绝大部分引用的代码都是博主根据书中的提示,到 openEuler 中翻出来的。为了尽量减少代码所占的篇幅,对引用的代码尽量进行了省略和删减
其他参考文章:
5.execve()到底干了啥?_chengonghao的博客-CSDN博客_execve
ELF文件的加载过程(load_elf_binary函数详解)--Linux进程的管理与调度(十三)_OSKernelLAB-CSDN博客
进程控制原语
【进程控制】,指的是 OS 使用一些程序段完成创建、销毁进程,以及完成进程各状态间的转换。
进程控制原语,主要包括创建、销毁、阻塞、唤醒
OS 对进程的控制,要通过【控制原语】来实现,每个控制原语都是一段指令代码,这段代码常驻内存,运行于内核态,对外暴露有系统调用。
称之为【原语】,是因为这段代码的执行是原子的,即执行过程中不可中断(可通过关中断实现)。OS 通常禁止原语并发,以避免指令交错执行可能导致的 PCB 数据错误。
在 Linux 中启动一个新程序分两步,第一步是使用 fork 基于当前进程复制出一个新的进程,第二步是使用 exec 载入新的程序,为新进程启动全新的任务
创建新进程:fork()
用法:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
使用 fork 创建的新进程是原有进程的副本,两个进程除了【PID、虚拟内存空间】之外完全一致。
进行 fork 时,操作系统需要进行的工作有:
- 创建新的 PCB 并初始化
- 将父进程 PCB 的 CPU 上下文复制给子进程的 PCB ,父子此时拥有相同的执行环境
- 为新进程分配物理内存
创建并复制 PCB
进行 PCB 页面申请的代码如下:
static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
struct task_struct *tsk;
unsigned long *stack;
struct vm_struct *stack_vm_area;
int err;
tsk = alloc_task_struct_node(node); // 分配 PCB 页面
// 创建内核栈
stack = alloc_thread_stack_node(tsk, node);
......
stack_vm_area = task_stack_vm_area(tsk);
// 复制 PCB
err = arch_dup_task_struct(tsk, orig);
tsk->stack = stack;
......
return tsk;
}
-
具体的
alloc_task_struct_node
有两种定义,一种是定义在kernel/fork.c
中如下static inline struct task_struct *alloc_task_struct_node(int node) { return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node); }
另一种是在
arch/ia64/include/asm/thread_info.h
中以类函数宏的方式定义的,此处略 -
对于 PCB 的复制,定义在
arch/arm64/kernel/process.c
中int arch_dup_task_struct(struct task_struct *dst, struct task_struct *src) { if (current->mm) fpsimd_preserve_current_state(); *dst = *src; // 主要就这么拷贝一下 BUILD_BUG_ON(!IS_ENABLED(CONFIG_THREAD_INFO_IN_TASK)); dst->thread.sve_state = NULL; clear_tsk_thread_flag(dst, TIF_SVE); return 0; }
复制 CPU Context
int copy_thread(unsigned long clone_flags, unsigned long stack_start,
unsigned long stk_sz, struct task_struct *p)
{
// pt_regs 保存从用户空间进入内核模式时,需要保存的用户寄存器状态
// 参数 p 是新进程的 task_struct
struct pt_regs *childregs = task_pt_regs(p);
// 新进程的内核态寄存器全部清零
memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context));
fpsimd_flush_task_state(p);
if (likely(!(p->flags & PF_KTHREAD))) {
*childregs = *current_pt_regs(); // 把当前寄存器赋给新进程
childregs->regs[0] = 0; // arm 用 X0 传返回值,成功了就用这个返回0
*task_user_tls(p) = read_sysreg(tpidr_el0);
if (stack_start) { // 若设置了用户栈起始地址
if (is_a32_compat_thread(task_thread_info(p)))
childregs->compat_sp = stack_start;
else
childregs->sp = stack_start;
}
...
} else {
...
}
p->thread.cpu_context.pc = (unsigned long)ret_from_fork;
p->thread.cpu_context.sp = (unsigned long)childregs;
ptrace_hw_copy_thread(p);
return 0;
}
pt_regs:arch/arm64/asm/ptrace.h
pt_regs 定义在异常的过程中,如何在栈中保存寄存器状态
struct pt_regs {
union {
struct user_pt_regs user_regs;
struct {
u64 regs[31];
u64 sp;
u64 pc;
u64 pstate;
};
};
u64 orig_x0;
u64 orig_addr_limit;
...
};
复制地址空间
fork() 会为新进程分配和父进程一样的地址空间,具体的分配方式是【写时复制】:即新进程会直接复制父进程的页表,这样新进程与父进程就会指向相同的物理内存,同时这段中的物理页会被标为只读(通过修改 PTE 页表项实现)。当任意一方要修改内存时,就会触发缺页异常,由 OS 将该页复制一份并修改页表指向新页。返回后程序便可写新页了。
arm64 支持四级页表,四个级别分别叫做:PGD(Page Global Directory)、PUD(Page Upper Directory)、PMD(Page Middle Directory)、PTE(Page Table Entry),PTE 中的就是页表项,每个页表项对应一个页面。页表的复制就是对这四级页表进行四层循环复制,这些层次结构的拷贝分别对应 copy_page_range
、copy_pud_range
、copy_pmd_range
、copy_pte_range
、copy_one_pte
都定义在 mm/memory.c
中:
int copy_page_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
struct vm_area_struct *vma)
{
pgd_t *src_pgd, *dst_pgd;
unsigned long next;
unsigned long addr = vma->vm_start;
unsigned long end = vma->vm_end;
bool is_cow;
...
// 检查是否是写时复制,如果是,下面会有相应的处理
is_cow = is_cow_mapping(vma->vm_flags);
if (is_cow)
...
do {
...
if (unlikely(copy_p4d_range(dst_mm, src_mm, dst_pgd, src_pgd,
vma, addr, next))) {
ret = -ENOMEM;
break;
}
} while (dst_pgd++, src_pgd++, addr = next, addr != end);
}
...
static inline int copy_p4d_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pgd_t *dst_pgd, pgd_t *src_pgd, struct vm_area_struct *vma,
unsigned long addr, unsigned long end)
{
...
do {
...
if (copy_pud_range(dst_mm, src_mm, dst_p4d, src_p4d,
vma, addr, next))
return -ENOMEM;
} while (dst_p4d++, src_p4d++, addr = next, addr != end);
return 0;
}
int copy_page_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
struct vm_area_struct *vma)
{
pgd_t *src_pgd, *dst_pgd;
unsigned long next;
unsigned long addr = vma->vm_start;
unsigned long end = vma->vm_end;
...
do {
...
if (unlikely(copy_p4d_range(dst_mm, src_mm, dst_pgd, src_pgd,
vma, addr, next))) {
ret = -ENOMEM;
break;
}
} while (dst_pgd++, src_pgd++, addr = next, addr != end);
...
return ret;
}
...
// 将一个 task 的一个 vm_area 拷贝给另一个 task
static inline unsigned long
copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,
unsigned long addr, int *rss)
{
...
// 如果是写时复制
if (is_cow_mapping(vm_flags) && pte_write(pte)) {
ptep_set_wrprotect(src_mm, addr, src_pte);
pte = pte_wrprotect(pte);
}
// 如果是共享内存
if (vm_flags & VM_SHARED)
pte = pte_mkclean(pte);
...
return 0;
}
对缺页异常原因的判断与处理
有很多原因引起缺页异常,对内存错误进行处理的函数也都定义在 mm/memory.c
中。
对 pte 错误进行处理的函数为 static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
,对写时复制引起缺页的处理也写在这里面
具体页面的复制由 static vm_fault_t wp_page_copy(struct vm_fault *vmf)
进行
运行新程序:exec 函数簇
【exec 函数簇】 指的是一组函数,这些函数都能进行装载并运行程序的任务。
exec 函数簇会将外存中的二进制文件装载进地址空间,替换掉原来的程序,分配新的用户堆栈,对 PCB 略作修改并开始执行新程序的指令。
exec 函数簇有execl
、execlp
、execle
、execv
、execvp
、execve
,这些函数只有最后一个是系统调用,其他的都是库函数(包一层之后本质上还是调用 execve
)。系统调用函数也是功能
最全的。
execve
int execve(char const *path, char const *argv[], char const *envp[]);
参数:
path
:可执行文件路径argv[]
:进程参数(会被传入被调用函数的main(int argc, char *argv[])
)envp[]
:环境变量(程序的 main 函数可以扩展为main(int argc, char *argv[], char *envp[])
,envp[]
就传到这里面)
找到程序文件并装载
查找文件
既然要运行一个文件,那么首先就要找到这个文件。内核解析文件名并找到 inode 对象,进而获取文件的物理位置。接下来内核会创建 file 对象,将文件路径、inode 对象、文件打开模式等信息填入 file 对象,之后内核就直接通过该 file 对象来对打开的文件进行访问。
文件的查找和解析定义在 fs/namei.c
中,两者分别由 path_init
、link_path_walk
函数执行。打开文件和填充 file 结构则由 fs/file_table.c
中的 alloc_file
定义
装载程序
execve
到内核中,实际调用的是 do_execve
,这里会一直调用一串函数,如下:
/*/fs/exec.c*/
// 系统调用的接口是这个,然后一串调用下去
SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
return do_execve(getname(filename), argv, envp);
}
int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}
static int do_execveat_common(int fd, struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp,
int flags)
{
return __do_execve_file(fd, filename, argv, envp, flags, NULL);
}
static int __do_execve_file(int fd, struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp,
int flags, struct file *file)
{
...
// 具体的执行,打开 ELF 并把所有的信息装入 bprm,执行:
retval = exec_binprm(bprm); //
...
}
static int exec_binprm(struct linux_binprm *bprm)
{
...
ret = search_binary_handler(bprm); // 寻找解析 ELF 的函数
...
return ret;
}
/**
* 参考了很多 CHENG Jian 博主文章的内容,在此对博主表示感激
*/
// 这里搜索 Linux支持的可执行文件类型队列,让各种可执行程序的处理程序前来认领和处理
// 如果类型匹配,则调用load_binary函数指针所指向的处理函数来处理目标映像文件
int search_binary_handler(struct linux_binprm *bprm)
{
...
list_for_each_entry(fmt, &formats, lh) {
retval = fmt->load_binary(bprm);
}
...
}
// 这里会根据具体的二进制文件格式来确定 load_binary 具体调哪个函数
// load_binary 的任务,是通过读存放在可执行文件中的信息为当前进程建立一个新的执行环境
// 对于我们的 ELF 而言,当然就是 load_elf_binary
/* fs/binfmt_elf.c */
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
// 这里调用的就是取 elf binary
.load_binary = load_elf_binary,
// 用于动态的把一个共享库捆绑到一个已经在运行的进程, 这是由uselib()系统调用激活的
.load_shlib = load_elf_library,
// 在名为core的文件中, 存放当前进程的执行上下文. 这个文件通常是在进程接收到一个缺省操作为”dump”的信号时被创建的, 其格式取决于被执行程序的可执行类型
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
};
static int load_elf_binary(struct linux_binprm *bprm)
{
// 一些光是看名字就能猜个一二三的变量
unsigned long elf_entry;
unsigned long interp_load_addr = 0;
unsigned long start_code, end_code, start_data, end_data;
unsigned long reloc_func_desc __maybe_unused = 0;
int executable_stack = EXSTACK_DEFAULT;
// 存 ELF Header
struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
struct pt_regs *regs = current_pt_regs();
struct {
struct elfhdr elf_ex; // 等下存 ELF Header 用
struct elfhdr interp_elf_ex;
} *loc;
struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;
loff_t pos;
// 内核就这么申请内存,一次不能超 128k
loc = kmalloc(sizeof(*loc), GFP_KERNEL);
if (!loc) {
retval = -ENOMEM;
goto out_ret;
}
/* 转为 ELF 的 Header 格式 */
loc->elf_ex = *((struct elfhdr *)bprm->buf);
retval = -ENOEXEC;
/* 检查映像类型, EXEC 是可执行文件, DYN 是共享库 */
if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
goto out;
/* 检查架构。下面还有一些其他的检查,略 */
if (!elf_check_arch(&loc->elf_ex))
goto out;
/* 从 ELF 中读 Program Header,里面包含了 kmalloc,直接返回指针 */
elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
// 该循环用于查找和处理目标映像的 interpreter 段
for (i = 0; i < loc->elf_ex.e_phnum; i++) {
if (elf_ppnt->p_type == PT_INTERP) { // PT_INTERP 即 interpreter 段的类型
// 分空间
elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
// 根据为位置,读 elf_interpreter。也就是读入解释器段
retval = kernel_read(bprm->file, elf_interpreter,
elf_ppnt->p_filesz, &pos);
// 打开解释器文件
interpreter = open_exec(elf_interpreter);
/* Get the exec headers */
pos = 0;
retval = kernel_read(interpreter, &loc->interp_elf_ex,
sizeof(loc->interp_elf_ex), &pos);
break;
}
elf_ppnt++;
}
elf_ppnt = elf_phdata;
/* 检查解释器头 */
if (elf_interpreter) {
/* Verify the interpreter has a valid arch */
if (!elf_check_arch(&loc->interp_elf_ex) ||
elf_check_fdpic(&loc->interp_elf_ex))
goto out_free_dentry;
/* 加载解释器程序头 */
interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,
interpreter);
if (!interp_elf_phdata)
goto out_free_dentry;
/* Pass PT_LOPROC..PT_HIPROC headers to arch code */
elf_ppnt = interp_elf_phdata;
for (i = 0; i < loc->interp_elf_ex.e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_LOPROC ... PT_HIPROC:
retval = arch_elf_pt_proc(&loc->interp_elf_ex,
elf_ppnt, interpreter,
true, &arch_state);
if (retval)
goto out_free_dentry;
break;
}
}
/* 把继承自父进程的代码 flush 掉 */
retval = flush_old_exec(bprm);
setup_new_exec(bprm);
install_exec_creds(bprm);
/* Do this so that we can load the interpreter, if need be. We will
change some of these later */
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
current->mm->start_stack = bprm->p;
/* 把 ELF 各段映射进内存 */
for(i = 0, elf_ppnt = elf_phdata;
i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
int elf_prot = 0, elf_flags, elf_fixed = MAP_FIXED_NOREPLACE;
unsigned long k, vaddr;
unsigned long total_size = 0;
// 只有类型为 PT_LOAD 的段才需要装载,不是的直接跳过,重新循环
if (elf_ppnt->p_type != PT_LOAD)
continue;
if (unlikely (elf_brk > elf_bss)) {
// 检查地址和页面信息
unsigned long nbyte;
// 设置 brk,生成 BSS
retval = set_brk(elf_bss + load_bias,
elf_brk + load_bias,
bss_prot);
nbyte = ELF_PAGEOFFSET(elf_bss);
if (nbyte) {
...
}
elf_fixed = MAP_FIXED;
}
if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
elf_flags |= elf_fixed;
} else if (loc->elf_ex.e_type == ET_DYN) { // 关于 DYN 的处理?
...
if (elf_interpreter) {
load_bias = ELF_ET_DYN_BASE;
if (current->flags & PF_RANDOMIZE)
load_bias += arch_mmap_rnd();
elf_flags |= elf_fixed;
} else
load_bias = 0;
load_bias = ELF_PAGESTART(load_bias - vaddr);
total_size = total_mapping_size(elf_phdata,
loc->elf_ex.e_phnum);
if (!total_size) {
retval = -EINVAL;
goto out_free_dentry;
}
}
// 确定了装入地址后,映射 ELF,建立用户空间虚拟地址空间
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
if (BAD_ADDR(error)) {
retval = IS_ERR((void *)error) ?
PTR_ERR((void*)error) : -EINVAL;
goto out_free_dentry;
}
...
}
// 调整各个 segment 的具体位置
loc->elf_ex.e_entry += load_bias; // 主函数入口地址
elf_bss += load_bias; // bss 起始地址
elf_brk += load_bias;
start_code += load_bias; // code 起始地址
end_code += load_bias;
start_data += load_bias; // data 起始地址
end_data += load_bias;
retval = set_brk(elf_bss, elf_brk, bss_prot);
if (retval)
goto out_free_dentry;
if (likely(elf_bss != elf_brk) && unlikely(padzero(elf_bss))) {
retval = -EFAULT; /* Nobody gets to see this, but.. */
goto out_free_dentry;
}
if (elf_interpreter) {
unsigned long interp_map_addr = 0;
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias, interp_elf_phdata);
if (!IS_ERR((void *)elf_entry)) {
/*
* load_elf_interp() returns relocation
* adjustment
*/
interp_load_addr = elf_entry;
elf_entry += loc->interp_elf_ex.e_entry;
}
reloc_func_desc = interp_load_addr;
allow_write_access(interpreter);
fput(interpreter);
kfree(elf_interpreter);
} else {
elf_entry = loc->elf_ex.e_entry;
}
kfree(interp_elf_phdata);
kfree(elf_phdata);
set_binfmt(&elf_format);
// 进一步设置堆栈,如辅助向量、环境变量,程序参数等
retval = create_elf_tables(bprm, &loc->elf_ex,
load_addr, interp_load_addr);
/* N.B. passed_fileno might not be initialized? */
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;
if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {
// 对于有 ELF randomization 的架构...
}
...
finalize_exec(bprm);
start_thread(regs, elf_entry, bprm->p); // 启动,下面是把入口地址送 PC
retval = 0;
out:
kfree(loc);
out_ret:
return retval;
/* 以下省略一堆 error cleanup */
}
// 填写目标文件的参数环境变量等必要信息
static int
create_elf_tables(struct linux_binprm *bprm, struct elfhdr *exec,
unsigned long load_addr, unsigned long interp_load_addr)
{
/* Create the ELF interpreter info */
elf_info = (elf_addr_t *)current->mm->saved_auxv;
sp = (elf_addr_t __user *)bprm->p; // sp 指向用户栈顶
/* Now, let's put argc (and argv, envp if appropriate) on the stack */
if (__put_user(argc, sp++)) // 往用户栈顶压参数数量
return -EFAULT;
/* Populate list of argv pointers back to argv strings. */
p = current->mm->arg_end = current->mm->arg_start;
while (argc-- > 0) { // 参数入栈
size_t len;
if (__put_user((elf_addr_t)p, sp++))
return -EFAULT;
len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
if (!len || len > MAX_ARG_STRLEN)
return -EINVAL;
p += len;
}
if (__put_user(0, sp++))
return -EFAULT;
current->mm->arg_end = p;
/* Populate list of envp pointers back to envp strings. */
// 环境变量入栈
current->mm->env_end = current->mm->env_start = p;
while (envc-- > 0) { // 环境变量逐一入栈
size_t len;
if (__put_user((elf_addr_t)p, sp++))
return -EFAULT;
len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
if (!len || len > MAX_ARG_STRLEN)
return -EINVAL;
p += len;
}
if (__put_user(0, sp++))
return -EFAULT;
current->mm->env_end = p;
/* Put the elf_info on the stack in the right place. */
// auxiliary vector 入栈
if (copy_to_user(sp, elf_info, ei_index * sizeof(elf_addr_t)))
return -EFAULT;
return 0;
}
在最后的 create_elf_tables
中,内核会将辅助向量(Auxiliary vector)、环境变量、参数等入栈。辅助向量是一种从内核到用户空间的信息交流机制
启动线程,定义在对应架构的 processor.h
中,例如arch/arm64/include/asm/processor.h
static inline void start_thread(struct pt_regs *regs, unsigned long pc,
unsigned long sp)
{
start_thread_common(regs, pc); // 进去了写 pc 寄存器
regs->pstate = PSR_MODE_EL0t; // 设置 pstate 寄存器
if (arm64_get_ssbd_state() != ARM64_SSBD_FORCE_ENABLE)
set_ssbs_bit(regs);
regs->sp = sp; // 设置 sp 寄存器
}
static inline void start_thread_common(struct pt_regs *regs, unsigned long pc)
{
memset(regs, 0, sizeof(*regs));
forget_syscall(regs);
regs->pc = pc; // 写 pc 辣
...
}
程序装载完,进程正常执行时, pt_regs
结构中的值就会写入 CPU 的寄存器,CPU 就从 PC 开始跑代码了