写在前面

我宁愿去处理汇编代码!这次的 lab 太繁杂太发散;汇编虽然难,起码有的放矢,一码是一码。

阅读官方指导文档

这次必须仔细阅读文档,否则必然踩坑,本篇文章十有八九也看不懂了:文档的链接在这里

要求

  1. 提供了一个模板可执行文件 ./tshref, 最后完成的 ./tsh 在运行时必须和模板表现一致;
  2. 提供了16个 trace 文件,lab 提交之后是比较 ./tsh 在这16个 trace 文件的输入后 的表现是否与同样条件下的 ./tshref 一致来评分的;
  3. 需要完成 ./tsh.c 中的7个函数,分别是:
    1. eval
    2. builtin_cmd
    3. do_bgfg
    4. waitfg
    5. sigchld_handler
    6. sigint_handler
    7. sigstp_handler

提示

lab 文档在最后给出了几条提示,有几条特别重要:

  1. 第三条:waitpid, kill, fork, execve, setpgid 和 sigprocmask 非常有用,在 waitpid 中使用 WUNTRACED 和 WNOHANG 参数选项讲极大的减少工作量(前提是,仔细 阅读 waitpid 的文档,了解其常规用法);
  2. 第四条:在实现 sigint_handler 和 sigstp_handler 的过程中,在调用 kill 函数时 第一个参数使用 -pid 而不是 pid, 这样就可以把 SIGINT 和 SIGSTP 信号发送到 整个前台进程组;
  3. 第五条:在待实现的 waitfg 函数中使用一个内部是 sleep 函数调用的 while 循环, 搭配着在 sigchld_handler 中只调用一次 waitpid 函数;
  4. 第六条:在待实现的 eval 函数中,在调用 fork 函数派生子进程之前使用 sigprocmask 来屏蔽 SIGCHLD 信号,在 eval 中调用 addjob 函数之后再调用一次 sigprocmask 来解除屏蔽,被调用的子进程在调用 execve 之前也必须解除屏蔽;
  5. 第八条:eval 中派生的子进程必须调用 setpid(0, 0) 来确保自己的进程组ID和自己的PID一样, 确保自己的进程组ID和 运行时的 ./tsh 不一样;因为 ./tsh 是在 Unix shell 中 运行的,它其实就是在前台进程组中运行的,如果 ./tsh 派生出子进程,默认这个子 进程就将是前台进程组中的一员;如果在控制台中键入 ctrl-c, SIGINT 信号将会被送 到前台进程组中的所有成员;

可以这样理解,这里每一条提示都不只是建议,而是可以大大减少工作量的最优解。

代码工作

鉴于代码中已经写了大量注释(以及实践过程中踩坑的记录以博一笑),这里就把代码搬运过来就可以了。

eval

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
void eval(char *cmdline)
{
  pid_t pid = 0;
  int state, bg_or_not, builtin_or_not, is_available_cmd;
  char *args[MAXARGS];
  char *first_cmd;
  sigset_t blockset;
  const char *err_msg = "%s: Command not found\n";
  /* 使用一个字符串数组来存储所有可以接受的命令 */
  /* 偷懒使用这样的硬编码方式,肯定有更好的在路径查询函数,比如查询 '/bin/echo' 是否存在 */
  /* 更新:这个函数就是C库函数 access, 这个硬编码套路留着以博一笑 :) */
  /* const char *available_cmds[5]={"./myint", "./myspin", "./mysplit", "./mystop", "/bin/echo"}; */
  /* 创建空的信号集合 */
  sigemptyset(&blockset);
  /* 添加指定信号到集合中 */
  sigaddset(&blockset, SIGCHLD);
  bg_or_not = parseline(cmdline, args);
  first_cmd = args[0];
  builtin_or_not = builtin_cmd(args);

  if(!builtin_or_not)
    {
      is_available_cmd = access(first_cmd, 00);
      /* 硬编码套路 */
      /* for(i=0;i<5;i++) */
      /* 	{ */
      /* 	  if(!strcmp(first_cmd, available_cmds[i])) */
      /* 	    { */
      /* 	      is_available_cmd = 1; */
      /* 	      break; */
      /* 	    } */
      /* } */
      if(is_available_cmd==-1)
	{
	  printf(err_msg, first_cmd);
	  return;
	}
      /* 阻塞对应信号,最后一个参数是 NULL 是因为不需要保存之前的集合为备份 */
      sigprocmask(SIG_BLOCK, &blockset, NULL);
      pid = fork();
      if(pid>0)
	{
	  if(bg_or_not)
	    {
	      state = BG;
	    }
	  else
	    {
	      state = FG;
	    }
	  addjob(jobs, pid, state, cmdline);
	  /* 在执行完 addjob 之后需要在父进程中取消阻塞 */
	  sigprocmask(SIG_UNBLOCK, &blockset, NULL);
	  if(state==FG)
	    {
	      /* 如果是 foreground job, 需要等待其完成 */
	      waitfg(pid);
	    }
	  else
	    {
	      printf("[%d] (%d) %s", maxjid(jobs), pid, cmdline);
	    }
	}
      if(pid==0)
	{
	  /* lab pdf 中的提示,后台子进程需要更改自己的组号 */
	  /* 因为 ./tsh 执行时其实就是一个 foreground process */
	  /* 任何本程序派生的子进程默认都在 foreground process group 里 */
	  setpgid(0, 0);
	  /* 子进程继承了父进程的阻塞状态,必须在 execve 前取消阻塞 */
	  sigprocmask(SIG_UNBLOCK, &blockset, NULL);
	  execve(args[0], args, environ);
	}
    }
  return;
}

builtin_cmd

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int builtin_cmd(char **argv)
{
  char *first_cmd = argv[0];
  if(!strcmp(first_cmd, "quit"))
    {
      exit(0);
    }
  if(!strcmp(first_cmd, "jobs"))
    {
      listjobs(jobs);
      return 1;
    }
  if(!strcmp(first_cmd, "bg") || !strcmp(first_cmd, "fg"))
    {
      do_bgfg(argv);
      return 1;
    }
  return 0;     /* not a builtin command */
}

do_bgfg

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
void do_bgfg(char **argv)
{
  int jid=0, pid=0;
  unsigned long i;
  const char *str_number;
  struct job_t *job;
  char *first_cmd = argv[0];
  char *second_cmd = argv[1];
  char e;
  /* 几个格式化字符串常量提示错误 */
  const char *err_msg1 = "%s command requires PID or %%jobid argument\n";
  const char *err_msg2 = "%s argument must be a PID or %%jobid\n";
  const char *err_msg3 = "%s: No such job\n";
  const char *err_msg4 = "(%d): No such process\n";

  if(second_cmd==NULL)
    {
      /* 如果 bg fg 没有带第二个参数 */
      printf(err_msg1, first_cmd);
      return;
    }
  if(second_cmd[0]=='%')
    {
      /* 如果第二个参数以 '%' 开头,就是一个 jobid */
      str_number = strtok(second_cmd, "%");
      /* 验证 '%' 后面的字符串是不是一个数字字符串 */
      for(i=0;i<strlen(str_number);i++)
	{
	  e = str_number[i];
	  if(e=='\0')
	    {
	      break;
	    }
	  if(e<'0' || e>'9')
	    {
	      /* 如果有数字字符以外的东西,报错 */
	      printf(err_msg2, first_cmd);
	      return;
	    }
	}
      /* 数字字符串转换为数字 */
      jid = atoi(str_number);
      job = getjobjid(jobs, jid);
      /* 如果 job 不存在的话,报错 */
      /* 其实这里也可以比较 jid 和 maxjid 执行获得的 jobs 中最大的 jid */
      if(!job)
	{
	  printf(err_msg3, second_cmd);
	  return;
	}
      else
	{
	  /* 这里需要拿到 pid, 后面会用 */
	  pid = job->pid;
	}
    }
  else
    {
      /* 同样的,对于不是以 '%' 开头的字符串,检查其是否是数字字符串 */
      for(i=0;i<strlen(second_cmd);i++)
	{
	  e = second_cmd[i];
	  if(e=='\0')
	    {
	      break;
	    }
	  if(e<'0' || e>'9')
	    {
	      printf(err_msg2, first_cmd);
	      return;
	    }
	}
      pid = atoi(second_cmd);
      job = getjobpid(jobs, pid);
      if(!job)
	{
	  /* 如果 job 不存在,报错 */
	  printf(err_msg4, pid);
	  return;
	}
    }
  /* 这也在 shlab pdf 中重点讲过 */
  /* The bg(fg) <job> command restarts <job> by sending it a SIGCONT signal */
  /* and then runs it in the background. */
  /* When you implement your signal handlers, be sure to send SIGINT and SIGSTP signals to the entire foreground process group */
  /* using "-pid" instead of "pid" in the argument to the kill function. */
  kill(-pid, SIGCONT);
  if(!strcmp(first_cmd, "bg"))
    {
      job->state = BG;
      printf("[%d] (%d) %s", jid, pid, job->cmdline);
    }
  else
    {
      job->state = FG;
      /* 当 job state 是 FG 的时候,要等待其完成 */
      waitfg(job->pid);
    }
  return;
}

waitfg

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void waitfg(pid_t pid)
{
  /* 我原来的做法是这样的,这当然可以解决问题,毕竟 forground job 结束之后会被 reap */
  /* 见 sigchld_handler, 最后执行的 deletejob 只是清理 job 的内容 */
  /* 指向这个 job 的指针仍然存在(内存空间没有被回收) */
  /* struct job_t *job = getjobpid(jobs, pid); */
  /* if(job) */
  /*   { */
  /*     while(job->state==FG) */
  /* 	{ */
  /* 	  sleep(1); */
  /* 	} */
  /*   } */
  while(pid == fgpid(jobs))
    {
      sleep(1);
    }
  return;
}

sigchld_handler

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void sigchld_handler(int sig)
{
  /* 这个就要需要详细了解 waitpid 这个函数了,见 man waitpid */
  /* :( 其实看了文档之后还是有点找不到北,按照指导说明拼凑下面的代码,改了多次才验证成功 */
  pid_t pid;
  int status;
  struct job_t *job;
  /* waitpid 手册和网上关于它的只言片语拼凑出来的 */
  /* 为什么代码是这样的?目前的理解来说也给不出一个完全自洽的解释 */
  /* 最开始 waitpid 第二个参数是 NULL, 无法通过 trace16 的验证 */
  /* 这样的话,./tsh 无法捕捉从进程中发送的 SIGSTP 和 SIGINT */
  while((pid = waitpid(-1, &status, WUNTRACED|WNOHANG))>0)
    {
      if(WIFEXITED(status))	      /* process is exited in normal way */
	{
	  deletejob(jobs, pid);
	}
      if(WIFSIGNALED(status))	      /* process is terminated by a signal */
	{
	  printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
	  deletejob(jobs, pid);
	}
      if(WIFSTOPPED(status))	      /* process is stop because of a signal */
	{
	  printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
	  job = getjobpid(jobs, pid);
	  if(job!=NULL)
	    {
	      job->state = ST;
	    }
	}
    }
}

sigint_handler

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void sigint_handler(int sig)
{
  pid_t fpid;
  fpid = fgpid(jobs);
  if(fpid)
    {
      /* kill 这个用法在 do_bgfg 中的 kill 调用前注释讲过*/
      kill(-fpid, sig);
    }
  return;
}

sigstp_handler

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void sigtstp_handler(int sig)
{
  pid_t fpid;
  struct job_t * fjob;
  fpid = fgpid(jobs);
  if(fpid)
    {
      fjob = getjobpid(jobs, fpid);
      if(fjob->state==ST)
	{
	  return;
	}
      /* kill 这个用法在 do_bgfg 中的 kill 调用前注释讲过*/
      kill(-fpid, sig);
    }
  return;
}