寒假的时候就看了两本书,一本是<<Linux 内核设计与实现>>,另一本就是<<可爱的python>>。两本书都只看了一半,主要是没有全心全意的去看,所以进度有点点慢。不过除了主观的原因,还有就是内核的那本书实在是有点难度,很多东西也不是那么简单能了解的,要理解书上的资料,又要去看看代码的实现,虽然只是先看看一个大概的流程,可是也是很有难度额感觉。
我想的话如果要讲进程这一章的话我可以分为几个部分来了解,那样我觉得条例也比较清楚,理解也比较好理解:
- 什么是进程,什么是线程,它们有什么联系,并且他们的功能有是什么?
我觉得进程就是正在运行的可执行代码,当然这不是完整的解释,但是我觉得这应该是最简单的解释。可执行的程序其实就是我们写的程序,然后最后经过编译器的编译就会产生可执行程序(比如.exe,.out等)。所以进程就是这些文件的运行状态。
完整的书上解释是:进程是处于执行期间的程序并且包含了其他的资源,比如打开的文件,信号量,处理器状态,内存状态等一系列的资源。进程是操作系统资源分配的基本单位,但它不是运行和调度的基本单位。
线程是进程中的组成部分,一个进程可以只有一个线程,也可以有好进程线程组成。线程与线程之间是分配了同一个进程的资源(比如内存地址之类的),但是它们有独立的程序计数器,进程栈和一组进程寄存器。线程是kernel调度的基本单位,也是运行的基本单位。
它们的作用就是给内核提供了两种机制:虚拟处理器和虚拟内存。虚拟处理器让进程以为自己霸占了cpu,整个cpu都是在给这个进程运行。虚拟内存就是让进程以为自己霸占了所有的内存空间,这样能让计算机运行更多的进程.
- 在linux kernel中线程与进程之间的区别:
Linux kernel对于进程和线程其实没有本质的区别,也就是说Linux并没有对线程作出了特别的对待。不像很多的系统一样对提供了一种特别的机制去支持线程,而Linux kernel不区分线程和进程。这可以根据linux中进程与线程的创建过程就可以很清楚的知道他们之间的区别.
- 在linux中进程是怎么样创建的,和进程是怎么样终止的。这个过程其实就是开始与结束,能弄懂这个过程就可以更加好的去了解进程,俗话说的好,你要了解他,就必须了解他的历史,这样你才能有逻辑的去解释现在的他为什么会这样。
在讲这两个状态的时候,先提出在kernel中一个很重要的结构体,struct task_struct,这是一个很重要很重要的结构体,它叫做进程描述符。它的结构体的成员很多很多,有进程id,进程状态,进程的子进程list,还有优先级等等,一切关于进程的描述都在这个结构体中,这个文件在<linux/sched.h>文件中。这个结构体重要在于对于进程的操作很多都取决这个结构体中的某一些成员,而且kernel还维护了一个task_struct list,可以通过一个task_struct就可以找到任何一个进程。
为什么它会被命名为task_struct呢?因为在Linux中进程也就是task,就是说task=进程,所以多进程也就是多任务。
在task_struct有一个成员变量就是state,它表示的是当前进程处于什么样的状态。Linux kernel关于状态值有5种值。
1. TASK_RUNNING:表示进程可执行状态,如果与操作系统原理相对应的话,那就是就绪状态和执行状态.因为它既表示执行状态又表示就绪状态
2.TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE:可以认为是阻塞状态,进程已经某种原因要等待某的事件,而进入阻塞状态。这两个状态的区别就是TASK_INTERRUPTIBLE是可以被信号唤醒的,而TASK_UNINTERRUPTIBLE是不行的。
3.TASK_ZOMBIE:僵死状态,这个状态会出现在子进程已经结束,但是父进程没有用wait的系列函数调用,这个时候子进程就是僵死状态.
4.TASK_STOPPED:停止状态,主要是进程接受了SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号以后就会使进程进入这样的状态
接下来就要开始介绍linux进程创建的过程了:
Linux创建进程和其他的系统创建进程不一样,很多系统都是通过先创建一个地址空间,然后将执行可执行文件,这样就创建了一个进程,但是linux创建进程是通过fork和exec来进行的,通过fork创建出子进程,然后从子进程中调用exec来调用可执行文件。这就是简单的进程创建的过程。
fork是创建子进程的时候使用,它主要的功能就是复制当前进程(父进程)的然后创建进程。这个时候的子进程与父进程的区别在于:pid,ppid和某些的资源和统计量不一样。fork本来是进行调用就开始复制,不过由于之后马上就开始调用exec,所以先前的地址空间和数据空间的复制基本就是无用的,然后现代的linux都已经对这个复制进行了改写,变成了写时复制.
fork的实质:
fork本身是一个系统调用,从源代码中可以看出来,fork调用了do_fork这个函数,这个是一个很基本的函数,比如clone,vfork这些函数都是调用do_fork这个函数的,之前已经说过Linux不区分线程和进程,那么在kernel的眼里线程就是进程,那么关于线程的共享的功能又是如何去达到呢??
long do_fork(unsigned long clone_flags,unsigned long stack_start,struct pt_regs *regs,unsigned long stack_size, int __user *parent_tidptr,int __user *child_tidptr)这是fork函数声明,上面的问题的答案就是clone_flags这个参数搞得鬼。数了一下好像有19个clone_flags的值,而且可以好几个值连着一起用通过|这个符号。介绍几个很常用的:
- clone_files:父子进程共享打开的文件
- clone_fs:父子进程共享文件系统的信息
- clone_sighand:父子进程共享处理信号函数
- clone_vfork:说明调用了vfork这个函数,而且父进程要等待子进程的完成
- clone_vm:父子进程共享地址空间
这些是最为常用的标识,比如线程的创建通常就是1,2,3,5几个标识一起合并使用的。
现在已经到了do_fork,不过它也不是干实事的函数,最主要的还是copy_process这个函数,这个函数做了很多事情,分别为:
- 调用了dup_task_struct这个函数,这个函数的作用可以说很简单就是复制,单纯的复制当前进程的进程的task_struct,thread_info,内核栈,而且是一模一样的。
- . 为了让子进程与父进程区别开来,就要对子进程的task_struct进行清零或者初始化(task_struct中的值不是继承得来的而是通过统计得来的)
- 将子进程的state设置成为TASK_UNINTERRUPTIBLE,不让子进程投入运行(在源代码中寻找这个过程,发现找不到)
- 调用copy_flags这个函数来更新子进程的flags这个数据成员。
- 这样以后后就是得到一个pid给予子进程
- 根据一开始给予的do_fork的clone_flags的值,来完成一些资源的共享与复制
- 最后就是平分父进程的时间片,然后返回子进程的描述符.
1: if ((retval = copy_semundo(clone_flags, p)))2: goto bad_fork_cleanup_security;3: if ((retval = copy_files(clone_flags, p)))4: goto bad_fork_cleanup_semundo;5: if ((retval = copy_fs(clone_flags, p)))6: goto bad_fork_cleanup_files;7: if ((retval = copy_sighand(clone_flags, p)))8: goto bad_fork_cleanup_fs;9: if ((retval = copy_signal(clone_flags, p)))10: goto bad_fork_cleanup_sighand;11: if ((retval = copy_mm(clone_flags, p)))12: goto bad_fork_cleanup_signal;13: if ((retval = copy_namespace(clone_flags, p)))14: goto bad_fork_cleanup_mm;15: retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);16: if (retval)17: goto bad_fork_cleanup_namespace;上面的代码就是复制共享资源的真实代码
通常kernel会让子进程比父进程先运行,这也是为了防止写时复制这个机制,如果父进程有可能会修改共享的资源,那么根据写时复制就会复制一份出来,那样只是一种无用的浪费.
接下来就是进程的终结的过程:不管显式的调用exit或者从某一个函数返回,或者是碰到进程既不能处理又不能忽略的异常时,它还可能被动的终结。反正不管是哪一种的终结,最后进程一定会通过do_exit这个函数来为进程进行收尾工作。do_exit主要完成了一下的工作:
- 将task_struct 的标志成员设置为PF_EXITING
- 调用del_timer_sync 删除任一内核定时器,确保没有这个进程的没有在排队
- 使用exit_mm函数放弃占有的mm_struct
- 使用exit_sem函数,让进程离开ipc队列
- __exit_files(tsk);__exit_fs(tsk);exit_namespace(tsk);exit_itimers(tsk);exit_thread();使用这一系列的函数来释放相应的资源,如果有的资源已经没有被共享的话,那么就释放.
- exit_notify用来通知父进程,说自己已经运行完成了,现在已经总结了,并且状态转为TASK_ZOMBIE.在这个函数中也调用了forget_original_parent这个函数,这个函数就是为了让该进程的子进程寻找父进程的作用.
- 显式的调用schedule这函数,切换别的进程来运行.
到这个时候,子进程已经不能在投入运行的,但是它还没有完全消失掉,它的task_struct,thread_info和内核栈都是保留着的,只有当父进程调用了wait系统函数以后才会被真正的释放掉。这是一个很不错的机制,把清理工作与进程描述符的清理分来来了,也就是说清理工作一定会被执行的。父进程通过调用wait函数以后,就可以接到子进程终止的消息,并且可以收集子进程是以什么原因终止的。子进程的描述符被清除的过程(release_task):
- 调用atomic_dec(&p->user->processes)用来减少进程拥有者的进程数目,
- 调用proc_pid_unhash从pidhash上删除该进程,同时用__unhash_process(p)把进程从task_list中删除进程
- 如果子进程正在ptrace跟踪,那么就把跟踪的进程的父进程设置为原先的父进程,然后从ptrace_list中删除这个进程
- 调用put_task_struct用来释放thread_info和内核栈,并且释放task_struct的slab的缓冲块.
到现在为止进程所占的资源已经释放了。很多地方我没有很仔细的看代码,大概的看了一下流程我也觉得差不多了!还有一些问题没有讲的很清楚,不过基本上有点理解进程本身了。