CSDN 上的APUE读书笔记之第八章 -- 进程控制

20 篇文章 0 订阅

第八章 进程控制


1、进程标识符 PID 的概念


A. 进程 ID(PID)唯一的标识了系统中的当前进程;

B. 已结束的进程,其 PID 以后将给信的进程使用,但一般不是马上;

C. 0 号进程(PID == 0)是内核的一部分,属于系统进程,其它进程均属于用户进程;

D. 1 号进程通常是 init,是一个以 root 特权运行的系统进程,孤儿进程都将由 init 进程接管;

E. 获取当前进程一些相关标识符的 API:

#include <unistd.h>
pid_t getpid(void); /* 返回当前的 PID */
pid_t getppid(void); /* 返回父进程的 PID */
uid_t getuid(void); /* 返回进程的 UID */
uid_t geteuid(void); /* 返回进程的 EUID */
gid_t getgid(void); /* 返回进程的 GID */
gid_t getegid(void); /* 返回进程的 EGID */

注意:以上函数都没有出错返回。


2、fork(2)函数

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

A. fork(2)生成一个新的进程,该进程是以当前进程的上下文为依据生成的子进程;fork 返回 0 表示当前处于子进程中,而在父进程中则返回所生成的子进程的PID,失败时返回-1;

B. 父进程和子进程的 text 段是共享的,但子进程实际上是从执行 fork 之后的代码段开始执行的;bss 段、堆、栈等在现代的系统中通常使用写时复制(COW,copy-on-write)的技术;

C. fork 之后是父进程还是子进程先被运行一般是不可知的。对于 Linux,为避免父进程先执行引起不必要的 COW(因为很多时候子进程将很快执行 exec(3)),生成新进程时曾试图使子进程先于父进程执行(参考 Understanding Linux Kernel 的“3.4.1.1. The do_fork( ) function”一节)。观察Linux 2.6.x 进程创建的相关源码,在版本 2.6.22 之前我们可以看到曾经存在这样一行注释(位于 kernel/sched.c 的 wake_up_new_task 函数中):

/*
* The VM isn't cloned, so we're in a good position to
* do child-runs-first in anticipation of an exec. This
* usually avoids a lot of COW overhead.
*/

但Linux Kernel Development 一书指出,这并非总能如此。事实上,在2.6.23 采用了新的进程调度机制以后,同时把 child-runs-first的技术放弃了。

D. 子进程生成时复制了父进程打开的文件描述符,包括 stdin,stdout、stderr,应注意对这些资源的共享可能引起并发问题;

E. 对于书中的程序清单 8-1,编译后的程序在命令行下直接执行和重定向到文件中执行,字符串"before fork\n"在前者只输出一次而后者输出了两次的原因是:前者的stdout是字符设备tty,默认使用行缓冲,子进程生成之前 stdout 的缓冲区已经被由于输出带换行符而进行了冲洗;而后者的stdout是一个普通文件,默认使用全缓冲,子进程生成时尚未通过输出冲洗的stdout缓冲区通过 fork 复制给了子进程,父进程和子进程退出时 exit(3)对缓冲区进行冲洗时输出了各自的"before fork\n";

F. 父子进程的主要区别包括: 

  • fork 的返回值、PID、PPID 不同;  
  • 子进程的 tms、utime 等相关时间统计信息在 fork()后被清零;  
  • 子进程不继承父进程的记录锁;  
  • 子进程的未决闹钟定时(alarm(2))将被清除;  
  • 子进程不继承父进程的未决信号集;

G. 关于 vfork(2)函数  

  • 该函数是专为子进程生成后不需要父进程的进程数据而直接执行 exec 而设计的,它只生成子进程,但永不拷贝父进程内存区域的数据,而且父进程将一直阻塞到子进程执行了 execve(2)或者_exit(2)调用为止。  
  • 事实上,在 Linux 中,vfork(2)和 fork(2)都使用了 clone(2)系统调用,但使用不同的参数。Linux 的手册页指出了 vfork(2)是为避免 fork(2)的实现未必使用了 copy-on-write 技术而为降低子进程生成的代价而实现的,手册页同时指出了vfork(2)的标准描述与Linux 语境下的描述,及其历史描述。  
  • 使用 vfork(2)时应注意:防止子进程因依赖父进程引起的死锁,特别是子进程并不继承父进程的记录锁,这时使用父进程打开的文件时可能会被阻塞。  
  • 如果在子函数中调用 vfork(2),从子函数中返回后再进行其它处理,这种情况下子进程可能将修改父子进程所共享的栈,从而将带来副作用。

3、取子进程终止状态的 wait 家族函数

一个进程终止时,将释放系统资源并将自身的运行态变为僵死态,这时内核将向其父进程发送SIGCHLD 信号(见第 10 章:信号),系统对该信号的默认动作是忽略。只有在父进程处理了 SIGCHLD 信号并收集子进程终止状态信息以后,进程才真正的从内核的进程表中释放掉。
#include <sys/wait.h>
pid_t wait(int *status);

wait 函数阻塞等待直到有一个子进程退出,并将相关状态记录到 status 处,返回该子进程的PID,出错时返回-1(例如不存在子进程);
对于指定的状态 status,可以用一系列宏进行测试,包括:
  •  WIFEXITED():返回真时表示子进程正常终止;
  •  WIFSIGNALED():返回真时表示子进程收到信号而导致异常终止; 
  • WIFSTOPPED():返回真时表示子进程处于停止状态; 
  • WIFCONTINUED():返回真时表示子进程进入暂停后继续的状态。 
  • WTERMSIG():返回导致子进程终止的信号; 
  • WCOREDUMP():返回真时表示子进程异常终止并导致了内核转储
前四个宏是互斥的,即它们只能同时有一个返回真值;
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

waitpid 扩展了 wait 的功能,它可以指定子进程的 PID,并设置相关阻塞选项。
参数 pid 各种取值的含义包括:
  •   -1:等待任何子进程;  
  • 正整数:等待指定 pid 的子进程;  
  • 0:等待其进程组 GID 等于当前进程组 GID 的任意子进程;  
  • 负整数:等待其进程组 GID 等于 abs(pid)的任意子进程;
参数 options 包括了: 
  • WCONTINUED:等待到子进程的状态从暂停变为继续,但未报告时取其状态;  
  • WNOHANG:不阻塞并返回 0;  
  • WUNTRACED:等待到子进程变成暂停状态,但未报告时取其状态。
waitpid 通过以下形式调用时与 wait(&status)效果相同:
waitpid(-1, &status, 0);

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

waitid 的参数 idtype 包括:
  •  P_PID:等待指定的进程,此时参数 id 表示所等待的子进程 PID; 
  •  P_PGID:等待指定的进程组中的子进程,此时参数 id 表示进程组 GID;
  •  P_ALL:等待任何子进程,此时参数 id 被忽略;
导致waitid 返回的信号信息将设置到参数infop 指定的地址中。
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resources.h>
#include <sys/wait.h>
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);

这两个函数用于等待并收集子进程及其所有子进程的全部资源信息到rusage 中。对Linux 而言,wait4 是 wait 家族各个函数的系统调用入口,其它几个函数都可以基于 wait4 重新实现。

4、exec 家族函数

exec 家族函数将指定的程序装入当前进程,使之替换掉当前进程大部分的上下文环境。一共 6 个变体,使用类似但形式不同的参数。
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ..., /* (char *)0 */);
int execlp(const char *filename, const char *arg, ..., /* (char *)0 */);
int execle(const char *pathname, const char *arg0, ..., /* (char *)0, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *filename, char *const argv[]);
int execve(const char *filename, char *const argv[], char *const envp[]);

这些函数的第一个参数为要装入的程序,后面的参数为该程序执行时的命令行参数表。函数执行失败时将返回-1并设置 errno,成功后不会在原来程序的 main 中返回;
在多数的 Unix 实现中,都以 execve(2)为系统调用入口,其它几个函数均通过调用 execve(2)来实现。
可以通过分类的方法记住这几种 exec 函数:首先可以分为 execl 和 execv 两类,前者的命令参数用列表的形式,后者的命令参数用类似 main 函数的向量(数组)指针的形式。这两大类下面又有两种变体:最后有 e 的可以附环境变量表(形式为“ENVIRONMENT=value”);而最后有 p 的变体的函数第一个参数文件名 filename 不使用路径(是否含有"/")表示时,将按环境变量 PATH指定的路径进行搜索。
不带有 p 的 exec 函数,第一个参数使用相对路径时将执行失败。
另外,在 Linux 中,上述原型中的(char *) 0一项是必选的,且在手册中指定其含义是 NULL 指针。而且,不管是 argv 还是 envp 向量表,其最后一项都必须为 NULL,否则将失败。而在 Solaris 中,(char *)0 则是可选的。
例如以下代码:
#include "apue.h" 
int main(void) 
{ char *arg[] = 
  {"uname", "-r"}; 
if (execv("/bin/uname", arg) < 0) 
{
  perror("execv");
  exit(EXIT_FAILURE); 
} exit(EXIT_SUCCESS); 
}

在 Linux 下编译执行:
mjxian@fedora ~/apuetest
$ uname
Linux

mjxian@fedora ~/apuetest
$ cc -o execvinFedora testexecv.c && ./execvinFedora
execv: Bad address

mjxian@fedora ~/apuetest
$ echo $?
1

而在Solaris 下编译执行:
mjxian@t1000 ~/apuetest
$ uname
SunOS

mjxian@t1000 ~/apuetest
$ cc -o execvinSolaris testexecv.c && ./execvinSolaris
5.10

mjxian@ t1000 ~/apuetest
$ echo $?
0

在 linux 下,要成功执行 uname -r这个命令,exec家族各个函数的例子为:
execv(3): 
char *arg[] = {"uname", "-r", NULL}; 
execv("/bin/uname", arg);

execl(3): 
execl("/bin/uname", "uname", "-r", NULL);

execve(2): 
char *arg[] = {"uname", "-r", NULL};
 char *env[] = {NULL}; 
execve("/bin/uname", arg, env);

execvp(3): 
char *arg[] = {"uname", "-r", NULL};
 execvp("uname", arg);

execle(3): 
char *env[] = {NULL}; 
execle(“/bin/uname", "uname", "-r", NULL, env);

exelp(3): 
execlp("uname", "uname", "-r", NULL);
关于 exec 家族函数的更多细节可以参考 Understanding Linux Kernel, 3rd Edition 的“20.4. The exec Functions” 。

5、一个例子:exec 一个脚本解释器文件
Unix 下的解释器文件一般指使用所指定的交互式命令行工具执行的脚本文件。其第一行的形式一般为:
#! cmd

"#!"必须顶格(即位于行首),cmd 之前可以有空格。如果#!不顶格或者没有这行声明,则以当前shell 来执行此文件;
对一个解释器文件进行 exec(3)函数调用时,指定的交互式命令所收到的参数依次为:#!后面的命令串、exec(3)函数指定的可执行文件,exec(3)函数指定的参数表;

6、setuid(2)和 setgid(2)
通过 fork(2)创建的子进程,其 UID 和 EUID 将继承自父进程。用 exec(3)执行一个程序时,若该进程的程序文件有 SetUID 位,则其 EUID(有效用户 id)默认为文件属主的 UID,否则继承自 exec(3)之前的上下文;EGID 的情况类似。在程序中可以用 setuid(2)和 setgid(2)对此作出改变。
#include <unistd.h>
int setuid(uid_t suid);
int setgid(gid_t sgid);
进程中调用函数 setuid(2)时:  
  • 如果是 root 进程,将会同时改变进程的 UID、EUID 和备份 EUID(在 setuid(2)成功时之前的EUID 将备份到内核中的进程表,但 Unix 没有提供接口函数用于读取其值)为参数 suid  
  • 非 root 进程,在参数 suid 为 EUID 或者备份 EUID 时,则将进程的 EUID 改为 suid; 
  •  不符合这些条件的,调用 setuid(2)时将返回-1 并置 errno 为 EPERM;
setgid(2)的使用情况也类似。
另外,还有 seteuid(2)和 setegid(2),它们仅改变进程的 EUID/EGID。

对于进程特权的改变,应遵循“使用能完成工作的最小特权”的原则,以避免用户进程越权操作。这个原则不应通过猜测程序模式是否有 SetUID 位而改变,典型的措施大致有:
  •   在不需要 SetUID 带来的权限时,使用 setuid(getuid())降低 EUID 的特权;
  • 在 setuid(2)之前,宜通过 geteuid(2)拿当前的 EUID 作备份,完成所需的工作时,再通过setuid(euid_backup)恢复;
  • 在子进程执行 exec(3)之前,应 setuid(getuid())以避免 SetUID 的进程传递特权;
7、system(3)函数
#include <stdlib.h>
int system(cont char *cmdstring);
这个函数使用/bin/sh执行指定的命令串执行标准的shell 命令。形如:
$ /bin/sh -c cmdstring

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

8、用于调度进程的函数
这几个函数不知为何没有在APUE2中提到,它们也是 POSIX.1 标准给出的库函数。Linux, Solaris, FreeBSD, AIX 等系统均支持这些函数。Linux Kernel Development 一书的“Chapter 4. Process Scheduling”对这几个 API 有具体的介绍。
#include <sched.h> 
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *p); 
int sched_getscheduler(pid_t pid); 
int sched_get_priority_max(int policy); 
int sched_get_priority_min(int policy);
int getpriority(int which, int who); 
int setpriority(int which, int who, int prio); 
int nice(int inc); 

sched_setscheduler (2)和 sched_getscheduler(2)分别设置和取得与某个特定进程相关的策略和参数。策略 policy 包括SCHED_OTHER、 SCHED_FIFO、SCHED_RR。后两者是用于特别看重时间的策略,会抢先于使用默认策略SHED_OTHER 的进程执行;
sched_get_priority_max(2)和 sched_get_priority_min(2)返回对于策略 policy 来说最大或最小的优先级。注意 policy 的值越小,其优先级越高;
setpriority(2)设置进程(which=PRIO_PROCESS)、进程组(which=PRIO_PGRP)、用户(which=PRIO_USER)的动态优先级;
getpriority(2)则返回匹配进程的最高优先级(最小值);
nice(2)通过为当前的进程优先级增加一个 inc 而降低其优先级。即 nice(2)的参数越大,将 CPU 让给其它进程使用的时间就越多(being nice to others)。
在 shell 中,也可以通过 nice(1)命令设置所执行命令的优先级。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值