OutputRedirect:
=NULL
>STRING
ExecFile:
=STRING
Args:
=NULL
STRINGArgs
STRING:
=NULL
CHARSTRING
CHAR:
=0|1|…|9|a|b|…|z|A|B|…|Z
7.实验步骤建议
(1)阅读关于fork、exec、wait和exit系统调用的man帮助手册。
(2)编写小程序练习使用这些系统调用。
(3)阅读关于函数tcsetpgrp和setpgid的man帮助手册。
(4)练习编写控制进程组的小程序,要注意信号SIGTTIN和SIGTTOU。
(5)设计命令行分析器(包括设计文档)。
(6)实现命令行分析器。
(7)使用分析器,写一个简单的shell程序,使它能执行简单的命令。
(8)增加对程序在后台运行的支持,不必担心后台作业运行结束时要打印一条信息(这属于异步通知)。
增加jobs命令(这对于调试很有帮助)。
(9)增加输入输出重定向功能。
(10)添加代码支持在后台进程结束时打印出一条信息。
(11)添加作业控制特征,主要实现对组合键Ctrl+Z、Ctrl+C的响应,还有实现fg和bg命令功能。
(12)增加对管道的支持。
(13)实现上面的所有细节并集成。
(14)不断测试。
(15)写报告。
(16)结束。
1.3相关基础知识
1.3.1shell与内核的关系
shell是用户和Linux内核之间的接口程序,如果把Linux内核想象成一个球体的中心,shell就是包围内核的外壳,如图5—1所示。
当从shell或其他程序向Linux传递命令时,内核会做出相应的反应。
shell是一个命令语言解释器,它拥有自己内建的shell命令集,shell也能被系统中其他应用程序所调用。
用户在提示符ysh>下输入的命令都是由shell先解释后传给Linux核心的
.
1.3.2系统调用
系统调用是一个“函数调用”,它控制状态的改变。
系统调用与普通函数过程的区别在于系统调用的执行会引起特权级的切换,因为被调用的函数处于操作系统内核中,是内核的一部分。
操作系统定义了一个系统调用集合。
为了安全起见,调用操作系统内部的函数必须谨慎地控制,这种控制是由硬件通过陷阱向量执行的。
只有那些在操作系统启动时填入陷阱向量的地址,才是正当而且有效的系统调用地址。
因此,系统调用就是一种在受约束的行为下进入保护核心的“函数调用”。
因为操作系统负责进程控制和调度,ysh就需要调用操作系统内部的函数来控制它的子进程。
这些函数叫做系统调用。
在Linux中,我们可以区分系统调用和用户应用层次的库函数,因为系统调用函数手册在“帮助”手册的第二部分,而库函数在手册的第三部分。
在Linux中可以通过man命令查询“帮助”手册。
例如,使用命令manfork会给出手册第二部分关于fork系统调用的描述,而命令man2exec会给出exec系统调用族的描述(2表示手册的第二部分)。
还有很多其他的系统调用,都可以通过man命令来查阅,你会发现man是很有用的查阅参考手册的命令。
下面是在实验中会用到的重要的UNIX系统调用。
●pid_tfork(void):
创建一个新的进程,它是原来进程的一个副本。
在fork成功返回后,父进程和子进程都要继续执行fork后的指令。
这两个进程通过fork的返回值进行区分,对父进程fork的返回值是子进程的进程号,对子进程的返回值是0。
●intexecvp(constchar*file,char*constargv[]):
加载一个可执行程序到调用进程的地址空间中,然后执行这个程序。
如果成功,它就会覆盖当前运行的进程内容。
有若干个类似的exec系统调用。
●voidexit(intstatus):
退出程序,使调用进程退出,程序结束。
它把status作为返回值返回父进程,父进程通过wait系统调用获得返回值。
链接器会为每一个程序结尾链接一个exit系统调用。
●intwait(int*stat_loc):
如果有退出的子进程,则返回退出的子进程的状态;如果没有任何子进程在运行,则返回错误。
如果当前有子进程正在运行,则函数会一直阻塞直到有一个子进程退出。
●pid_twaitpid(pid_tpid,int*stat_loc,intoptions):
类似于函数wait,但允许用户等待某个进程组的特定进程,并可以设置等待选项,例如WNOHANG。
●inttcsetpgrp(intfildes,pid_tpgid_id):
将前台进程组ID设置为pgid_id,fildes是与控制终端相联系的文件描述符。
终端通常指标准输入、标准输出和标准错误输出(文件描述符为0、1、2)。
●intsetpgid(pid_tpid,pid_tpgid):
把pid进程的进程组ID设置为pgid。
●intdup2(intfildes,intflides2):
把ftildes文件描述符复制到fildes2。
如果fildes2已经打开,则先将其关闭,然后进行复制,使filedes和fildes2指向同一文件。
●intpipe(intfildes[2]):
创建一个管道,把管道的读和写文件描述符放到数组fildes中。
1.进程创建
使用fork系统调用创建新的进程。
fork克隆了调用进程,两者之间只有很少的差别。
新进程的进程号pid和父进程号ppid与原来的进程不同。
其他不同之处可以查看man手册得到。
fork的返回值是程序中惟一能够区别父进程和子进程的地方。
fork对父进程返回子进程的进程号,对子进程则返回0。
利用这个细小的区别可以使两个进程执行不同的程序段。
wait函数族允许父进程等待子进程执行结束。
在ysh创建一个前台进程时会用到它。
须特别注意的是wait函数族会在子进程状态改变时返回,而不仅仅是在子进程运行结束或者退出时才返回,其中有些状态的变化可以被忽略。
在man手册中有关于函数waitpid的参数说明,其中有WNOHANG和其他一些有用的参数(WNOHANG指定在没有子进程退出时,父进程不阻塞等待)。
下面的例子介绍创建进程和等待子进程运行结束。
intmain(intargc,char*argv[])
{
intstatus;
intpid;
char*prog_arv[4];
/*建立参数表*/
prog_argv[0]=”/bin/ls”;
prog_argv[1]=”-1”;
prog_argv[2]=”/”;
prog_argv[3]=NULL;
/*
*为程序ls创建进程
*/
if((pid=fork())<0)
{
perror(”Forkfailed”);
exit(errno);
}
if(!
pid)
{
/*这是子进程,执行程序ls*/
execvp(prog_argv[0],prog_argv);
}
if(pid)
{
/*
*这是父进程,等待子进程执行结束
*/
waitpid(pid,NULL,0);
}
}
shell程序等待子进程执行结束是很重要的。
对一个作业等待子进程发出信号进行处理可以采用阻塞等待的方式,或是采用非阻塞的方式。
尽管在进程死亡时它的许多资源都会被释放,但是进程控制块和其他的一些信息还没有释放,这种状态被称为defunct。
进程控制块包含了退出的状态信息,它可以通过wait函数族获得。
在waitpid函数调用完之后,进程控制块就被释放了。
如果父进程在子进程之前结束,那么子进程就会成为init进程的孩子,init进程会等待任何子进程的结束,释放进程控制块。
那些已经终止,但父进程尚未对其进行状态搜集的进程就成为僵尸进程。
2.exec系统调用
exec函数族允许当前进程执行另外一个程序。
典型的应用是一个程序调用fork生成自身的一个副本,然后子进程调用exec执行另外一个程序。
exec有许多不同形式,它们最终都是调用内核中的同一个函数,只是它们给用户提供了更多的调用形式。
调用exec的进程不是和原来完全不同,而是调用后进程继承了原来的父进程标识、组标识和信号掩码,但不包括信号处理程序。
详细信息可以查看man手册。
除非产生错误,否则exec函数从来不返回(从此时开始执行新的程序代码)。
上面的例子介绍了函数execvp被系统调用的使用方法。
3.I/0重定向
为了实现I/0重定向,需要使用函数dup2:
intdup2(intfiides,intflides2);
每个进程都有一张它所打开的文件描述符表,每个表项包含了文件描述符标识和指向系统文件表中相对应表项的指针。
这个系统文件表是由内核维护,它记录了系统当前打开的所有文件的信息,其中包括打开这个文件的进程数目、文件状态标志(读、写、非阻塞等)、当前文件指针、指向该文件inode节点表项的指针。
还应当认识到许多非文件类的机制也使用文件接口,只是它们的操作被包装了。
例如很多场合终端也被当做文件来操作。
默认情况下,进程的文件列表中的前三个入口都指向终端:
标准输入(0),标准输出
(1)和标准错误输出
(2)。
为实现I/O重定向,我们要打开一个文件,并把它的文件描述符入口复制给标准输入或者标准输出(或者标准错误输出)。
如果需要在后面恢复原来的入口项,我们可以事先把它保存在文件列表中别的地方。
4.信号
信号是最简单的进程间通信(IPC)原语。
信号允许一个进程在某一事件发生时与另一个进程通信。
信号的值表明发生了哪种事件。
信号对本实验来说是很重要的,它们指出了后台运行的子进程状态发生的变化,比如子进程的正常终止。
当一个进程接收到一个信号时,它会采取某些动作。
许多信号都有默认的动作。
比如,某些信号默认产生coredumps,或者进程自身挂起。
我们也可以声明让自己的进程处理某个信号。
通过声明一个信号处理程序可以做到这一点。
Linux主要有两个函数实现对信号的处理,即signal和sigaction。
其中signal是库函数,在可靠信号系统调用的基础上实现。
它只有两个参数,不支持信号传递信息;而sigaction是较新的函数,有三个参数,支持信号传递信息,同样支持非实时信号的安装。
函数sigaction优于signal,主要体现在支持信号带有参数。
1)函数signal
格式
#include
typedefvoid(*sighandler_t)(int);
sighandler_tsignal(intsignum,sighandler_thandler);
参数说明
signum指定信号的值。
handler指定针对前面信号值的处理,可以忽略该信号(参数设置为SIG-IGN);可以采用系统默认方式处理信号(参数设置为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。
如果signal调用成功,返回最后一次为安装信号signum而调用signal时的handler值;失败则返回SIG_ERR。
2)函数sigaction
梧式
#include
intsigaction(intsignum,conststructsigaction*act,structsigaction
*oldact));
参数说明
sigaction函数用于改变进程接收到特定信号后的行为。
signum为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致错误)。
act是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以默认方式对信号处理。
这个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽哪些函数。
oldact是指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。
如果把signum和act都设置为NULL,那么该函数可用于检查信号的有效性。
sigaction结构定义
structsigaction{
void(*sa_handler)(int);
void(*sa_sigaction)(int,siginfo_t*,void*);
sigset_tsa_mask;
unsignedlongsa_flags;
void(*sa_restorer)(void);
}
数据结构中的两个元素sa_hanlder和sa_sigaction是指定信号关联函数。
除了可以是用户自定义的处理函数外,还可以为SIGDFL(采用默认的处理方式),也可以为SIG_DFL(忽略信号)
参数说明
sa_handler:
由它指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息
sa_sigaction:
由它指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),这个信号处理函数的第一个参数(int)为信号值,第三个参数(void*)没有使用(POSIX标准中没有规范使用该参数的标准),第二个参数(siginfo_t*)是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下:
siginfo_t{
intsi_signo;/*信号值*/
intsi_errno;
intsi_code;
pid_tsi_pid;/*发送信号的进程ID‘/
uid_tsi_uid;
intsi_status;
clock_tsi_utime ;
clock_tsi_stime ;
sigval_tsi_value ;
intsi_int ;
void*si_ptr ;
void*si_addr ;
intsi_band ;
intsi_fd ;
}
sa_mask:
指定在信号处理程序执行过程中,哪些信号应当屏蔽,如果不指定SA_NODEFER或者SA_NOMASK标志位,默认情况下则屏蔽当前信号,防止信号的嵌套发送。
sa_flags :
其中包含了许多标志位,包括前面提到的SA_NODEFER及SA_NOMASK标志位。
另一个比较重要的标志位是S