深入理解linux内核-进程和程序
进程描述符task_struct
task_struct
{
//进程基本信息
pid 进程id号
tgid 线程组id号,与线程组领头线程pid号相同 getpid()返回该值
tasks init_struct链接所有task_struct结构
run_list; //当前进程所处的运行链表
array 指向与进程相关的prio_array_t结构
real_parent当前进程的父进程,没有的话将会变成进程1(init)的描述符
parent 被执行跟踪时的跟踪父进程(ptrace)
children 链接所有子进程
sibling 兄弟进程链表
group_loader 进程组领头进程的描述符指针
signal->pgrp 进程组领头进程的PID
signal->session 登录会话领头进程的pid
ptrace_children 所有被本进程跟踪的子进程链表
ptrace_list 指向所跟踪进程其实际父进程的链表的前一个和下一个元素????
pid pids[4]; //四个结构用于查找指定 进程id、进程组id、线程组id、会话id。每个结构用于分别保存相同散列值的相同或不同pid列表
//资源限制
signal->rlim[rLIMIT_CPU...].rlim_cur; //当前资源限制
signal->rlim[rLIMIT_CPU...].rlim_max; //普通用户最大权限
//进程切换
thread_struct thread;//保存进程切换时内核硬件上下文
进程优先级
进程运行状态(可运行、可中断等待、不可中断等待、暂停、跟踪、僵死、僵死撤销)
//进程地址空间 mm_struct
//当前目录 fs_struct
//进程访问文件 files_struct
//所接收的信号 signal_struct
//相关的tty tty_struct
}
不可中断状态:驱动在进行一些不能被中断的操作,因此进程处于该状态。
暂停状态:进程收到SIGSTOP SIGTSTP SIGTTIN SIGTTOU进入暂停状态
跟踪状态:进程执行处于被debugger程序暂停的状态
僵死状态:进程执行终止,等待返回进程信息时。
僵死撤销状态:为防止多线程同时等待进程终止时的信息,信息被获取后的状态
线程描述符thread_info
thread_info与内核态堆栈放在一起占用一个(4K)或两个页(8k)(用户态堆栈不在这里)
内核态时可以通过堆栈寄存器获取当前thread_info结构的地址。
thread_info
{
cpu//当前CPU
}
prio_array_t
{
int nr_active; 链表中进程描述符的数量
unsigned long[5] bitmap; 当某个优先权链表不为空时对应位为1
struct list_head[140] queue; 140个优先权队列
}
将不同优先权的进程排入不同链表
进程组和线程组的概念
进程组:表示一个作业(job),例如 ls|sort|more三个进程处于一个进程组
进程组:进程描述符中signal->pgrp相同的所有进程处于一个进程组
线程组:进程描述符中tgid相同的所有进程处于一个线程组。
getpid() kill() _exit()对线程组整体起作用。
线程组所有成员死亡后才会产生一个信号通知线程组的领头进程的父进程
通过pid快速查找进程
为了能快速找到对应进程描述符,内核引入四个散列表(保存在pid_hash数组)
进程pid散列表
线程组tgid散列表
进程组pgrp散列表
会话session散列表
pid
{
int nr;//对应类型的pid数值
struct hlist_node pid_chain;//相同散列值但pid不同的链表 链接pid结构
struct list_head pid_list;//相同散列值相同pid的进程双向链表 链接pid结构
}
对于每一个散列表,进程描述符有一个pid数据结构对应
进程的组织
运行状态有对应链表
停止、僵死、僵死撤销 状态没有对应的链表
可中断等待和不可中断等待有多种独立的等待队列
等待队列
struct __wait_queue_head
{
spinlock_t lock; //自旋锁
struct list_head task_list;//非互斥进程从第一个位置放,互斥进程放在最后一个
}
wait_queue_t
{
unsigned int flags;//1表示等待队列是互斥资源的访问,0等待队列是非互斥资源
struct task_struct *task; //对应的进程描述符
wait_queue_func_t func;//表示等待队列的唤醒函数
struct list_head list;//所有排入等待队列的wait_queue_t
}
sleep_on类函数在一些条件不能使用:必须测试条件并且当条件还没得到验证时又紧接着让进程去睡眠????
sleep_on(wait_queue_head_t) (非互斥:一旦条件满足则所有非互斥等待都会唤醒!!!)
interruptible_sleep_on() (非互斥)
sleep_on_timeout() (非互斥)
interruptible_sleep_on_timeout() (非互斥)
prepare_to_wait() //(非互斥)需要自己调用schedule()或者schedule_timeout()
prepare_to_wait_exclusive() //(互斥)需要自己调用schedule()或者schedule_timeout()
finish_wait()
唤醒函数
wake_up 不带nr和all则只唤醒一个互斥进程
wake_up_nr nr代表唤醒互斥进程的数量
wake_up_all all代表唤醒所有互斥进程
wake_up_interruptible interruptible代表只唤醒可中断睡眠,不带该后缀唤醒两种睡眠
wake_up_interruptible_nr
wake_up_interruptible_all
wake_up_interruptible_sync sync代表如果唤醒 进程优先级更高**不会**立即执行该高优先级进程
wake_up_locked 当等待队列中的自旋锁已经被持有时使用
进程资源限制
地址空间最大数 RLIMIT_AS
内存信息转储空间大小 RLIMIT_CORE
进程使用CPU的最长时间 RLIMIT_CPU
堆大小的最大值 RLIMIT_DATA
文件大小最大值 RLIMIT_FSIZE
文件锁数量最大值 RLIMIT_LOCKS
非交换内存的最大值 RLIMIT_MEMLOCK
消息队列中的最大字节数 RLIMIT_MSGQUEUE
打开文件描述符的最大数 RLIMIT_NOFILE
用户拥有进程最大数 RLIMIT_NPROC
进程拥有页框最大数 RLIMIT_RSS
进程挂起信号的最大数 RLIMIT_SIGPENDING
栈大小的最大数 RLIMIT_STACK
进程切换
thread_struct
{
eip//进程恢复执行后需要执行的首地址(保存+加载)
esp//进程切换时内核态指针(保存+加载)
esp0//内核态初始指针(仅加载)
tls_array[3];//线程局部存储段(仅加载)
fs gs;段寄存器(保存+加载)
debugreg;调试寄存器dr0-dr3 dr6-dr7(仅加载)
io_bitmap_ptr//表示IO权限位图是否有数据
}
硬件上下文:进程恢复执行前必须装入寄存器的一组数据
硬件上下文的一部分放在TSS段中,剩余部分在内核态堆栈中
可执行上下文:进程执行时需要的所有信息
可执行上下文包含硬件上下文
进程切换只发生在内核态,在切换之前,用户态进程使用的所有寄存器内容已保存在内核态堆栈上(包括用户态堆栈信息)
任务状态段(TSS):保存内核态堆栈地址,检查in out指令执行时是否有IO许可权,linux中每个CPU只有一个TSS段
thread_struct:在任务描述符中在进程切换时保存内核硬件上下文(包含大部分CPU寄存器,不包括eax、ebx这些通用寄存器(这些在内核堆栈中))
switch_to函数执行步骤(主要堆栈切换、执行指针切换)
1.将prev和next分别存入eax和edx防止堆栈切换导致指针变化
2.保存需要保存的寄存器信息eflag和ebp(pushfl ;pushl ebp)
3.将原堆栈指针esp保存在prev的结构中
4.从next中将新堆栈指针写到esp中
5.将原进程恢复后需要执行的地址存入prev中
6.将新进程next的eip压入到新进程的栈中(栈已经切换完成)
7.调用__switch_to,该函数引用eax和edx获得两个进程的其他硬件上下文并进行切换(FPU、MMX、XMM寄存器)
8.恢复堆栈中的寄存器eflag和ebp
9.从eax寄存器拷贝到last变量(本次进程切换被切出的进程描述符地址)
__switch_to函数执行步骤(引用eax和edx获得本次切出和切入进程的进程描述符,栈顶保存了需要恢复执行的地址)
1.__unlazy_fpu()保存切出进程的FPU、MMX和XMM寄存器
2.获得cpu下标(内核栈切换完毕,通过栈指针找到thread_info,内部的cpu字段)
3.将thread.esp0装入对应本地CPU的TSS的esp0字段(用户态切内核态后内核态的堆栈指针)
4.装入线程局部存储段(TLS)thread.tls_array[0-2]
5.保存原线程的fs、gs
6.加载新线程的fs、gs(实际可能会产生无效的段寄存器值异常,并触发修正!!!!)
7.加载6个调试寄存器dr0-dr3 dr6-dr7
8.根据io_bitmap_ptr,懒惰模式设置iobitmap,需要时会产生异常,然后更新。
9.返回值为eax被切出的进程描述符,返回的执行地址为栈顶的标号1地址
考虑进程A的切出和切入
prev、next、last为局部变量(保存在堆栈中)
last用于返回被切出的进程描述符
切出A 切入A
进程A切换为进程B 进程C切换成进程A
A B C A
prev=A => prev=B prev=C => prev=A
next=B => next=other next=A => next=B
eax=prev => last=eax=A eac=prev=C last=eax=C
第一列和第四列能看出,切出是的堆栈状态和切入时相同
创建进程
轻量级进程:共享页表、打开文件表、信号处理。
vfork:使用clone实现,指定SIG_CHLD信号 flag为CLONE_VM CLONE_VFORK,堆栈为当前堆栈
//创建的子进程和父进程共享内存地址空间,父进程在子进程退出或运行一个新的程序前阻塞。clone
fork:使用clone实现,子进程结束给父进程发送SIG_CHLD信号,clone标志为0,堆栈为当前堆栈(依赖写时复制机制可以同时运行)
clone:(需要传递进程函数、参数、新的堆栈、线程局部存储段(TLS)、ptid、ctid)
CLONE_VM 共享页表
CLONE_FS 共享根目录和当前工作目录umask、 不能和CLONE_NEWNS同时设置!!!
CLONE_FILES 共享打开文件
CLONE_SIGHAND 共享信号处理 必须共享内存描述符CLONE_VM!!!
CLONE_PTRACE 共享被调试状态、
CLONE_VFORK VFORK?????
CLONE_PARENT 共享父进程、
CLONE_THREAD 共享线程组 必须共享信号CLONE_SIGHAND!!!
CLONE_NEWNS 新建命名空间 不能和CLONE_FS同时设置!!!
CLONE_SYSVSEM 共享IPC取消信号量操作、
CLONE_SETTLS 新建TLS(局部存储段)
CLONE_PARENT_SETTID 将子进程PID返回给父进程ptid、?????????
CLONE_CHILD_CLEARTID 子进程退出或执行新程序时清除指定变量ctid、?????
CLONE_UNTRACED 禁止内核线程跟踪进程、
CLONE_CHILD_SETTID 将字进程PID返回给子进程ptid、??????????
CLONE_STOPPED 子进程默认停止状态
内核线程
使用dofork实现,CLONE_VM、CLONE_UNTRACED
进程0
从无到有创建的内核线程,初始化内核需要的所有数据结构,每个CPU都会启动一个进程用于空闲时运行,启动进程1
进程1
完成内核初始化,调用execve系统调用装入可执行程序init,变为普通进程,拥有自己的每进程内核数据结构???。