[单刷APUE系列]第八章——进程控制[2]

目录

[单刷APUE系列]第一章——Unix基础知识[1]
[单刷APUE系列]第一章——Unix基础知识[2]
[单刷APUE系列]第二章——Unix标准及实现
[单刷APUE系列]第三章——文件I/O
[单刷APUE系列]第四章——文件和目录[1]
[单刷APUE系列]第四章——文件和目录[2]
[单刷APUE系列]第五章——标准I/O库
[单刷APUE系列]第六章——系统数据文件和信息
[单刷APUE系列]第七章——进程环境
[单刷APUE系列]第八章——进程控制[1]
[单刷APUE系列]第八章——进程控制[2]
[单刷APUE系列]第九章——进程关系
[单刷APUE系列]第十章——信号[1]

waitid函数

由于前文中waitwaitpid函数有很多不灵活的地方,SUS标准规定了以外一个进程终止状态获取函数

int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

int waitid(idtype_t, id_t, siginfo_t *, int) __DARWIN_ALIAS_C(waitid);

上面两个一个是原著实现,还有一个是苹果系统实现,但是笔者没有在函数手册中找到具体描述,所以这里就不介绍,各位直接看原著。

wait3和wait4函数

除了上面的等待函数,还有两个从BSD时代延续下来的函数,也成为了事实意义上的Unix标准,

pid_t wait3(int *stat_loc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *stat_loc, int options, struct rusage *rusage);

其实从参数的个数和命名基本也能猜出这两个函数干嘛用的了。不过还是附上Unix系统手册

The wait4() call provides a more general interface for programs that need to wait for certain child processes, that need resource utilization statistics accumulated by child processes, or that require options.  The other wait functions are implemented using wait4().
The waitpid() call is identical to wait4() with an rusage value of zero.  The older wait3() call is the same as wait4() with a pid value of -1.

wait4函数提供更通用的接口,而且还包括了资源的统计数据,

struct rusage {
        struct timeval ru_utime; /* user time used */
        struct timeval ru_stime; /* system time used */
        long ru_maxrss;          /* max resident set size */
        long ru_ixrss;           /* integral shared text memory size */
        long ru_idrss;           /* integral unshared data size */
        long ru_isrss;           /* integral unshared stack size */
        long ru_minflt;          /* page reclaims */
        long ru_majflt;          /* page faults */
        long ru_nswap;           /* swaps */
        long ru_inblock;         /* block input operations */
        long ru_oublock;         /* block output operations */
        long ru_msgsnd;          /* messages sent */
        long ru_msgrcv;          /* messages received */
        long ru_nsignals;        /* signals received */
        long ru_nvcsw;           /* voluntary context switches */
        long ru_nivcsw;          /* involuntary context switches */
}

这两个函数的最后一个参数就是一个结构体,能够让我们通过这个结构体来了解子进程的资源统计。

竞争条件

资源是有限的,所以操作系统的一个功能就是提供了资源的分配,竞争条件是操作系统原理概念中的资源竞争。例如,fork函数调用后有某种逻辑的正确执行需要依赖父进程和子进程运行先后,那么这就是一种竞争条件,在现代操作系统中,一般都是多种调度算法,所以我们无法预料到哪个进程先运行。
在原著中一个例程,需要第二个子进程在第一个子进程之前运行。就如同事件循环能马上想到while (true)一样,当碰到这种问题,第一反应就是轮询,while (getppid() != -1),很容易理解的代码,但是这样就浪费了CPU时间。
为了防止这种恶性的竞争和不必要的轮询,Unix系统引入了信号机制,但是这里暂且先不讨论具体实现,等到后面章节再讨论。

exec函数族

exec函数族是很重要的一个学习概念,在学习fork函数的时候讲到过有两种用法,一种是网络服务的master-worker进程模型,一种是子进程执行程序,通常叫做spawn,在Unix系统中,没有提供spawn类似的函数,但是将其拆分开来,forkexec组合就是spawn,当进程调用一种exec函数时,该进程的正文段、数据段、bss段和堆栈将完全被替换,但是其他的进程属性不变。

int execl(const char *path, const char *arg0, ... /*, (char *)0 */);
int execle(const char *path, const char *arg0, ... /*, (char *)0, char *const envp[] */);
int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvP(const char *file, const char *search_path, char *const argv[]);

原著上讲有7个函数,但是实际上笔者在苹果系统中只找到6个函数族,而在CentOS系统上也只找到5个函数。实际上这些函数都是execve函数的前端,最终都是execve函数来完成实际工作。

int execve(const char *path, char *const argv[], char *const envp[]);

这是最终调用的函数,从这个函数中我们可以看到,exec函数族想要运行需要三个参数,文件路径、传入的参数数组和环境变量。其他基本就是这个函数的简化版本或者变体。在前面的描述中我们可以看到有两个函数不使用path作为参数,而是以file作为参数,当file参数为绝对路径时,则使用绝对路径的位置,否则在PATH环境变量的指示中搜索文件。从前面可以看到,只有一个函数execle带有环境变量指针参数,如果没有带此参数的函数,都会复制现有的进程的环境变量,每个系统基本都有不同的实现,所以应当使用共有的函数,并结合使用条件编译,来做跨平台开发。
在前面的Unix文件章节,提到了FD_CLOEXEC标志,进程的每个文件描述符都有一个执行时关闭标志,如果设置了这个参数,则在exec执行时自动关闭该文件。默认情况下,exec后仍然保持描述符打开。
在前面也提到了,用户组ID可以分为实际用户族ID和有效用户组ID,实际用户组ID在exec后依旧是不变的,但是有效用户组ID变化与否则取决于执行程序的设置用户组ID,如果有,则有效用户组ID改为拥有者用户组ID,否则不变,这个很好理解,shell启动一个程序,实际用户组ID是肯定不变的,因为要标识一个进程的使用者,但是用于权限检查的有效用户组ID则是取决于设置用户组ID,sudo命令也是类似。

更改用户组ID

在Unix系统中,进程拥有实际用户组ID和有效用户组ID,在权限检查的时候,检查的是有效用户组ID,在设计开发的时候,Unix的哲学就是最小特权,我们经常需要修改进程的权限,所以这里也有了系统提供的函数。

int setuid(uid_t uid);
int setgid(gid_t gid);
int seteuid(uid_t euid);
int setegid(gid_t egid);

在讲解函数之前,先讲解一下进程的三种ID,实际用户组ID用于标识进程是谁拥有负责的,有效用户组ID则是用于权限检查的,保存的设置用户组ID则是用于恢复有效用户ID的,在早期Unix系统设计中,只存在一种用户组ID,它承担了所有的功能,后来,由于set-uid bit的存在,就分离出了实际用户组ID和有效用户组ID,但是,在很多情况下,并不是一直需要有效用户组ID等于拥有者用户组ID的,所以就有了保存设置用户组ID。保存设置用户组ID是最近一次setuid系统调用或是exec一个setuid程序的结果,用于恢复最早的有效用户组ID。

The setuid() function sets the real and effective user IDs and the saved set-user-ID of the current process to the specified value. The setuid() function is permitted if the effective user ID is that of the super user, or if the specified user ID is the same as the effective user ID. If not, but the specified user ID is the same as the real user ID, setuid() will set the effective user ID to the real user ID.

setuid函数为当前进程设置实际用户ID、有效用户ID和保存的set-user-ID为指定值。如果有效用户ID是超级用户,setuid函数则被放行,换言之,root权限是有特权的,或者说如果指定的有效用户ID等同现有的有效用户ID,也是可以放行的,如果不等同,但是指定的有效用户ID等同于实际用户ID,setuid函数将会将有效用户ID设置为实际用户ID。
对于内核维护的三种ID,有几点需要注意:

  1. root权限进程才能更改实际用户组ID

  2. 只有程序设置了设置用户ID位,exec函数族才会设置有效用户ID,有效用户ID只能是实际用户ID或者保存的设置用户ID

  3. 保存的设置用户ID是exec函数复制原先的有效用户ID得到的,如果程序设置了设置用户ID位,那么就会存在这个值。

seteuid和setegid类似setuid和setgid,但是它们只更改有效用户组ID,root权限进程可以任意修改有效用户组ID,但是非特权用户只能将其设置为实际用户组ID或者保存设置用户组ID,

setreuid和setregid函数

int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);

这两个函数用于分别设置实际用组ID和有效用户组ID,当然,只有root权限才能任意更改,而非特权用户一般都是用于交换实际用户组ID和有效用户组ID。
其实一言以蔽之,上面的六个函数实际上都有root权限运行和非root权限运行两种形式,任何情况下,只有root权限才能更改实际用户组ID,而有效用户组ID代表的是权限检查,所以root权限进程可以降低自身权限,但是其他进程不行。

解释器文件

解释器文件实际上就是普通文件,只是权限位增加了执行权限,并且在头部以#!pathname[optional-arguments]的形式表明了解释器。解释器文件一般用于脚本编写,没什么可说的,所以这里就省略了。

system函数

int system(const char *command);

The system() function hands the argument command to the command interpreter sh(1).  The calling process waits for the shell to finish executing the command, ignoring SIGINT and SIGQUIT, and blocking SIGCHLD.

If command is a NULL pointer, system() will return non-zero if the command interpreter sh(1) is available, and zero if it is not.

system函数将字符串传递给sh解释器,并且一直会等待shell结束执行,如果command参数是NULL指针,并且sh解释器存在,则返回非0,否则就是0,这主要用于判断sh存在与否。这里也没什么可讲,原著中都讲完了,需要注意的是,这是一个非常实用的函数。

进程会计

进程会计不是任何标准规定的东西,所以各个Unix系统实现都将其按照自己的理解方式实现,所以在跨平台开发中就非常的麻烦,而且由于各个平台提供的查询命令都不一致,所以并没有实际上的意义所在,这节可以忽略。

用户标识

在实际的Unix系统中,uid和gid是标志一个用户的方式,也叫作凭据,但是用户不需要以数字标志的形式管理系统,所以就有了以英文形式提供的用户标识,系统也提供了对应的映射

char *getlogin(void);
int setlogin(const char *name);

登录名在一个会话中是不改变的,即使是一些更改uid的程序,例如su命令,而setlogin则是设置登录名,这个函数只能由root权限调用。

进程调度

进程调度是由操作系统决定,开发者和用户不可得知系统会如何调度自己的进程,但是却可以设置进程的优先级。Unix系统也提供了相关接口用于修改和查询。当然,由于CPU也是一种资源,所以就如同其他的资源限制一样,只有root权限进程才能提高自身的优先级,其他进程只能降低自身优先级。

int nice(int incr);

nice函数用于获得设置自身的有限度,并且这是一个累加的值,由于这个函数局限性,系统还提供了另一个函数

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

which参数取RIO_PROCESS, PRIO_PGRP, or PRIO_USER,而who参数的解释取决于which参数,当who为0时,表示当前的进程、进程组或用户,而prio则是-20到20的值,默认为0,低优先级则有高CPU使用。

进程时间

前面讲过Unix系统有三种时间:墙上时钟时间、用户CPU时间和系统CPU时间

clock_t times(struct tms *buffer);

struct tms {
    clock_t tms_utime;
    clock_t tms_stime;
    clock_t tms_cutime;
    clock_t tms_cstime;
};

上面是结构体和函数声明,结构体没有包含墙上时钟时间,但是函数返回了时钟时间,结构体按照顺序包含了用户CPU时间、系统CPU时间、包含子进程的用户CPU时间和包含子进程的系统CPU时间。

阅读 1.9k

推荐阅读
静雅斋
用户专栏

码代码|Minecraft|Node.jser&PHPer|iOS移动端开发者|Web前端码农

74 人关注
74 篇文章
专栏主页
目录