题目1shell程序设计.docx
- 文档编号:4173839
- 上传时间:2022-11-28
- 格式:DOCX
- 页数:30
- 大小:174.50KB
题目1shell程序设计.docx
《题目1shell程序设计.docx》由会员分享,可在线阅读,更多相关《题目1shell程序设计.docx(30页珍藏版)》请在冰豆网上搜索。
题目1shell程序设计
题目1shell程序设计
1.1实验目的
Linux操作系统中shell是用户与系统内核沟通的中介,它为用户使用操作系统的服务提供了一个命令界面。
用户在shell提示符($或#)下输入的每一个命令都由shell先解释,然
后传给内核执行。
本实验要求用C语言编写一个简单的shell程序,希望达到以下目的:
●用C语言编写清晰易读、设计优良的程序,并附有详细的文档。
●熟悉使用Linux下的软件开发工具,例如gcc、gdb和make。
●在编写系统应用程序时熟练使用man帮助手册。
●学习使用POSIX/UNIX系统调用、对进程进行管理和完成进程之间的通信,例如使用信号和管道进行进程间通信。
●理解并发程序中的同步问题。
●锻炼在团队成员之间的交流与合作能力。
1.2实验要求
1.2.1ysh解释程序的重要特征
本实验要实现一个简单的命令解释器,也就是Linux中的shell程序。
实验程序起名为ysh,要求其设计类似于目前流行的shell解释程序,如bash、csh、tcsh,但不需要具备那么复杂的功能。
ysh程序应当具有如下一些重要的特征:
●能够执行外部程序命令,命令可以带参数。
.。
●能够执行fg、bg、cd、history、exit等内部命令。
●使用管道和输入输出重定向。
●支持前后台作业,提供作业控制功能,包括打印作业的清单,改变当前运行作业的前台/后台状态,以及控制作业的挂起、中止和继续运行。
除此之外,在这个实验中还须做到:
●使用make工具建立工程。
●使用调试器gdb来调试程序。
●提供清晰、详细的设计文档和解决方案。
1.2.2ysh解释程序的具体要求
1.Shell程序形式
本实验的ysh程序设计不包括对配置文件和命令行参数的支持。
如果实现为像bash那样支持配置文件,当然很好,但本实验并不要求。
ysh应提供一个命令提示符,如ysh>,表示接收用户的输入,每次执行完成后再打印下一个命令提示符ysh>。
当用户没有输入时,ysh需要一直处于随时等待输入状态,同时在屏幕上显示一些必要的信息。
2.外部命令和内部命令
在大多数情况下,用户输入的命令是执行存储在文件系统中的可执行程序,我们叫做外部命令或外部程序。
ysh应当支持在执行这些程序时可以将输入输出重新定向到一个文件,并允许若干个程序使用管道串联起来。
从本实验的角度来讲,我们把由管道连接起来的复合命令以及单独使用的命令统称为作业。
外部命令的形式是一系列分隔的字符串。
第一个字符串是可执行程序的名字,其他的是传给这个外部程序的参数。
如果第一个字符串所声明的可执行文件并不存在或者不可执行.则认为这个命令是错误的。
解释器还须支持一些内部命令,这些命令在ysh程序内部实现了特定的动作,下面是一些内部命令,如果用户提交了一个内部命令,ysh应当按照下面的描述执行相应动作。
●exit:
结束所有的子进程并退出ysh。
●jobs:
打印当前正在后台执行的作业和挂起的作业信息。
输出信息应采用便于用户理解的格式。
jobs自身是一条内部命令,所以不需要显示在输出上。
●fg%
把
如果这个作业原来已经停止,那么让它继续运行。
shell应当在打印新的命令提示符之前等待前台运行的子进程结束。
●bg%
在后台执行
3.命令行
当用户在提示符后面输入命令时,输入的整行内容叫做“命令行字符串”,ysh应当保存每一条命令行字符串,直到它表示的作业执行结束,其中包括后台作业和被挂起的作业。
ysh应当给每一个命令行字符串赋一个非负整数标识符。
这个整数用来标识存储作业的数据结构,作业的数据结构应包含整个命令行字符串所表示的内容。
一旦命令行字符串代表的作业执行结束,ysh就要删掉表示这个作业的数据结构。
标识符可以循环使用。
对于包含内部命令的命令行字符串,不需要为它们建立作业的数据结构,因为它们本身的内容全部包含在ysh程序中。
4.前台和后台作业
ysh应当能够执行前台和后台作业。
shell在前台作业执行结束之前要一直等待。
而在开始执行后台作业时要立刻打印出提示符ysh>,让用户输入下一条命令。
前台作业的执行总是优先于执行一个后台作业,ysh不需要在打印下一个提示符前等待后台作业的完成,无论是否有后台作业的执行,只要完成一个前台作业,便立即输出提示符ysh>。
一个后台作业结束时,ysh应当在作业执行结束后立刻打印出一条提示信息。
下面语法中会在命令语法分析程序中介绍相应的语法来支持后台作业。
5.特殊键
又称组合键。
通过终端驱动程序,特殊的组合键可以产生信号给ysh,程序应当对这些信号做出适当的响应。
●Ctrl+Z:
产生SIGTSTP信号,这个信号不是挂起ysh,而是让shell挂起在前台运行的作业,如果没有任何前台作业,则该特殊键无效。
●Ctrl+C:
产生SIGINT信号,这个信号不是中止ysh,而是通过ysh发出信号杀死前台作业中的进程。
如果没有任何前台作业,则该特殊键无效。
6.分析用户输入
1)分隔符和特殊字符
分析用户输入的语法分析器应具有下面介绍的功能,它能够检查用户的输入错误。
如果用户输入的某些地方出错了,ysh应提供合理的出错信息。
就像商业级别的shell一样,ysh每次接受用户输入的一行命令,在用户按下回车键(Enter)后开始执行分析动作。
空命令不产生任何操作,而只是打印一个新提示符。
定义空格符为分隔符,ysh应能处理命令行中间和前后出现的重复空格符。
某些字符被称做“元字符",它们在用户输入的上下文中具有特定的含义。
这些字符包括“&、|、<、>“。
shell假设这些字符不会出现在程序名、参数名和文件名中,它们是ysh的保留字符。
下面几小节会解释这些元字符的含义。
2)内部命令
如果命令行字符串符合前面介绍的内部命令的格式,它就作为一个内部命令被解释。
如果不是,就要考虑可能是外部程序的执行,或者是错误的。
3)I/O重定向
一个程序命令后面可能还跟有元字符“<”或“>”,它们是重定向符号,而在重定向符号后面还跟着一个文件名。
在“<”的情况下,程序的输入被重定向到一个指定的文件中。
在“>”的情况下,程序的输出被重定向到一个指定的文件中。
如果输出文件不存在,需要创建一个输出文件。
如果输入文件不存在,则认为是出现了错误。
4)管道和协同程序
在一条命令行中当若干个命令被元字符“|”分隔开时,这个元字符代表管道符号。
在这种情况下,ysh为每一个子命令都创建一个进程,并把它们的输入/输出用管道连接起来。
例如下面这条命令行:
progAargA1argA2
应生成progA和progB两个进程,progA的输入来自文件infile,progA的输出是progB的输入,并且progB的输出是文件outfile。
这种命令行可以通过进程间通信中的管道来实现。
含有一个和多个管道的命令会在如下几种情况下产生错误:
●当其任何一个子程序执行出错时。
●除了第一个子程序以外的其他子程序的输入被重定向。
●除了最后一个子程序以外的其他子程序的输出被重定向。
由管道连接的多个进程所组成的作业只有当其所有的子进程都执行完毕后才算结束。
5)后台作业
当用户需要在后台执行一个作业时,可以在作业命令的后面加上元字符“&”。
用户以该种方式输入的作业命令都必须放在后台执行,同时并不影响用户与终端的交互。
6)语法
下面给出的语法规则描述图提供了控制用户输入的一种更规范的描述形式。
如果使用Linux中的现有工具lex和yacc来建立命令行分析器,还需要对这个语法进行修改,以便使它支持LALR
(1)(LookAheadLeftReduction)分析方法。
这个语法并不包括特殊键,因为它们不会在用户输入时显示出来,而是需要单独处理。
CommandLine代表用户的合法输入,它作为ysh要执行的一条“指令”。
这里假设存在一个词法分析器,它将空格符作为分隔符,并识别元字符作为一个词法记号等。
CommandLine:
=NULL
FgCommandLine
FgCommandLine&
FgCommandLine:
=SimpleCommand
FirstCommandMidCommandLastCommand
SimpleCommand:
=ProgInvocationInputRedirectOutputRedirect
FirstCommand:
=ProgInvocationInputRedirect
MidCommand:
=NULL
|ProgInvocationMidCommand
LastCommand:
=|ProgInvocationOutputRedirect
ProgInvocation:
=ExecFileArgs.
InputRedirect:
=NULL,
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标志位。 另一个比较重要的标志位是SA_SIGNFO,当设
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 题目 shell 程序设计