您的位置:首页 > 运维架构 > Shell

自己动手写一个简单的Shell之二:运行程序

2009-08-17 22:39 573 查看
通过前面的学习我们知道了什么是Shell以及它的分类和功能,现在我们将一起学习Shell是怎么执行程序的,也就是Shell的第一个功能。在这里我们首先理解Shell怎么执行程序,学习相关知识(什么是进程、如何创建进程、如何运行程序),然后实现这个功能,这样一个简单的Shell就诞生了(我把这个Shell叫A Shell,简称ash)。

什么是进程

Linux是如何运行程序的?这看起来很容易:首先登录,然后Shell打印提示符,输入命令并按回车键,程序立即就开始运行了。当程序结束后,Shell打印一个新的提示符。但这些是如何实现的呢?Shell做了些什么?内核又做了些什么?程序是什么?运行一个程序意味着什么?

一个程序是存储在文件中的机器指令序列。运行一个程序意味着将这个机器指令序列载入内存然后让处理器逐条执行这些指令。在Linux术语中,一个可执行程序是一个机器指令及其数据的序列。一个进程是程序运行时的内存空间和设置,它由进程控制块、程序段和数据段三部分组成。简单的说,进程就是运行中的程序。

我们可以使用ps命令查看用户空间的内容,这个命令会列出当前的进程。这里有两个进程在运行:bash(Shell)和ps命令。每个进程都有一个可以唯一标识它的数字,称为进程ID。一般简称PID。ps有很多选项,和ls命令一样,ps支持-a、-l选项。-a选项列出所有进程,但是不包括Shell;-l选项用来打印更多细节。

[


名为S的一列表示各个进程的状态。S列的值为R说明该进程正在运行,值为S说明该进程处于睡眠状态。每个进程都属于由UID指明的用户,每个进程都有一个进程ID,同时也有一个父进程ID(PPID)。标记为PRI和NI的列分别是进程的优先级和niceness级别。标记为TTY的列表示与进程相连的终端,值为?就表示该进程为系统进程。

Shell是如何运行程序的

Shell打印提示符,用户输入命令,Shell就执行这个命令,然后Shell再次打印提示符——如此反复。那么Shell到底是怎么运行程序的呢?一个Shell的主循环执行下面的4步:

1、 接受命令

2、 建立一个新的进程来运行这个命令

3、 将程序从磁盘载入

4、 程序在它的进程中运行直到结束

例如我们依次输入ls和ps命令,那么下图就表示事件发生的次序。



Shell从用户那读入字符串ls。Shell建立一个新的进程,接着在新进程中运行ls程序并等待新进程结束。然后Shell读入新的一行输入,建立一个新进程,在这个进程中运行程序并等待这个进程结束。从图2可以看出,要实现这个流程,我们就需要解决三个问题:如何建立新进程,父进程如何等待子进程结束以及如何在一个程序中运行另一个程序。

如何建立新进程

我们可以使用fork()系统调用来创建进程。由fork创建的新进程被称为子进程。fork成功调用后,就会存在两个进程,一个是父进程,另一个是子进程。子进程是新进程,是父进程的副本,简单的说父进程有的东西子进程也都复制了一份。下图显示了进程调用fork前后发生了什么。



从图上可以看出,内核通过复制父进程来创建子进程,它将父进程的代码和当前运行到的位置都复制给子进程。其中当前运行的位置是由随着代码向下移动的箭头表示的。子进程从fork返回的地方开始运行。fork返回后,父子进程有相同的代码,运行到同一行有相同的数据和进程属性。这时我们通过fork的返回值来分辨父子进程。不同的进程,fork的返回值是不同的。在子进程中fork返回0,在父进程中fork返回子进程的pid。

父进程如何等待子进程结束


进程调用wait可以等待子进程结束,使用方法是:pid = wait(&status);这里系统调用wait做两件事。首先,wait暂停调用它的进程直到子进程结束。然后,wait取得子进程结束时exit的值。下图显示了wait是如何工作的。



当子进程调用exit,内核唤醒父进程同时将传递exit的参数。图中从exit的括号到父进程的箭头表示唤醒和传递exit值的动作。这样wait执行两个操作:通知和通信。通知就是告诉父进程子进程已经结束了,wait的返回值是调用exit的子进程的PID,因此父进程总是可以找到是哪个子进程终止了。通信就是告诉父进程子进程是以何种方式结束的,终止进程的终止状态通过wait的参数返回。wait系统调用的参数status是一个整形指针,如果status不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。一个进程有3种结束方式:

1、 顺利完成它的任务。在Linux中,成功的程序调用exit(0)或者从main函数中return 0。

2、 进程失败。程序遇到问题而要调用exit退出时,程序需要传给exit一个非零的值。这个值由程序员分配。

3、 程序被一个信号杀死。通常情况下,一个既没有被忽略又没有被捕获的信号会杀死进程。

如何在一个程序中运行另一个程序

在Linux系统中一个函数族可以解决这个问题——exec函数族(下面简称exec)。exec一共包含六个成员函数,每个函数都通过系统调用execve来调用内核服务。当进程调用exec执行一个程序时, exec系统调用从当前进程中把当前程序的机器指令清除,然后在空的进程中载入调用时指定的程序代码,最后运行这个新的程序。也就说exec就像换脑,原来的进程被将要执行的程序替换。下面是这个函数族的一些成员简介:

int execl(char *pathname, char *arg0,...,argn,(char*)0)

int execv(char *pathname, char *argv[])

int execle(char *pathname, char *arg0,...,argn,(char*)0,envp)

int execve(char *pathname, char *argv,char *const envp[])

int execvp(char *filename, char *argv[])

int excelp(char *filename, char *arg0,…,argn,(char*)0)

我们这里主要使用的是execvp函数,execvp有两个参数:要运行的程序名和那个程序的命令行参数数组。当程序运行时命令行参数以argv[]传给程序。注意:将数组的第一个元素置为程序的名称,最后一个元素必须是null。

execlp不像execvp那样用一个参数数组。execlp和execvp中的p代表路径(path),这两个函数在环境变量PATH中列出的路径中查找由第一个参数指定的程序。除了不在PATH中查找程序文件外,execv和execvp非常相似。这6个exec函数的参数很难记忆,我们可以根据函数名中的字符来记忆。字母p表示该函数取filename作为参数,并且用PATH环境变量寻找可执行文件。字母l表示该函数取一个参数表,v表示该函数取一个argv[]数组。最后,字母e表示该函数取envp[]数组,而不是当前环境。

到此,我们就可以写出一个简单Shell了。这个简单Shell实现了Shell三大功能中的运行程序的功能,我们这里把它编译成ash。ash接受命令名称、参数列表、运行命令、报告结果,然后再重新接受和运行其他程序。这里ash由于没有实现管理输入输出的功能,因此用户还不能在一行中输入所有参数。下面是相关源码:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#define MAXARGS  20
#define ARGLEN  100
main()
{
char *arglist[MAXARGS+1];
int numargs;
char argbuf[ARGLEN];
char *makestring();
numargs = 0;
while (1)
{
printf('Arg[%d]? ', numargs);
if ( fgets(argbuf, ARGLEN, stdin) && *argbuf != '/n' )
arglist[numargs++] = makestring(argbuf);
else
{
if ( numargs > 0 )
{
arglist[numargs]=NULL;
execute( arglist );
numargs = 0;
}
}
}
return 0;
}
execute( char *arglist[] )/* * 使用 fork、execvp和wait实现 */
{
int pid,exitstatus;
pid = fork();
if(pid < 0) //创建进程失败
{
perror('fork failed');
exit(1);
}else if(pid == 0)  //子进程执行代码
{
execvp(arglist[0], arglist);
perror('execvp failed');
exit(1);
}else  //父进程执行代码
{
while( wait(&exitstatus) != pid )
;
printf('child exited with status %d,%d/n',exitstatus>>8, exitstatus&0377);
}
}
char *makestring( char *buf )/* * 去掉换行符并且为字符串申请存储空间 */
{
char *cp;
buf[strlen(buf)-1] = '/ 0';
cp = (char *)malloc( strlen(buf)+1 );
if ( cp == NULL )
{
fprintf(stderr,'no memory/n');
exit(1);
}
strcpy(cp, buf);
return cp;
}


更多精彩请点击:http://www.armjishu.com/bbs/viewforum.php?id=39&flag=412
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: