Unix系统进程图像
Unix系统进程图像包括两部分,一部分是常驻内存图像,如proc结构;另一部分是可交换图像,如PPDA、数据区域、栈区域等,这一部分可以被交换到磁盘上。而代码段是只读的,用来存放作为程序指令的机器代码。某个程序在被同时执行多次时,各进程共享同一个代码段,代码段通过数组text[]进行管理。
进程的状态信息和控制信息等由proc结构体和user结构体管理。每个进程都会分配1组proc结构体和user结构体。proc结构体常驻内存,而user结构体有可能会被交换到硬盘上的交换区。
proc结构体
proc结构体管理着在进程状态、执行优先级等于进程相关的信息中需要被内核访问的那部分信息。
内核在选择下一个被执行的进程时,会首先检查所有的进程状态,这就需要遍历所有进程的proc结构。假如proc可以被移交到交换区中,则内核需要访问外设才能取得相应数据,这会导致花费过多时间并引发性能下降。
//proc.h
struct proc{
char p_stat; #状态
char p_flag; #进程标志
char p_pri; #执行优先级。数值越小优先级越高
char p_sig; #接收到的信号
char p_uid; #用户ID
char p_time; #在内存或交换空间存在的时间
char p_cpu; #占用CPU的累计时间
char p_nice; #用来降低执行优先级的补正系数
int p_ttyp; #正在操作进程的终端
int p_pid; #进程ID
int p_ppid; #父进程ID
int p_addr; #指向进程的可交换图像在内存或磁盘上的地址
int p_size; #可交换图像的大小
int p_wchan; #使进程进入休眠状态的原因
int *p_textp; #使用的代码段
}proc[NPROC];
/* p_stat codes */
#define SSLEEP 1 #高优先级休眠状态。执行优先级为负值
#define SWAIT 2 #低优先级休眠状态。执行优先级为0或正值
#define SRUN 3 #可执行状态
#define SIDL 4 #进程生成中
#define SZOMB 5 #僵尸状态
#define SSTOP 6 #等待被跟踪
/* p_flag codes */
#define SLOAD 01 #进程图像处于内存中
#define SSYS 02 #系统进程,不会被交换到交换空间
#define SLOCK 04 #进程调度锁。设置之后不允许进程图像被换出至交换空间
#deifne SSWAP 010 #进程图像已被交换至交换空间
#deinfe STRC 020 #处于被跟踪状态
#define SWTED 040 #在被跟踪时使用
//param.h
#define NPROC 60 #proc数组的长度,即系统可存在的最大进程数
user结构体
user结构体用来管理进程打开的文件或目录等信息。由于内核只需要当前执行进程的user结构体,因此当进程被换出至交换空间时,对应的user结构体也会被移除内存。
内核可以通过全局变量u访问执行进程的user结构体。
struct user{
int u_rsav[2]; #进程切换时用来保存r5和r6的值
int u_fsav[25];
char u_segflg; #读写文件时使用的标志变量
char u_error; #出错时用来保存错误代码
char u_uid; #实效用户ID
char u_gid; #实效组ID
char u_ruid; #实际用户ID
char u_rgid; #实际组ID
int *u_procp; #此user结构体对应的数组proc[]的元素
char *u_base; #读写文件时用于传递参数
char *u_count; #读写文件时用于传递参数
char *u_offset[2]; #读写文件时用于传递参数
int *u_cdir; #当前目录对应的数组inode[]的元素
char u_dbuf[DIRSIZ];#供函数namei()使用的临时工作变量,用来存放文件和目录名
char *u_dirp; #在读取由用户程序或内核程序传来的文件的路径名时使用
struct {
int u_ino;
char u_name[DIRSIZ];
} u_dent; #供namei()函数使用的临时工作变量
int *u_pdir; #供namei()函数存放对象文件和目录的父目录
int u_uisa[16]; #用户PAR值
int u_uisd[16]; #用户PDR值
int u_ofile[NOFILE];#由进程打开的文件
int u_arg[5]; #用户程序向系统调用传递参数使用
int u_tsize; #代码段长度
int u_dsize; #数据段长度
int u_ssize; #栈区长度
int u_sep; #
int u_qsav[2]; #在系统调用处理信号时用来保存r5和r6的当前值
int u_ssav[2]; #当进程被换出交换区时,导致user.u_rsav[]值不再有效时,用于保存r5和r6值
int u_signal[NSIG]; #用于设置收到信号后的动作
int u_utime; #用户模式下占用CPU的时间
int u_stime; #内核模式下占用CPU的时间
int u_cutime[2]; #子进程在用户模式下占用CPU的时间
int u_cstime[2]; #子进程在内核模式下占用CPU的时间
int *u_ar0; #系统调用处理中,操作用户进程的通用寄存器或PSW时使用
int u_prof[4]; #用于统计
char u_intflg; #标志变量,用于判断系统调用处理中是否发生了对信号的处理
} u;
在user结构中,我们可以看到error错误代码,这说明在同一个进程中,errno只有一份,所以对于包含errno的代码是不可重入的,在多线程编程时尤其需要注意。
可交换图像
可交换图像包括PPDA(Per Process Data Area)、数据区域、栈区域组成。
- PPDA区域包括user结构体和内核栈区域,从用户空间无法访问。内核栈区域被用作内核处理时的临时工作区域。每个进程都具有供内核模式使用的工作区域。
为什么要为每一个进程都设置一个内核栈?假设系统中所有进程共用一个全局内核栈,若某个进程通过系统调用运行在内核态时被强占,另一个进程开始运行,然后又通过系统调用进入内核态,则上一个进程的内核栈就会被破坏。所以为每一个进程分配一个内核栈是必要的。
-
数据区域:包括数据段(通常存放程序中的初始化后的全局变量和静态变量)、BSS段(通常存放程序中未初始化的全局变量和静态变量)、堆(通常用来存放程序中运行时动态分配的内存段)。长度由user.u_dsize表示。
-
栈区域:用来暂时存放函数的参数或局部数据。长度根据需要自动拓展。栈区域从虚拟地址的最高位向低位方向进行。长度由user.u_ssize表示。
虚拟内存空间
虚拟内存是计算机系统内存管理的一种技术,它使得进程认为它拥有连续可用的内存,而实际上在物理内存中。它通常被分割成多个内存碎片,还有部分暂时被交换到外部磁盘上。每个进程都有独立的虚拟内存空间。
为什么要使用虚拟内存空间?
- 使得程序能够使用以任意地址为起点的内存空间,而不需要考虑虚拟地址和物理地址之间的关系。程序员在编写程序时,不需考虑进程运行时物理内存地址的变化。
- 便于实现对内存访问的管理。如果程序能够直接访问物理内存,也就有可能访问其他进程正在使用的物理内存区域。而使用虚拟地址,通过将各个进程的虚拟地址映射到不同的物理地址,就可以保证虚拟地址空间的独立性。
- 提高内存的使用效率。在需要确保一定长度的连续内存区域时,通过将不连续的物理内存区域映射到连续的虚拟内存区域,可以提高物理内存的使用效率。
虚拟地址转换为物理地址
现代操作系统通常使用MMU(Memory Management Unit)将虚拟地址转换到物理地址。
详细参考:
参考文献:《Unix内核源码剖析》 青柳隆宏