UNIX环境高级编程学习笔记(九)进程控制

1.进程标识

每个进程都有一个非负整型表示的唯一进程ID。因为进程ID标识符总是唯一的,常将其用作其他标识符的一部分以保证其唯一性。进程ID是可复用的,当一个进程终止后,其进程ID就成为复用的候选者。

ID为0的进程通常是调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,它不执行任何磁盘上的程序,因此也被称为系统进程。进程ID 1 通常是init进程,在自举过程结束时由内核调用。该进程的程序文件在UNIX早期版本中是/etc/init,在较新版本中是/sbin/init。此进程负责在自举内核后启动一个UNIX系统。init 通常读取与系统有关的出事后文件(/etc/rc*文件或/etc/inittab文件,以及在/etc/init.d中的文件),并将系统引导到一个状态(如多用户)。init进程决不会终止。它是一个以超级用户特权运行的普通用户进程(不同于交换进程,它不是内核的系统进程)。init 会成为所有孤儿进程的父进程。

每个UNIX系统实现都有它自己的一套操作系统服务的内核进程,如在某些UNIX的虚拟存储器实现中,进程ID 2是页守护进程,此进程负责支持虚拟存储器系统的分页操作。

下面这些函数返回进程的相关标识:

#include <unistd.h>
pid_t getpid(void);                 返回值:调用进程ID
pid_t getppid(void);                返回值:调用进程的父进程ID          
uid_t getuid(void);                 返回值:调用进程实际用户ID
uid_t geteuid(void);                返回值:调用进程有效用户ID
gid_t getgid(void);                 返回值:调用进程实际组ID
gid_t getegid(void);                返回值:调用进程有效组ID

2.函数fork和vfork

一个现有的进程可以调用fork函数创建一个新进程。

#include <unistd.h>
pid_t fork(void);
pid_t vfork(void);

由fork创建的新进程被称为子进程,fork被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新建子进程的进程ID。

将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid获得其父进程的进程ID(进程ID 0 为内核交换进程使用,所有一个子进程的进程ID不可能为0)。

由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程的数据段、栈和堆的完全副本。作为替代,使用了写时复制(Copy-On-Write, COW)技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限修改为只读。如父进程或子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。

fork之后是由父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。
fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。且共享同一文件偏移量。

fork之后处理文件描述符有以下两种常见情况:

  • 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理,子进程终止后,它曾经进行读写操作的任一共享描述符的文件偏移量做了相应更新。
  • 父进程和子进程各自执行不同的程序段。在这种情况下,父子进程都关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。

子进程会继承很多父进程的属性,包括:文件描述符、实际用户ID、实际组ID、有效用户ID、有效组ID、附属组ID、进程组ID、会话ID、控制终端、设置用户ID标志和设置组ID标志、当前工作目录、根目录、文件模式创建屏蔽字、信号屏蔽和安排、对任一打开文件描述符的执行时关闭(close-on-exec)标志、环境、连接的共享存储段、存储映像、资源限制

父子进程之间的区别具体如下:

  • fork的返回值不同
  • 进程ID不同
  • 进程的父进程ID不同
  • 子进程的tms_utime、tms_stime、tms_cutime和tms_ustime被设置为0
  • 子进程不继承父进程设置的文件锁
  • 子进程未处理的闹钟被清除 子进程的未处理信号集被设置为空集

fork失败的主要原因:

  • 系统中已经有了太多的进程
  • 该实际用户ID的进程总数超过了系统限制

fork的两种用法:

  • 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。
  • 一个进程要执行一个不同的程序。

vfork和fork一样都创建一个子进程,但它并不完全复制父进程的地址空间,因为子进程会立刻调用exec(或exit),于是也就不会引用该地址空间。不过在子进程调用exec和exit之前,它在父进程的空间中运行。
vfork与fork的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,当子进程调用者两个函数其中任意一个时,父进程会恢复运行。

3.函数exit

进程的5种正常终止及3种异常终止方式:

  1. 在main函数内执行
  2. return语句
  3. 调用exit函数
  4. 调用_exit和_Exit函数
  5. 进程的最后一个线程在启动例程中执行return语句 进程的最后一个线程调用pthread_exit函数
  6. 调用abort
  7. 当进程接收到某些信号时
  8. 最后一个线程对“取消”请求作出响应

对于上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于3个终止函数(exit、_exit、_Exit),实现这一点的方法是,将其退出状态作为参数传送给函数。在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能用wait、waitpid函数取得其终止状态。

退出状态与终止状态有所区别。在最后调用_exit时,内核将退出状态转换成终止状态。

对于父进程已经终止的所有进程,它们的父进程都改变为init进程。我们称这些进程由init进程收养。其操作过程大致是:一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID)。

如果子进程在父进程之前终止,父进程如何在做相应检查时得到子进程的终止状态呢?如果子进程完全消失了,父进程无法获取其终止状态。内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或者waitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态以及该进程使用CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。

一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占有的资源)的进程被称为僵死进程。 ps命令将僵死进程的状态打印为 Z 。如果编写一个长期运行的程序,它fork了很多子进程,那么除非父进程等待取得子进程的终止状态,这些子进程终止后就会变成僵死进程。

init进程收养的进程终止时不会变成僵死进程,因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。

4.函数wait和waitpid
当一个进程正常或者异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是异步事件,所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。对于这种信号,系统的默认动作是忽略它。

调用wait和waitpid的进程可能发生:

  1. 如果其所有子进程都还在运行,则阻塞
  2. 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
  3. 如果它没有任何子进程,则立即出错返回。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);

这两个函数的区别如下:

  • 在一个子进程终止之前,wait使其调用者阻塞,而waitpid有一选项,可使调用者不阻塞。
  • waitpid并不等待在其调用之后的第一个终止的子进程,它有若干选项,可以控制它所等待的进程。

两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,可将该参数指定为空指针。
waitpid根据参数pid等待一个特定的进程。options使得我们能进一步控制waitpid的操作。

POSIX.1规定,终止状态用定义在sys/wait.h中的各个宏来查看,有4个互斥的宏可用来取得进程终止的原因,它们的名字都以WIF开始。

说明
WIFEXITED 返回真时表示子进程正常终止
WIFSIGNALED 返回真时表示子进程收到信号而导致异常终止
WIFSTOPPED 返回真时表示子进程处于停止状态
WIFCONTINUED 返回真时表示子进程进入暂停后继续的状态

fork两次可以避免僵死进程。

第一个子进程fork后在第二个子进程之前终止,则第二个子进程会由init进程收养,然后用wait函数获取第一个子进程的终止状态防止其成为僵死进程。

5.函数waitid和wait3、wait4

Single UNIX Specification 包括了另一个获取进程终止状态的函数,即waitid,此函数类似于waitpid,但提供了更多的灵活性。

#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

与waitpid相似,waitid允许一个进程指定要等待在子进程。但它使用两个独立的参数表示要等待的子进程所属的类型,而不是将此进程与进程ID或进程组ID组合成一个参数,id参数的作用与idtype的值相关。

常量 说明
P_PID 等待一特定进程:id包含要等待子进程的进程ID
P_PGID 等待一特定进程组中任一子进程:id包含要等待子进程的进程组ID
P_ALL 等待任一子进程:忽略id

options参数是下列各标志的按位或运算。这些标志指示调用者关注哪些状态变化。

常量 说明
WCONTINUED 等待一子进程,它以前曾被停止,此后又已继续,但其状态尚未报告
WEXITED 等待已退出的进程
WNOHANG 如无可用的子进程退出状态,立即返回而非阻塞
WNOWAIT 不破坏子进程退出状态。该子进程的退出状态可由后续的wait函数获取
WSTOPPED 等待一子进程,它已经停止,但其状态尚未报告

WCONTINUED、WEXITED或WSTOPPED这3个常量之一必须在options参数中指定。

wait3和wait4函数比起wait、waitpid和waitid提供的功能多一个。这与附加参数有关。该参数允许内核返回由终止进程及其所有子进程使用的资源概况。资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接受到信号的次数等。

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);

6.竞争条件
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则认为发生了竞争条件。

7.函数exec

fork函数创建新的子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。

有7种不同的exec函数可供使用,它们常常被统称为exec函数,可以使用这7个函数中的任一个。这些exec函数使得UNIX系统进程控制原语更加完善。用fork可以创建新进程,用exec可以初始执行新的程序。exit函数和wait函数处理终止和等待终止。这些是我们需要的基本的进程控制原语。使用这些原语可构造另外一些如popen和system之类的函数。

#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);

8.更改用户id和更改组id

#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
int seteuid(uid_t uid);
int setegid(gid_t gid);

设置不同用户ID的各函数如下图所示:

UNIX环境高级编程学习笔记(九)进程控制_第1张图片

9.解释器文件
所有现今的UNIX系统都支持解释器文件(interpreter file )。这种文件是文本文件,其起始行的形式是:

#! pathname [optional-argument]

pathname通常是绝对路径名,对它不进行什么特殊的处理(不使用PATH进行路径搜索)。对这种文件的识别是由内核作为exec系统调用处理的一部分来完成的。内核使调用exec函数的进程实际执行的并不是该解释器文件,而是在该解释器文件第一行中pathname所指定的文件。一定要将解释器文件(文本文件,它以#!开头)和解释器(由该解释器文件第一行中的pathname指定)区分开来。

很多系统对解释器文件第一行有长度限制。这包括#!、pathname、可选参数、终止换行符以及空格数。

10.函数system

这个函数使用/bin/sh执行指定的命令串执行标准的shell命令。形如:

$?/bin/sh?-c?cmdstring
#include <stdlib.h>
int system(const char *cmdstring);

应注意的是,设置了SetUID 或 SetGID 的程序不应使用 system 函数。另外,作为服务器程序时,也不应使用system 处理客户程序提供的字符串参数,以避免恶意用户利用 shell 中的特殊操作符进行越权操作。

使用system而不是直接使用fork和exec的优点是:system进行了所需的各种出错处理以及各种信号处理。

11.进程调度
UNIX系统历史上对进程提供的只是基于调度优先级的粗粒度的控制。调度策略和调度优先级是由内核确定的。进程可以通过调整nice值选择以更低优先级运行(通过调整nice值降低它对CPU的占有,因此该进程是“友好的”)。只有特权进程允许提高调度权限。

#include <unistd.h>
int nice(int incr);

incr参数被增加到调用进程的nice值上。如果incr太大,系统直接把它降到最大合法值,不给出提示。类似地,如果incr太小,系统也会无声息地把它提高到最小合法值。由于-1是合法的成功返回值,在调用nice函数之前需要清楚errno,在nice函数返回一1时,需要检查它的值。如果nice调用成功,并且返回值为-1,那么errno仍然为0。如果errno不为0,说明nice调用失败。

getpriority函数可以像nice函数那样用于获取进程的nice值,但是getpriority还可以获取一组相关进程的nice值。setpriority函数可用于为进程、进程组和属于特定用户ID的所有进程设置优先级。

#include <sys/resource.h>
int getpriority(int which, id_t who);
int setpriority(int which, id_t who, int value);

12.进程时间
我们可以度量的3个时间:墙上时钟时间、用户CPU时间和系统CPU时间。任一进程都可调用times函数获得它自己以及己终止子进程的上述值。

#include <sys/times.h>
clock_t times(struct tms *buf);

struct tms {
    clock_t tms_utime;  /* user CPU time */
    clock_t tms_stime;  /* system CPU time */
    clock_t tms_cutime; /* user CPU time, terminated children */
    clock_t tms_cstime; /* system CPU time, terminated children */
};

你可能感兴趣的:(进程控制,操作系统相关,unix环境)