在做shell lab之前,有必要先对操作系统的进程管理进行了解;此外实现shell的关键是要对waitpid()、signal()、sigprocmask()等函数非常熟悉,以及要对信号这一部分非常了解。
重点参考资料:CSAPP课本,异常控制流课件;实验指导书。重点理解在什么情况下使用信号阻塞以及为什么使用。
鉴于信号概念比较多,有必要梳理一下。
信号使用的几种场景:内核检测到底层硬件的异常,通过信号的方式通知用户进程发生了这些异常,比如一条进程试图除以0,那么内核就发送给它一个SIGFPE信号(内核通过更新目的进程的上下文中的某个状态,发送一个信号给目的进程);一个进程可以通过向另一个进程发送一个SIGKILL信号强制终止它;当一个子进程终止或者停止时,内核会发送一个SIGCHLD信号给父进程。
当目的进程被内核强迫以某方式对信号的发送做出反应时,称进程接收了信号。接收信号后目的进程可以选择忽略、终止或者执行一个信号处理程序。一个(内核)发出而没有被目的进程接收的信号叫做待处理信号,注意一种类型至多只有一个待处理信号,多余的同种待处理信号不会排队等候,会被简单丢弃。一个进程可以有选择地阻塞接收某种信号,当一个信号被阻塞时,它仍然可以被发送,但产生的待处理信号不会被目的进程接收,直到进程取消对这种信号的阻塞。
看一个关于信号的例子:
下面这段代码的含义是将信号2和15与信号处理函数hdfunc绑定,当主程序收到2,15信号后就会去执行信号处理函数,这里信号处理函数的功能是首先输出信号名,然后循环5次输出。
可以看到发送四个信号2,只接收到了2个,并且当信号处理函数返回后才继续主程序的执行,并且收到的2个信号是紧接着执行信号处理函数的。
可以看到发送信号2和信号15两个不同的信号,信号15中断了信号2的信号处理。
当一个信号到达后,调用处理函数,如果这时候有其它的不同信号发生,会中断之前的处理函数,等新的信号处理函数执行完成后再继续执行之前的处理函数,但是如果新的信号是同一种信号则会阻塞,成为待处理信号,一旦主程序从信号处理程序返回,就开始执行这个待处理信号,并且有且只有一个待处理信号,没有解决的信号不会排队等候,会被抛弃。
疑问:这里看到信号处理程序完成后,才继续执行主程序,那么处理程序与主程序并发运行体现在哪里呢?
答:这里可以这样理解,signal()的调用是在发送信号之前的,signal()处理程序等待接收一个信号,而主程序继续往下执行,此时我们认为signal()处理程序与主程序并发运行。在主程序运行的过程中,如果收到一个信号,那么信号处理程序开始运行。
再详细介绍一下:
waitpid()函数
其原型为pid_t waitpid(pid_t pid, int *statusp, int options);
默认情况下(当options=0时),waitpid()挂起调用(waitpid函数)的进程的执行,直到它的等待集合中一个子进程终止;如果等待集合中的一个进程在刚调用的时候就已经终止了,那么waitpid()就立即返回,在这两种情况中,waitpid()返回的是已终止进程的PID。此时,已终止的进程已经被回收,内核会从系统中删除掉他的所有痕迹。
waitpid()函数参数的含义:
(1)pid_t pid
- 如果pid>0,那么等待集合就是一个单独的子进程,子进程ID等于pid。
- 如果pid=-1,那么等待集合就是由父进程所有的子进程组成的。
(2)int options
可以通过将options的值设置为常量WNOHANG、WUNTRACED和WCONTINUED的各种组合来修改默认行为。
-
WNOHANG:如果等待集合中没有任何子进程终止,那么就立即返回(返回值为0)。默认的行为是挂起调用进程,直到有子进程终止。
-
WUNTRACED:挂起调用进程的执行,直到等待集合中的一个进程变成终止的或者被暂停。返回的PID为导致返回的终止或暂停子进程的PID。默认的行为是只返回已终止的子进程。
-
WCONTINUE:挂起调用进程的执行,直到等待集合中一个正在运行的进程终止或等待等待集合中一个被停止的进程收到SIGCONT信号重新开始执行。
-
WNOHANG|WUNTRACED:立即返回,如果等待集合中任何子进程都没有停止或终止,那么返回值为0;如果有一个停止或终止,则返回值等于该子进程的PID。
(3)int *statusp
如果statusp参数是非空的,那么waitpid()就会在status中放入关于导致返回的子进程的状态信息,status是statusp指向的值。
- WIFEXITED(status):如果子进程通过调用exit或者一个返回(return)正常终止,就返回真。
(4)错误条件
- 如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。
- 如果waitpid函数被一个信号中断,也返回-1,设置errno为EINTR。
getpgrp()函数
每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。getpgrp函数返回当前进程的进程组ID:pid_t getpgrp(void);
setpgid()函数
默认地,一个子进程和它的父进程同属于一个进程组,一个进程可以通过使用setpgid函数来改变自己或者其他进程的进程组:pid_t setpgid (pid_t pid, pid_t pgid);
setpgid 函数将进程pid的进程组改为pgid。如果pid是0,那么就使用当前进程的PID。如果pgid是0,那么就用pid指定的进程的PID作为进程组ID。例如,如果进程15213是调用进程,那么setpgid(0, 0);会创建一个新的进程组,其进程组ID是15213,并且把进程15213加入到这个新的进程组中,也就是说新建立的子进程会是一个单独的进程组,进程组ID就是其PID。
kill()函数
进程通过调用kill函数发送信号给其他进程(包括它们自己):int kill(pid_t pid, int sig);
如果pid大于零,那么kill 函数发送信号号码sig给进程pid。如果pid小于零,那么kill发送信号sig给进程组|pid|(pid的绝对值)中的每个进程。
整个shell lab如果没有给其他的模块,从零开始完成,不亚于一个小项目了。一上来就开始敲代码是大忌,最后导致删删改改,也不知道自己做到哪了,思维混乱。如果不能对整个项目有个清晰的思路是很难做出来的,并且在做的时候做完一个模块就进行单元测试,这也是实验所希望的,实验中也给出了每个阶段的测试工具。可以从shell功能、程序执行顺序,模块功能划分这几个方向进行切入。
下面给出自己写的代码乱七八糟的,跟shit一样,后面再好好梳理,优化下。
/*
* tsh - A tiny shell program with job control
*
* <Put your name and login ID here>
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
/* Misc manifest constants */
#define MAXLINE 1024 /* max line size */
#define MAXARGS 128 /* max args on a command line */
#define MAXJOBS 16 /* max jobs at any point in time */
#define MAXJID 1<<16 /* max job ID */
/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1 /* running in foreground */
#define BG 2 /* running in background */
#define ST 3 /* stopped */
/*
* Jobs states: FG (foreground), BG (background), ST (stopped)
* Job state transitions and enabling actions:
* FG -> ST : ctrl-z
* ST -> FG : fg command
* ST -> BG : bg command
* BG -> FG : fg command
* At most 1 job can be in the FG state.
*/
/* Global variables */
extern char **environ; /* defined in libc */
char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */
int verbose = 0; /* if true, print additional output */
int nextjid = 1; /* next job ID to allocate */
char sbuf[MAXLINE]; /* for composing sprintf messages */
struct job_t {
/* The job struct */
pid_t pid; /* job PID */
int jid; /* job ID [1, 2, ...] */
int state; /* UNDEF, BG, FG, or ST */
char cmdline[MAXLINE]; /* command line */
};
struct job_t jobs[MAXJOBS]; /* The job list */
/* End global variables */
/* Function prototypes */
/* Here are the functions that you will implement */
void eval(char *cmdline);
int builtin_cmd(char **argv);
void do_bgfg(char **argv);
void waitfg(pid_t pid);
void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);
/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv);
void sigquit_handler(int sig);
void clearjob(struct job_t *job);
void initjobs(struct job_t *jobs);
int maxjid(struct job_t *jobs);
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);
int deletejob(struct job_t *jobs, pid_t pid);
pid_t fgpid(struct job_t *jobs);
struct job_t *getjobpid(struct job_t *jobs, pid_t pid);
struct job_t *getjobjid(struct job_t *jobs, int jid);
int pid2jid(pid_t pid);
void listjobs(struct job_t *jobs);
void usage(void);
void unix_error(char *msg);
void app_error(char *msg);
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);
//初始化部分,包括任务列表的初始化,信号的初始化
sigset_t mask_all,mask_one,prev_one,prev;
sigfillset(&mask_all);//在信号处理函数中调用其他函数前要阻塞所有信号
sigemptyset(&mask_one);
sigaddset(&mask_one,SIGCHLD);
/*
* main - The shell's main routine
*/
int main(int argc, char **argv)
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */
/* Redirect stderr to stdout (so that driver will get all output
* on the pipe connected to stdout) */
//将stderr重定向到stdout(这样驱动程序将获得连接到stdout的管道上的所有输出)
//换句话说两个文件描述符现在指向同一个文件,并且是函数第一个参数指向的文件,即出错的信息将通过标准输出显示出来
dup2(1, 2);//0即标准输入,1