linux 进程控制
第八章 进程控制
1、进程标识符pid的概念
-
进程ID(pid)唯一的标识了系统中的当前进程;
-
已结束的进程,其pid以后将给信的进程使用,但一般不是马上;
-
0号进程(pid == 0)是内核的一部分,属于系统进程,其它进程均属于用户进程;
-
1号进程通常是init,是一个以root特权运行的系统进程,孤儿进程都将由init进程接管;
-
获取当前进程一些相关标识符的API:
#include <unistd.h>
pid_t getpid(void); /* 返回当前的pid */
pid_t getppid(void); /* 返回父进程的id */
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);
-
fork(2)生成一个新的进程,该进程是以当前进程的上下文为依据生成的子进程;fork返回0表示当前处于子进程中,而在父进程中则返回所生成的子进程的pid,失败时返回-1;
-
父进程和子进程的text段是共享的,但子进程实际上是从执行fork之后的代码段开始执行的;bss段、堆、栈等在现代的系统中通常使用写时复制(COW,copy-on-write)的技术;
-
fork之后是父进程还是子进程先被运行一般是不可知的。对于Linux,为避免父进程先执行引起不必要的COW(因为很多时候子进程将很快执行exec),生成新进程时曾试图使子进程先于父进程执行(参考《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的技术放弃了。
-
子进程生成时复制了父进程打开的文件描述符,包括stdin,stdout、stderr,应注意对这些资源的共享可能引起并发问题;
-
对于书中的程序清单8-1,编译后的程序在命令行下直接执行和重定向到文件中执行,字符串"before fork\n"在前者只输出一次而后者输出了两次的原因是:前者的stdout是字符设备tty,默认使用行缓冲,子进程生成之前stdout的缓冲区已经被由于输出带换行符而进行了冲洗;而后者的stdout是一个普通文件,默认使用全缓冲,子进程生成时尚未通过输出冲洗的stdout缓冲区通过fork复制给了子进程,父进程和子进程退出时exit(3)对缓冲区进行冲洗时输出了各自的"before fork\n";
-
父子进程的主要区别包括:
-
fork的返回值、pid、ppid不同;
-
子进程的tms、utime等相关时间统计信息在fork()后被清零;
-
子进程不继承父进程的记录锁;
-
子进程的未决闹钟(alarm(2))将被清除;
-
子进程不继承父进程的未决信号集;
-
-
关于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():返回真时表示子进程收到信号而导致异常终止;
-
WTERMSIG():返回导致子进程终止的信号;
-
WCOREDUMP():返回真时表示子进程异常终止并导致了内核转储;
-
(*)WIFSTOPPED():返回真时表示子进程处于停止状态;
-
(*)WIFCONTINUED():返回真时表示子进程进入暂停后继续的状态。
前面标准了星号的四个宏是互斥的,即它们只能同时有一个返回真值;
#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@t1000 ~/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函数调用时,指定的交互式命令所收到的参数依次为:#!后面的命令串、exec函数指定的可执行文件,exec函数指定的参数表;
6、setuid(2)和setgid(2)
通过fork(2)创建的子进程,其uid和euid将继承自父进程。用exec执行一个程序时,若该进程的程序文件有setuid位,则其euid(有效用户id)默认为文件属主的uid,否则继承自exec之前的上下文;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之前,应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)命令设置所执行命令的优先级。