1. Linux进程管理
文章目录
1.1. 进程的概念
进程(处于执行期的程序),一段可执行的程序代码+相关的的资源
程序:完全有可能存在两个进程共同执行同一个程序
资源包括:打开的文件,挂起的信号,内核的内部数据,处理器的状态,内存地址空间,一个或多个执行线程
线程(执行线程),都拥有一个独立的程序计数器,进程栈和一组进程寄存器。
内核的调度对象是线程而不是进程。
在线程之间,可以共享虚拟内存,但是每个都拥有各自的虚拟处理器。
对于Linux而言,线程只不过是一种特殊的进程(共享某些资源)。
1.2. 进程描述符及任务结构
进程描述符: 结构体task_struct,包含一个具体进程的所有信息:打开的文件,进程的地址空间,挂起的信号,进程的状态等。<linux/sched.h>
任务队列:内核把进程的列表存放在叫做任务队列(task list)的双向循环列表中保存进程描述符的双向循环链表。
1.2.1. 进程的描述表分配和存放
Linux通过slab分配器分配task_struct结构,这样可以达到对象的复用(避免动态分配和释放所带来的资源消耗)和缓存着色。
task_strack结构的存放位置:
tread_info(<asm/thread_info.h>),在分配task_struct结构的时候,在内核栈底创建一个tread_info结构。其中内部成员task指向当前进程的task_struct。
struct thread_info {
struct task_struct *task; /* main task structure */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
};
//通过hread_info->task 获取当前进程 current
static inline struct task_struct *get_current(void)
return current_thread_info()->task;
//通过sp寄存器找到当前进程的 thread_info
static inline struct thread_info *current_thread_info(void)
register unsigned long sp asm ("sp");
return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
1.2.2. 进程上下文
一般程序运行在用户空间,当它执行了系统调用或者触发了某个异常,就会陷入内核空间,此时,称内核“代表进程执行”并处于进程上下文中。
系统调用和异常处理程序是对内核明确定义的接口,进程只有通过这些接口才能陷入内核执行——对内核的所有访问都必须通过这些接口。
1.2.3. 进程的状态
-
TASK_RUNNING :进程可执行,正在执行或者在运行队列中等待执行;进程在用户空间中执行的唯一可能的状态
-
TASK_INTERRUPTIBLE:进程正在睡眠(被阻塞),等待某些条件完成;进程可以被信号唤醒。
-
TASK_UNINTERRUPTIBLE:除了就算是接收到信号也不会被唤醒或准备投入运行外,这个状态与可打断状态相同。
-
TASK_STOPPED:进程停止执行;进程没有投入运行也不能投入运行。通常,此状态发生在SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。
-
TASK_TRACED:被其他进程跟踪的过程。
1.2.4. 进程的家族树
所有进程都是PID为1的init进程的后代,每一个进程必须有一个父进程。每一个task_struct都包含一个指向其父进程task_struct的指针parent。还包含一个称为children的子进程链表。
1.3. 进程的创建
fork通过拷贝当前进程,创建一个新进程,
exec负责读取可只执行文件,并将其载入到地址空间开始执行
1.3.1. 写时拷贝(copy-on-write)
fork函数使用写时拷贝页实现,在创建新进程的时候,不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制。
地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。
fork的开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。
1.3.2. fork
Linux通过clone系统调用实现fork,通过一系列的参数标志来指明父子进程需要共享的资源。fork,vfork,__clone库函数都根据各自的需要的参数标志调用clone。
clone去调用do_fork。
do_fork调用copy_process函数,然后让进程开始运行。
copy_process函数:(创建唯一的进程描述符)
1)调用dup_task_struct为进程创建一个内核栈、thread_info结构和task_struct。此时,子进程和父进程的描述符完全相同
2)检查当前用户所拥有的进程数目是否超出限制
3)子进程部分描述符成员清0或设置为初始值,主要是统计信息。
4)子进程的状态设置为TASK_UNINTERRUTIBLE,不会投入运行。
5)调用copy_flags以更新task_struct的flag成员。
6)调用alloc_pid为新进程分配一个有效的PID
7)根据传递给clone的参数标志,copy_process拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
8)做扫尾工作,并且返回一个指向子进程的指针
do_fork函数:在copy_process成功返回后,新创建的进程被唤醒并投入运行,内核有意选择子进程首先执行。
1.3.3. vfork
不拷贝父进程的页表项。
父进程被阻塞,直到子进程退出或者执行exec()。
1.4. 线程在Linux中的实现
在Linux中,对内核来说,并没有线程这个概念,它把所有的线程当做进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。
每个线程都有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,比如地址空间)。
对于linux来说,线程只是一种进程间共享资源的手段。
Linux,线程仅仅被视为一个与其他进程共享某些资源的进程。
1.4.1. 创建线程
clone的工作原理和fork相同,但新进程不是独立于父进程的,而可以与其共享某些资源,可以指定需要共享和复制的资源种类,例如:父进程的内存数据,打开文件或安装的信号处理程序。
在调用clone的时候需要传递一些参数标志来指明需要共享的资源
clone(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND,0)
与父进程共享地址空间,文件系统,打开的文件,信号处理函数及被阻断的信号
1.4.2. 内核线程
内核线程与普通进程的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm指针被设置为NULL)。它们只运行在内核空间,从来不切换到用户空间
1.5. 进程的终结
进程的析构是由自身引发的,发生在进程调用exit系统调用时。(C语言编译器会在main函数的返回点后面放置调用exit的代码)
大部分的工作都依靠do_exit来完成。进程所有的资源都被释放掉,进程不可运行并处于EXIT_ZOMBIE退出状态。
它现在占用的所有内存就是内核栈,thread_info结构和task_struct结构。用来向父进程提供信息。
1.5.1. 删除进程描述符
在父进程获得已终结的子进程的信息后或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。
1.5.2. 孤儿进程
如果父进程在子进程之前退出,给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。