在讲进程之前先说一下进程的堆栈的吧:
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;