回首向来萧瑟处,也无风雨也无晴
全部博文(37)
分类: LINUX
2015-11-19 09:34:16
在讲进程之前先说一下进程的堆栈的吧:
1.进程的堆栈
内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。
2.进程用户栈和内核栈的切换
当进程因为中断或者系统调用而陷入内核态之行时,进程所使用的堆栈也要从用户栈转到内核栈。
进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内 核态恢复到用户态之行时,在内核态之行的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。
那么,我们知道从内核转到用户态时用户栈的地址是在陷入内核的时候保存在内核栈里面的,但是在陷入内核的时候,我们是如何知道内核栈的地址的呢?
关键在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内核态运行的相关信息,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核的时候得到的内核 栈都是空的。所以在进程陷入内核的时候,直接把内核栈的栈顶(从下往上增长的栈)地址给堆栈指针寄存器就可以了。
再来理解一下线程进程的概念:
线程:是进程中的活动对象,每个线程都用有意个独立的程序计数器,进程栈和一组进程寄存器,内核调度的是线程,而不是进程。
进程:进程就是处于执行期的程序,但是进程并不仅仅局限于一段可执行的程序代码,通常进程还包含其他资源,像打开的文件,挂起的信号,内核内部的数据结构,处理器状态,一个或者多个具有内存映射的内存地址空间及一个或者多个线程,也就是说进程是处于执行期的程序以及相关资源的总称。
进程提供两种虚拟机制:虚拟内存和虚拟处理器;提供给每个进程一个独立的虚拟处理器以及独立的地址空间,给进程一种独享处理器和拥有整个系统的内存资源的假象;
内核把进程描述符结构(task_struct)存放在一个双向循环链表的任务队列中;
Linux通过slab分配起分配task_struct结构,用slab分配生成的task_struct只需要在栈顶(向上增长的栈)或者栈底(向下增长的栈)创建一个新的结构struct thread_info
进程描述符的存放
内核通过唯一的进程标识值或者PID来标识每一个进程,部分硬件体系架构可以用一个专门的寄存器来存放当前进程的task_stuct指针,用于加快访问速度,x86架构寄存器并不多,所以智能在内核栈尾端创建thread_info结构,通过计算偏移间接地查找task_struct结构;
进程状态:
TASK_RUNNING:运行,进程是可执行的,或者是它正在执行;
TASK_INTERRUPTIBLE:可中断,进程正在睡眠或者阻塞,等待某些条件的达成;
TASK_UNINTERRUPTIBLE:不可中断,就算是接受到信号也不会被唤醒或者运行,不对信号做任何响应;用的较少;
TASK_TRACED:被其他进程跟踪的进程;
TASK_STOPPED:停止,进程没有投入运行或者不能投入运行;
设置当前进程的状态可用函数:set_task_state(task,state);
进程的创建
一般的操作系统的产生(spawn)进程的机制:新的地址空间里创建进程,读入可执行文件,最后开始执行;
Linux采用UNIX的实现方式将上述步骤分解到两个单独的函数中去执行:fork()和exit(),fork函数通过拷贝当前进程创建一个子进程,exec函数负责读取可执行文件将其载入地址空间开始运行;
写时拷贝技术:
传统的fork系统调用直接把所有资源复制给子进程,但是可能新创建的子进程执行的程序文件映像并不需要那么多资源那么拷贝就就显得很浪费了,写时拷贝是一种推迟甚至免除拷贝的技术,只有在需要写入是数据才会被拷贝,内核并不复制整个进程的地址空间,而是父子进程共享一个拷贝。在数据需要被写入时才会复制;
下图是明显的比较:
普通操作系统创建子进程直接全拷贝数据段,堆,栈:
写时复制技术:内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
vfork():这个做法更加火爆,内核连子进程的虚拟地址空间结构也不创建了,直接共享了父进程的虚拟空间,当然了,这种做法就顺水推舟的共享了父进程的物理空间
fork(),vfork(),_clone()--------> clone()系统调用-------------->do_fork()在kernel/fork.c中,完成大部分的创建工作----------------->copy_process()让进程开始运行;
Copy_process():完成创建task_struct内核栈,thread_info结构,task_struct结构,分配PID,拷贝父进程必要的资源,
线程在linux中的实现:
从内核的角度来看,是没有线程的概念的,linux把所有的线程当进程来实现的,内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程,线程仅仅被视为一个与进程共享某些资源的进程,每个线程都有自己的task_struct,
线程的创建:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND , 0 );
上面的代码产生的结果和fork差不多,只是父子俩共享地址空间,文件系统资源,文件描述符和信号处理程序;一个普通的fork如下:
clone( SIGHLD , 0);
Vfork的实现:
Clone(CLONE_VFORK | CLONE_VM | SIGHLD,0);
内核线程并没有独立的地址空间,它们只有在内核空间中运行,从不切换到用户空间去;新任务使用kthread内核进程通过clone系统调用创建的,新创建的内核线程处于不可运行状态;如果不通过wake_up_process唤醒他不主动运行;不过创建一个内核线程并让它运行起来可以通过kthread_run来达到;相当于kthread_create()和wake_up_process()运行;
进程终结:进程析构它发生在进程调用exit系统调用时,do_exit()完成大部分工作:删除内核定时器,释放mm_struct,离开队列,引用计数减少(为0释放),为子进程找养父,处于僵死状态,删除进程(任务队列),释放僵死进程资源,通知其父进程,释放task_struct内核栈,thread_info占的页,并释放task_struct所占的告诉缓存的slab;