第五章 基本进程编程
5.1 概述
UNIX系统为程序员提供了一个强有力的工具:在一个程序中执行另一个程序。执行一个程序最简单的途径就是使用库函数system。该函数使用一个参数:一个包含要被执行的命令的字符串。这一库函数的特点是用法简单,在程序中调用简单的UNIX命令时很有用。但是由于它的调用要由SHELL进程来实现,故效率并不高,在实际的编程中应用并不广泛。本章主要介绍在实际编程中经常使用的有关进程控制和管理方面的系统调用,它们包括:
fork - 创建一子进程 exec - 执行子进程 exit - 终止进程执行 wait - 等待子进程暂停或终止 setpgrp - 设置进程标识符 getpid、getppid - 获取进程标识符 setuid、setgid - 设置进程的用户标识符 getuid、geteuid、getgid、getegid - 获取进程的用户标识符
5.2 进程控制
1. fork系统调用
系统调用fork是UNIX操作系统创建新进程的唯一手段,习惯上将新创建的进程称为子进程,调用fork的进程称为父进程。 fork系统调用的格式为:
int fork()
fork系统调用没有参数,如执行成功,则创建一子进程,子进程继承了父进程的某些属性。当从该系统调用返回时,系统中已有两个用户级环境完全相同的进程存在。这两个进程从fork调用中得到的返回值不同,其中子进程得到的返回值为零,父进程得到的返回值是最新创建的子进程的进程标识符。
2. exec系统调用
fork系统调用只是将父进程的环境拷贝到新进程中,而没有启动执行一个新的目标程序。UNIX系统提供了exec系统调用,用它更换进程的执行映象,启动新的目标程序。例如:UNIX系统中的所有命令都是通过exec来执行的。
exec系统调用有六种不同的使用格式,但在核心中只对应一个调用入口。它们有不同的调用格式和调用参数。这六种调用格式分别为:
#include int execl (const char *path, const char *arg0, ..., const char *argn, (char *)0); int execv (const char *path, char *const *argv); int execle (const char *path, const char *arg0, ..., const char *argn, (char *0), const char *envp[]); int execve (const char *path, char *const *argv, char *const *envp); int execlp (const char *file, const char *arg0, ..., const char *argn, (char *)0); int execvp (const char *file, char *const *argv);
说明:参数path指出一个可执行目标文件的路径名;参数file指出可执行目标文件的文件名。arg0作为约定同path一样指出目标文件的路径名;参数arg1到argn分别是该目标文件执行时所带的命令行参数;参数argv是一个字符串指针数组,由它指出该目标程序使用的命令行参数表,按约定第一个字符指针指向与path 或file相同的字符串;最后一个指针指向一个空字符串,其余的指向该程序执行时所带的命令行参数;参数envp同argv一样也是一个字符指针数组,由它指出该目标程序执行时的进程环境,它也以一个空指针结束。
exec的六种格式在以下三点上有所不同: (1) path是一个目标文件的完整路径名,而file是目标文件名,它是可以通过环境变量PATH来搜索的; (2) 由path或file指定的目标文件的命令行参数是完整的参数列表或是通过一指针数组argv来给出的; (3) 环境变量是系统自动传递或者通过envp来给出的。
下图说明了exec系统调用的六种不同格式对以上三点的支持。
系统调用 参数形式 环境传送 路径搜索 Execl 全部列表 自动 否 Execv 指针数组 自动 否 Execle 全部列表 不自动 否 Execve 指针数组 不自动 否 Execlp 全部列表 自动 是 Execvp 指针数组 自动 是
3. exit、wait系统调用 (1) exit系统调用格式如下: #include void exit(int status); #include void _exit(int status); 说明:exit的功能是终止进程的执行,并释放该进程所占用的某些系统资源。参数status是调用进程终止时传递给其父进程的值。如果调用进程执行exit系统调用时,其父进程正在等待子进程暂停或终止(使用wait系统调用),则父进程可立刻得到该值;如果此时父进程并不处在等待状态,那么一旦父进程使用wait调用,便可立刻得到子进程传过来的status值,注意:只有status的低八位才传递给其父进程。
系统调用_exit与exit之间的差异是_exit只做部分的清除,因此建议不要轻易地使用这种调用形式。 每个进程在消亡前都要调用该系统调用,没有显示地使用该系统调用,则生成目标文件的装载程序为该进程隐含地做这一工作。
(2) wait系统调用格式如下: #include #include pid_t wait (int *statptr); 说明:wait系统调用将调用进程挂起,直到该进程收到一个被其捕获的信号或者它的任何一个子进程暂停或终止为止。如果在wait调用之前已有子进程暂停或终止,则该调用立刻返回。格式wait((int *)0) 的功能是等待所有子进程终止。Wait返回时,其返回值为该子进程的进程号。参数statptr的值为该子进程的终止原因:
1、 如果子进程暂停,statptr目的高八位存放使该子进程暂停的信号值(在第七章中介绍信号),低八位为0177 2、 如果子进程由于调用exit终止,则该值的低八位为0,高八位为子进程终止时,exit系统调用中参数status值的低八位; 3、 如果子进程因信号终止,该值的高八位为0,低八位为引起终止的信号值。此外如低七位为1,则表示产生了一个core文件。 下面我们来看一个例子,该例是一个fork、exec、exit和wait联合使用的一个实例程序,我们称之为feew.c:
/* feew.c */ main(argc, argv) int argc; char *argv[]; { int pid, stat; if (argc != 1){ if ((pid = fork()) == 0){ printf("Child pid = %d\n", getpid()); execl(argv[1], argv[1], 0); exit(5); } } pid = wait(&stat); printf("pid=%d, H_stat=%d, L_stat=%d\n", pid, stat>>8, stat&0xff); }
当命令行参数的个数不为1时,程序使用fork系统调用产生一个子进程。子进程通过系统调用getpid获得自己的进程标识符,然后调用exec执行命令行中用户提交的命令,如果exec执行失败,则子进程调用exit(5)终止。父进程使用wait系统调用等待子进程暂停或终止,然后输出从wait中返回的信息。下面以三种方式执行该程序: 1〕 不带命令行参数 % ./feew pid=-1, H_stat=0, L_stat=0 % 不产生子进程,从运行结果来看,当无子进程时,wait的返回值为-1。
2〕 带命令行参数,参数为合法的可执行命令 % ./feew /bin/date Child pid = 1725 1998年 2月16日(星期一) 15时59分14秒 CST pid=1725, H_stat=0, L_stat=0 % 产生子进程。子进程输出其进程标识符后,再调用exec执行从命令行中提交的命令(/bin/date),同时父进程等待子进程暂停或终止,然后输出从wait中得到的信息:子进程标识符或状态参数stat的高八位、低八位的内容。从中可以看到:子进程因调用一个隐含的exit(0)而终止,终止时传给父进程的值为0。
3〕 带命令行参数,但参数不合法 %./feew /etc/shudown Child pid = 1760 /etc/shutdown: 只有超级用户(root)能运行 /etc/shutdown。 pid=1760, H_stat=2, L_stat=0 % 子进程创建成功。但由于以普通用户的身份执行/etc/shutdown,因此exec失败,尔后调用exit(5)而终止;父进程调用wait得到返回值:子进程号和状态参数stat的高八位、低八位的内容。从执行结果可以看出:子进程因调用exit(5)而终止,终止时传给父进程的值为5。
5.3 进程管理
进程管理包括的面很广,诸如进程的用户标识符管理、进程标识符管理等。进程的用户标识符有两个:实际用户标识符(real user id)和有效用户标识符(effective user id),其对应的组标识符分别称为实际组标识符(real group id)和有效组标识符(effective groud id)。一般而言,进程的实际用户标识符为运行该进程的用户标识符,通常只用于系统记帐,其他功能由有效用户标识符来完成,如用有效用户标识符来完成对新创建文件赋予属性关系、检查文件的存取权限和利用kill系统调用向进程发送信号的权限。一般情况下,一进程的有效用户标识符和实际用户标识符是相等的,但系统允许改变进程的有效用户标识符。
1. 进程的用户标识符管理 UNIX系统提供了一组系统调用来管理进程的用户标识符,它们的使用形式是: #include #include uid_t getuid (void); uid_t geteuid (void); gid_t getgid (void); gid_t getegid (void); int setuid(uid_t uid); int setgid(gid_t gid); 说明:前四个系统调用没有参数,分别返回调用进程的实际用户标识符、有效用户标识符、实际用户组标识符和有效组标识符。这些系统调用的执行总能获得成功,不会发生任何错误。系统调用setuid和setgid用于设置进程的实际用户(组)标识符和有效用户(组)标识符,如调用进程的有效用户标识符是超级用户标识符,则将调用的进程实际用户(组)标识符和有效用户(组)标识符设置为uid或gid;如调用进程的有效用户标识符不是超级用户标识符,但其实际用户(组)标识符等于uid或gid时,则其有效用户(组)标识符被设置为uid或gid;否则setuid或setgid调用失败。系统调用setuid或setgid调用成功时返回0,失败时返回-1。
2. 进程标识符管理 UNIX系统使用进程标识符来管理当前系统中的进程。为对具有某类似特性的进程统一管理,系统又引入了进程组的概念,以组标识符来区别进程是否同组。进程的组标识符是从父进程继承下来的,所以,通常进程的组标识符就是和它相关联的注册进程的标识符。进程的标识符是由系统为之分配的,不能被修改;组标识符可通过setpgrp系统调用修改。
相关系统调用的格式如下: #include #include pid_t getpid(void); pid_t getpgrp(void); pid_t getppid(void); pid_t getpgid(pid_t pid); 说明:前三个系统调用分别返回调用进程的进程标识符、进程组标识符和其父进程标识符。它们总能成功地返回。第四个调用置进程组标识符,它将调用进程的进程组标识符改为调用进程的进程标识符,使其成为进程组首进程,并返回这一新的进程组标识符。
下面我们来看一个实例: /* setuid.c */ main(argc, argv) int argc; char *argv[]; { int ret, uid; uid = atoi(argv[1]); printf("Before uid=%d, euid=%d\n", getuid(), geteuid()); ret = setuid(uid); printf("After uid=%d, euid=%d\n", getuid(), geteuid()); printf("ret = %d\n", ret); }
下面分三种情况讨论该程序的执行: 1、 如果执行该程序的用户为超级用户,则只要命令行所给的用户标识符大于0,无论所给的用户标识符是否存在,执行总能成功。 #./setuid 3434 Before uid=0, euid=0 After uid=3434, euid=3434 ret = 0 #
结果分析:将进程的实际和有效用户标识符均改为3434。
2、 如果执行该程序的用户为一般用户,用id命令得到用户uid和gid后,再调用该程序,过程如下: %id uid=1111(yds) gid=20(user) %./setuid 3434 Before uid=1111, euid=1111 After uid=1111, euid=1111 ret = -1 %./setuid 1111 Before uid=1111, euid=1111 After uid=1111, euid=1111 ret = 0 %
结果分析:当命令行参数为1111时,setuid执行成功,因为用户的uid就是1111。
值得注意的是:注册程序login 是个典型的setuid系统调用程序,login进程的有效用户是超级用户,该进程在建立用户的Shell进程前,调用setuid将实际和有效用户标识符调整为注册用户的用户实际和有效标识符。
| | |