一、进程
进程就是出于执行期的程序,但进程并不仅仅局限于一段可执行的程序代码。通常进程还要包含其他资源,像打开的文件,挂起的信号,内核的内部数据,处理器状态,一个或多个具有内存映射的内存地址空间以及一个或多个执行线程,存放全局变量的数据段。
执行线程,进程线程,是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调用的对象是线程不是进程。程序本身不是进程,进程是处于执行期的程序以及相关的资源的总称。完全可能存在两个或多个不同的进程执行的是同一个程序。并且两个或两个以上并存的in成还可以共享许多诸如打开的文件、地址空间之类的资源。
二、进程描述符及任务结构
内核把进程的列表存放在叫做任务队列的双向循环链表中。链表中的每一个都是类型为task_struct、称为进程描述符的结构,结构定义在<linux/sched.h>文件中。进程描述符中包含一个具体进程的所有信息。此结构体相当大,但包含了内核管理一个进程所需的所有信息。进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号、进程的状态......
1)分配进程描述符
linux通过slab分配器分配task_struct结构,现在用slab分配器动态生成task_struct,只需在栈底或栈顶创建一个新的结构struct thread_info.
struct thread_info{
struct task_struct *task;
struct exec_domain *exec_domain;
_u32 flags;
_u32 status;
_u32 cpu;
int preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
void *sysenter_return;
int uaccess_err;
};
每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针。
进程描述符中的state域描述了进程的当前状态。系统中的每个进程都必然处于五种进程状态的一种。
- TASK_RUNNING (运行):无论进程是否正在占用 CPU ,只要具备运行条件,都处于该状态。 Linux 把处于该状态的所有 PCB 组织成一个可运行队列 run_queue ,调度程序从这个队列中选择进程运行。事实上, Linux 是将就绪态和运行态合并为了一种状态。
- TASK_INTERRUPTIBLE(可中断)--Linux 将阻塞态划分成 TASK_INTERRUPTIBLE 、 TASK_UNINTERRUPTIBLE 、 TASK_STOPPED 三种不同的状态。处于 TASK_INTERRUPTIBLE 状态的进程在资源有效时被唤醒,也可以通过信号或定时中断唤醒。
- TASK_UNINTERRUPTIBLE(不可中断)--另一种阻塞状态,处于该状态的进程只有当资源有效时被唤醒,不能通过信号或定时中断唤醒。
- _TASK_STOPPED(停止)-第三种阻塞状态,进程被停止,通常是通过接收一个信号(SIGSTOP, SIGTSTP, SIGTTIN, or SIGTTOU )。正在被调试的进程可能处于停止状态。处于该状态的进程只能通过其他进程的信号才能唤醒。
- TASK_ZOMBILE (僵死)进程已结束但尚未消亡,已经释放了大部分资源, PCB 仍未被释放,在task数据中仍然保留task_struct结构。一旦父进程调用了wait4(),进程描述符就会被释放。
我们通常所说的进程上下文是指用户程序通过系统调用陷入内核之间的转换,此时,我们称内核“代表进程执行”并处于进程上下文中。
系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行--对内核的所有访问都必须通过这些接口。
系统中的每个进程必有一个父进程,每个进程也可以拥有零个或者多个子进程。拥有同一个父进程的所有进程称为兄弟。进程间的关系存放在进程描述符中。每个task_struct都包含一个指向其父进程task_struct、叫做parent的指针,还包含一个称为children的子进程链表。
可以通过继承体系从系统的任何一个进程出发找到任意知道你个的其他进程。但大多数时候,只需要通过简单的重复方式就可以遍历系统中的所有进程,因为任务队列本例就是一个双向的循环链表。
三、进程创建
许多其他操作系统都提供了产生进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。但是linux采用了将上述步骤分解到两个单独的函数中执行:fork()和exec()。潜在通过拷贝当前进程创建一个子进程。子进程与父进程的区别在于PID、PPID和某些资源和统计量。exec()函数负责读取可执行文件并将其载入地址空间开始运行。
fork()使用写时拷贝页实现,写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。
fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。
1)fork()
Linux通过clone()系统调用实现fork().这个调用通过一系列的参数标志来指明父、子进程需要共享的资源。通过调用clone(),然后clone()去调用do_fork(),这个函数在完成了部分工作后,调用copy_process()函数:
- 调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,父子进程描述符完全相同
- 检查并确保新创建这个子进程后,当前用户所拥有的进程数据没有超过给它分配的资源限制。
- 子进程着手使自己与父进程区别开来。
- 子进程状态被设置为TASK_UNINTERRUPTIBLE,保证不会被投入运行。
- copy_process()调用copy_flags()以更新task_strut
的flags成员。 - 调用alloc_pid()为新进程分配一个有效的pid
- 根据传递个clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息.......
四、线程在linux中的实现
从内核的角度看,linux没有线程这个概念,linux把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或定义特别的数据结构来表征线程。线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,如地址空间)
但是关于线程创建的问题,在现在操作系统里,已经有了专门的函数来创建线程,pthread_create(),这个函数的详细讲解不在这里。
五、进程终结
进程的终结一般由自身完成,发生在进程调用exit()系统调用时,既可现实地调用这个系统调用,也可能隐式地从某个主程序的主函数返回。接收到某些信号,都可能终结进程。终结的大部分任务依靠do_exit()来完成:
- 将tast_struct中的标志成员设置为PF_EXITING.
- 调用del_timer_sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行。
在调用了do_exit()之后,尽管线程已经僵死,但是系统还保留了它的进程描述符。这么做可以让系统有办法在子进程终结后仍能获得它的信息。因此,进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。wait()函数的标准动作时挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的PID。此外,调用该函数时提供的指针会包含子函数退出时的退出代码。
如果子进程在父进程结束之前接受,那么需要找到一个新的父进程。