`
qsv26qsv
  • 浏览: 15071 次
最近访客 更多访客>>
社区版块
存档分类
最新评论

linux 进程编程

阅读更多

linux 进程编程
2010年06月13日
  进程:一个具有独立功能的程序关于某个数据集合的一次可以并发执行的运行活动,是处于活动状态的计算机程序。进程作为构成系统的基本细胞,不仅是系统内部独立运行的实体,而且是独立竞争资源的基本实体。
  进程(执行的程序)会占用一定数量的内存,它或是用来存放从磁盘载入的程序代码,或是存放取自用户输入的数据等等。不过进程对这些内存的管理方式因内存用途不一而不尽相同,有些内存是事先静态分配和统一回收的,而有些却是按需要动态分配和回收的。对任何一个普通进程来讲,它都会涉及到5种不同的数据段。 
  Linux进程的五个段 
  下面我们来简单归纳一下进程对应的内存空间中所包含的5种不同的数据区都是干什么的。 
  1)代码段:代码段是用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作--它是不可写的。 
  2)数据段:数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。 
  3)BSS段:BSS段包含了程序中未初始化的全局变量,在内存中 bss段全部置零。 
  4)堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)它的物理内存空间是由程序申请的,并由程序负责释放。 
  5)栈:栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧"{}"中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。 
  它是由操作系统分配的,内存的申请与回收都由OS管理。 
  进程状态和状态转换
  进程状态:
  1、用户状态:进程在用户状态下运行的状态。 
  2、内核状态:进程在内核状态下运行的状态。 
  3、内存中就绪:进程没有执行,但处于就绪状态,只要内核调度它,就可以执行。 
  4、内存中睡眠:进程正在睡眠并且进程存储在内存中,没有被交换到SWAP设备。 
  5、就绪且换出:进程处于就绪状态,但是必须把它换入内存,内核才能再次调度它进行运行。 
  6、睡眠且换出:进程正在睡眠,且被换出内存。 
  7、被抢先:进程从内核状态返回用户状态时,内核抢先于它,做了上下文切换,调度了另一个进程。原先这个进程就处于被抢先状态。 
  8、创建状态:进程刚被创建。该进程存在,但既不是就绪状态,也不是睡眠状态。这个状态是除了进程0以外的所有进程的最初状态。 
  9、僵死状态(zombie):进程调用exit结束,进程不再存在,但在进程表项中仍有纪录,该纪录可由父进程收集。
  进程状态转换:
  进程创建过程:首先父进程通过系统调用fork来创建子进程,调用fork时,子进程首先处于创建态,fork调用为子进程配置好内核数据结构和子进程私有数据结构后,子进程就要进入就绪态3或5,即在内存中就绪,或者因为内存不够,而导致在SWAP设备中就绪。
  假设进程在内存中就绪,这时子进程就可以被内核调度程序调度上CPU运行。内核调度该进程进入内核状态,再由内核状态返回用户状态执行。该进程在用户状态运行一定时间后,又会被调度程序所调度而进入内核状态,由此转入就绪态。有时进程在用户状态运行时,也会因为需要内核服务,使用系统调用而进入内核状态,服务完毕,会由内核状态转回用户状态。要注意的是,进程在从内核状态向用户状态返回时可能被抢占,进入状态7,这是由于有优先级更高的进程急需使用CPU,不能等到下一次调度时机,从而造成抢占。
  进程还会因为请求的资源不能得到满足,进入睡眠状态,直到它请求的资源被释放,才会被内核唤醒而进入就绪态。如果进程在内存中睡眠时,内存不足,当进程睡眠时间达到一个阀值,进程会被SWAP出内存,使得进程在SWAP设备上睡眠。这种状况同样可能发生在就绪的进程上。
  进程调用exit系统调用,将使得进程进入内核状态,执行exit调用,进入僵死状态而结束。
  进程的上下文:
  进程的上下文是由用户级上下文、寄存器上下文以及系统级上下文组成。主要内容是该进程用户空间内容、寄存器内容以及与该进程有关的内核数据结构。当系统收到一个中断、执行系统调用或内核做上下文切换时,就会保存进程的上下文。一个进程是它的上下文中运行的,若要调度进程,就要进行上下文切换。内核在四种情况下允许发生上下文切换:
  1. 当进程自己进入睡眠时; 
  2. 当进程执行完系统调用要返回用户状态,但发现该进程不是最有资格运行的进程时; 
  3. 当内核完成中断处理后要返回用户状态,但发现该进程不是最有资格运行的进程时; 
  4. 当进程退出(执行系统调用exit后)时。 
  有时内核要求必须终止当前的执行,立即从先前保存的上下文处执行。这可由setjmp和longjmp实现,setjmp将保存的上下文存入进程自身的数据空间(u区)中,并继续在当前的上下文中执行,一旦碰到了longjmp,内核就从该进程的u区,取出先前保存的上下文,并恢复该进程的上下文为原先保存的。这时内核将使得进程从setjmp处执行,并给setjmp返回1。
  进程因等待资源或其他原因,进入睡眠态是通过内核的sleep算法。该算法与本章后面要讲到的sleep函数是两个概念。算法sleep记录进程原先的处理机优先级,置进程为睡眠态,将进程放入睡眠队列,记录睡眠的原因,给该进程进行上下文切换。内核通过算法wakeup来唤醒进程,如某资源被释放,则唤醒所有因等待该资源而进入睡眠的进程。如果进程睡眠在一个可以接收软中断信号(signal)的级别上,则进程的睡眠可由软中断信号的到来而被唤醒。
  进程控制:程的控制,主要是通过内核对fork、exec、wait、exit的处理过程。
  系统启动(boot)的过程以及进程init的作用:
  在Linux系统中,用户创建一个进程的唯一方法就是使用系统调用fork。内核为完成系统调用fork要进行几步操作。第一步,为新进程在进程表中分配一个表项。系统对一个用户可以同时运行的进程数是有限制的,对超级用户没有该限制,但也不能超过进程表的最大表项的数目。第二步,给子进程一个唯一的进程标识号(PID)。该进程标识号其实就是该表项在进程表中的索引号。第三步,复制一个父进程的进程表项的副本给子进程。内核初始化子进程的进程表项时,是从父进程处拷贝的。所以子进程拥有与父进程一样的uid、euid、gid、用于计算优先权的nice值、当前目录、当前根、用户文件描述符表等。第四步,把与父进程相连的文件表和索引节点表的引用数加1。这些文件自动地与该子进程相连。第五步,内核为子进程创建用户级上下文。内核为子进程的u区及辅助页表分配内存,并复制父进程的区内容。这样生成的是进程的静态部分。第六步,生成进程的动态部分,内核复制父进程的上下文的第一层,即寄存器上下文和内核栈,内核再为子进程虚设一个上下文层,这是为了子进程能"恢复"它的上下文。这时,该调用会对父进程返回子进程的pid,对子进程返回0。
  Linux系统的系统调用exit,是进程用来终止执行时调用的。进程发出该调用,内核就会释放该进程所占的资源,释放进程上下文所占的内存空间,保留进程表项,将进程表项中纪录进程状态的字段设为僵死状态。内核在进程收到不可捕捉的信号时,会从内核内部调用exit,使得进程退出。父进程通过wait得到其子进程的进程表项中纪录的计时数据,并释放进程表项。最后,内核使得进程1(init进程)接收终止执行的进程的所有子进程。如果有子进程僵死,就向init进程发出一个SIGCHLD的软中断信号。
  一个进程通过调用wait来与它的子进程同步,如果发出调用的进程没有子进程则返回一个错误,如果找到一个僵死的子进程就取子进程的PID及退出时提供给父进程的参数。如果有子进程,但没有僵死的子进程,发出调用的进程就睡眠在一个可中断的级别上,直到收到一个子进程僵死(SIGCLD)的信号或其他信号。
  进程控制的另一个主要内容就是对其他程序引用。该功能是通过系统调用exec来实现的,该调用将一个可执行的程序文件读入,代替发出调用的进程执行。内核读入程序文件的正文,清除原先进程的数据区,清除原先用户软中断信号处理函数的地址,当exec调用返回时,进程执行新的正文。
  一个系统启动的过程,也称作是自举的过程。该过程因机器的不同而有所差异。但该过程的目的对所有机器都相同:将操作系统装入内存并开始执行。计算机先由硬件将引导块的内容读到内存并执行,自举块的程序将内核从文件系统中装入内存,并将控制转入内核的入口,内核开始运行。内核首先初始化它的数据结构,并将根文件系统安装到根"/",为进程0形成执行环境。设置好进程0的环境后,内核便作为进程0开始执行,并调用系统调用fork。因为这时进程0运行在内核状态,所以新的进程也运行在内核状态。新的进程(进程1)创建自己的用户级上下文,设置并保存好用户寄存器上下文。这时,进程1就从内核状态返回用户状态执行从内核拷贝的代码(exec),并调用exec执行/sbin/init程序。进程1通常称为初始化进程,它负责初始化新的进程。
  进程init除了产生新的进程外,还负责一些使用户在系统上注册的进程。例如,进程init一般要产生一些getty的子进程来监视终端。如果一个终端被打开,getty子进程就要求在这个终端上执行一个注册的过程,当成功注册后,执行一个shell程序,来使得用户与系统交互。同时,进程init执行系统调用wait来监视子进程的死亡,以及由于父进程的退出而产生的孤儿进程的移交。以上是系统启动和进程init的一个粗略的模型。
  进程调度
  Linux系统是一个分时系统,内核给每个进程分一个时间片,该进程的时间片用完就会调度另一个进程执行。LINUX系统上的调度程序属于多级反馈循环调度。该调度方法是,给一个进程分一个时间片,抢先一个运行超过时间片的进程,并把进程反馈到若干优先级队列中的一个队列。进程在执行完之前,要经过这样多次反馈循环。
  进程调度分成两个部分,一个是调度的时机,即什么时候调度;一个是调度的算法,即如何调度和调度哪个进程。先来看看调度的算法,假设目前内核要求进行调度,调度程序从"在内存中就绪"和"被抢先"状态的进程中选择一个优先权最高的进程,如果有若干优先权一样高的进程,则在其中选择等待时间最长的进程。切换进程上下文,继续执行该进程。如果没有选择到进程,则不做操作,等待下一次调度时机的到来。
  每一个进程都有一个用于调度的优先权域。进程的优先权由低到高粗略地分为用户优先权和内核优先权。每种优先权有若干优先权值(优先数)与其对应。每个优先权都有一个逻辑上与其相连的进程队列。进程从内核状态返回用户状态时被抢先,从而得到用户优先权。进程在内核算法sleep中得到内核优先权。内核优先权高于用户优先权,即内核优先权和用户优先权之间存在一个阀值,所有用户优先权低于该阀值,而内核优先权高于该阀值。内核优先权中又划分为可中断和不可中断,即进程在收到一个软中断信号时,低内核优先权的进程可被唤醒,而有高内核优先权的进程继续睡眠。
  计算一个进程优先权的时机是:内核将一个优先权值赋给一个将进入睡眠的进程,这个优先权值是固定的,且与睡眠原因相联系;另一个时机是,时钟处理程序每隔一定时间(如每隔1秒)调整用户状态下的所有进程的优先权,并使内核运行调度算法。时钟处理程序还根据一个衰减函数,每秒一次的调整每个进程的最近CPU使用时间。例如可按如下公式调整:
  decay(CPU) = CPU/2;
  再根据公式重新计算在"就绪"和"被抢先"状态下的每个进程的优先权。
  Priority = ("recent CPU usage"/constant) + (base priority) + (nice value);
  其中constant是个系统常量(一般取值为"2")。base priority值也是系统的一个常量,一般base priority取值为60。最后,nice的值是由进程发出nice调用时给出的值,这样就可以使得用户通过降低优先权而让出一些执行时间。只有超级用户才能指定提高优先权的nice值。
  进程的一般操作
  fork系统调用:
  系统调用fork是用来创建一个子进程。创建的过程前面已经介绍过。现在,再介绍一个系统调用vfork,这个调用的产生是因为认识到创建子进程时对父进程的所有页不进行拷贝能带来性能上的改善。该调用假定进行vfork调用后,将立即调用exec,这样就不需要拷贝父进程的所有页表。因为它不拷贝页表,所以比fork调用快。有些系统的fork也采用了其他方法来提高性能,比较典型的一种是增加"写时拷贝"。这种fork调用,产生子进程时,并不拷贝父进程的所有页面,而是置父进程所有页面的写时拷贝位,子进程共享父进程的所有页面。直到父进程或子进程写某个页面时,就会发生一个保护性错误,并拷贝该页面。这样不仅提高了内核的性能,而且改善了内存的利用。
  系统调用fork和vfork的声明格式如下:
  pid_t fork(void);
  pid_t vfork(void);
  在使用该系统调用的程序中要加入以下头文件:
  #include 
  当调用执行成功时,该调用对父进程返回子进程的PID,对子进程返回0。调用失败时,给父进程返回-1,没有子进程创建。
  下面是发生错误时,可能设置的错误代码errno:
  ?? EAGAIN:系统调用fork不能得到足够的内存来拷贝父进程页表。或用户是超 级用户但进程表满,或者用户不是超级用户但达到单个用户能执行的最大进程数。 
  ?? ENOMEM:对创建新进程来说没有足够的空间,该错误是指没有足够的空间分配给必要的内核结构。 
  注意:父进程打开了一个文件。父子进程都可以对该文件操作,该程序父子进程都向文件中写入了一行。
  exec 系统调用:
  系统调用exec是用来执行一个可执行文件来代替当前进程的执行映像。需要注意的是,该调用并没有生成新的进程,而是在原有进程的基础上,替换原有进程的正文,调用前后是同一个进程,进程号PID不变。但执行的程序变了(执行的指令序列改变了)。它有六种调用的形式,随着系统的不同并不完全与以下介绍的相同。它们的声明格式如下:
  int execl( const char *path, const char *argv, ...);
  int execlp( const char *file, const char *argv, ...);
  int execle( const char *path, const char *argv, ..., char* const envp[]);
  int execv( const char *path, char *const argv[]);
  int execve( const char *filename, char *const argv[], char *const envp[]);
  int execvp( const char *file, char *const argv[]);
  在使用这些系统调用的程序中要加入以下头文件和外部变量:
  #include 
  下面先详细讲述其中的一个,然后再给出它们之间的区别。在系统调用execve中,参数filename是将要执行的文件,参数argv是要传递给文件的参数,参数envp是要传递给文件的环境变量。当参数path所指的文件替换原进程的执行映像后,文件path开始执行,参数argv和envp便传递给进程。
  区别:
  在系统调用execl、execlp、execle中,参数是以argv0、argv1、argv2、…的方式传递的。按照惯例,argv0应该是要执行的程序名。在调用execl、execlp中环境变量的值是自动传递的,即不用象调用execve、execle那样在调用中指定参数envp。在调用execve、execv、execvp中参数是以数组的方式传递的。另一个区别是,调用execlp、execvp可以在环境变量PATH定义的路径中查找执行程序。
  系统调用exec和fork经常结合使用,父进程fork一个子进程,在子进程中调用exec来替换子进程的执行映像,并发的执行一些操作。
  wait 系统调用:
  系统调用wait的功能是发出调用的进程只要有子进程,就睡眠直到它们中的一个终止为止。该调用声明的格式如下:
  pid_t wait(int *status)
  pid_t waitpid(pid_t pid, int *status, int options);
  在使用这些系统调用的程序中要加入以下头文件
  #include 
  #include 
  发出wait调用的进程进入睡眠直到它的一个子进程退出时或收到一个不能被忽略的信号时被唤醒。如果调用发出时,已经有退出的子进程(这时子进程的状态是僵死状态),该调用立即返回。其中调用返回时参数status中包含子进程退出时的状态信息。
  调用waitpid与调用wait的区别是waitpid等待由参数pid指定的子进程退出。其中参数pid的含义与取值方法如下:
  1、参数pid  0时,等待进程ID等于参数pid的子进程退出。 
  4、参数pid = -1时,等待任何子进程退出,相当于调用wait。 
  对于调用waitpid中的参数options的取值及其含义如下:
  ?? WNOHANG:该选项要求如果没有子进程退出就立即返回。 
  ?? WUNTRACED:对已经停止但未报告状态的子进程,该调用也从等待中返回和报告状态。如果status不是空,调用将使status指向该信息。下面的宏可以用来检查子进程的返回状态。前面三个用来判断退出的原因,后面三个是对应不同的原因返回状态值: 
  1. WIFEXITED(status):如果进程通过系统调用_exit或函数调用exit正常退出,该宏的值为真。 
  2. WIFSIGNALED(status):如果子进程由于得到的信号(signal)没有被捕捉而导致退出时,该宏的值为真。 
  3. WIFSTOPPED(status):如果子进程没有终止,但停止了并可以重新执行时,该宏返回真。这种情况仅出现在waitpid调用中使用了WUNTRACED选项。 
  4. WEXITSTATUS(status):如果WIFEXITED(status)返回真,该宏返回由子进程调用_exit(status)或exit(status)时设置的调用参数status值。 
  5. WTERMSIG(status):如果WIFSIGNALED(status)返回为真,该宏返回导致子进程退出的信号(signal)的值。 
  6. WSTOPSIG(status):如果WIFSTOPPED(status)返回真,该宏返回导致子进程停止的信号(signal)值。 
  该调用返回退出的子进程的PID;或者发生错误时返回-1;或者设置了WNOHANG选项没有子进程退出就返回0;发生错误时,可能设置的错误代码如下:
  ?? ECHILD:该调用指定的子进程pid不存在,或者不是发出调用进程的子进程。
  ?? EINVAL:参数options无效。 
  ?? ERESTARTSYS:WNOHANG没有设置并且捕获到SIGCHLD或其它未屏蔽信号。
  注意:子进程退出(SIGCHLD)信号设置不同的处理方法,会导致该调用不同的行为,详细情况见Linux信号处理机制。
  sleep 函数调用:
  函数调用sleep可以用来使进程挂起指定的秒数。该函数调用的声明格式如下:
  unsigned int sleep(unsigned int seconds)
  在使用这个函数调用的程序中加上以下的头文件:
  #include 
  该函数调用使得进程挂起一个指定的时间,直到指定时间用完或者收到信号。系统的活动对指定的时间有一定的影响。Linux系统是用SIGALRM实现的,在Linux系统里,sleep函数不能和alarm()调用混用。如果指定挂起的时间到了,该调用返回0;如果该函数调用被信号所打断,则返回剩余挂起的时间数(指定的时间减去已经挂起的时间)。
  进程的特殊操作
  主要是关于进程的各种ID、对进程的设置用户ID、改变进程的工作目录、改变进程的根、改变进程的优先权值等操作。
  获取进程相关ID:
  与进程相关的ID有:
  1. 真正用户标识号(UID):该标识号负责标识运行进程的用户。 
  2. 有效用户标识号(EUID):该标识号负责标识以什么用户身份来给新创建的进程赋所有权、检查文件的存取权限和检查通过系统调用kill向进程发送软中断信号的许可权限。 
  3. 真正用户组标识号(GID):负责标识运行进程的用户所属的组ID。 
  4. 有效用户组标识号(EGID):用来标识目前进程所属的用户组。可能因为执行文件设置set-gid位而与gid不同。 
  5. 进程标识号(PID):用来标识进程。 
  6. 进程组标识号(process group ID):一个进程可以属于某个进程组。可以发送信号给一组进程。注意,它不同于gid。前面的系统调用wait中指定参数pid时,就用到了进程组的概念。 
  如果要获得进程的用户标识号,用getuid调用。调用geteuid是用来获得进程的有效用户标识号。有效用户ID与真正用户ID的不同是由于执行文件设置set-uid位引起的。这两个调用的格式如下:
  uid_t getuid(void);
  uid_t geteuid(void);
  在使用这两个调用的程序中加入下列头文件:
  #include 
  #include 
  如果要获得运行进程的用户组ID,使用getgid调用来获得真正的用户组ID,用getegid获得有效的用户组ID。标识gid与egid的不同是由于执行文件设置set-gid位引起的。这两个调用的格式如下:
  gid_t getgid(void);
  gid_t getegid(void);
  在使用这两个调用的程序中加入下列头文件:
  #include 
  #include 
  如果要获得进程的ID,使用getpid调用;要获得进程的父进程的ID,使用getppid调用。这两个调用的格式如下:
  pid_t getpid(void);
  pid_t getppid(void);
  在使用这两个调用的程序中加入下列头文件:
  #include 
  如果要获得进程所属组的ID,使用getpgrp调用;若要获得指定PID进程所属组的ID用getpgid调用。这两个调用的格式如下:
  pid_t getpgrp(void);
  pid_t getpgid(pid_t pid);
  在使用这两个调用的程序中加入下列头文件:
  #include 
  注意一下gid和pgrp的区别,一般执行该进程的用户的组ID就是该进程的gid,如果该执行文件设置了set_gid位,则文件所属的组ID就是该进程的gid。对于进程组ID,一般来说,一个进程在shell下执行,shell程序就将该进程的PID赋给该进程的进程组ID,从该进程派生的子进程都拥有父进程所属的进程组ID,除非父进程将子进程的所属组ID设置成与该子进程的PID一样。
  setuid和setgid 调用:
  调用的声明格式如下:
  int setuid(uid_t uid);
  int setgid(gid_t gid);
  在使用这两个调用的程序中加入下面的头文件:
  #include 
  调用setuid为当前发出调用的进程设置真正和有效用户ID。参数uid是新的用户标识号(该标识号应该在/etc/passwd文件中存在)。如果发出调用的进程的有效用户ID是超级用户,内核将进程表中和u区中的真正用户标识号和有效用户标识号置为参数uid。如果发出调用的进程的有效用户ID而不是超级用户,那么内核将根据指定的参数uid来执行,如果这时指定的参数uid的值是真正用户标识号或者是保存用户标识号(saved user ID),则内核将u区中的有效用户标识号改为参数uid,否则,该调用返回错误。该调用成功时,返回值为0;发生错误时,返回-1,并设置相应的错误代码errno,下面是经常可能发生的错误代码:
  ?? EPERM:用户不是超级用户,并且指定的参数uid与发出调用的进程的真正用户ID或保存用户ID不匹配。 
  调用setgid设置当前发出调用的进程的真正、有效用户组ID。该调用允许进程指定进程的用户组ID为参数gid,如果进程的有效用户ID不是超级用户,该参数gid必须等于真正用户组ID、有效用户组ID中的一个。如果进程的有效用户ID是超级用户,可以指定任何存在的用户组ID(在/etc/group文件中存在)。
  注意: 对于setuid程序尤其要小心,当进程的euid是超级用户时,如果将进程setuid到其他用户,就无法再得到超级用户的权力。可能这样用这个调用,某个程序,开始需要root权力才能完成开始的工作,但后续的工作不需要root的权力,所以,将程序的执行文件设置set_uid位,并使得执行文件的属主是root,这样进程开始时,就具有了root的权限,在不再需要root权限的地方,用setuid(getuid)恢复进程的uid、euid。对于可执行文件设置set_uid位,一定要注意,尤其是对那些属主是root的更要注意。因为LINUX系统中root拥有任何权力。使用不当,会对系统安全有极大的损害。
  setpgrp和setpgid 系统调用:
  这两个调用是用来设置进程组ID的,其声明格式如下:
  int setpgrp(void);
  int setpgid(pid_t pid, pid_t pgid);
  在使用这两个调用的程序中加入下面的头文件:
  #include 
  调用setpgrp用来将发出调用的进程的进程组ID设置成与该进程的PID相等。注意,以后由这个进程派生的子进程都拥有该进程组ID(除非修改子进程的进程组ID)。
  调用setpgid用来将进程号为参数pid的进程的进程组ID设定为参数pgid。如果参数pid为0,则修改发出调用进程的进程组ID。如果参数pgid为0,将进程号为pid的进程改为与发出调用的进程同组。如果不是超级用户发出的调用,那么被指定的进程必须与发出调用的进程有相同的EUID,或者被指定的进程是发出调用进程的子进程。
  进程组可用于信号的发送,或者终端输入的仲裁(与终端控制进程有相同的进程组ID且在前台可以读取终端,其他进程在企图读的时候被阻塞并发送信号给该进程)。
  该调用成功时,返回值为0;如果请求失败,返回-1,并设置全局变量errno为对应的值。下面是可能遇到的错误代码:
  ?? ESRCH:参数pid指定的进程不存在。 
  ?? EINVAL:参数pgid小于0。 
  ?? EPERM:指定进程的EUID与发出调用进程的euid不同,且指定进程不是发出调用进程的子进程。 
  chdir 系统调用系统调用:
  chdir是用来将进程的当前工作目录改为由参数指定的目录。该调用的声明格式如下:
  int chdir(const char *path);
  在使用该调用的程序中加入下面的头文件:
  #include 
  使用该调用时要注意,发出该调用的进程必须对参数path指定的目录有搜索(execute)的权力。调用成功时,返回值为0;错误时,返回-1,并设置相应的错误代码。
  chroot 系统调用:
  系统调用chroot用来改变发出调用进程的根("/")目录。该调用声明的格式如下:
  int chroot(const char *path);
  在使用该调用的程序中加入下面的头文件:
  #include 
  调用chroot将进程的根目录改到由参数path所指定的地方。以后该进程中以"/"(根)开始的路径,都从指定目录处开始查找。发出调用进程的子进程都继承这个根目录的位置。该调用只能由超级用户(root)发出。注意,该调用并不改变当前工作目录,所以有可能当前工作目录"."在根目录"/"之外。调用成功时,返回值为0;错误时,返回-1,并设置相应的错误代码。
  注意: 如果用chroot调用改变根后,不能由调用chroot("/")来返回真正的根,因为调用中的参数"/"会被理解成新设置的根。该调用一般可以用在login程序中,或者现在国内常见的BBS系统等应用程序中,用户登录后执行系统的一个程序,该程序将根改变成用户登录的目录(例如/home/bbs)。这样使用的好处是,利于调试和安装;也利于安全。
  nice 系统调用:
  系统调用nice用来改变进程的优先权。该调用的声明格式如下:
  int nice(int inc);
  在使用该调用的程序中加入下面的头文件:
  #include 
  调用nice将发出调用进程的优先权值增加inc大小。只有超级用户才有权指定一个负的增加量inc。发出调用的进程的子进程都继承该优先权。注意,进程的优先权值越低,优先权越高,即优先权值越低,调度上CPU的机会越大。所以只有root才能指定负值,一般用户只能指定正值,该值降低了进程的优先权,使得进程使用更少的CPU时间。
  该调用成功返回时,返回值为0;错误时,返回-1,并设相应的错误代码:
  ?? EPERM:非超级用户指定参数inc为负值,企图增加进程的优先权。 
  这个调用适用于你的程序需要长时间运行,你不希望它对别的进程影响过大。而且执行的快慢对你来说并不十分重要这种情况。
  进程间使用管道通信
  管道允许在进程之间按先进先出的方式传送数据,管道也能使进程同步执行。管道传统的实现方法是通过文件系统作为存储数据的地方。有两种类型的管道:一种是无名管道,简称为管道;另一种是有名管道,也称为FIFO。进程使用系统调用open来打开有名管道,使用系统调用pipe来建立无名管道。使用无名管道通讯的进程,必须是发出pipe调用的进程及其子进程。使用有名管道通讯的进程没有上述限制。在以后的叙述中,所有无名管道都简称为管道。
  pipe 系统调用:
  系统调用pipe是用来建立管道的。该调用的声明格式如下:
  int pipe(int filedes[2]);
  在使用该调用的程序中加入下面的头文件:
  #include 
  一个管道拥有两个文件描述符用来通信,它们指向一个管道的索引节点,该调用将这两文件描述符放在参数filedes中返回。现在的许多系统中管道允许数据双向流动,但一般习惯上,文件描述符filedes[0]用来读数据,filedes[1]用来写数据。如果要求程序的可移植性好,就按照习惯的用法来编程。调用成功时,返回值为0;错误时,返回-1,并设置错误代码errno:
  ?? EMFILE:进程使用了过多的文件描述符。 
  ?? ENFILE:系统文件表满。 
  ?? EFAULT:参数filedes无效。 
  下面介绍管道的操作的情况:
  1. 对于写管道: 
  写入管道的数据按到达次序排列。如果管道满,则对管道的写被阻塞,直到管道的数据被读操作读取。对于写操作,如果一次write调用写的数据量小于管道容量,则写必须一次完成,即如果管道所剩余的容量不够,write被阻塞直到管道的剩余容量可以一次写完为止。如果write调用写的数据量大于管道容量,则写操作分多次完成。如果用fcntl设置管道写端口为非阻塞方式,则管道满不会阻塞写,而只是对写返回0。
  2. 对于读管道: 
  读操作按数据到达的顺序读取数据。已经被读取的数据在管道内不再存在,这意味着数据在管道中不能重复利用。如果管道为空,且管道的写端口是打开状态,则读操作被阻塞直到有数据写入为止。一次read调用,如果管道中的数据量不够read指定的数量,则按实际的数量读取,并对read返回实际数量值。如果读端口使用fcntl设置了非阻塞方式,则当管道为空时,read调用返回0。
  3. 对于管道的关闭: 
  如果管道的读端口关闭,那么在该管道上的发出写操作调用的进程将接收到一个SIGPIPE信号。关闭写端口是给读端口一个文件结束符的唯一方法。对于写端口关闭后,在该管道上的read调用将返回0。
  dup 系统调用:
  系统调用dup是用来复制一个文件描述符,也就是将进程u区的文件描述符表中的一项复制一份,使得这两项同时指向系统文件表的同一表项。该调用的声明格式如下:
  int dup(int oldfd);
  int dup2(int oldfd, int newfd);
  在使用该调用的程序中加入下面的头文件:
  #include 
  系统调用dup复制由参数oldfd指定的文件描述到进程文件描述符表的第一个空表项处。而系统调用dup2复制由参数oldfd指定的文件描述到参数newfd指定的文件描述符表项处。老的文件描述符和新复制的文件描述符可以互换使用。它们共享锁、文件指针和文件状态。例如,对其中一个文件描述符使用系统调用lseek修改文件指针的位置,对另一文件描述符来说文件指针也改变了,其实了解了内核的工作原理,这一点很容易理解。因为文件指针是放在系统文件表中的。但这两个文件描述符具有不同的close-on-exec标志,因为该标志是存放在文件描述符表中的。
  该调用成功时,返回值为新的描述符;错误时,返回-1,并设置相应的错误代码errno:
  ?? EBADF:参数oldfd不是一个已经打开的文件描述符;或者参数newfd超出允许的文件描述符的取值范围。 
  ?? EMFILE:进程打开的文件描述符数量已经到达最大值,但仍然企图打开新的文件描述符。 
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics