1.15k likes | 1.37k Views
第 4 章 Linux 进程管理及进程通信. 计算机学院 潘 薇 panwei117@qq.com. 本章要点. 4.1 Linux 进程及描述 4.2 进程控制 4.3 进程调度 4.4 进程间通信 4.5 进程管理命令. 4.1 Linux 进程及描述. 什么是进程?. 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
E N D
第4章 Linux进程管理及进程通信 计算机学院 潘 薇 panwei117@qq.com
本章要点 • 4.1 Linux进程及描述 • 4.2 进程控制 • 4.3 进程调度 • 4.4 进程间通信 • 4.5 进程管理命令
什么是进程? • 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。 • 第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。 • 第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。
什么是线程? • 通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。 • 在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。 • 由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。 • 因而近年来推出的通用操作系统都引入了线程,以便进一步提高系统的并发性,并把它视为现代操作系统的一个重要指标。
xinetd crond httpd mingetty … shell shell shell 4.1.1 Linux系统中的进程 • Linux操作系统是多进程并发环境,进程和进程之间的关系呈现为多级结构,如图4.1所示。 root(进程0) init(进程1) kswapd 图4.1 进程多级结构关系
4.1.1 Linux系统中的进程 • 最上层的进程是系统的根进程,也称为0进程,也可以称为空闲进程,是系统所有进程的起点,在系统引导时创建。 • 根进程通过系统函数调用fork创建初始化进程(init进程,也称为1进程)。根进程在创建初始化进程之后,将被隐藏起来,在系统内无法查看。 • 初始化进程通过系统函数调用fork创建一系列的系统进程,如crond、xinetd、kswapd、mingetty进程等。这些进程长期运行在系统中,被称为驻守进程(daemon进程)。
4.1.1 Linux系统中的进程 • 所有进程都会生成一个或多个子进程。除根进程之外,所有进程都有一个父进程。 • 为了管理方便,Linux操作系统给每个进程分配一个唯一的进程标识符pid。操作系统通过进程标识符管理进程。
7 6 停止 (stopped) 僵死 (zombie) 终止 终止 中断 5 4 用户态运行 (running) 系统函数调用或中断 调度 就绪 (running) 1 2 3 不可中断睡眠 (uninterruptible) 可中断睡眠 (interruptible) 返回 唤醒 唤醒 睡眠 睡眠 核心态运行 (running) 图4.2 Linux进程状态及转换 4.1.2 进程状态及其转换
4.1.2 进程状态及其转换 • 就绪(Running):进程处于预备运行状态,等待系统分配处理器。 • 核心态运行(Running):进程运行在操作系统核心空间。在用户程序中如果有系统函数调用,则进程状态会从用户态执行转入核心态执行。任何一个进程都不可能抢占一个处于核心态执行的进程。 • 用户态运行(Running):进程运行在用户空间。处于用户态运行的进程可被抢占; • 通常,将处于内核态运行和用户态运行的进程状态统一称为进程的运行态。
4.1.2 进程状态及其转换 • 可中断睡眠(interruptible):由于进程CPU运行时发生了需要等待I/O事件或进程通信时需要相互配合而等待。如果等待的事件已经完成,此时进程可被唤醒,被唤醒后的进程转入就绪状态; • 不可中断睡眠(uninterruptible):不可中断睡眠状态较少使用,可用于进程必须等待时。 • 停止(stopped):进程被停止。如果进程接收到信号,如信号SIGSTOP 或 SIGTSTP 等时,进程则进入停止状态。 • 僵死(zombie):由于父进程死亡而被终止的进程,虽然进程已经死亡,但没有释放系统资源,处于僵死状态。
4.1.3 进程映像与进程上下文 • 一个程序经过编译和链接之后,成为可执行文件。操作系统核心将可执行文件作为进程的实体装入内存时,进程实体分为正文段、数据段和堆栈段。 • 正文段由程序中的代码构成; • 数据段由程序运行所用到的数据构成; • 堆栈段由函数调用传递参数、保留现场、存放返回地址和局部变量构成。
4.2.1 创建进程 • 在Linux操作系统中,用于创建和使用进程的系统调用有fork、exec、wait、exit。 • fork系统调用用于创建当前进程的一个副本。新进程成为创建进程的子进程,创建进程为新进程的父进程。 • 新进程除继承父进程的资源外,还拥有自己的数据空间和运行指针。 • fork函数会返回2次,返回值为0的是子进程,父进程中返回的是子进程的id号。返回-1则表示失败。
4.2.1 创建进程 • wait系统调用用于强制进程暂停进行等待,直到另一个进程结束执行(一般是指子进程)。 • exec系调用用于改变进程正在运行的程序。 • exit系统调用用于终止进程。
4.2.1 创建进程 • 以在shell内运行外部命令ls为例: • 当你从键盘输入ls回车之后,shell所做的第一件事就是调用fork创建一个子进程。一旦子进程创建成功,那么父进程和子进程将各自完成自己的工作。 • 子进程将调用exec将它自己从运行shell的进程变成运行ls命令的进程。而父进程调用wait使自己暂停,等待子进程执行结束。 • ls程序运行结束时,子进程调用exit终止自己的运行,而shell父进程等待完成,继续运行,这时返回到shell的系统提示符。
4.2.2 结束进程 • 进程代码运行结束或进程中有系统函数调用exit都会结束进程。 • 进程结束需要完成一系列的清理工作,特别是释放进程所占用的一切资源并通知其父进程。 • 在Linux系统中,通过向进程发送信号SIGKILL也可以结束进程,但是,这使得操作系统核心程序没有任何机会进行进程结束的清理工作,可能会造成系统资源不能完全释放。因此,发送信号SIGKILL结束进程是进程的异常结束。
4.2.2 结束进程 • 如果父进程意外终止,那么剩下的子进程将成为孤儿进程。它仍然能够正常工作。但是当它终止的时候没有父进程被唤醒,那么它的资源无法被释放,将成为僵死进程。 • 而父进程如果创建子进程后不等待子进程终止,也可能发生上述情况,使得子进程终止时成为僵死进程。 • 为了解决孤儿进程的问题,现代linux系统中由init进程负责自动收养孤儿进程,确保正确处理他们的终止。
4.3.1 进程调度 • Linux进程调度首先将进程分为实时进程和普通进程,并分别采用不同的调度策略。 • 进程调度准则以CPU的时间片为单位,并根据进程相关参数policy、priority、counter、rt_priority的值进行调度。 • 在普通进程与实时进程上,实时进程调度总是优先于普通进程调度。 • 如果进程为普通进程,则其实时优先级(rt_priority)为0;如果为实时进程,则其实时优先级(rt_priority)大于0。
4.3.1 进程调度 • policy:进程的调度策略,用来区分实时进程和普通进程。 • 当policy的值为SCHED_OTHER时,表示普通的用户进程,这是进程的缺省类型,采用动态优先调度策略。 • 当policy的值为SCHED_FIFO,表示实时进程,遵守POSIX1.b标准的FIFO(先入先出)调度策略。 • 当policy的值为SCHED_RR,也是表示实时进程,遵守POSIX1.b标准的RR(循环round-robin)调度策略。
4.3.1 进程调度 • priority:实时进程和普通进程的静态优先级。在应用中,priority代表分配给进程的时间片。 • counter:进程剩余的时间片,可以看作是进程的动态优先级。在应用中,counter表示进程剩余的时间片。counter的起始值是priority的值。 • 进程创建时,优先级priority被赋一个初值,一般为0~70之间的数字,这个数字同时也是计数器counter的初值,就是说进程创建时两者是相等的。 • 在进程运行过程中,counter不断减少,而priority保持不变,以便在进程用完所有分配的时间片,counter变为0的时候,priority再对counter重新赋值。
4.3.1 进程调度 • rt_priority:实时进程特有的实时优先级,用于实时进程间的选择。 • 系统函数goodness()在综合考虑以上4个参数的基础上,给每个处于可运行状态的进程赋予一个权值(weight),该权值是调度程序调度进程的唯一依据。进程调度由程序kernel/sched.c完成。
4.3.2 动态优先级进程调度 • 对于普通进程,Linux采用的是基于动态优先级的时间片多级队列进程调度。 • 动态优先级高的进程获得CPU的运行权,被CPU运行一个时间片后,被动态优先级更高的进程,或者是处于普通进程就绪队列首部的进程抢占,被抢占后的进程则进入下一个就绪队列。 • 因为动态优先级counter的值是变化的,随counter值减小,其它的进程才有运行的机会,当counter减为0时进程完全放弃CPU。
4.3.2 动态优先级进程调度 • 当一个普通进程的时间片用完以后,并不马上用priority对counter进行赋值,直到所有处于就绪状态的普通进程的时间片都用完了以后,才令counter等于priority,并对counter重新赋值,这时普通进程才有了再次被调度的机会。 • 对Linux系统,在缺省时钟嘀嗒为10ms的情况下,若priority为20,则分配给该进程的时间片为20*10ms=200ms。在内核创建进程时,分配给进程的缺省时间片的值为200ms。但是,用户可以通过系统函数调用改变该值。
4.3.3 先来先服务和时间片轮转调度 • 对于实时进程,Linux采用的是先来先服务(FCFS)和时间片轮转(RR)两种调度策略。对应这两种调度策略,实时就绪进程被组织成SCHED_FIFO和SCHED_RR队列。 • 实时进程强调紧迫性,用实时优先级(rt_priority)表现进程的紧迫程度。 • 对于先来先服务调度策略,实时优先级高的进程,更紧迫,可优先分配CPU。在运行过程中,如果没有优先级更高的进程则一直运行,直到完成;如果有优先级更高的进程,则会被抢占,进程放弃CPU,被放到SCHED_FIFO队列的末尾排队。
4.3.3 先来先服务和时间片轮转调度 • 对于时间片轮转调度策略,在实时进程运行过程中,counter为进程的剩余时间片。 • 只要时间片没有用完,进程会一直运行,直到有一个进程因I/O阻塞,或者主动释放CPU,或者是CPU被另一个具有更高rt_priority的实时进程抢先,进程放弃CPU,被放到SCHED_RR队列的末尾排队。
4.4 进程间通信 • 由于Linux系统是多进程并发,所以,进程与进程之间需要相互交换数据,即进程间通信。 • Linux进程通信继承了Unix进程通信方式,有信号、管道、消息、共享存储区和信号量。
4.4.1 信号 • 信号是一种用软件来模拟中断方式的异步通信机制,也被称为软中断(Signal)。 • 当某事件发生时进程会向其他相关进程发出信号,也就是通常所谓的发出通知。 • 接收进程事先并不知道什么时候信号到达,当接收到信号后,会按照通信双方事先约定的信息处理方式进行处理。
4.4.1 信号 • 信号按照发生的事件不同可分为硬件信号和软件信号。 • 硬件信号指通过计算机硬件引起信号发送,如按下键盘的某个键或某个硬件故障。 • 软件信号指通过系统函数调用形式引起信号发生,如kill、raise、alarm和setitimer、sigqueue以及一些非法运算操作。
4.4.1 信号 • 操作系统提供了按下键盘上的Ctrl-C来停止进程的运行。实际上,这是相当于用户进程通过“Ctrl-C”操作向系统的控制进程发送了信号。系统控制进程接收到该信号后,按事先约定的停止进程运行方式,控制进程的运行。 • 每个进程在运行时都要通过信号机制检查是否有其他进程发送的信号到达。如果有信号到达,则中断正在执行的程序并转向与信号相对应的处理程序,执行对异常事件的处理。在处理完成后再返回原中断点继续执行。 • 信号对于进程相当于中断对于处理器。 • 用命令“kill –l”可得到Linux支持的所有信号,如表4.1所示。
4.4.1 信号 表4.1 Red hat 7.2中的所有信号
4.4.1 信号 • Linux进程对信号的响应有三种方式: • 忽略信号:对信号不做任何处理。其中SIGKILL和SIGSTOP信号不能忽略; • 捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数; • 执行缺省操作:对每种信号都有默认操作。其中,对实时信号的缺省操作是终止进程。 • 采用何种方式来响应信号,取决于传递给相应API函数的参数。
4.4.1 信号 • 信号发送是发送进程调用发送函数,通过核心向一个或一组进程发送信号。 • 普通用户只能向自己的进程或同组进程发送信号,超级用户可以向系统中的所有进程发送信号。 • 信号发送后,如果目标进程正在一个可被中断的优先级上睡眠,则核心会立即将其唤醒。 • Linux的发送信号的函数有:kill()、sigqueue()、raise()、alarm()、setitimer()和abort()。
4.4.1 信号 • kill() 函数:用于发送信号,其语法格式如下: • int kill(pid_t pid, int sig); • 其中,pid是指定信号发往的进程或进程组的标识符,通常: • pid为正值,则核心将信号发送给直接指定接收信号的进程标识符; • pid为0,则核心将信号发送给与发送进程同组的所有进程; • pid为-1,则核心将信号发送给除自身以外的所有进程; • pid小于-1,则核心将信号发送给进程组gid为-pid的所有进程;
4.4.1 信号 • 如果调用者不允许将信号传送到由pid指定的进程,则kill函数调用失败,函数返回值为-1,并且设定错误信号errno。 • 参数sig指定欲发送信号的类型,可以使用信号的号码,也可以使用符号名。 • 当sig的值为0时,表示不发送信号,但是,仍会进行错误检查,函数调用成功则返回0。 • 因此,利用发送0信号可以检查接收进程是否存在,或当前进程是否具有向接收进程发送信号的权限。超级用户权限的进程可以向任何进程发送信号。
4.4.1 信号 • 例:将软件终止信号(信号码为15)发送给进程pro1,查得pro1的pid为129。 kill(129,15)或 kill(129,SIGTERM); • 另外,在Shell中也可以用kill作为命令发送信号,命令格式为: kill [-sig] pid • 其中pid为接收进程的标识符。 • 例 用命令kill将信号SIGUSR2送到进程129。 $kill –17 129
4.4.1 信号 • 函数sigqueue(): 用于发送具有传递参数的信号,其语法格式如下: int sigqueue(pid_t pid,int sig,const union sigval val); • 其中,第一个参数是指定接收信号的进程标识符,第二个参数是即将发送的信号,第三个参数是联合数据结构union sigval,指定了信号传递的参数,定义如下: typedef union sigval { int sival_int; void *sival_ptr; }sigval_t; • 如果函数调用成功,则返回 0;否则,返回 -1。
4.4.1 信号 • 虽然函数sigqueue()能够传递更多的信息,功能更加灵活和强大。但是,函数sigqueue()只能向一个进程发送信号,不能向一个进程组发送信号。 • 同样,sig的值为0,表示不发送信号。函数调用成功返回0值。0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。
4.4.1 信号 • raise(): 函数用于向进程本身发送信号。 • 调用成功返回 0;否则,返回 -1。 • raise()函数的语法格式如下: int raise(int sig) • 参数sig为即将发送的信号值。
4.4.1 信号 • 函数alarm(): 专门为SIGALRM信号而设,在指定时间后,会向调用函数的进程发送信号SIGALRM,所以SIGALRM信号又称为闹钟时间。 • alarm()函数的语法格式如下: unsigned int alarm(unsigned int seconds) • 参数seconds为闹钟时间(秒)。 • 进程调用alarm后,任何以前的alarm()调用都将无效。 • 如果参数seconds为零,那么进程内将不再包含任何闹钟时间。 • 如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
4.4.1 信号 • 函数setitimer(): 比函数alarm()功能强大,支持3种类型的定时器: • ITIMER_REAL:设定闹钟的绝对时间。经过指定的时间后,发送SIGALRM信号给本进程; • ITIMER_VIRTUAL:设定程序在用户态执行的时间。经过指定的时间后,发送SIGVTALRM信号给本进程; • ITIMER_PROF:设定进程在内核态和用户态消耗的时间和,经过指定的时间后,发送SIGPROF信号给本进程;
4.4.1 信号 4.4.1 信号 • setitimer()函数的语法格式如下: int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue)); • 该函数调用成功返回0,否则返回-1。其中:第一个参数指定定时器类型;第二个参数是结构itimerval的一个实例,第三个参数可不做处理。 • struct itimerval { struct timeval it_interval; struct timeval it_value; }; • struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ };
4.4.1 信号 4.4.1 信号 • 函数abort()用于向进程发送SIGABORT信号,默认情况下进程会异常退出。其语法格式如下: void abort(void); • 该函数无返回值。 • 即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。
4.4.1 信号 4.4.1 信号 • 信号的安装和接收: 进程要能够接收信号并作相应处理,则必须在进程中安装信号。 • 用函数signal()和sigaction()安装信号。
4.4.1 信号 4.4.1 信号 • 函数signal()不支持信号传递信息,主要用于前32种非实时信号的安装。其语法格式如下: signal(sig,func); • sig是进程接收信号的类型。 • func分为三种情形: • func=1,sig类信号被屏蔽,进程不接收该类信号; • func=0,进程在接收到信号后终止自己,进程在自我终止前,核心在当前目录中建立core文件并将进程的内存映像写入其中,给程序员调试程序带来方便; • func为非0、非1整数,func的值是信号处理程序的指针。 • 如果函数调用signal失败,则返回值为-1。
4.4.1 信号 4.4.1 信号 • 函数sigaction()用于改变进程接收到特定信号后的行为,支持信号传递参数。其语法格式如下: int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact)); • 其中:第一个参数为信号的值,是除SIGKILL及SIGSTOP外的任何一个特定有效的信号。 • 第二个参数用于指定对特定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等。如果为空,进程会以缺省方式对信号处理; • 第三个参数oldact指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。
4.4.1 信号 4.4.1 信号 • 在linux系统中信号应用非常广泛,如系统对异常事件的处理。下面通过一些例子说明信号的应用。 • 例:用kill()函数在两个进程之间发送信号。 程序建立了两个进程,通过向对方发送信号SIGUSR1实现这两个进程的同步。
main() { int pid,ppid; int p_catch( ),c_catch; signal(SIGUSR1,p_catch); switch (pid=fork()) { case –1: printf(“fail fork\n”); exit(1); case 0: signal(SIGUSR1,c_catch); ppid=getppid(); for(;;) { sleep(1); /*在子进程中发送信号SIGUSR1给父进程 */ kill(ppid,SIGUSR1); pause(); } break; default: for(;;) { pause(); sleep(1); /* 在父进程中发送信号SIGUSR1给子进程*/ kill(pid,SIGUSR1); } } } void p_catch() { printf(“parent process caught the signal %d\n”,++ntimes); } void c_catch() { printf(“child process caught the signal %d\n”,++ntimes); }