UNIX环境高级编程(3) 第八章

本文深入探讨了进程控制的基础,包括进程标识、fork函数的详细解析、进程间的资源复制与共享,以及exec函数如何用于执行新程序。此外,还介绍了进程终止、等待、退出状态和僵尸进程的概念,以及如何通过wait和waitpid函数处理子进程的终止状态。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

8 进程控制

8.1 引言

8.2 进程标识

#include <unistd.h>
    pid_t getpid(void);
        return: 调用进程的进程ID

    pid_t getppid(void);
        return: 父进程ID

    uid_t getuid(void);
        return: 实际用户ID

    uid_t geteuid(void);
        return: 有效用户ID

    gid_t getgid(void);
        return: 实际组ID

    gid_t getegid(void);
        return: 有效组ID

    上函数都没有出错返回。

    pid_t fork(void);
        return: 子进程返回0,父进程返回子进程ID(>0),error: -1
    创建一个新进程(子进程)

8.3 函数fork

#include <unistd.h>
pid_t fork(void);
返回值:子进程返回0,父进程返回子进程ID,若出错,返回-1.

生成当前进程的一个相同副本,该副本称之为”子进程”。原进程的所有资源都以适当的方式复制到子进程,因此执行了该系统调用之后,原来的进程就有了2个独立的实例,包括

  1) 同一组打开文件
  2) 同样的工作目录
  3) 内存中同样的数据(2个进程各有一个副本)

如果父进程和子进程写同一描述符指向的文件,但有没有任何形式的同步,那么它们的输出就会出现混乱。虽然这种情况可能会发生,但这并是常用的操作模式。在fork之后处理文件描述符有以下两种常见的情形:

a,父进程等待子进程完成。在这情况下,父进程无需对其描述符做任何处理。当子进程终止后,他曾进行过读、写操作的任一共享描述符的文件偏移量做了相应更新。
b,父进程和子进程各自执行不同的程序段。这种情况下,在fork之后,父进程和子进程各自关闭它们不需要使用的文件描述符,这样就不会干扰对方使用的文件描述符。是网络服务进程常用的方法。

除了打开文件之外,父进程的很多其他属性也由子进程继承,包括:

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

父进程和子进程之间的区别在于:

  • fork的返回值不同。
  • 进程ID不同
  • 这两个进程的父进程ID不同:子进程的父进程ID是创建它的进程ID,而父进程的ID则不变。
  • 子进程的tms_utime、tms_stime、tms_cutime和tms_ustime的值设置为0.
  • 子进程不继承父进程设置的文件锁。
  • 子进程的未处理闹钟被清除。
  • 子进程的未处理信号集设置为空集。

fork函数被调用一次,返回两次。子进程中返回值是0,父进程中返回值是子进程的pid

子进程是父进程的副本,子进程获得父进程的数据空间、堆和栈的副本。注意,在是子进程拥有的副本。父子进程并不共享这些存储空间部分。父子进程共享正文段。

由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、堆和栈的完全副本。作为替代,使用了写时复制技术。

4种平台都支持的变体:vfork;Linux的变体:clone系统调用,允许调用者控制哪些部分由父子进程共享。

fork之后是父进程先执行还是子进程先执行是不确定的

父进程中的所有打开文件描述符都被复制到子进程中,父子进程为每个相同的打开描述符共享一个文件表项,故共享同一文件偏移量。如果父子进程写同一描述符执行的文件,又没有任何形式的同步,那么它们的输出就会混合。
在fork之后处理文件描述符有以下两种常见的情况:

  • 父进程等待子进程完成。这种情况下,父进程无需对其描述符做任何处理。
  • 父进程和子进程各自执行不同的程序段。这种情况下,fork之后,父子进程各自它们不需要使用的文件描述符。

strlen和sizeof的区别:前者不包括null字节,一次函数调用;后者包括null字节,编译时计算

除了文件描述符之外,父进程的很多其他属性也由子进程继承,包括:
实际用户ID、实际组ID、有效用户ID、有效组ID

附属组ID
进程组ID
会话ID
控制终端
SUID和SGID标志(stat结构的st_mode成员)
当前工作目录
根目录
文件模式创建屏蔽字umask
信号屏蔽和处理
对任一打开文件描述符的执行时关闭(close-on-exec)标志
环境
连接的共享存储段
存储映像
资源限制
是否继承nice值由具体实现自行决定

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

1, fork的返回值不同
2, pid不同
3, 这两个进程的父进程不同
4, 子进程的tms_utime、tms_stime、tms_cutime和tms_ustime的值设置为0
5, 子进程不继承父进程设置的文件锁
6, 子进程的未处理闹钟被清除
7, 子进程的未处理信号集设置为空集

fork失败的两个主要原因:

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

fork有以下两种用法:

1) 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务器中是常见的。
2) 一个进程要执行一个不同的程序。这对shell是常见的情况。某些系统将fork+exec组合成一个操作spawn

vfork和fork函数的区别:
* vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序,故不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会引用该地址空间。不管在子进程调用exec或exit之前,它在父进程的空间中运行。
* 另一个区别是vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。故如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

8.5 函数exit

5种正常终止方式:

1,从main中执行return,等效于调用exit
2,调用exit函数,调用各终止处理程序,关闭标准I/O流,最后调用_exit函数
3,调用_exit或_Exit
4,进程的最后一个线程在其启动例程执行return语句,该进程以终止状态0返回
5,进程的最后一个线程调用pthread_exit,进程终止状态总是0

3种异常终止方式:

1,调用abort,它产生SIGABRT信号
2,当进程接收到某些信号时,信号可由进程自身(如调用abort函数)、其他进程或内核产生
3,最后一个线程对“取消”请求做出响应

不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应的进程关闭所有打开描述符,释放它所使用的存储器等。
注意:“退出状态”(3个exit函数的参数或main的返回值)区别于“终止状态”。在最后调用_exit时,内核将退出状态转换为终止状态。

  • 如果父进程在子进程之前终止,则称子进程为孤儿进程。子进程 ppid变为1,称这些进程由init进程收养。一个init进程收养的进程终止时,init会调用一个wait函数取得其终止状态,防止它成为僵尸进程。
  • 如果子进程在父进程之前终止,内核为每个终止子进程保存了一定量的信息,至少包括pid、该进程的终止状态以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在Unix术语中,一个已经终止、但其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵尸进程zombie/defunct。

8.6 函数wait和waitpid

#include <sys/wait.h>
    pid_t wait(int *statloc);
    pit_t waitpid(pid_t pid, int statloc, int options);
        return: 进程ID,error: 0 or -1
  • 调用wait或waitpid的进程可能:
    如果其所有子进程都还在运行,则阻塞
    如果一个子进程终止,正等待其父进程获取其终止状态,则取得该子进程的终止状态立即返回
    如果它没有任何子进程,则立即出错返回
  • 如果进程由于收到SIGCHLD信号而调用wait,我们期望wait会立即返回。
  • wait与waitpid的区别
    waitpid有一选项,可使调用者不阻塞
    waitpid可以控制它所等待的进程
  • 若statloc不是NULL,则终止进程的终止状态就存放在它所指向的单元内。该整型状态字由实现定义,其中某些位表示退出状态(正常返回),其他位则指示信号编号(异常返回),有一位指示是否产生了core文件。

  • waitpid函数中的pid参数的解释:
    pid == -1,等待任一子进程,等价于wait函数
    pid > 0,等待pid等于该值的子进程
    pid == 0,等待组ID等于调用进程组ID的任一子进程
    pid < 0,等待组ID等于pid绝对值的任一子进程

  • waitpid函数中的options参数:WNOHANG(不阻塞)、WCONTINUED、WUNTRACED
  • 如果一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵尸状态直到父进程终止,实现这一要求的诀窍是调用fork两次。
#include <sys/wait.h>
    int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
        return: 0, error: -1
* idtype参数:P_PID、P_PGID、P_ALL
* options参数:WEXITED、WNOHANG...
* infop参数是指向siginfo结构的指针

8.8 函数wait3、wait4

    #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);
    pit_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);
        Both return: 进程ID, error: -1
  • 允许内核返回由终止进程及其所有子进程使用的资源概况,包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。 man 2 getrusage

8.9 竞争条件

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则发生了竞争条件。fork函数是竞争条件活跃的滋生地。

8.10 函数exec

一个可执行的二进制文件来加载另一个应用程序,来”代替”当前运行的进程,即加载了一个新的进程。因为exec并不创建新进程,所以必须首先使用fork复制一个旧的程序,然后调用exec在系统上创建另一个应用程序

总体来说:fork负责产生空间、exec负责载入实际的需要执行的程序函数exec

当进程调用exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。

因为调用exec并不创建新进程,所以前后的进程ID并未改变。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[]);
        All seven return: −1 on error, no return on success
  • l表示列表list,新程序的每个命令行参数都是一个单独的参数,空指针结尾
  • v表示矢量vector,指针数组
  • e代表传递一个指向环境字符串指针数组的指针
  • p代表使用调用进程中的environ变量为新程序复制现有的环境
  • 在执行exec后,pid没有改变。但新程序从调用进程继承了下列属性:

    • pid和ppid
    • 实际用户ID和实际组ID
    • 附属组ID
    • 进程组ID
    • 会话ID
    • 控制终端
    • 闹钟尚余留的时间
    • 当前工作目录
    • 根目录
    • 文件模式创建屏蔽字umask
    • 文件锁
    • 进程信号屏蔽
    • 未处理信号
    • 资源限制
    • nice值
    • tms_utime、tms_stime、tms_cutime、tms_cstim

    对打开文件的处理:若文件描述符的执行时关闭(close-on-exec,默认通过fcntl设置)标志被设置(默认没有设置),则在执行exec时关闭该描述符;否则仍保持打开。POSIX.1明确要求在exec时关闭打开目录流。

8.11 更改用户ID和更改组ID

#include <unistd.h>
    int setuid(uid_t uid);
    int setgid(gid_t gid);
        return: 0; error:-1
  • 关于谁能更改ID的若干规则(这里讨论更改用户ID的规则,同样适用于组ID)
  • 实际用户ID ruid、有效用户ID euid、保存的设置用户ID sSUID。假定_POSIX_SAVED_IDS为真
    若进程具有root特权,则setuid函数将ruid、euid和sSUID设置为参数uid的值
    若进程没有root特权,但uid等于ruid或sSUID,则setuid函数只将euid设置为uid
    如果上面两个条件都不满足,则errno设置为EPERM,并返回-1
  • 关于内核所维护的3个用户ID,还有注意以下几点:
    • 只有root进程可以更改ruid。通常,ruid是在用户登录时,由login程序设置的,而且决不会改变它。
    • 仅当对程序文件设置了SUID位,exec函数才设置euid。没有设置SUID位,则euid = ruid。
    • sSUID是由exec复制euid而得到的。
#include <unistd.h>
    int setreuid(uid_t ruid, uid_t euid);
    int setregid(gid_t rgid, gid_t egid);
        return: 0, error: -1
  • 交换实际用户ID和有效用户ID的值,如若其中任一参数的值为-1,则表示相应的ID应当保持不变。
  • 规则:一个非root用户总能交换ruid和euid,这就允许一个设置了SUID的程序交换成普通用户权限后,可以再次交换会SUID权限
#include <unistd.h>
    int seteuid(uid_t uid);
    int setegid(gid_t gid);
        Both return: 0 if OK, −1 on error
  • 对于非root用户,可将euid设置为其ruid或sSUID;这与setuid函数一样
  • 对于root用户,可将其euid设置为uid,而ruid、sSUID保持不变
  • 组ID:上面所说的一切都以类似方式适用于各个组ID。附属组ID不受setgid、setregid、setegid函数的影响。

8.12 解释器文件

现今UNIX系统都支持解释器文件(interpreter file),它是一种文本文件,起始行形式是:
#! pathname[optinal-argument]
shell脚本中常见的行开始:#! /bin/sh

上面pathname是绝对路径名,对它不进行特殊处理,对此文件的识别是由内核作为exec系统调用处理的一部分完成的。所以真正执行文件的是exec函数进程,而不是解释器文件。

解释器是pathname指定的。

8.13 函数system

#include <stdlib.h>
    int system(const char *cmdstring);
        Returns: (see below)

如果cmdstring是一个空指针,则仅当命令处理程序可用时,system返回非0值,这一特征可以确定给定的操作系统上是否支持system函数。在UNIX中,system总是可用的。

因为system在其实现中调用了fork、exec和waitpid,所以有3种返回值:

  1. fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,并且设置errno以指示错误类型。
  2. 如果exec失败(表示不能执行shell),则其返回值如同shell执行了exit(127)一样。
  3. 否则所有3个函数(fork、exec和waitpid)都成功,那么system的返回值是shell的终止状态,其格式已在waitpid中说明。

8.14 进程会计

  • accton命令启用会计处理;在Linux中,该文件是/var/account/pacct
#include <sys/acct.h>
typedef u_short comp_t; /* 3-bit base 8 exponent; 13-bit fraction */
struct acct
{
 char ac_flag; /* flag (see Figure 8.26) */
 char ac_stat; /* termination status (signal & core flag only) */
 /* (Solaris only) */
 uid_t ac_uid; /* real user ID */
 gid_t ac_gid; /* real group ID */
 dev_t ac_tty; /* controlling terminal */
 time_t ac_btime; /* starting calendar time */
 comp_t ac_utime; /* user CPU time */
 comp_t ac_stime; /* system CPU time */
 comp_t ac_etime; /* elapsed time */
 comp_t ac_mem; /* average memory usage */
 comp_t ac_io; /* bytes transferred (by read and write) */
 /* "blocks" on BSD systems */
 comp_t ac_rw; /* blocks read or written */
 /* (not present on BSD systems) */
 char ac_comm[8]; /* command name: [8] for Solaris, */
 /* [10] for Mac OS X, [16] for FreeBSD, and */
 /* [17] for Linux */
};

大多数UNIX系统提供了一个选项一进行进程会计(process accounting)处理。启用该选项后,没当进程结束时内核就写一个会计记录。典型的会计记录包含总量较小的二进制数据,一般包括命令名、所使用的的CPU时间总量、用户ID和组ID、启动时间等。

会计记录所需的各个数据(各CPU时间、传输的字符数等)都有内核保安在进程表中,并在一个新进程被创建是初始化(如fork之后再子进程中)。进程终止时写一个会计记录。这产生两个后果:

1, 我们不能获取永远不终止的进程的会计记录。像init这样的内核守护进程。
2, 在会计文件中记录的顺序对应于进程终止的顺序,而不是他们启动的顺序。

会计记录对应于进程而不是程序。在fork之后,内核位子进程初始化一个记录,而不是在一个新进程被执行时初始化。虽然exec并不创建一个新的会计记录,但相应记录中的命令名改变了,AFORK标志则被清除。这意味着,如果一个进程顺序执行了3个程序(A exec B、B exec C,最后是C exit),只会写一个会计记录。在该记录中命令名对应于程序C,但CPU时间是程序A、B和C之和。

8.15 用户标识

  • 获取登陆名
#include <unistd.h>
    char *getlogin(void);
    Returns: pointer to string giving login name if OK, NULL on error
  • 如果调用此函数的进程没有连接到用户登陆时所用的终端,则函数会失败。通常称这些进程为守护进程。

8.16 进程调度

  • 进程通过调整nice值选择以更低优先级运行。只有特权进程允许提高调度权限。
  • nice值的范围在0~(2*NZERO)-1之间,NZERO为系统默认的nice值。nice值越小,优先级越高
#include <unistd.h>
    int nice(int incr);
        Returns: new nice value − NZERO if OK, −1 on error

#include <sys/resource.h>
    int getpriority(int which, id_t who);
        Returns: nice value between −NZERO and NZERO−1 if OK, −1 on error

#include <sys/resource.h>
    int setpriority(int which, id_t who, int value);
        Returns: 0 if OK, −1 on error

8.17 进程时间

我们可以度量的3个时间:

1,墙上时钟时间;
2,用户CPU时间;
3,系统CPU时间。
#include <sys/times.h>
    clock_t times(struct tms *buf );
        Returns: elapsed wall clock time in clock ticks if OK, −1 on error
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 */
};

上结构没有包含墙上时钟时间,times函数返回墙上时钟时间作为其函数值。此值相对于过去的某一时刻度量的。前后使用相减就是时间差。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值