进程:
进程就是处于执行期的程序以及它包含的资源总和。
线程是进程中的活动对象,每个线程拥有一个独立的程序计数器、进程栈和一组进程寄存器。
内核调度的是线程,而不是进程。
进程描述符:
内核的进程描述符为task_struct结构体,定义在<linux/sched.h>,进程描述符包含了一个进程的所有信息。包括:进程标识符、进程当前状态、栈地址空间、内存地址空间、文件系统、打开的文件、信号量等。
内核把进程的列表存放在叫做任务列表(task list)的双向循环链表,链表中每一项都是类型为task_struct的进程描述符。
进程描述符在内存的中存放位置比较有特点,由于系统需要频繁的获取当前进程描述符的地址,为提高效率,linux设置了current宏。
Linux在内核栈的末端存放一个特殊的结构体thread_info,在thread_info中的task存放着task_struck的位置,于是就能找到进程描述符。
进程状态:
task_struck中的state描述进程的状态
- TASK_RUNNING(运行):进程正在执行或者在等待队列中等待执行
- TASK_INTERRUPTIBLE(可中断):进程正在睡眠(就是被阻塞)等待某些条件达成,条件达成后内核就会把进程状态设置为运行,处在这个状态的进程可能会收到信号而提前被唤醒
- TASK_UNINTERRUPBLE(不可中断):在等待的过程中对信号不作响应,较少使用
- _TASK_TRACED:被其他进程跟踪的进程
- _TASK_STOPPED:停止执行,通常发生在进程收到SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU等信号后进入该状态
盗一张图来说明一下进程状态转换的过程:
设置当前进程的状态:
set_task_state(task,state);
必要时设置内存屏障来强制其它处理器重新排序(SMP)
进程上下文
应用程序一般在用户空间执行,当执行系统调用时或者触发某个异常,就会陷入内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。系统调用和异常处理程序是对内核明确定义的接口,进程只有通过这些接口才能陷入内核执行—-对内核的所有访问都必须通过这些接口。
进程上下文和中断上下文是操作系统中很重要的两个概念,不太好理解。处理器总处于以下三种状态之一:
- 内核态,运行于进程上下文,内核代表进程运行于内核空间
- 内核态,运行于中断上下文,内核代表硬件运行于内核空间
- 用户态,运行于用户空间
用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递 很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存 器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。
进程家族树
Linux进程之间存在一个继承关系,所有进程都是PID为1的init进程的后代。系统中每个进程必有一个父进程,每个进程可以拥有0个或过个子进程。
进程间的关系存放在进程描述符中(task_struct),每个进程描述符都包含一个指向父进程task_struct的指针parent,还包含一个称为children的子进程链表。
获取某个进程的父进程的描述符: struct task_struct *my_parent = current->parent
依次访问子进程:
struct task_struct *task;
struct list_head *list;
list_for_each(list,¤t->children)
{
task = list_entry(list, struct task_struct, sibling);
}
解释:list_head_each实际上就是一个for循环,用传入的list_head结构的指针作为循环变量,对children链表进行遍历,就实现了读取所有的子进程;
然后list_entry()就是获取list_head链表节点所在的整个结构体的指针,也就是根据list_head获取子进程的task_struct。
任务队列task list也是双向列表,对于给定进程就能获取上一个和下一个进程:
list_enty(task->tasks.next,struct task_struct, tasks);
list_enty(task->tasks.prev,struct task_struct, tasks);
有一个专门的宏用来依次访问整个任务队列:
for_each_process(task);
struct task_struct *task;
for_each_process(task)
{
//打印每个进程的名称和pid
printk("%s [%d] \n", task->comm, task->pid);
}
创建进程
Linux使用两个函数进行进程的创建和执行:fork()和exec()。fork()是拷贝当前进程创建一个子进程,子进程与父进程的区别在于PID、PPID和某些资源和统计量(如挂起的信号,没必要继承)。exec()负责将可执行程序载入地址空间开始运行。
fork()使用写时拷贝(copy on write),内核在fork进程时不复制整个进程地址空间,而是共享一个拷贝,当需要写入时才进行复制。比如fork之后立即执行exec,就无需复制了。fork的实际开销就只有复制父进程的页表以及给子进程创建一个进程描述符。
vfork()除了不拷贝父进程的也表外,与fork()功能相同。vfork保证子进程先运行,在调用exec 或exit 之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。实际上现在fork也保证子进程先运行。
线程在Linux中的实现
在Linux内核的角度并没有线程的概念,都当做进程来实现。线程仅仅被视为一个与其他进程共享某些资源的进程。对Linux来说,线程只是一种进程间共享资源的手段。
创建线程,线程的创建和进程的创建相同,同样是调用clone()函数,只不过创建线程需要多传递几个参数来表明共享的资源:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
//参数分别表明父子俩共享地址空间、文件系统资源、文件描述符、信号处理程序,其他与创建进程相同
内核线程:内核线程和普通的进程间的区别在于,内核线程没有独立的地址空间,只能在内核空间运行,不能切换到用户空间。
进程终结
一个进程终结时,内核必须释放它占有的所有资源,并通知其父进程。
进程终止的方式有很多种,进程的析构发生在调用exit()之后。当进程接收到不能处理也不能忽略的信号或异常时,也可能被动的终结。不管怎么终结,该任务大部分靠do_exit()完成:
1.将tast_struct中的标志成员设置为PF_EXITING.
2.如果BSD的进程记账功能是开启的,要调用acct_process来输出记账信息。
3.调用__exit_mm()函数放弃进程占用的mm_struct,如果没有别的进程使用它们即没被共享,就彻底释放它们。
4.调用sem_exit()函数。如果进程排队等候IPC信号,它则离开队列。
5.调用__exit_files(), __exit_fs(), __exit_namespace()和exit_sighand()以分别递减文件描述符,文件系统数据,进程
名字空间和信号处理函数的引用计数。当引用计数的值为0时,就代表没有进程在使用这些资源,此时就释放。
6.把存放在task_struct的exit_code成员中的任务退出代码置为exit()提供的代码中,或者去完成任何其他由内核机制
制定的退出动作。
7.调用exit_notify()向父进程发送信号,将子进程的父进程重新设置为线程组中的其他线程或init进程,并把进程状态
设为TASK_ZOMBIE.
8.最后,调用schedule()切换到其他进程。
经过上述过程,进程相关资源释放掉,进程进入EXIT_ZOMBIE状态。现在占用的资源就是内核栈、thread_info结构、task_struct结构。现在进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者告知内核那是无关信息后,子进程的task_struct结构才会被释放。
wait()函数族都是通过系统调用wait4()来实现的。它的动作就是挂起调用它的进程,直到其中的一个子进程退出,此时函数返回该子进程的PID。最终释放进程描述符时会调用release_task(),完成以下工作:
1.调用free_uid()来减少该进程拥有者的进程使用计数。
2.调用unhash_process()从pidhash上删除该进程,同时也要从task_list中删除该进程。
3.如果这个进程正在被ptrace追踪,将追踪进程的父进程重设为其最初的父进程并将它从ptrace_list上删除。
4.最后,调用put_task_struct释放进程内核栈和thread_info结构所占的页,并释放task_struct所占的slab高速缓存.
至此,进程描述符和所有进程资源都释放掉了。
孤儿进程:
如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程在退出时永远处于僵死状态,占用了内存。解决办法就是给子进程在当前线程组内找一个线程作为父亲,如果不行就让init做父进程。