MP77的UNIX课件笔记(9)

本章和下一章讨论UNIX进程控制和进程间通信机制和相关System call。从“操作系统原理”中我们了解到,进程是OS进行系统资源管理的基本单位。其具体的内容则不属于本章讨论范围,但我们假设读者已经阅读过有关操作系统进程管理方面的文献。

1 进程控制

进程是如何创建的?我们以日常工作的shell平台为起点展开讨论。

1.1 用程序运行程序

简单地说,shell是用于管理程序的系统工具,其实质也是一个程序。

用程序运行程序的思想来实现一个简单shell,我们想到被称为exec的System call。exec的特点是:

1、exec创建执行其它程序的进程,而不必返回。

2、这些例程简单的把新程序覆盖在旧程序上,并以调用者提供的参数去执行这个新代码。

下面是一段使用exec运行ls的程序示例:

main()

{

/*the definition below can also be made like this:

char args[4]={“ls”,“-l”,“/usr/bin”,“(char)0”}*/

char *args[4];

args[0] = “ls”; / build the arglist /

args[1] = “-l”;

args[2] = “/usr/bin”;

args[3] = (char ) 0; / terminate with NULL */

printf(“about to execute ls -l /usr/bin\n” );

execvp( “ls” , args );

printf( “that was ls, cool, eh?\n”);

}

exec包含六种形式,除了建立在exec基础上之外:

l与v,指定命令行参数的两种方式,l代表的形式在函数参数中列举出命令行参数,v要实现组织成一个数组。函数参数中的命令行参数表,参数数目可变,所以最后一个要以0为结束标识。

e,用envp数组的内容初始化环境参数,否则使用与当前相同的环境参数environ。

p,函数第一个参数,是程序文件名,在环境变量PATH指定的多个查找目录中查询可执行文件,否则不按PATH指定路径自动搜索。

由于exec覆盖源进程直接运行目标程序,因此上段C代码中的最后一行输出that was ls,cool,eh?并不会显示在标准输出。

很明显,仅仅使用exec无法满足我们的需求,这种需求主要表现在:

1、为了保护旧程序,要把它分割成两个副本:其中一个被覆盖,而另一个则等待新的覆盖进程的结束。

2、调用成功不返回;失败返回。

为了实现以上功能,需要建立新的进程供exec调用。

1.2 进程创建

Unix涉及进程创建的基本System call是int fork()。fork()调用成功内核建立一个新进程。新进程是调用fork()的进程的副本。即新创建的进程继承了其父进程的环境。

新进程(子进程)运行与创建者(父进程)一样的程序,其中的变量与创建进程的变量相同.

父进程与子进程并发执行,哪一个进程能够占用CPU由进程调度程序决定。它们都是从fork()调用后的那条语句开始执行。例如程序段:

main()

{int pid;

printf(“for example”);

pid=fork();

printf(“fok system calll”);

……

}

标准输出中至少会出现fok system call * 2的显示,且表象上根本无法分别“父”与“子”。简单地说,父子进程间互不影响其数据堆栈的情况。

注意到fork的返回值为整型,实际上就是子进程的process id。也就是说,fork会同时返回两个分别指示父子进程的pid。其中:

1、父进程的返回值为子进程的PID;

2、子进程的返回值为0;

3、出错返回值为-1,并将errno置为相应值。

include

main(){

int ret_from_fork, my_pid;

my_pid = getpid();

printf(“Hi, my pid is %d\n”, my_pid );

ret_from_fork = fork();

sleep(1);

printf(“after fork(). It returned %d and my pid is %d\n”,

ret_from_fork, getpid());

}

本段程序演示了fork父子进程的pid状态。至于父子进程标识谁最先显示,完全由CPU决定。

1.3 空闲进程

现在回过头来再看shell,似乎心中有数了许多。紧接着问题来了,我们现在知道shell程序通过创建子进程运行新程序,那么此时父进程处于什么地位?子程序运行结束后又是如何回退至父进程的?

显然父进程需要做的是在子进程运行过程中等待,直到子进程结束。这就需要用到/usr/include/sys/wait.h中的System call了。

pid_t wait(int *status);

其中pid_t定义在/usr/include/sys/types.h中,返回值代表已终止的子进程的pid号。status的作用是返回一个整型值,其代表了子进程终止的原因。子进程的终止一般有两种情况:“自杀”和“被杀”,“自杀”即自愿终止,在程序中调用函数exit()或主函数return均可。“被杀”是指由其它进程或者操作系统内核向进程发送信号将其杀死。第一种情况可以获得进程正常终止的返回码,第二种情况可以获得进程被杀死的信号值,“信号值”会在下一篇文章讨论。

在/usr/include/sys/wait.h中定义了几个宏支持返回status的调用:

WIFEXITED(status),如果进程正常终止,则为真。调用WEXITSTATUS(status)可以返回子进程的返回码。

WIFSIGNALED(status),如果进程异常终止,则为真。调用WTERMSIG(status)获得子进程被杀的信号值。

严格说来,status的低8位反映了子进程的终止状态:0表示子进程正常结束,非0表示出现了各种各样的问题。status高8位带回了exit的返回值。低7位记录信号序号,bit 7 用来指明发生了错误并产生了内核映像(core dump)。

1.4 简单shell的实现

至此,我们完全有能力制作一个简单的仿真shell程序了,尽管尚无法实现有关信号、文本编辑器以及一些其它特殊功能的调用。

从本篇开始,我们将构建一个shell及其基本系统命令的C语言程序,此项工作会持续到本系列连载的结束。

/ example of simple shell /

include

include

define MAXARGS 20 / cmdline args /

define ARGLEN 100 / token length /

main()

{char arglist[MAXARGS+1]; / an array of ptrs */

int numargs; / index into array /

char argbuf[ARGLEN]; / read stuff here /

char makestring(); / malloc etc */

numargs = 0;

while ( numargs < MAXARGS )

{ printf(“Arg[%d]? ”, numargs);

if ( fgets(argbuf, ARGLEN, stdin) && *argbuf != ‘\n’ )

arglist[numargs++] = makestring(argbuf);

else

{ if ( numargs > 0 ) / any args? /

{ arglist[numargs]=NULL; / close list /

execute( arglist ); / do it /

numargs = 0; / and reset /

}

}}

return 0;}

execute( char *arglist[] )

{ int pid, exitstatus; / of child /

pid = fork(); / make new process /

switch( pid ){

case -1:

perror(“fork failed”);

exit(1);

case 0:

execvp(arglist[0], arglist); / do it /

perror(“execvp failed”);

exit(1);

default:

while( wait(&exitstatus) != pid );

printf(“child exited with status %d,%d\n”,

exitstatus>>8, exitstatus&0377);

}

}

char makestring( char buf )

/*

  • trim off newline and create storage for the string

*/

{

char cp, malloc();

buf[strlen(buf)-1] = ‘\0’; / trim newline /

cp = malloc( strlen(buf)+1 ); / get memory /

if ( cp == NULL ){ / or die /

fprintf(stderr,“no memory\n”);

exit(1);

}

strcpy(cp, buf); / copy chars /

return cp; / return ptr /

}

/ end of example /

1.5 字符串解析的C语言细节处理

通过上述程序我们可以获得一种对输入字符串进行格式解析的方法,事实上C语言库函数中即给出了一些极为方便的函数调用。

char strtok(char string, char *tokens);

1、给定字符串string以及“单词”分界符的集合tokens,从字符串中分析出一个“单词”。忽略字符串中连续多个的单词分隔符。

2、函数返回值是指向单词的首字符的指针。

3、函数的副作用是,会修改原先给定的字符串存储空间中的内容。

以下是strtok运行原理解析:

首先键盘输入 who am i后按enter,应当注意who之前有一个空格和一个制表符(tab),who和am之间有两个空格。

下面是用fgets(s, sizeof s, stdin)读入到内存中的字符串s的存储结构,每个字节用十六进制列出。

\t w h o a m i \n \0

s 20 09 77 68 6f 20 20 61 6d 20 69 0a 00

第一次执行p=strtok(s,“ \t\n”)之后的状态:

\t w h o a m i \n \0

s 20 09 77 68 6f 00 20 61 6d 20 69 0a 00

p

第二次执行p=strtok(NULL,“ \t\n”)后的状态:

\t w h o a m i \n \0

s 20 09 77 68 6f 00 20 61 6d 00 69 0a 00

p

第三次执行p=strtok(NULL,“ \t\n”)后的状态:

\t w h o a m i \n \0

s 20 09 77 68 6f 00 20 61 6d 00 69 00 00

p

这时,strtok扫描结束的位置指向了字符串尾部的’\0’,第四次再执行strtok就会返回NULL,表示找不到新的单词。

1.6 另类的进程创建方法

vfork()创建一个进程,而新进程的目的是exec一个新程序。vfork与fork一样都创建一个子进程,但它并不把父进程的地址空间完全复制到子进程,因为子进程会立即调用exec(或exit),于是也就不访问该地址空间,不过它在exec(或exit)调用之前,它在父进程空间运行。

vfork()保证子进程先运行,在它调用exec(或exit)之后父进程才运行。

这里需要明确两个终止操作的区别exit和exit,它们均定义在/usr/include/unistd.h中,不同的是前者属于C语言库函数的范畴,而exit属于System call,也就是说,在具体实现中,exit最终仍需要调用_exit。

但exit终止进程时比系统调用exit多做一些操作。这些操作主要是自动关闭用fopen等函数创建的缓冲I/O文件,将缓冲区里的数据写到磁盘上。exit释放缓冲文件的文件缓冲区和缓冲文件所需要的数据结构,因为库函数相关的这些数据都存放在进程的数据区内,这会影响子进程借用的父进程的数据段,所以,当采用vfork()创建进程时,必须直接使用系统调用exit。

例如下段程序:

cat vf.c

int main(void){

int a = 1;

if (vfork() == 0) {

a++;

_exit(0);

} else {

a++;

}

printf(“a=%d\n”, a);

}

cc vf.c -o vf

./vf

a=3

显然子进程使用了父进程的数据区进行操作。

1.7 另类的程序运行方法

在C语言标准库中还定义了这样的函数,其本质上使用了fork、exec、wait等System call,用于执行某一行命令。

int system(char *cmd)

其中cmd是可执行程序的名称。该系统调用的功能是执行参数cmd所指定的命令。正确返回0,否则返回错误代码(非0值)。它的执行与命令行键入的命令具有同样的效果。

现在考虑如下实际问题:

编程时希望在程序中获得系统当前的IP转发路由表,一时又找不到编程接口,但是,执行netstat -rn命令后可以获得这个表格。选项r指的是路由表,选项n指的是输出IP地址时用数字形式,而不是自动将地址转换为主机名。

将命令输出重定向到一个临时文件中,然后从程序中读这个文件的内容,获取所希望的信息。最后,把文件删除。具体实现如下:

cat cmd.c

include

main(){

char fname[256], cmd[256], buf[256];

FILE *f;

tmpnam(fname);

sprintf(cmd, “netstat -rn > %s”, fname);

printf(“Execute \”%s\“\n”, cmd);

system(cmd);

f = fopen(fname, “r”);

while(fgets(buf, sizeof buf, f))

printf(“%s”, buf);

fclose(f);

printf(“Remove file \”%s\“\n”, fname);

unlink(fname);

}

唯一需要解释的是char * tmpnam(char *str);

其作用是获得一个临时文件的文件名,库函数会保证这个文件名与已有文件名不冲突。

库函数sprintf的用法和printf类似,只是打印的字符存到了字符串而不是显示在终端输出。sprintf使用和printf一样的格式控制字符串,程序员还经常用它将二进制数字转换成ASCII码串。

程序把system(cmd)得到的文件打印出来。需要时,程序应该解读这个文件,获得所需要的信息。

1.8 进程信息获取命令

ps(process status)命令列出系统中进程的当前状态,实际上就是将进程系统数据段中的部分进程属性有选择地打印出来。不同的UNIX系统,ps命令的使用也有些差别。

-a (all)列出所有当前终端上启动的进程;

-t (tty)指定一个终端名字,列出这个终端上启动的所有进程,如ps -t pts/1;

-u (User)指定一个用户名,列出这个用户的所有进程,如ps -u jiang;

-e (everything)列出系统中所有的进程;

-f (full)以full格式列出进程;

-l (long)以long格式列出进程。

下一节将介绍进程间通信的相关原理和方法。