主要介绍了关于进程及其相关的知识,更详细内容还需看书。感觉linux进程很深奥,很有意思。
进程
什么是进程
在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述(静态),进程是程序的实体(运动的,在执行中)。即进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
进程的概念主要有两点
第一,进程是一个实体。
每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
第二,进程是一个“执行中的程序”。
程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
操作系统引入进程的原因
从理论角度看,是对正在运行的程序过程的抽象。
从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。
进程的基本特征是什么
动态性
进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
多道程序系统是指计算机内存中同时存放几道相互独立的程序,使他们在管理程序控制下,相互穿插的运行。 两个或两个以上程序在计算机系统中同处于开始和结束之间的状态。引入多道程序设计技术的根本目的是为了提高CPU的利用率,充分发挥计算机系统部件的并发行,现在计算机系统都采用了多道程序设计技术。
并发性
任何进程都可以同其他进程一起并发执行。宏观上并发,微观上串行。
独立性
进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
异步性
由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的,不可预知的速度向前推进。
进程和程序的区别
(1)程序是永存的,是一组指令的有序集合;进程是暂时的,是程序在数据集上的一次执行,有生命周期;
(2)程序是静态的观念,进程是动态的观念;
(3)进程具有并发性,而程序没有;
(4)进程是竞争计算机资源的基本单位,程序不是。
(5)进程和程序不是一一对应的: 一个程序可对应多个进程即多个进程可执行同一程序; 一个进程可以执行一个或几个程序
进程的基本状态和转换
进程执行时的间断性,决定了进程可能具有多种状态。事实上,运行中的进程可能具有以下三种基本状态。
1)就绪状态(Ready):
进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
2)运行状态(Running):
进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
3)阻塞状态(Blocked):
由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行。同步的概念
我们把异步环境下的一组并发进程因直接制约而互相发送消息、进行互相合作、互相等待,使得各进程按一定的速度执行的过程称为进程间的同步。具有同步关系的一组并发进程称为合作进程,合作进程间互相发送的信号称为消息或事件。 如果我们对一个消息或事件赋以唯一的消息名,则我们可用过程 wait (消息名) 表示进程等待合作进程发来的消息,而用过程 signal (消息名) 表示向合作进程发送消息。
转换
就绪状态 -> 运行状态:处于就绪状态的进程被调度后,获得处理机资源(分派处理机时间片),于是进程由就绪状态转换为运行状态。
运行状态 -> 就绪状态:处于运行状态的进程在时间片用完后,不得不让出处理机,从而进程由运行状态转换为就绪状态。此外,在可剥夺的操作系统中,当有更高优先级的进程就绪时,调度程度将正执行的进程转换为就绪状态,让更高优先级的进程执行。
运行状态 -> 阻塞状态:当进程请求某一资源(如外设)的使用和分配或等待某一事件的发生(如I/O操作的完成)时,它就从运行状态转换为阻塞状态。进程以系统调用的形式请求操作系统提供服务,这是一种特殊的、由运行用户态程序调用操作系统内核过程的形式。
阻塞状态 -> 就绪状态:当进程等待的事件到来时,如I/O操作结束或中断结束时,中断处理程序必须把相应进程的状态由阻塞状态转换为就绪状态。
进程控制块
是什么
进程控制块(PCB Processing Control Block)是系统为了管理进程设置的一个专门的数据结构。
记载着如下信息
作用
系统用它来记录进程的外部特征,描述进程的运动变化过程。同时,系统可以利用PCB来控制和管理进程,所以说,PCB(进程控制块)是系统感知进程存在的唯一标志。
Linux的进程控制
进程的创建与映像更换
进程不能凭空出世,它是由另一个进程创建的。新创建的进程称为子进程,创建子进程的称为父进程。系统中所有进程都是由1号进程init的子孙进程创建的。
fork()系统调用
若fork()调用成功,它向父进程返回子进程的PID,并向新建的子进程返回0。
更换进程映像
通常用户需要的是创建一个新进程来执行某个指定的程序。linux系统的做法是创建子进程后,在子进程中调用exec()来更换进程映像,使自己脱胎换骨,变为一个全新的进程。
exec()系统调用的功能是根据参数指定的路径名找到可执行文件,把它装入进程中的地址空间,覆盖原来的进程映像,从而形成一个不同于父进程的全新的子进程。除了进程映像被更换外,子进程的其他PCB属性均保持不变,就像一个新的进程借壳原来的子进程开始运行。
1234567891011121314 > >> >> > int main(){> > int rid;> > rid = fork();> > if(rid>0){> > printf("i am parent \n");> > }else{> > printf("i ma child i will change to echo \n");> > execl("/bin/echo","echo","hello",0);> > }> > return 0;> > }> >
####进程的终止和等待
导致一个进程终止运行的方式有两种:使用退出语句主动退出,我们称之为正常终止;另一种是被某个信号所杀死,我们称为非正常终止。
c语言编程时,以下4种主动终止运行的方式。
调用exit(status)函数来结束程序。
在main()函数中使用return status语句结束。
在main() 函数中用return语句结束。
程序执行至main函数结束。(隐式的终止)
Linux是如何创建进程的
首先,要明白这样一个事情:在内核启动的时候会生成0号进程;之后0号进程创建1号进程和2号进程;再之后1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先。也就是我们使用操作系统创建进程和线程的时候都不是原始的编写一个出来的,而是通过复制并修改父进程的结构得来的。
说起进程的结构就不得不提起PCB(进程控制块)。PCB是描述一个进程的数据块。在内核中对应task_struct代码。而子进程的PCB是复制并修改父进程的PCB得来的。那么就有必要看一看PCB也就是task_struct中到底定义了什么。这里得交代一下,task_struct结构体有400多行代码,结构复杂不易理解。这里我贴出一份博客,那里有详细的解释。请各位看官务必先阅读下这篇详解task_struct的博客http://blog.csdn.net/jnu_simba/article/details/11724277。我也一并贴出task_struct源码的地址,如有兴趣请进去查阅http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235。之后,我们从具体的内核函数来分析一下创建进程的具体流程。
创建一个进程可以通过clone(),fork(),vfork()这三个系统调用来完成。而这三个系统调用都是由do_fork()函数来负责处理。在《深入理解Linux内核(第三版)》中写到,实现clone()系统调用的是sys_clone()服务例程,实现fork()系统调用的是clone(),实现vfork()的也是clone()。但是负责处理clone(),fork()和vfork()的函数是do_fork()。概括的流程就是:clone(),fork(),vfork()——>sys_clone()——>do_fork()。具体细节可以点击访问
创建进程示例1
1234567891011121314151617181920 >>> int main(){> pid_t fpid;//fpid表示fork函数返回的值> int count = 0;> fpid = fork();> if(fpid < 0)> {> printf("error in fork!\n");> }else if(fpid == 0){> printf("i am the child process,my process id is %d \n",getpid());> count++;> }else{> printf(" i am the parent process,my process id is %d \n",getpid());> count++;> }> printf("统计结果是:%d \n",count);> return 0;> }>
>
结果如下:
i am the parent process,my process id is 12379
统计结果是:1
i am the child process,my process id is 12380
统计结果是:1
示例讲解
fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)……
为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
引用一位网友的话来解释fpid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其fpid为0.
fork出错可能有两种原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
fork执行完毕后,出现两个进程。
创建进阶进程示例2
123456789101112131415161718 >>> int main(){> int i = 0;> printf("i son/pa ppid pid fpid \n");> //ppid指当前进程的父进程pid> //pid指当前进程的pid> //fpid指fork返回给当前进程的值> for(i=0;i<2;i++){> pid_t fpid = fork();> if(fpid==0)> printf("%d child %4d %4d %4d \n",i,getppid(),getpid(),fpid);> else> printf("%d parent %4d %4d %4d \n",i,getppid(),getpid(),fpid);> }> return 0;> }>
>
执行结果
i son/pa ppid pid fpid
0 parent 2043 3224 3225
0 child 3224 3225 0
1 parent 2043 3224 3226
1 parent 3224 3225 3227
1 child 1 3227 0
1 child 1 3226 0分析
第一步
在父进程中,指令执行到for循环中,i=0,接着执行fork,fork执行完后,系统中出现两个进程,分别是3224 ,3225 (后面我都用pxxxx表示进程id为xxxx的进程)。可以看到父进程p3224的父进程是p2043,子进程p3225的父进程正好是p3224。我们用一个链表来表示这个关系:p2043->p3224->p3225
第一次fork后,p3224(父进程)的变量为i=0,fpid=3225(fork函数在父进程中返向子进程id),代码内容为:
123456789 > > for(i=0;i<2;i++){> > pid_t fpid=fork();//执行完毕,i=0,fpid=3225 和 0> > if(fpid==0)> > printf("%d child %4d %4d %4d/n",i,getppid(),getpid(),fpid);> > else> > printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);> > }> > return 0;> >>
打印结果
parent 2043 3224 3225
0 child 3224 3225 0 第二步
> 假设父进程p3224先执行,当进入下一个循环时,i=1,接着执行fork,系统中又新增一个进程p3226,对于此时的父进程,p2043->p3224(当前进程)->p3226(被创建的子进程)。 > 对于子进程p3225,执行完第一次循环后,i=1,接着执行fork,系统中新增一个进程p3227,对于此进程,p3224->p3225(当前进程)->p3227(被创建的子进程)。从输出可以看到p3225原来是p3224的子进程,现在变成p3227的父进程。父子是相对的,这个大家应该容易理解。只要当前进程执行了fork,该进程就变成了父进程了,就打印出了parent。 > 所以打印出结果是: > > 打印结果 > > > 1 parent 2043 3224 3226 > > 1 parent 3224 3225 3227
第三步
第二步创建了两个进程p3226,p3227,这两个进程执行完printf函数后就结束了,因为这两个进程无法进入第三次循环,无法fork,该执行return 0;了,其他进程也是如此。
以下是p3226,p3227打印出的结果:1 child 1 3227 0
1 child 1 3226 0细心的读者可能注意到p3226,p3227的父进程难道不该是p3224和p3225吗,怎么会是1呢?这里得讲到进程的创建和死亡的过程,在p3224和p3225执行完第二个循环后,main函数就该退出了,也即进程该死亡了,因为它已经做完所有事情了。p3224和p3225死亡后,p3226,p3227就没有父进程了,这在操作系统是不被允许的,所以p3226,p3227的父进程就被置为p1了,p1是永远不会死亡的,至于为什么,这里先不介绍,留到“三、fork高阶知识”讲。
总结一下,这个程序执行的流程如下:
进程调度
功能
主要功能如下:
选择占有处理机的进程
进程调度的主要功能是按照一定的策略选择—个处于就绪状态的进程,使其获得处理机执行。根据不同的系统设计目的,有各种各样的选择策略,例如系统开销较少的静态优先数调度法,适合于分时系统的轮转法(Round RoLin)和多级互馈轮转法(Round Robin with Multip1e feedback)等。这些选择策略决定了调度算法的性能。
进行进程上下文切换
—个进程的上下文(context)包括进程的状态、有关变量和数据结构的值、机器寄存器的值和PCB以及有关程序、数据等。一个进程的执行是在进程的上下文中执行。当正在执行的进程由于某种原因要让出处理机时,系统要做进程上下文切换,以使另一个进程得以执行。当进行上下文切换时点统要首先检查是否允许做上下文切换(在有些情况下,上下文切换是不允许的,例如系统正在执行某个不允许中断的原语时)。然后,系统要保留有关被切换进程的足够信息,以便以后切换回该进程时,顺利恢复该进程的执行。在系统保留了CPU现场之后,调度程序选择一个新的处于就绪状态的进程、并装配该进程的上下文,使CPU的控制权掌握在被选中进程手中。
调度策略
先进先出算法
算法总是把处理机分配给最先进入就绪队列的进程,一个进程一旦分得处理机,便一直执行下去,直到该进程完成或阻塞时,才释放处理机。
例如,有三个进程P1、P2和P3先后进入就绪队列,它们的执行期分别是21、6和3个单位时间,
执行情况如下图:
对于P1、P2、P3的周转时间为21、27、30,平均周转时间为26。
可见,FIFO算法服务质量不佳,容易引起作业用户不满,常作为一种辅助调度算法。
短进程优先算法
最短CPU运行期优先调度算法(SCBF–Shortest CPU Burst First)
该算法从就绪队列中选出下一个“CPU执行期最短”的进程,为之分配处理机。
例如,在就绪队列中有四个进程P1、P2、P3和P4,它们的下一个执行期分别是16、12、4和3个单位时间,执行情况如下图:
P1、P2、P3和P4的周转时间分别为35、19、7、3,平均周转时间为16。
该算法虽可获得较好的调度性能,但难以准确地知道下一个CPU执行期,而只能根据每一个进程的执行历史来预测。
轮转算法
前几种算法主要用于批处理系统中,不能作为分时系统中的主调度算法,在分时系统中,都采用时间片轮转法。
简单轮转法:系统将所有就绪进程按FIFO规则排队,按一定的时间间隔把处理机分配给队列中的进程。这样,就绪队列中所有进程均可获得一个时间片的处理机而运行。
多级队列方法:将系统中所有进程分成若干类,每类为一级。
多级反馈队列
多级反馈队列方式是在系统中设置多个就绪队列,并赋予各队列以不同的优先权。
###进程直接的切换
在linux下,fg和bg命令是进程的前后调度命令,即将指定号码(非进程号)的命令进程放到前台或者后台运行。比如一个需要长时间的运行命令,我们就希望把它放入到后台,这样就不会阻塞当前的操作;而一些服务器的命令进程我们则希望能把它们长期的运行在后台。
ctrl+c 终止并退出前台命令的执行,回到shell。
ctrl+z 暂停前台命令的执行,将进程放入后台,回到shell。
jobs 查看当前在后台执行的命令,可以查看命令进程的号码。
& 运行命令时,在命令末尾加上&命令在后台执行。
fg N 将命令进程号码为N的命令进程放到前台执行,同%N。
bg N将命令进程号码为N的命令放到后台执行。
锁
###死锁
定义
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
产生死锁的条件
1)互斥条件:**指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2**)请求和保持条件:**指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3**)不剥夺条件:**指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4**)环路等待条件:**指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
Shell
shell是什么
Linux系统的shell相当于操作系统的“一层外壳”,它是命令语言解释器,它为用户提供了使用操作系统的接口,它不属于内核,而是在内核之外以用户态方式运行。它的基本功能是解释并执行用户打入的各种命令,实现用户与Linux内核的接口。
shell工作原理
- 首先,检查用户输入的命令是否是内部命令,如果不是在检查是否是一个应用程序;
- shell在搜索路径或者环境变量中寻找这些应用程序;
- 如果键入命令不是一个内部命令并且没有在搜索路径中查找到可执行文件,那么将会显示一条错误信息;
- 如果能够成功找到可执行文件,那么该内部命令或者应用程序将会被分解为系统调用传给Linux内核,然后内核在完成相应的工作;