目录
1、实验文档注释
命令行的第一个字是一个内置命令的名称或一个可执行文件的路径名,其余的字是命令行参数。
如果第一个词是一个内置的命令,shell会立即在当前进程中执行该命令。否则,该词被认为是一个可执行的 程序的路径名。在这种情况下,shell会叉开一个子进程,然后在子进程的上下文中加载并运行程序。
由于解释一个命令行而创建的子进程被统称为 称为工作。一般来说,一个作业可以由多个子进程组成,通过Unix管道连接。
如果命令行以逗号"&"结束,那么作业就在后台运行,这意味着shell在打印提示符和等待下一个命令行之前不会等待作业的终止。否则,作业就在前台运行,这意味着shell在等待下一个命令行之前会等待作业的终止。因此,在任何时间点上,最多只有一个作业在前台运行。然而,任意数量的作业可以在后台运行。
提供了几个内置命令:
- jobs: 列出正在运行和停止的后台工作。
- bg <job>: 将一个已停止的后台作业改为正在运行的后台作业。
- fg <job>: 将一个已停止或正在运行的后台作业改为在前台运行。
- kill <job>: 终止一个作业。
会有一些辅助函数,功能在文档中均有解释:
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);
2、实验题目
本作业的目的是使学生更加熟悉过程控制和信号的概念。为此,您将编写一个支持作业控制的简单Unix shell程序。
• 输入命令tar xvf shlab-讲义。tar展开tarfile。
• 键入命令make来编译和链接一些测试例程。
• 在tsh.c顶部的标题注释中键入您的团队成员名称和Andrew id。
查看tsh.c(微型shell)文件,您将看到它包含一个简单Unix shell的功能框架。为了帮助您入门,我们已经实现了不太有趣的函数。你的任务1是完成下面列出的剩下的空函数。作为对您的完整性检查,我们列出了大致的数字
• eval:解析和解释命令行的主例程。(70行)
• builtin_cmd:识别和解释内置命令:quit, fg, bg和jobs。(25行)
• do_bgfg:实现内置命令bg和fg。(50行)
• waitfg:等待前台作业完成。(20行)
• sigchld_handler:捕获SIGCHILD信号。(80行)
• sigint_handler:捕获sigint (ctrl-c)信号。(15行)
• sigtstp_handler:捕获sigtstp (ctrl-z)信号。(15行)
2.1 eval
函数作用:解析和解释命令行
思路参考书上代码
1、调用 parseline 函数,解析以空格分割的命令行参数,并构造argv
2、若是内置 shell 名字,解释该命令
3、若是可执行的目标文件,则在新的子进程的上下文中加载并运行这个文件
4、一些需要注意的点放注释里
void eval(char *cmdline){
char *argv[MAXARGS]; // execve()函数的参数
int state = UNDEF; // job在何种状态工作?
pid_t pid; //进程id
sigset_t mask_all, mask_one, prev; // 信号集
state = parseline(cmdline, argv); // 解析命令行。返回给argv。
if(argv[0] == NULL) { // 空命令不执行
return;
}
if(!builtin_cmd(argv)){ // 是否是内置指令?
// 初始化了两个信号集
sigfillset(&mask_all);
sigemptyset(&mask_one);
sigaddset(&mask_one, SIGCHLD);
// 阻塞掉SIG_BLOCK
// 我的理解是,防止在创建的过程中, 被其他进程影响
// 避免出现 addjob 前, 进程就退出了的情况
// 信号集或,做添加,原有的屏蔽字保存到prev
sigprocmask(SIG_BLOCK, &mask_one, &prev);
// 创建子进程
if((pid = fork()) < 0){ //创建失败
unix_error("sigprocmask error");
}
else if((pid = fork()) == 0){ // 为 0 代表是新生成的子进程
// 此时的子进程先解除阻塞,避免收不到自己子进程的信号
// 利用 prev 恢复到之前的屏蔽字
sigprocmask(SIG_BLOCK, &mask_one, &prev);
// int setpgid(pid_t pid, pid_t pgid);
// 设置进程 pid 的进程组为 pgid
// pid = 0,代表对当前进程操作
// pgid = 0,代表设置进程组号为当前组号
// 创建新进程组,并设置组号为当前进程号
if(setpgid(0, 0) < 0){
perror("SETPGID ERROR");
exit(0);
}
// 将当前进程执行的程序替换为指定的可执行文件
if(execve(argv[0], argv, environ) < 0){ // < 0 则执行失败
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}
// 添加至工作列表
// 先阻塞所有, 然后添加, 然后恢复
sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(jobs, pid, state, cmdline);
sigprocmask(SIG_SETMASK, &prev, NULL);
// 不同的运行模型执行不同的操作
if(state == BG){ // 后台打印
printf("[%d] (%d) %s",pid2jid(pid), pid, cmdline);
}
else{ // 前台等待
waitfg(pid);
}
}
return;
}
2.2 builtin_cmd
判断是否是内置指令,4 个指令分别执行 4 个操作就行
int builtin_cmd(char **argv){
if(!strcmp(argv[0], "quit")){
exit(0);
}
if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")){
do_bgfg(argv);
return 1;
}
if(!strcmp(argv[0], "jobs")){
listjobs(jobs);
return 1;
}
if(!strcmp(argv[0], "&")){
return 1;
}
return 0;
}
2.3 do_bgfg
实现内置命令bg和fg, 回顾一下作用:
- bg <job> 命令通过向 <job> 发送一个 SIGCONT 信号来重新启动 <job>,然后在后台运行它。背景下运行。<job> 参数可以是一个PID或一个JID。
- fg <job> 命令通过发送 SIGCONT 信号重新启动 <job>,然后在前台运行。前台运行。<job> 参数可以是一个 PID 或一个 JID。
- "%5" 表示JID 5,而 "5 "表示PID 5
void do_bgfg(char **argv){
struct job_t *job; // 待处理的job
int id; // pid/jid
if(!argv[1]){
printf("%s command requires PID or %%jobid argument\n",argv[0]);
}
if(sscanf(argv[1], "%%%d", &id) > 0){ // 尝试读jid
if((job = getjobjid(jobs, id)) == NULL){ // 没读到
printf("%%%d:No such job\n", id);
return;
}
}
else if(sscanf(argv[1], "%d", &id) > 0){ // 尝试读pid
if((job = getjobpid(jobs, id)) == NULL){ // 没读到
printf("%d:No such precess\n", id);
return;
}
}
else{ // 读入格式不对
printf("%s:argument must be a PID or %%jobid\n", argv[0]);
return;
}
if(!strcmp(argv[0], "bg")){ // 是否修改为bg运行
job->state = BG; // 设置运行状态
kill(-(job->pid), SIGCONT); // 重启 以 job->pid 为组标识的进程
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
}
if(!strcmp(argv[0], "fg")){ // 是否修改为fg运行
job->state = FG; // 设置运行状态
kill(-(job->pid), SIGCONT); // 重启 以 job->pid 为组标识的进程
waitfg(job->pid); // 等待
}
return;
}
2.4 waitfg
等待一个前台作业结束
void waitfg(pid_t pid){
sigset_t mask;
sigemptyset(&mask); // 信号集清空
while(fgpid(pid) != 0){ // 获取前台运行的pid,为 0 则前台结束了
// sigprocmask(SIG_SETMASK, &mask, &prev);
// pause();
// sigprocmask(SIG_SETMASK, &prev, NULL);
sigsuspend(&mask); // 相当于上述1、2行原子版,然后执行3
}
return;
}
2.5 sigchld_handler
每当一个子作业终止(成为僵尸),或者因为收到SIGSTOP或SIGTSTP信号而停止,内核就会向shell发送SIGCHLD。该处理程序收割所有可用的僵尸子任务,但不等待任何其他当前运行的子任务终止。
主要用到 waitpid 函数,细节在注释
已回收子进程的退出状态:
1、WIFEXITED--WEXITSTATUS:子程序通过 exit 或 return 返回时,前者为 true。此基础上后者返回一个正常终止的子进程的退出状态;
2、WIFSIGNALED--WTERMSIG:子程序因为一个未被捕获的信号终止时,前者为 true。此基础上后者返回导致子进程终止的信号的编号;
3、WIFSTOPPED--WSTOPSIG:引起返回的子程序当前是停止的时,前者为 true。此基础上后者返回引起子程序停止的信号的编号。
void sigchld_handler(int sig){
int olderrno = errno; // 保存旧值,可以在本段程序任意输出错误信息
int status; // 保存子进程的终止状态
pid_t pid; // 进程的pid
struct job_t * job;
sigset_t mask, prev; // 信号集
sigfillset(&mask); // 用来阻塞信号和恢复,防止竞态,如多个子程序退出
// 获取退出进程的pid,
// -1 代表等待任意一个子进程
// status 保存子进程的终止状态
// WNOHANG 不阻塞,若进程没终止立即返回 0
// WUNTRACED 返回已停止或终止的 pid,有这个 WIFSTOPPED 才可能 true
// WNOHANG | WUNTRACED 立即返回,若等待集合中都没停止或终止返回 0
// 若 1 个停止或终止返回对应的 pid
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞全部,防竞态
if(WIFEXITED(status)){ // 正常退出
deletejob(jobs, pid);
}
else if(WIFSIGNALED(status)){ // 因为一个未被捕获的信号终止时
deletejob(jobs, pid);
}
else if(WIFSTOPPED(status)){
job = getjobpid(jobs, pid);
job->state = ST;
}
sigprocmask(SIG_BLOCK, &prev, NULL);
}
errno = olderrno; // 还原 errno 的值,以确保之后的代码不会受到影响
return;
}
2.5 sigint_handler
当用户在键盘上输入ctrl-c时,内核会向shell发送一个SIGINT。 捕获它并将其发送到前台工作。
void sigint_handler(int sig){
int olderrno = errno; // 保存旧值,可以在本段程序任意输出错误信息
pid_t pid = fgpid(jobs); // 获取前台进程 pid
if(pid != 0){
kill(-pid, sig);
}
errno = olderrno; // 还原 errno 的值,以确保之后的代码不会受到影响
return;
}
2.6 sigtstp_handler
每当用户在键盘上输入ctrl-z时,内核就会向shell发送一个SIGTSTP。捕获它并通过发送SIGTSTP暂停前台工作。
void sigtstp_handler(int sig){
int olderrno = errno; // 保存旧值,可以在本段程序任意输出错误信息
pid_t pid = fgpid(jobs); // 获取前台进程 pid
if(pid != 0){
kill(-pid, sig);
}
errno = olderrno; // 还原 errno 的值,以确保之后的代码不会受到影响
return;
}
2.7 测试
太长我就不贴啦。
3、总结
懵懵懂懂,请多指教!