本系列学习笔记基本上是博主的《 openEuler 操作系统》读书笔记,中间插入一些自己查的资料以及翻到的感觉有用的源代码
默认架构为 ARM
程序及其加载执行
类 UNIX 的二进制程序一般为 ELF 格式,一个【逻辑意义上作为整体的程序】会被按照内容类型划分为数个 Segment 进行存储。主要的 Segment 有 :.text ,存机器指令序列;.data 存可变的全局变量及静态局部变量;.rodata 存只读数据、常量;.bss 存未初始化的全局变量。
加载,是操作系统将 ELF 读入内存的过程。首先检查 ELF 头,确定是否可以运行。然后读端头表,得到各个段的信息,为需要装入内存的段分配空间,然后把这些段加载到内存中。
执行。程序入口地址存在 ELF 头中,OS 读到该地址后到 .text 找到入口,将入口地址赋给 PC ,程序获得 CPU 控制权。
运行过程中,PC 保存即将执行的指令地址,LR(链接寄存器)保存函数调用返回后的下一条指令地址。
每个运行中的函数都拥有一个栈帧,其为函数的每次调用构建独立的上下文。当函数调用新的函数时,新的函数会被分配新的栈帧。函数运行结束后栈帧会被弹出,系统从下面的栈帧(即发起对刚刚结束的函数调用的函数的栈帧)中取出之前保存的 FP、LR 值,继续运行
进程的描述
操作系统使用 PCB (Process Control Block,进程控制块)对进程进行描述,操作系统通过 PCB 感知进程。
PCB
PCB 定义在 include/linux/sched.h 的 struct task_struct,是一个六百二十多行的结构体。
启动一个程序时,操作系统先创建 PCB ,然后根据其中的信息对进程进行管理和控制,程序运行后系统释放 PCB 。其中的主要信息包含描述信息、控制信息、 CPU 上下文、资源管理信息
描述信息
进程标识符,OS 用它来标记每个进程;
用户标识号,区分某个进程属于哪个用户
kuid_t loginuid;
unsigned int sessionid;
家族关系,表明该进程与其父进程、祖先进程、子进程、兄弟进程等的关系
/*
* Pointers to the (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/
/* 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 children:
*/
struct list_head children;
struct list_head sibling;
struct task_struct *group_leader;
real_parent 是创建该进程的进程,而 parent 是响应信号相关的父进程——比如 SIGCHLD 就会被发送给 parent。这么设计是因为:有些情况下真父进程可能先终止,这样如 init 这样的其他进程就会成为新的父进程,但不会改变真父进程的值。
控制信息
进程状态(就绪、阻塞、运行、终止)
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
/* 相关宏定义如下 */
/* Used in tsk->state: */
#define TASK_RUNNING 0x0000
#define TASK_INTERRUPTIBLE 0x0001
#define TASK_UNINTERRUPTIBLE 0x0002
#define __TASK_STOPPED 0x0004
#define __TASK_TRACED 0x0008
#define TASK_PARKED 0x0040
#define TASK_DEAD 0x0080
#define TASK_WAKEKILL 0x0100
#define TASK_WAKING 0x0200
#define TASK_NOLOAD 0x0400
#define TASK_NEW 0x0800
#define TASK_STATE_MAX 0x1000
进程优先级
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
静态优先级:进程启动时指定,值越小越高。一般不用,但可用系统调用 nice() 修改;
动态优先级:可以因调度策略的改变而临时变动;
实时:有限程度仅与实时优先级有关,值越小越高。实时进程调度总优于普通进程
记账信息:记录进程占有、利用资源的情况,调度以这些信息为依据进行(如剥夺等),如时间方面、上下文切换方面的:
u64 utime;
u64 stime;
#ifdef CONFIG_ARCH_HAS_SCALED_CPUTIME
u64 utimescaled;
u64 stimescaled;
#endif
u64 gtime;
struct prev_cputime prev_cputime;
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
struct vtime vtime;
#endif
#ifdef CONFIG_NO_HZ_FULL
atomic_t tick_dep_mask;
#endif
/* Context switch counts: */
unsigned long nvcsw;
unsigned long nivcsw;
/* Monotonic time in nsecs: */
u64 start_time;
/* Boot based time in nsecs: */
u64 real_start_time;
CPU 上下文
CPU 上下文指某时刻 CPU 各寄存器中的值,用于进程切换时保存状态。其保存于 task_struct->thread_struct->cpu_context
从下面的代码中可以看到,cpu_context 就是各个寄存器中的值的集合
/* arch/arm64/include/asm/processor.h */
struct cpu_context {
unsigned long x19;
unsigned long x20;
unsigned long x21;
unsigned long x22;
unsigned long x23;
unsigned long x24;
unsigned long x25;
unsigned long x26;
unsigned long x27;
unsigned long x28;
unsigned long fp;
unsigned long sp;
unsigned long pc;
};
struct thread_struct {
struct cpu_context cpu_context; /* cpu context */
/* 省略其他变量,thread_struct 结构体中包含所有与 CPU 相关的状态信息 */
};
资源管理信息
这部分信息在 PCB 中占比最大,包含存储器、文件系统、使用 IO 设备的信息等,主要与内存和文件相关。
void *stack; // 指向进程内核栈,内核栈是进程在内核态用的栈,在内核空间
// 进程的用户空间描述符
struct mm_struct *mm;
struct mm_struct *active_mm;
/* Filesystem information: */
struct fs_struct *fs; // 与进程相关的文件系统
/* Open file information: */
struct files_struct *files; // 进程正打开的文件列表
// 内存描述符,定义在 include/linux/mm_types.h
struct mm_struct {
spinlock_t arg_lock; /* protect the below fields,自旋锁 */
// 描述内存中各个段的起始位置,包括栈、映射段、堆、BSS段、数据段、代码段
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
//......
};
// 进程关联的文件系统信息,include/linux/fs_struct.h
struct fs_struct {
int users; // 该结构的引用用户数
spinlock_t lock;
seqcount_t seq;
int umask;
int in_exec;
struct path root, pwd; // 根与当前目录
} __randomize_layout;
/* include/linux/fdtable.h
* Open file table structure
*/
struct files_struct {
/*
* read mostly part
*/
atomic_t count; // 引用计数
struct fdtable __rcu *fdt; // 默认指向 fdt,可用于动态申请内存
struct fdtable fdtab; // 为 fdt 提供初始值
// fdt、fdtable 是管理文件描述符用的
// ...
};
// path. include/linux/path
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
} __randomize_layout;
struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
unsigned long *full_fds_bits;
struct rcu_head rcu;
};
进程的状态
进程当前状态由 PCB 中的状态值描述。这里的状态就是运行、就绪、阻塞、终止(僵尸、死亡)
/* Used in tsk->state: */
#define TASK_RUNNING 0x0000
#define TASK_INTERRUPTIBLE 0x0001
#define TASK_UNINTERRUPTIBLE 0x0002
#define __TASK_STOPPED 0x0004
#define __TASK_TRACED 0x0008
/* Used in tsk->exit_state: */
#define EXIT_DEAD 0x0010
#define EXIT_ZOMBIE 0x0020
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_PARKED 0x0040
#define TASK_DEAD 0x0080
#define TASK_WAKEKILL 0x0100
#define TASK_WAKING 0x0200
#define TASK_NOLOAD 0x0400
#define TASK_NEW 0x0800
#define TASK_STATE_MAX 0x1000
就绪:进程位于运行队列中,已获得 CPU 外的所有资源,等待 OS 选中占用 CPU
运行:当前进程主动放弃或被抢占,则转为就绪;当前进程等待资源或事件时,转为阻塞;运行结束,进入终止状态(僵尸/等待)
阻塞:通常是等待外部事件(如 I/O ),需要等来了才就绪。但可被系统调用、信号等唤醒(阻塞分轻度中度深度,不同的程度需要的条件不同)
终止:
僵尸:父进程未回收进程及其占用的资源(包括 PCB )。若父进程先结束,init 会成为子进程的 parent ,待结束后回收资源
死亡:结束后由父进程回收资源