标签:
这是本专栏中进程相关的系统调用的最后一篇,用2个实例演示了以往学习的内容。其一是Mini Shell,仿常用的Bash而做,但对其作了大大简化;其二是一个Daemon程序,可以使读者一窥服务器编程的端倪。
对Linux不是太陌生的读者都应该对Shell有一定的了解,就是这个程序在我们登陆后自动执行,打印出一个$符号,然后等待我们输入命令。Linux下最常用的Shell应用程序是Bash,绝大部分Linux发行版默认安装的都是它。下面我们也来亲手编写一个Shell程序,这个Shell远远不如Bash复杂,但也能满足我们一般的使用,下面,我们就开始。
首先,给这个Shell取一个名字,不妨就叫做Mini Shell。
Linux系统的命令分为内部命令和外部命令两种,内部命令由Shell程序实现,如cd、echo等,Linux的内部命令数量有限,而且绝大部分都很少用到。而每一个Linux外部命令都是一个单独的应用程序,我们非常熟悉的ls、cp等绝大多数命令都是外部命令,这些命令都以可执行文件的形式存在,绝大部分放在目录/bin和/sbin中。这样一来,我们编程的难度就可以大大下降了,我们只需要实现很有限的内部命令,对于其它的输入,统统当作应用程序来执行即可。
为了简单明了起见,Mini Shell只实现了2个内部命令:
1、cd 用于切换目录,和我们熟悉的命令cd类似,除了没有那么多的附加功能。
2、quit 用于退出Mini Shell。
下面是程序清单:
/* mshell.c */ #include <sys/types.h> #include <unistd.h> #include <sys/wait.h> #include <string.h> #include <errno.h> #include <stdio.h> void do_cd(char *argv[]); void execute_new(char *argv[]); main() { char *cmd=(void *)malloc(256*sizeof(char)); char *cmd_arg[10]; int cmdlen,i,j,tag; do{ /* 初始化cmd */ for(i=0;i<255;i++) cmd[i]=‘\0‘; printf("-=Mini Shell=-*| "); fgets(cmd,256,stdin); cmdlen=strlen(cmd); cmdlen--; cmd[cmdlen]=‘\0‘; /* 把命令行分解为指针数组cmd_arg */ for(i=0;i<10;i++) cmd_arg[i]=NULL; i=0; j=0; tag=0; while(i<cmdlen && j<10){ if(cmd[i]==‘ ‘){ cmd[i]=‘\0‘; tag=0; }else{ if(tag==0) cmd_arg[j++]=cmd+i; tag=1; } i++; } /* 如果参数超过10个,就打印错误,并忽略当前输入 */ if(j>=10 && i<cmdlen){ printf("TOO MANY ARGUMENTS\n"); continue; } /* 命令quit:退出Mini Shell */ if(strcmp(cmd_arg[0],"quit")==0) break; /* 命令cd */ if(strcmp(cmd_arg[0],"cd")==0){ do_cd(cmd_arg); continue; } /* 外部命令或应用程序 */ execute_new(cmd_arg); }while(1); } /* 实现cd的功能 */ void do_cd(char *argv[]) { if(argv[1]!=NULL){ if(chdir(argv[1])<0) switch(errno){ case ENOENT: printf("DIRECTORY NOT FOUND\n"); break; case ENOTDIR: printf("NOT A DIRECTORY NAME\n"); break; case EACCES: printf("YOU DO NOT HAVE RIGHT TO ACCESS\n"); break; default: printf("SOME ERROR HAPPENED IN CHDIR\n"); } } } /* 执行外部命令或应用程序 */ void execute_new(char *argv[]) { pid_t pid; pid=fork(); if(pid<0){ printf("SOME ERROR HAPPENED IN FORK\n"); exit(2); }else if(pid==0){ if(execvp(argv[0],argv)<0) switch(errno){ case ENOENT: printf("COMMAND OR FILENAME NOT FOUND\n"); break; case EACCES: printf("YOU DO NOT HAVE RIGHT TO ACCESS\n"); break; default: printf("SOME ERROR HAPPENED IN EXEC\n"); } exit(3); }else wait(NULL); }
这个程序稍稍有点长,我们来对它作一下详细的解释:
函数main:
14行:定义字符串cmd,用于接收用户输入的命令行。
15行:定义指针数组cmd_arg,它的形式和作用都与我们熟悉的char *argv[]一样。
从以上2个定义可以看出Mini Shell对命令输入的2个限制:首先,用户输入的命令行必须在255个字符之内(除去字符串结束标志‘\0‘);其次,命令行的参数个数不得超过10个(包括命令本身在内)。
18行:进入一个do-while循环,这个循环是本程序的主体部分,基本思想是"等待输入命令--处理已输入命令--等待输入命令"。
22行:打印输入提示信息。在Mini Shell中,你可以随意定自己喜欢的命令输入提示信息,本程序中使用了"-=Mini Shell=-*| ",是不是有点像一个CS高手?如果不喜欢,你可以用任意的字符替换它。
23行:接收用户输入。
25-27行:fgets接受输入时,会将输入字符串时末尾的换行符("\n")一起接受,这是我们不需要的,所以要把它去掉。本程序中简单的用字符串结束标志‘\0‘覆盖了字符串cmd的最后一个字符来实现这个目的。
30行:初始化指针数组cmd_arg。
32-42行:对输入进行分析,将cmd中参数间的空格用‘\0‘填充,并把各参数的起始地址分别赋与cmd_arg数组。这样就把cmd分解成了cmd_arg,但分解后的各命令参数仍然使用着cmd的内存空间,所以在命令执行结束前不宜对cmd另外赋值。
45行:如果还未分析到输入字符串的末尾(i<cmdlen),而分析出的参数已经达到或超过了10个(j>=10),就认为输入的命令行超出了10个参数的限制,打印错误并重新接收命令。
51-52行:内部命令quit:字符串cmd_arg[0]就是命令本身,如果命令是quit,则退出循环,也就等于退出该程序。
55-58行:内部命令cd:调用函数do_cd()完成cd命令的动作。
61行:对于其它的外部命令和应用程序,调用函数execute_new()执行。
函数do_cd:
68行:仅仅考虑紧跟在命令后面的参数argv[1],而不再考虑其它的参数。如果这个参数存在,就把它作为要转换的目录。
69行:调用系统调用chdir切换当前目录,参见附录1。
70-82行:对chdir可能出现的错误进行处理。
函数execute_new:
92行:调用系统调用fork产生新的子进程。
93行:如果返回负值,说明fork调用出现错误。
96行:如果返回0,说明当前进程是子进程。
97行:调用execvp执行新的应用程序,并检测调用是否出错(返回负值)。这里使用execvp的原因是它可以自动在各默认目录里寻找目标应用程序的位置,而不必我们自己编程实现。
98-107行:对execvp可能出现的错误进程处理。
108行:如果execvp的执行出现错误,子进程在这里终止。表面上看起来,这个exit是接着97行的错误判断的下一行语句,而非if语句的一部分,似乎无论调用execvp成功与否都会接着执行exit。但事实上,如果execvp调用成功的话,这个进程将会被新的程序代码填充,因而根本不可能执行到这一行。反之,如果执行到了这一行,说明前面的execvp调用一定出现了错误。这样的效果和exit被包含在if语句中的效果是完全一样的。
109行:如果fork返回其它值,说明当前进程是父进程。
110行:调用系统调用wait。wait在这里有两个作用:
关于Mini Shell的编译和运行,这里就不再敷述了,有兴趣的读者可以自行动手实验,或者对这个程序进行改进,使之更接近甚至超过我们正使用的Bash。
这又是一个有趣的概念,daemon在英语中是"精灵"的意思,就像我们经常在迪斯尼动画里见到的那些,有些会飞,有些不会,经常围着动画片的主人公转来转去,啰里啰唆地提一些忠告,时不时倒霉地撞在柱子上,有时候还会想出一些小小的花招,把主人公从敌人手中救出来,正因如此,daemon有时也被译作"守护神"。所以,daemon进程在国内也有两种译法,有些人译作"精灵进程",有些人译作"守护进程",这两种称呼的出现频率都很高。
与真正的daemon相似,daemon进程也习惯于把自己隐藏在人们的视线之外,默默为系统做出贡献,有时人们也把它们称作"后台服务进程"。daemon进程的寿命很长,一般来说,从它们一被执行开始,直到整个系统关闭,它们才会退出。几乎所有的服务器程序,包括我们熟知的Apache和wu-FTP,都用daemon进程的形式实现。很多Linux下常见的命令如inetd和ftpd,末尾的字母d就是指daemon。
为什么一定要使用daemon进程呢?Linux中每一个系统与用户进行交流的界面称为终端(terminal),每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端(Controlling terminal),当控制终端被关闭时,相应的进程都会被自动关闭。关于这点,读者可以用X-Window中的XTerm试验一下,(每一个XTerm就是一个打开的终端,)我们可以通过键入命令启动应用程序,比如:
$netscape
然后我们关闭XTerm窗口,刚刚启动的netscape窗口也会随之一同突然蒸发。但是daemon进程却能够突破这种限制,即使对应的终端关闭,它也能在系统中长久地存在下去,如果我们想让某个进程长命百岁,不因为用户或终端或其他的变化而受到影响,就必须把这个进程变成一个daemon进程。
如果想把自己的进程变成daemon进程,我们必须严格按照以下步骤进行:
下面,就然我们亲眼看一个daemon进程的诞生:
/* daemon.c */ #include<unistd.h> #include<sys/types.h> #include <sys/stat.h> #define MAXFILE 65535 main() { pid_t pid; int i; pid=fork(); if(pid<0){ printf("error in fork\n"); exit(1); }else if(pid>0) /* 父进程退出 */ exit(0); /* 调用setsid */ setsid(); /* 切换当前目录 */ chdir("/"); /* 设置文件权限掩码 */ umask(0); /* 关闭所有可能打开的不需要的文件 */ for(i=0;i<MAXFILE;i++) close(i); /* 到现在为止,进程已经成为一个完全的daemon进程, 你可以在这里添加任何你要daemon做的事情,如: */ for(;;) sleep(10); }
编译和运行的任务就交给读者们自己完成。daemon进程不像其他进程一样有很抢眼的运行结果,基本上它只是毫不声张地做自己的事。你不可能看到任何东西,但可以用"ps -ajx"命令观察一下你的daemon进程的状态和一些参数。
#include <unistd.h> int chdir(const char *path);
chdir的作用是改变当前工作目录。进程的当前工作目录一般是应用程序启动时的目录,一旦进程开始运行后,当前工作目录就会保持不变,除非调用chdir。chdir只有1个字符串参数,就是要转去的路径。例如:
chdir("/");
进程的当前路径就会变为根目录。
#include <unistd.h> pid_t setsid(void);
一个会话(session)开始于用户登陆,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话,除非进程调用setsid系统调用。
系统调用setsid不带任何参数,调用之后,调用进程就会成立一个新的会话,并自任该会话的组长。
#include <sys/types.h> #include <sys/stat.h> mode_t umask(mode_t mask);
系统调用umask可以设定一个文件权限掩码,用户可以用它来屏蔽某些权限,以防止误操作导致给予某些用户过高的权限。
标签:
原文地址:http://www.cnblogs.com/yiyide266/p/5538116.html