1、进程和线程
进程就是出于执行期的程序,但进程并不仅仅局限于一段可执行代码。通常还要包括其它资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存空间地址及一个或多个执行线程,当然还包括用来存放全局变量的数据段等。
执行线程,简称线程,是在进程中活动的对象,每个线程拥有独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。
对 Linux 而言,它对线程和进程并不特别区分,线程只不过是一种特殊的进程罢了。
在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。许多进程共同分享一个处理器,但虚拟处理器给进程一种假象,让这些进程觉得自己在独享处理器。虚拟内存让进程在分配和管理内存时觉得自己拥有整个系统所有的资源。
2、进程描述符
内核把进程的列表存放在一个叫任务队列的(task_list)的双向环链中。链表中每一项都是类型为 task_struct 称为进程描述符的结构。进程描述符中数据完整的描述一个正在执行的程序:包括打开的文件,进程的地址空间,挂起的信号,进程的状态,等等。
进程描述符中的 PID 是内核用来标识一个进程的唯一ID,PID 的最大默认值一般为 32768 ,系统管理员可以修改 /proc/sys/kernel/pid_max 来提高上限。
通过 current 宏,可以查找到当前运行的进程的进程描述符。
3、进程的状态
设置进程的状态:
set_task_state(task, state);
进程上下文:
当一个程序执行了系统调用或者触发了某个异常,就陷入了内核空间,此时,我们称内核“代表进程执行”并处于进程上下文中,在此上下文中 current 宏是有效的。
4、进程创建
Linux中创建进程与其他系统有个主要区别,Linux中创建进程分2步:fork()和exec()。
- fork: 通过拷贝当前进程创建一个子进程
- exec: 读取可执行文件,将其载入到内存中运行
写时拷贝
写时拷贝是一种可以推迟甚至免除拷贝数据的技术,内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各自进程拥有各自的拷贝。
fork() 创建的流程:
- 调用dup_task_struct()为新进程分配内核栈,task_struct等,其中的内容与父进程相同。
- check新进程(进程数目是否超出上限等)
- 清理新进程的信息(比如PID置0等),使之与父进程区别开。
- 新进程状态置为 TASK_UNINTERRUPTIBLE
- 更新task_struct的flags成员。
- 调用alloc_pid()为新进程分配一个有效的PID
- 根据clone()的参数标志,拷贝或共享相应的信息
- copy_process()做一些扫尾工作并返回新进程指针
- 如果 copy_process() 函数返回成功,新创建的子进程被唤醒并且让其投入运行,内核有意让子进程先执行,但是并非总能如此。
vfork()
除了不拷贝父进程的页表项外,vfork() 系统调用和 fork() 功能相同。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或者执行 exec() 。
clone()
创建进程的fork()函数实际上最终是调用clone()函数。
创建线程和进程的步骤一样,只是最终传给clone()函数的参数不同。比如,通过一个普通的fork来创建进程,相当于:clone(SIGCHLD, 0)
创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即一个线程:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
vfork() 的实现是:clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0)
在内核中创建的内核线程与普通的进程之间还有个主要区别在于:内核线程没有独立的地址空间,它们只能在内核空间运行。
5、进程的终止
和创建进程一样,终结一个进程同样有很多步骤:
子进程上的操作(do_exit)
- 设置task_struct中的标识成员设置为PF_EXITING
- 调用del_timer_sync()删除内核定时器, 确保没有定时器在排队和运行
- 调用exit_mm()释放进程占用的mm_struct
- 调用sem__exit(),使进程离开等待IPC信号的队列
- 调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源
- 把task_struct的exit_code设置为进程的返回值
- 调用exit_notify()向父进程发送信号,并把自己的状态设为EXIT_ZOMBIE
- 切换到新进程继续执行
子进程进入EXIT_ZOMBIE之后,虽然永远不会被调度,关联的资源也释放掉了,但是它本身占用的内存还没有释放,比如创建时分配的内核栈,task_struct结构等。这些由父进程来释放。
父进程上的操作(release_task)
- 父进程受到子进程发送的exit_notify()信号后,将该子进程的进程描述符和所有进程独享的资源全部删除。
从上面的步骤可以看出,必须要确保每个子进程都有父进程,如果父进程在子进程结束之前就已经结束了会怎么样呢?
子进程在调用exit_notify()时已经考虑到了这点。如果子进程的父进程已经退出了,那么子进程在退出时,exit_notify()函数会先调用forget_original_parent(),然后再调用find_new_reaper()来寻找新的父进程。
find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程。(init进程是在linux启动时就一直存在的)