1 UNIX进程环境
进程的入口:main( )
进程的退出:exit( ), _exit( )
进程的命令行参数:
main(int argc, char *argv[ ])
环境变量
环境变量与环境变量表
每个程序除了从命令行接收命令行参数外,还从环境接收,环境变量表,进程可从环境变量表中,访问环境变量。
环境变量的格式为:var_name=var_value
环境变量的传递方法为:
main(int argc, char *argv[ ], char *envp[ ] );
可以使用函数:getenv( )和putenv( )来访问环境变量。
getenv, putenv, setenv
#include <stdlib.h>
char *getenv(const char *name) ;
返回指向name的指针,比如PATH.
int putenv(const char *str) ;
int setenv(const char *name, const char *value, int rewrite) ;
返回值:若成功则为0,若出错则为非0
void unsetenv(const char *name) ;
无返回值。
使用环境变量的例子
#include <stdlib.h>
main(int argc, char *argv[ ], char *envp[ ]){
int i;
for(i=0; envp[i]!=NULL; i++)
printf("Var_env[%d]: %s\n", i, envp[i]);
printf("ENV:PATH=%s\n", getenv("PATH"));
printf(" ENV:HMOE=%s\n", getenv("HOME"));
}
函数间的跳转—长跳转
在C中,不允许使用跳越函数的goto语句。甚至有的学者建议,取消goto语句。
在UNIX系统中,函数setjmp和longjmp可实现函数间的跳转—长跳转。
这两个函数对于处理发生在很深的嵌套函数调用中的出错情况非常有用。
setjmp, longjmp
功能:实现函数间的跳转—长跳转。
用法:
#include <setjmp.h>
int setjmp(jmp_buf env) ;
返回值:若直接调用则为0,若从longjmp返回则为非0
void longjmp(jmp_buf env, int val) ;
第一个参数是在调用setjmp时所用的env;第二个val,是个非0值,它成为从setjmp处返回的值。使用第二个参数的原因是对于一个setjmp可以有多个longjmp。可以在按longjmp的调用顺序安排val值。
2 进程控制
每个进程都有一个非负整型的唯一进程ID。因为进程ID标识符总是唯一的,常将其用做其他标识符的一部分以保证其唯一性。
有某些专用的进程:
0#进程—是调度进程,常常被称为交换进程(swapper)。该进程并不执行任何磁盘上的程序—它是内核的一部分,因此也被称为系统进程。
1#进程—通常是init进程,在自举过程结束时由内核调用。该进程的程序文件在UNIX的早期版本中是/etc/init,在较新版本中是/sbin/init。此进程负责在内核自举后起动一个UNIX系统。init通常读与系统有关的初始化文件(/etc/rc*文件),并将系统引导到一个状态(例如多用户)。init进程决不会终止。它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。它也是所有孤儿进程的父进程。
与进程ID相关的系统调用或函数
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); 返回:调用进程的进程ID
pid_t getppid(void); 返回:调用进程的父进程ID
uid_t getuid(void); 返回:调用进程的实际用户ID
uid_t geteuid(void); 返回:调用进程的有效用户ID
gid_t getgid(void); 返回:调用进程的实际组ID
gid_t getegid(void); 返回:调用进程的有效组ID
fork()系统调用
功能:创建子进程。
用法:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
返回值:
在子进程中执行时为0;
在父进程中为子进程ID;
出错为-1。
关于fork的说明
由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是:
子进程的返回值是0;
父进程的返回值则是新子进程的进程I D。
将子进程ID返回给父进程的理由是:因为一个进程的子进程可以多于一个,所以没有一个函数使一个进程可以获得其所有子进程的进程ID。
fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,子进程总是可以调用getppid以获得其父进程的进程ID。
由于ID为0的进程总是由交换和调度进程使用,所以一个子进程的进程I D不可能为0。
父子进程的关系
子进程和父进程继续执行fork之后的指令。子进程是父进程的复制品。
(如果正文段是只读的,则)子进程与父进程共享。
子进程将继承父进程的所有资源,比如已经打开的文件等。(参见pp144-145)
fork后处理文件描述符的两种常见情况
(1) 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件位移量已做了相应更新。
(2) 父、子进程各自执行不同的程序段。在这种情况下,在fork之后,父、子进程各自关闭它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。这种方法是网络服务进程中经常使用的。
fork的两种用法
(1) 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待委托者的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
(2) 一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程在从fork返回后立即调用exec执行另一程序。
某些操作系统将( 2 )中的两个操作(fork之后执行exec)组合成一个,并称其为spawn。UNIX将这两个操作分开,因为在很多场合需要单独使用fork,其后并不跟随exec。另外,将这两个操作分开,使得子进程在fork和exec之间可以更改自己的属性。
fork()创建进程实例
main() {
int i;
printf("PPID=%d\n",getppid());
while((i=fork())==-1);
printf( "i=%d\n",i);
if(i){
sleep(3);
printf( "Parent!,PPID=%d,PID=%d\n",getppid(),getpid());
}
else{
printf( "Child!, PPID=%d, PID=%d\n",getppid(),getpid());
}
printf("I am finished!!!\n");
}
进程终止
进程有三种正常终止法及两种异常终止法。
(1) 正常终止:
(a) 在main函数内执行return语句。这等效于调用exit。
(b) 调用exit函数。其操作包括调用各终止处理程序,然后关闭所有标准I / O流等。因为ANSI C并不处理文件描述符、多进程(父、子进程)以及作业控制,所以这一定义对UNIX系统而言是不完整的。
(c) 调用_exit系统调用函数。此函数由exit调用,它处理UNIX特定的细节。
(2) 异常终止:
(a) 调用abort。它产生SIGABRT信号,所以是下一种异常终止的一种特例。
(b) 当进程接收到某个信号时。
退出码
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等等。
对于任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于exit和_exit,这是依靠传递给它们的退出状态(exit status)参数来实现的。在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(termination status)。
在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。
注意,这里使用了“退出状态”(它是传向exit或_exit的参数,或main的返回值)和“终止状态”两个术语,以表示有所区别。在最后调用_exit时内核将其退出状态转换成终止状态。
孤儿进程
正常情况下,父进程的结束,将导致其子进程的消亡。但是也存在父进程先于其子进程结束的情况,如果是这样,子进程就变为了孤儿进程。
UNIX系统中,所有孤儿进程将由init进程领养。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID)。保证了每个进程有一个父进程。
如果子进程在父进程之前终止,内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid 时,可以得到有关信息。这种信息至少包括进程ID、该进程的终止状态、以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储器,关闭其所有打开文件。
僵死进程
一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程(zombie)。
ps命令将僵死进程的状态打印为Z。
如果编写一个长期运行的程序,它fork了很多子进程,那么除非父进程等待取得子进程的终止状态,否则这些子进程就会变成僵死进程,而占有系统资源的情况。
一个由init进程领养的进程终止,该进程不会变成僵死进程,因为init被编写成只要有一个子进程终止,init就会调用一个wait/waitpid函数取得其终止状态。这样也就防止了在系统中有很多僵死进程。
当提及“一个init的子进程”时,这指的是init直接产生的进程,或者是其父进程已终止,由init领养的进程。
等待子进程结束:wait或waitpid
当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。
父进程可以忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。对于这种信号的系统默认动作是忽略它。
现在需要知道的是调用wait或waitpid的进程可能会:
阻塞(如果其所有子进程都还在运行)。
带子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终止状态)。
出错立即返回(如果它没有任何子进程)。
如果进程由于接收到SIGCHLD信号而调用wait,则可期望wait会立即返回。但如果在任一时刻调用wait,则进程可能会阻塞。
wait和waitpid
功能:等待了进程 结束
用法:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int * statloc) ;
pid_t waitpid(pid_t pid, int *statloc, int options) ;
返回值:若成功则为进程ID,若出错则为-1。
这两个函数的区别是:
在一个子进程终止前, wait 使其调用者阻塞,而waitpid 有一选择项,可使调用者不阻塞。
waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的进程。
参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。
参数statloc的值可由以下宏来检测:
WIFEXITED(status) 若为正常终止子进程返回的状态,则为真。
WIFSIGNALED(status) 若为异常终止子进程返回的状态,则为真。
WIFSTOPPED(status) 若为当前暂停子进程的返回的状态,则为真。
关于waitpid的参数说明
Pid:
pid == -1 等待任一子进程。与wait等效。
pid > 0 等待其进程ID与pid相等的子进程。
pid == 0 等待其组ID等于调用进程的组ID的任一子进程。
pid < -1 等待其组ID等于pid的绝对值的任一子进程。
options参数:使我们能进一步控制waitpid的操作。此参数或者是0,或者以下值按位或运算:
WNOHANG 若由pid指定的子进程并不立即可用,则waitpid不阻塞,此时其返回值为0
WUNTRACED 若某实现支持作业控制,则由pid指定的任一子进程状态已暂停,且其状态自暂停以来还未报告过,则返回其状态。WIFSTOPPED宏确定返回值是否对应于一个暂停子进程。
竞争条件
在竞争的条件下,先执行的进程不一定先结束。这取决于当时进程的执行环境。
因此,我们不能约定哪个进程在预定的时间内到达某个状态。
exec系列函数
用fork可以创建新进程,用exec可以执行新的程序。
有六种不同的exec函数可供使用,它们常常被统称为exec函数。这些exec函数都是UNIX进程控制原语。
exit函数和两个wait函数处理终止和等待终止。
exec
功能:执行新程序;
用法:
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ...);
int execv(const char *pathname, char *const argv [ ] );
int execle(const char *pathname, const char *arg 0, ... /* (char *)0, char *const envp[] */);
int execve(const char * pathname, char *const argv [ ], char *const envp [] );
int execlp(const char *filename, const char *arg0, ...);
int execvp(const char *filename, char *const argv [ ] );
返回值:
若出错则为- 1,若成功则不返回
system函数
功能:在程序中执行另一个程序。
用法:
#include <stdlib.h>
int system(const char *cmdstring) ;
返回值:
(1) 如果fork失败或者waitpid返回除EINTR之外的出错,则返回-1,而且errno中设置错误类型;
(2) 如果exec失败(表示不能执行shell ),则其返回值如同shell执行了exit (127 +rs)一样;
(3) 否则所有三个函数(fork , exec和waitpid)都成功,并且system的返回值是shell的终止状态。
示例:shell模拟程序
试按照shell程序的基本功能,利用UNIX系统提供的进程控制的系统调用,设计一个程序来模拟shell功能。要求至少要做到:
1)、从终端键盘接收命令,若是合法,则执行之;
2)、设置一条内部命令,比如echo,用于显示被执行命令的返回状态和它自己的参数。
用exec调用实现shell的处理过程。
(1)利用fork创建子进程。
(2)子进程利用exec启动命令程序。
(3)父进程利用wait和子进程同步。
shell程序实现的C语言描述
#include <stdio.h>
main()
{
char cmd[32],char *prompt=”$”;
while(printf(“\n%s”,prompt),gets(cmd)!=NULL)
{
if(fork()==O)
exec(cmd,cmd,(char *)O);
else
wait(O);
}
}
一个shell的实现实例
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
main(){
int i, j, k, vc, ret_code, len;
char *p, cmd[256];
char *arg[100];
char *incmd[ ]={“echo”,“exit”,“prompt”,NULL}
char prompt[11],*p1=prompt;
strcpy(prompt,“%”); //初始提示符$
while(printf("%s",p1),gets(cmd)!=NULL) {
len=strlen(cmd); for(i=len;i<100-1;i++) cmd[i]=0x00;
vc=-1;
k=validstr(cmd); //计算前导空格,k=-1 表示空行
if(k==-1&&vc==-1) continue; //空行继续
p=cmd+k; //跳过空格
while(1){ //处理以空格或tab分隔命令行参数
vc++;
j=onestr(p); //得到一个参数字符串(已置结束符)
arg[vc]=p; //argv[vc] 指向刚得到的参数
p=p+j+1; //p 指向下一个参数字符串或 ‘\0’
k=validstr(p);
if(k==-1) break; else p=p+k;
}
vc++;
for(i=vc;i<100;i++) arg[i]=NULL; //处理多余的arg
//以下几行处理内容命令,比如echo, exit, prompt等
if(!strcmp(arg[0],incmd[0])){//echo: 显示命令返回状态
printf("The last cmd return status:%d\n",ret_code);
continue;
}
if(!strcmp(arg[0],incmd[1])) exit(0); //exit
if(!strcmp(arg[0],incmd[2])){ //prompt: 改变提示符
if(arg[1]==NULL||strlen(arg[1])==0){ printf("Cmd error.\n Usage: prompt promt_str\n");
continue;
}else{ if(strlen(arg[1])>10){
printf("The New Prompt:%s too long!!!\n",arg[1]);
continue;
}else{ strcpy(prompt,arg[1]);continue; }
} }
while((i=fork())==-1);
if(i==0) execvp(arg[0],arg);//exec:执行命令
else{ wait(&ret_code);
} }}
validstr(char *x){ //White space counter
int i,L;
L=0;
for(i=0;x[i]!='\0';i++){
if((x[i]==' '||x[i]=='\t')){L++;continue;} else return L; }
return -1;}
onestr(char *x){
int i;
for(i=0;x[i]!='\0';i++){
if(!(x[i]==' '||x[i]=='\t')) continue;
else break; }
x[i]='\0';
return i;}