Chinaunix首页 | 论坛 | 博客
  • 博客访问: 15141960
  • 博文数量: 7460
  • 博客积分: 10434
  • 博客等级: 上将
  • 技术积分: 78178
  • 用 户 组: 普通用户
  • 注册时间: 2008-03-02 22:54
文章分类

全部博文(7460)

文章存档

2011年(1)

2009年(669)

2008年(6790)

分类: BSD

2008-04-06 21:20:52

第三章: 进程和内核服务


译者:孙轩@chinaunix.net



该章节的例程:


3.1 进程和内核服务
process:一系列用于实现目标的行为和操作;特别指:制造中指定的连续的工作和处理。

  前面的章节有提到BSD系统的启动过程。现在让我们看看一旦内核被导入并运行会发生什么。像大部分其他的现代操作系统,BSD是一个多用户多任务操作系统,这意味着它支持多用户运行不同的多进程来使用系统资源。进程的概念提出了一个对操作系统管理的所有活动的有力的提取。这个概念作为在多道程序设计环境中面向工作的一个特定术语是在19世纪60年代由Multics(一个失败的分时系统计划)的设计者提出的。

3.2 调度

  一个进程是一个运行程序的单独实例。例如,当你在BSD系统中运行Netscape,它将在被运行时创建一个进程。
如果有3个用户登陆BSD系统并全部同时运行相同的Netscape程序,每一个用户将拥有区别于其他用户的自己的
Netscape实例。BSD系统可以支持同时多个这样的进程。每一个进程将和一个进程标志号(PID)关联。这些进程
需要资源并且可能不得不使用一些设备比如外部存储。

  当多个程序在一个系统上运行的时候,操作系统操控出他们都在运行的假象。操作系统特定的调度算法将管理
程序的优先权指派。该领域的计算科学是广阔的,和十分专业的。更多的信息请看资源章节。

  操作系统实际上将运行的多进程不断调入或调出CPU(s),通过这种方法,每个进程得到一段特定的CPU(s)运行
时间。这段时间被叫做时间片。时间片的长短基本上由内核的调度算法决定。该算法有一个可以调整的值叫‘nice’,它提供给进程修改运行优先权的程序能力。这些优先权的值如下:
      Nice values                Priority
        -20    - 0            Higher priority for execution
         0      - 20          Lower priority for execution
      
注意:一个高的nice值导致低的运行级别,这可能看上去有点怪异。然而,考虑到下面这个计算:
(scheduling algo calculation) + (nice value)
这个简单的算术表明:添加一个负值到一个正值将得到一个较小的数,而当这些数被排序的时候,较小的数将排在
运行队列的前面。

所有进程默认的nice值是0,运行的进程可以提高自己的nice值(就是降低自己的优先权),但是只有以root运行的
进程可以降低nice(就是提高优先权)。BSD系列提供了两个基本的借口用于改变和获得这些值。它们是:

 
int getpriority(int which, int who);
int setpriority(int which, int who, int prio);


 
int nice(int incr);
nice函数会把调用它的进程的nice值设置成传给它的参数incr的值。可以看到nice函数是比较不好用的,因为它不是
十分灵活,实际上nice函数不过是对前两个函数的封装。首选的方法是使用setpriority()。

因为可能的的优先权值可以为-1,所以getpriority的返回值不能作为程序运行成功与否的判断,而应该检查errno(错误号)是否被设置。因此在调用getpriority的时候要清空该值(errno)。(更多的关于使用errno信息,可以使用man(2)察看关于erron的页面和读例程:)setpriority 和 getpriority可以通过设置‘which’和‘who’的值作用于外部进程的优先级,这以后在论述。

例程nice_code.c示范如何获取和设置当前进程的nice值。如果它被root以-20的命令行参数运行,系统将看上去没有反应。这是因为这个程序拥有了最高的运行级别,将会比系统优先。所以当设置丢了小于0的值时,需要谨慎使用和更多的耐心。取决于CPU,完全运行完可能要消耗20分钟。建议使用time指令来运行程序,如下:

 
bash$ time nice_code 10
然后,调整参数的值。这样进程消耗了多少运行时间就很明显了。比如调整参数使之小于0:

 
$ time nice_code -1
下面把参数调大:

 
bash$ time nice_code 20
同时,尝试用其他非root用户运行程序,并使用小于0的参数。系统将会拒绝操作。只有root运行的进程可以降低他们的nice值。(因为Unix系统时多用户系统,每个进程应该获得合理的CPU时间片,只有root可以改变进程的优先级到0以下,这样可以避免用户通过降低优先级来独占CPU资源.)

3.3 系统进程
保护模式的概念在前一章已有介绍,简单的说,它是现代CPU系列支持的一个特定的模式,通过该模式操作系统可以保护内存.据此,现在有两种这样的操作模式。一是内核态,意味着进程将运行在内核的内存空间,并因此,以有内核特权的保护模式运行于CPU中。二是用户态,即运行并且不是运作在内核保护模式的一切。

这区别是十分重要的,因为所有的给定进程都在内核态和用户态使用资源。这些资源有多种格式,就如用来标志进程的内核结构,进程指派的内存,打开的文件,和其他执行段。

在一个FreeBSD系统中,有一些关键的进程帮助内核来执行任务。这些进程中的一部份是完全运行于内核空间,而有些运行于用户态。这些进程如下:

PID  Name
0    swapper
1    init
2    pagedaemon
3    vmdaemon
4    bufdaemon
5    syncer

以上所有的进程除了init,都是运行于内核。这意味着它们不存在相应的二进制程序。这些进程有点类似用户态进程,并且因为它们在内核内运行,他们运作于内核特权模式。这种体系结构是出于多种设计考虑。例如,pagedaemon进程,为了减少负载,只在系统缺乏内存时被唤醒。因此如果系统许多空闲的内存,它就没有被唤醒的必要。如此,比这个进程运行于用户态的优点就是除非真正需要,可以避免使用CPU。但是,它在系统增加了一个需要被调度的进程。不过这些调度计算消耗如此小几乎可以忽略不计。

init进程是所有进程的宗主进程。除了运行于内核态特权模式的进程,每个进程都是init的后裔。并且,僵尸或孤儿进程都会被init接管。它也运行一些管理任务,比如调用产生系统ttys的gettys,和运行系统的有序关机。

3.4 进程创建和进程ID系统

就像上面所说,当一个进程被运行,内核指派给它一个唯一PID。这个PID是个正整数并且它的值在0到PID_MAX之间,由系统决定。(对于FreeBSD,在/usr/include/sys/proc.h中设定PID_MAX为99999。)内核将用下一个相续的可用的值来指派PID。因此,当PID_MAX到达的时候,值将会循环。这个循环是重要的,当使用PID于当前进程统计的时候。
每一个在系统上运行的进程都是由其他进程产生的。这是有一些系统调用完成的,将在下一章论述。当一个进程创建一个新的进程,原进程就成为父进程,新进程就是子进程。这种父子关系提供了很好的类推-每一个父进程可以有很多子进程而父进程们是由另外一个进程派生的。进程可以通过getpid/getppid函数得到自己或他们父进程的PID。

进程也可以用进程组来分组。这些进程组可以通过一个唯一的grpID来标志。进程组作为一个机制被引入BSD是为了使shells能够控制工作。看下面这个例子:

 
bash$ ps auwx  | grep root  | awk {'print $2' }
这些程序,ps,grep,awk和print都归属于一个相同的组,这允许所有的这些指令可以通过一个单一的工作来控制。

一个进程可以获得它的组ID/和父ID通过调用getpgrp或getppid:

 
  #include
  #include
  
     pid_t   getpid(void);
     pid_t   getppid(void);
     pid_t   getpgrp(void);
所有以上例出的函数都使绝对可以运行成功的。然而,FreeBSD man pages 强烈建议,PID不应该用来创建一个唯一文件,因为这个PID只在创建的时候唯一。一旦进程退出,PID值会回到未使用PID池,将会被接着被使用(当然是在系统一直运行情况下)。

一个获得这些值的源程序代码在 proc_ids.c中列出。


当你运行下面程序:

 
bash$ sleep 10 |  ps auwx |  awk {'print $2'}  | ./proc_ids
同时在另一个终端运行:

 
bash$ ps -j
只有这些指令将被一个shell运行并且每个都有相同的PPID和PGID。

3.5 子进程

进程可以由其他进程创建,在BSD中有3个方法来实现这个目的。他们是:fork,vfork 和rfork 。其他的调用例如system不过是对这3个的封装。

fork

当一个进程调用frok,一个新的复制父进程的进程奖被创建:

 
#include
  #include

     pid_t   fork(void);
不像其他的函数调用,在成功的时候,fork会反馈两次-一次在父进程,返回的是新创建的子进程的PID,而第二次在子进程,返回将是0。这样一来,你就可以区分两个进程。当一个新的进程被fork创建,它几乎是父进程的精确复制。他们的共同点包括一下:(不是创建的顺序)

Controlling terminal
Working directory
Root directory
Nice value
Resource limits
Real, effective and saved user ID
Real, effective and saved group ID
Process group ID
Supplementary group ID
The set-user-id bits
The set-group-id bits
All saved environment variables
All open file descriptors and current offsets
Close-on-exec flags
File mode creation (umask)
Signals handling settings (SIG_DFL, SIG_IGN, addresses)
Signals mask

在子进程中不同的是它的PID,而PPID被设置成父进程的PID,进程的资源利用值是0,并且拷贝了父进程的文件描述符。子进程可以关闭文件描述符而不干扰父进程。如果子进程希望读写它们,将会保持父进程的偏移量。注意一个潜在的问题,如果父子进程同时试图读写相同的文件描述符会导致输出异常,或程序崩溃。

当一个新的进程被创建后,运行的顺序将不可知。最好控制运行顺序的方法是使用semiphores,pipes,或signals,这以后论述。通过这些,读写就可以控制,使得一个进程可以避免扰乱其他进程导致一起崩溃。

wait

父进程应该使用下面wait系统调用的一种来搜集子进程的退出状态:

 
    #include
    #include

     pid_t     wait(int *status);


     #include
     #include

     pid_t     waitpid(pid_t wpid, int *status, int options);
     pid_t     wait3(int *status, int options, struct rusage *rusage);
     pid_t     wait4(pid_t  wpid, int *status, int options, struct rusage *rusage);
在调用wait的时候,options参数是一个位值,或者是以下的一个:

WNOHANG -在调用的时候不阻塞程序,他会导致wait马上反馈即使没有子进程被终止。

WUNTRACED -当你想要等待停止并且不可跟踪的子进程(可能由SIGTTIN, SIGTTOU, SIGTSTP或SIGSTOP信号导致)的状态时设置该值。

WLINUXCLONE -如果你想等待通过linux_clone产生的内核线程。

当使用wait4和waitpid调用时,要注意wpid参数是要等待的PID值。如果将其设置成-1将会导致该调用等待所有可能的子进程的终止。在调用时使用rusage结构,如果结构不是指向NULL将返回终止进程的资源使用统计。当然如果进程已经停止,那么这些使用信息就没有什么用处。

wait函数调用提供给父进程一个方法去获取它子进程退出的信息。一当调用,调用它的进程将被阻塞知道一个子进程终止或接收到一个信号。这可以通过设置WNOHANG 值来避免。当调用成功时,status参数将包含结束进程的信息。如果调用进程对退出状态没有兴趣,可以将status设置成NULL。关于使用WNOHANG 的更多细节将在信号章节描述。

如果对退出状态信息有兴趣,它们的宏定义在/usr/include/sys/wait.h中。最好使用它们来获得更好的跨平台兼容性。下面列出它们3个和使用说明:

WIFEXITED(status) -如果返回true(也就是返回非0值)那么进程是通过调用exit()或_exit()正常终止的。

WIFSIGNALED(status) -如果返回true那么进程是由信号终止的。

WIFSTOPPED(status) -如果返回true那么进程已经停止并且可以重新开始。这个宏应该和WUNTRACED一起使用,或当子进程被跟踪的时候(就像使用ptrace)

如果有必要,以下的宏可以进一步提取保存的状态中信息

WEXITSTATUS(status) - 这只能在WIFEXITED(status) 宏评估为true时使用,他会评估exit()和_exit()传递参数的低8位。

WTERMSIG(status) -这只能在WIFSIGNALED(status)评估为true时使用,他会得到导致进程终止的信号的值。

WCOREDUMP(status) -这只能在WIFSIGNALED(status)评估为true时使用,如果终止的进程在收到信号的点创建了core dump文件那么该宏会返回true。

WSTOPSIG(status) -这只能在WIFSTOPPED(status) 评估为true时使用。该红会得到导致进程停止的信号。

如果父进程没有收集子进程的退出状态,或父进程在子进程结束前就已结束,init将会接管子进程并收集它们的退出状态。

vfork 和 rfork

vfork函数和fork相似,是在2.9BSD被引入的。它们两者的区别是,vfork会停止父进程的运行并且使用父进程的运行线程。这是为了调用execv函数设计的,这样可以避免低效的复制父进程的地址空间。实际上vfork是不被建议使用的,因为它不是跨平台的。例如Irix的5.x就没有vfork.下面是vfork的调用例子:

 
#include
#include
  
    int  vfork(void);
rfork函数也和fork与vfork相似.他是在Plan9引入的。它的主要目的是增加更成熟的方法来控制进程的创建和创建内核线程。FreeBSD/OpenBSD支持伪线程和Linux clone调用。换句话说,rfork允许比fork更快更小的进程创建。这个调用可以设置子进程可以和它们共享的资源。下面是一个rfork调用例程:

 
  #include

    int    rfork(int flags);
rfork可以选择的资源如下:

RFPROC -设置该值表示你想创建一个新的进程;而其他的标志将只影响当前进程。该值是默认值。

RFNOWAIT -设置该值表示你希望创建一个和父进程分离的子进程。一当该子进程被退出,他不会留下状态等待父进程收集。

RFFDG -设置该值表示你希望复制父进程的文件描述符表。否则父子进程将会共享一个文件描述符表。

RFCFDG  -该标志和RFDG标志互斥。设置该值表示你希望子进程拥有一个干净的文件描述符表。

RFMEM -设置该值表示你想强制内核共享完整的地址空间。这是直接共享硬件页面表的典型做法。这不能直接在C中调用,因为子进程会返回和父进程相同堆栈。如果你想这么做,最好使用下面列出的rfork_thread函数:

 
#include

   int     rfork_thread(int flags, void *stack, int (*func)(void*arg), void *arg);
这将创建一个新的进程运行于指定的堆栈,并且调用参数指定的函数。和fork不同,成功的时候只会返回新创建的进程PID给父进程,因为子进程将直接运行指定的函数。如果调用失败将返回-1,并且设置errno的值。

RFSIGSHARE -这是一个FreeBSD特有的标志并且最近众所周知地被用于FreeBSD4.3的缓冲区溢出。该值将允许子进程和父进程共享信号。(通过共享sigacts结构)。

RFLINUXTHPN -这是另一个FreeBSD特有的值。这将导致内核在子进程退出时使用信号SIGUSR1代替SIGCHILD。我们会在下一章论述它,现在可以把它看作rfork模仿linux clone调用。

rfork的返回值和fork相似。子进程获得0值而父进程获得子进程的PID。一个微小的区别是-rfork会在需要时休眠直到必要的系统资源可用。而一个fork调用和使用RFFDG | RFPROC来调用rfork相似。但是他不设计向后兼容。如果rfork调用失败会返回-1并且设置errno。(更多的error 标号信息可以看man page和头部文件。)

显然,rfork调用提供了一个较小消耗的fork版本。缺点是有一些众所周知的安全漏洞。并且他即使在跨BSD系列平台上也不是很兼容,在不同的Unix版本中是独一无二的。比如当前的NetBSD-1.5就不支持rfork,而且不是所有FreeBSD可用的标志都可以用于OpenBSD。正因为如此,推荐的线程接口是使用pthreads。

3.6 运行二进制程序

一个进程如果仅仅是父进程的拷贝是没有很大用途的。因此,需要使用exec函数。这些是设计来使用新的进程镜像代替当前进程的镜像。好,举个例子,shell运行ls指令。首先shell会按照执行运行fork或vfork接着它会调用exec函数。一旦exec被成功调用一个新的进程将由ls代替运行,并且exec自己没有返回。如果像shell或perl这样的脚本是目标程序,这个过程就像二进制程序运行。那里有一个附加的程序调用解析器。那就是,头两个字符将是#! 例如,下面展示了一个带参数的解析器调用:

 
#!  interpreter [arg]
下面的指令将使用-w参数调用Perl解析器:

 
#!/usr/bin/perl  -w
下面列出了所有的exec调用。源代码 exec.c
调用它们的基本结构是(目标程序),(参数),(必要的环境变量)。

 
  #include
  #include
   
    extern char **environ;
   
    int     execl(const char *path, const char *arg0, ...  const char *argn,   NULL);
    int     execle(const char *path, const char *arg0, ... const char  *argn, NULL, char *const envp[]);
    int     execlp(const char *file, const char *arg0, ... const char *argn , NULL );
    int     exect(const char *path, char *const argv[], char *const envp[]);
    int     execv(const char *path, char *const argv[]);
    int     execvp(const char *file, char *const argv[]);
注意;在使用带有参数arg的exec时,arg参数必须是以NULL截止的字符串,就如在arg0,arg1,arg2 ..argn中必须以NULL或0结尾。如果没有确定结束符调用会失败。这字符串将成为目标程序运行的参数。

调用exec使用*argv[]参数时其必须是一组包含结束符的字符串的数组,它们将成为目标程序运行的参数。

另一个区分二进制或脚本运行exec的是目标程序是如何指定的。如果目标没有带路径,函数execlp和execvp会搜寻你的环境目录来查找目标程序。而其他函数调用将要求绝对路径。

简索:

数据参数和序列参数

array: exect, execv, execvp

sequence: execl, execle, eseclp

路径搜索和直接文件

path: execl, execle, exect, execv

file: execlp, exevp

指定环境和继承环境

specify: execle, exect

inherit: execl, execlp, execv, execvp

system

 
   
   #include

     int     system(const char *string);
另外一个关键的运行函数调用是system调用。这个函数十分直观。提供的参数将直接传送给shell。如果指定的参数
为NULL,如果shell可用函数会返回1否则返回0。一旦调用,进城会等待shell结束并返回shell的结束状态。如果返回-1表示fork或waitpid失败,127表示shell运行失败。

一旦调用,一些信号例如SIGINT和SIGQUIT将被忽略,并且会阻塞SIGCHILD。同样如果子进程写输出到标准错误输出可能会使调用进程崩溃。

显然,BSD结构提供了简单同时丰富的进程创建接口。这种成熟的设计可以和任何一个现在操作系统媲美。下一章将包括信号和进程管理,包含资源使用,线程和进程限制。
阅读(531) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~