1. 进程管理
进程包括可执行代码,打开的文件,挂起的信号,处理器状态,内核内部数据,地址空间,拥有的线程,存储全局变量的数据段。
linux对进程和线程不特别区分
linux中通常用fork()系统调用复制一个进程创建一个新的进程,分别为父进程和子进程。之后父子进程同时执行。
最后通过exit()系统调用退出执行。父进程可以通过wait4()查询子进程是否结束。父进程调用wait()、wait4()或waitid()之前,退出执行的进程设置为僵死状态。
1.1 进程描述符及任务结构
Struct task_struct Process descriptor;它包括了描述进程的完整信息。
任务队列是装了Process descriptor的task_struct结构体。
1.1.1.分配进程描述符
通过slab分配器分配task_struct结构。实现对象复用和缓存着色。
创建时只需在进程内核栈栈底创建thread_info(包含一个指向task_struct的指针)。
1.1.2.Process descriptor的存放
Task_struct中定义了pid_t pid;用于存放唯一进程标识值。
通过current_thread_info()->task返回当前进程的task_struct地址。
PowerPC的寄存器多,所以可以直接把task_struct的地址存于寄存器中,而x86只有通过current_thread_info()函数完成,还是比较快。
1.1.3.进程状态
Task_struct中的state保存了进程了五种基本状态:Task_running、Task_interruptible、Task_uninteruptible、Task_zomble、Task_stop
其中就绪状态分为了可以接收信号而被唤醒的task_interruptible状态和不可被信号唤醒的task_uninterruptible状态。Task_stop通常发生在接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOUT信号的时候。
1.1.4.设置当前进程状态
Set_task_state(tase,state)
Set_current_state(state);等价于set_task_state(current,state)
1.1.5.进程上下文
系统调用和异常处理程序是内核明确定义的接口,只有通过他们才能陷入内核执行,访问内核数据。
陷入内核执行后,就称内核代表进程执行,并处于进程上下文中,Current宏此时有用。若调度器没有做出调整,内核执行完后恢复用户空间执行。
1.1.6. 进程家族树
所有进程都是PID为的init进程的后代
获得父进程:struct task_struct *my_parent=currnet->parent;
依次访问子进程:list_for_each(list,$current->children){
task=list_entry(list,struct task_struct,sibling);}
获取链表下一个或上一个进程:list_entry(task->task.next,struct task_struct,tasks)
list_entry(task->task.prev,struct task_struct,tasks)
遍历进程:struct task_struct *task
for_each_process(task){
printf(“%s:[%d]/n”,task->comm,task->pid);//打印每个进程的名称和PID
1.2 进程创建:
Fork()负责拷贝当前进程创建新进程。Exec()负责读取可执行文件载入地址空间运行。
1.2.1.写时拷贝
Fork()只有在需要写入时才进行资源的复制,在此之前子进程只读共享父进程资源。
注意:只调用fork时父子进程共享进程地址空间,只要谁对数据进行了写操作,就会新建地址空间,并且拷贝其他数据,至此,两个进程有各自独立的拷贝。如果立即调用了ecec族函数,就会立即创建地址空间,载入新的可执行文件,至此,两个进程有了各自独立的拷贝。
1.2.2.Fork()
Fork(),vfork(),__clone都是通过调用不同参数的clone()系统调用实现的,这些参数决定了父子进程之间需要共享的资源。然后通clone()调用do_fork(),do_fork()调用copy_process()完成大部分创建工作。
Copy_process()的过程:
1、 调用dup_task_struct()为新进程创建一个内核栈,尾加thread_info(),创建task_struct,值完全和父进程相同。注意这些东西都是要新建并复制的,并不能共享,所谓的写时拷贝不是指的它们。
2、 出错检查,看进程数目有没有超出限制
3、 区分父子进程,清理或者设置某些task_struct成员
4、 设当前进程为不可中断的睡眠状态
5、 调用copy_flag(),清零PF_SUPERPRIV表明新进程不再具有超级用户权限,设置PF_FORKNOEXEC表明新进程还未执行ecec()
6、 get_pid()获取一个PID
7、 根据clone的参数标志,复制不应共享的东西。例如要新建一个线程,clone就会带CLONE_VM CLONE_FS CLONE_FILES CLONE_SIGHAND标志指明要共享的东西,这些东西就不会被复制。创建普通fork实际上的标志是SIGCHLD,vfork的标志是CLONE_VFORK CLONE_VM SIGCHLD.
8、 父子进程平分剩余时间片
9、 扫尾并返回指向子进程的指针
返回do_fork()有意让子进程先执行,因为很可能exec马上就会执行,这样就避免了父进程的写操作引发了写时拷贝。
1.2.3.vfork
所有都和fork()一样,除了不拷贝页表项,父进程一直阻塞,直到子进程退出或者调用exec,子进程不能向地址空间写入。
1.3 线程在linux中的实现
线程只是一个能和其他进程共享地址空间,文件系统信息,文件描述符,信号处理函数的特殊进程。而创建过程和普通进程创建过程类似,只是clone()的参数会表明它们能够共享的东西。
附共享标志表:
1.3.1.内核线程
内核在后台执行的操作,始终运行于内核空间。
由其他内核线程调用:
int kernel_thread(int (*fn)(void *),void *arg,unsigned log flags)
返回时父线程退出,*fn带*arg参数开始执行。
通常它会一直执行,直到系统退出,需要时唤醒,完成时休眠。
1.4 进程终结
主动结束:显示或者隐式调用exit()
被动结束:信号或者异常。
无论如何都会调用do_exit()完成:
1、 设置task_struct中的PF_EXITING
2、 删除内核定时器,确保没有定时器在排队
3、 如果要进程记账,调用acct_process()
4、 调用_exit_mm()放弃mm_struct.【如果没有被共享就彻底放弃】
5、 调用_exit_sem()【如果进程在等待IPC信号,离开队列】
6、 调用_exit_files() _exit_fs() _exit_namespace() _exit_sighand()递减文件描述符,文件系统数据,进程命名空间,信号处理函数计数器。【如果已经为零,则可以释放】
7、 执行规定的退出动作或者置task_struct中的退出码给exit()
8、 调用exit_notify()通知父进程,该状态为TASK_ZOMBIE
9、 调用schedule()切换到其他进程
此时该进程只剩内核栈,thread_info和task_struct了,父进程处理后释放
1.4.1.删除进程描述符
父进程已经获取子进程结束信息或者通知内核不需要它们时,通过release_task()来释放task_struct
Release_task()完成的工作:
1.4.2.孤儿进程造成的进退维谷
如果父进程在子进程退出之前退出,则所有子进程将认同组的其他进程为父,如果没有,就认init为父