当前位置:网站首页>csapp shell lab
csapp shell lab
2022-07-06 09:25:00 【湖大金胜宇】
完整的代码会放在文章的最后。
实验目的:
修改tsh.c文件补全以下七个函数:
eval:解析命令行语句并运行进程
builtin_cmd:判断是否是内建指令
do_bgfg:执行 bg < job > 和 fg < job > 内置命令
waitfg:等待 pid 进程不再是前台进程
sigchld_handler:回收所有可获得的僵死子进程
sigtstp_handler:响应 SIGTSTP(ctrl-z) 信号
sigint_handler:响应 SIGINT(ctrl-c) 信号
实验内容:
通过阅读实验指导书我们知道此实验要求我们完成tsh.c中的七个函数从而实现一个简单的shell,能够处理前后台运行程序、能够处理ctrl+z、ctrl+c等信号。
我们知道七个函数是在tsh.c中实现,我们看到tsh.c里面的具体实现,我们发现里面定义了一些辅助函数如下:
int parseline(const char *cmdline, char **argv); //获取参数列表,返回是否为后台运行命令
void sigquit_handler(int sig); //处理SIGQUIT信号
void clearjob(struct job_t *job); //清除job结构体
void initjobs(struct job_t *jobs); //初始化任务jobs[]
int maxjid(struct job_t *jobs); //返回jobs链表中最大的jid号。
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline); //向jobs[]添加一个任务
int deletejob(struct job_t *jobs, pid_t pid); //在jobs[]中删除pid的job
pid_t fgpid(struct job_t *jobs); //返回当前前台运行job的pid号
struct job_t *getjobpid(struct job_t *jobs, pid_t pid); //根据pid找到对应的job
struct job_t *getjobjid(struct job_t *jobs, int jid); //根据jid找到对应的job
int pid2jid(pid_t pid); //根据pid找到jid
void listjobs(struct job_t *jobs); //打印jobs
接着就是mian函数,作用是在文件中逐行获取命令,并且判断是不是文件结束(EOF),将命令cmdline送入eval函数进行解析。我们需要做的就是逐步完善这个过程。
接下来开始实验:
1.学会编译tsh.c,调用tsh文件traceXX.txt的功能验证方法使用make命令编译tsh.c文件(文件有所改变的话需要先使用make clean指令清空);
2.使用make testXX指令比较traceXX.txt文件在编写的shell和reference shell的运行结果;或者也可以使用”./sdriver.pl -t traceXX.txt -s ./tsh -a “-p”;
3.如果在文件名前面加上r,则是执行标准的tshref,或者将tsh变为tshref。通过比对标准tshref和自制tsh的执行结果结果,可以观察tsh的功能是否正确。如果tsh的执行结果和tshref结果一致,说明结果是正确的。
实现
eval()函数:
函数功能:如果用户已请求内置命令(退出、作业、bg或fg)然后立即执行它。否则,派生一个子进程并在子对象的上下文中运行作业。如果作业正在运行前台,等待它终止,然后返回。注:每个子进程必须具有唯一的进程组ID,以便后台子级不从内核接收SIGINT(SIGTSTP) 当我们在键盘上键入ctrl-c(ctrl-z)时。
函数的原型是书上的void eval(char *cmdline);
这个函数要不停的循环在循环体内 解析命令行 我们通过已经给的parseline函数来解析成一个一个字符指针 如果发现是内置 则直接进行内置命令即可
实现思路:
- 首先初始化个变量,接下来调用parseline函数来解析成一个一个字符指针,由于builtin_cmd函数实现了内建指令的执行,所以我们只需要判断输入的指令是否是内置指令即可,返回0说明不是内置命令。
- 如果不是内置指令,则使用fork函数创建子进程,同时为了避免父子进程间的竞争引发的同步问题,我们需要在创建子进程前屏蔽掉SIGCHLD信号。
- 在子进程中,首先需要解除阻塞,然后使用setpgid(0,0)设置进程组,使子进程的进程组编号和父进程不同,然后调用execve函数来执行job。
- 父进程判断作业是否后台运行,是的话调用addjob函数将子进程job加入job链表中,解除阻塞,然后调用waifg函数等待前台运行完成。如果不在后台工作则打印进程组jid和子进程pid以及命令行字符串。
注意:
这个函数的重点就是要小心同步并发流中的竞争,此外,setpgid也是很有必要的,它将子进程组与tsh进程组分开,避免tsh收到莫名的信号而停止。
子进程继承了父进程的被阻塞集合,在调用execve之前,要先小心解除子进程中阻塞的SIGCHID信号。
在fork()新进程前后要阻塞SIGCHLD信号,防止出现竞争的同步错误。否则,可能会出现子进程先运行结束从jobs中删除,而deletejob什么都不做,因为父进程还没有把子进程添加到列表中;然后再执行父进程,调用addjob错误地把不存在的子进程添加到作业列表中,永远也不会被删除。
每个子进程必须拥有自己的进程组id,以便向子进程组发送信号。否则,所有的子进程与tsh shell进程为同一个进程组,发送信号时,前后台的子进程均会收到。—— 实现:在fork()之后的子进程中setpgid(0,0)
setpgid(pid_t pid, pid_t pgid):将进程pid的进程组改为pgid。如果pid是0,那么就是用当前进程的pid,如果pgid是0,那么就用pid。
void eval(char *cmdline)
{
char* argv[MAXARGS]; //execve()函数的参数
int state = UNDEF; //工作状态,FG或BG
sigset_t set;
pid_t pid; //进程id
// 处理输入的数据
if(parseline(cmdline, argv) == 1) //解析命令行,返回给argv数组
state = BG;
else
state = FG;
if(argv[0] == NULL) //命令行为空直接返回
return;
// 如果不是内置命令
if(!builtin_cmd(argv))//builtin命令用于执行指定的shell内部命令,并返回内部命令的返回值。
{
if(sigemptyset(&set) < 0)//该函数的作用是将信号集初始化为空。
unix_error("sigemptyset error");
if(sigaddset(&set, SIGINT) < 0 || sigaddset(&set, SIGTSTP) < 0 || sigaddset(&set, SIGCHLD) < 0)//该函数的作用是把信号signo添加到信号集set中,成功时返回0,失败时返回-1。
unix_error("sigaddset error");
//在它派生子进程之前阻塞SIGCHLD信号,防止竞争
if(sigprocmask(SIG_BLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
if((pid = fork()) < 0) //fork创建子进程失败
unix_error("fork error");
else if(pid == 0) //fork创建子进程
{
// 子进程的控制流开始
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0) //解除阻塞
unix_error("sigprocmask error");
if(setpgid(0, 0) < 0) //设置子进程id,放到同一个进程组中
unix_error("setpgid error");
if(execve(argv[0], argv, environ) < 0){
printf("%s: command not found\n", argv[0]);
exit(0);
}
}
// 将当前进程添加进job中,无论是前台进程还是后台进程
addjob(jobs, pid, state, cmdline);
// 恢复受阻塞的信号 SIGINT SIGTSTP SIGCHLD
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
// 判断子进程类型并做处理
if(state == FG)
waitfg(pid); //前台作业等待
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); //将进程id映射到job id
}
return;
}
builtin_cmd()函数
**函数功能:**builtin命令用于执行指定的shell内部命令,并返回内部命令的返回值。这里也就是识别并执行内置命令: quit, fg, bg, 和 jobs。
**参数设置:**参数为argv 参数列表
实现思路:
- jobs 列出所有状态的任务(除已经结束了 暂停的也要列)
- bg <jid/pid> 使一个后台任务(暂停或者正在运行) 重新后台启动
- fg <jid/pid> 使一个任务 重新运行 并前台运行
- quit 直接退出程序
- 不是内置命令时返回0
int builtin_cmd(char **argv)
{
if(!strcmp(argv[0], "quit")) //如果命令是quit,退出
exit(0);
else if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) //如果是bg或者fg命令,执行do_fgbg函数
do_bgfg(argv);
else if(!strcmp(argv[0], "jobs")) //如果命令是jobs,列出正在运行和停止的后台作业
listjobs(jobs);
else
return 0; /* not a builtin command */
return 1;
}
do_bgfg()函数
**函数功能:**实现bg和fg两条内置命令
bg :将停止的后台作业更改为正在运行的后台作业。通过发送SIGCONT信号重新启动,然后在后台运行它。参数可以是PID,也可以是JID。ST -> BG
fg :将已停止或正在运行的后台作业更改为前台正在运行的作业。通过发送SIGCONT信号重新启,然后在前台运行它。参数可以是PID,也可以是JID。ST -> FG,BG -> FG
实现思路:
先通过argv[0]判断前后台
id=argv[1]可能的情况:
1.null
2.%jid(判断是否存在)
3.pid(判断是否存在)
4.既不是%jid也不是pid如果进程存在:发送给进程继续执行的信号
然后使用strcmp函数判断是bg命令还是fg指令:
如果是前台进程,设置状态为FG,等待其执行完毕
如果是后台进程,设置状态为BG,输出提示信息。
注意:
区分bg和fg命令,以及传入pid或者jid参数对应的进程的状态。
注意用户输入错误处理,比如参数数量不够或者参数传入错误的情况
void do_bgfg(char **argv)
{
int num;
struct job_t *job;
// 没有参数的fg/bg应该被丢弃
if(!argv[1]){
//命令行为空
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return ;
}
// 检测fg/bg参数,其中%开头的数字是JobID,纯数字的是PID
if(argv[1][0] == '%'){
//解析jid
if((num = strtol(&argv[1][1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobid\n",argv[0]);//失败,打印错误消息
return;
}
if((job = getjobjid(jobs, num)) == NULL){
printf("%%%d: No such job\n", num); //没找到对应的job
return;
}
} else {
if((num = strtol(argv[1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobid\n",argv[0]);//失败,打印错误消息
return;
}
if((job = getjobpid(jobs, num)) == NULL){
printf("(%d): No such process\n", num); //没找到对应的进程
return;
}
}
if(!strcmp(argv[0], "bg")){
// bg会启动子进程,并将其放置于后台执行
job->state = BG; //设置状态
if(kill(-job->pid, SIGCONT) < 0) //采用负数发送信号到进程组
unix_error("kill error");
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
} else if(!strcmp(argv[0], "fg")) {
job->state = FG; //设置状态
if(kill(-job->pid, SIGCONT) < 0) //采用负数发送信号到进程组
unix_error("kill error");
// 当一个进程被设置为前台执行时,当前tsh应该等待该子进程结束
waitfg(job->pid);
} else {
puts("do_bgfg: Internal error");
exit(0);
}
return;
}
waitfg()函数
**函数功能:**等待一个前台作业结束,或者说是阻塞一个前台的进程直到这个进程变为后台进程
**参数设置:**只有一个参数并且参数设置为进程ID
实现思路:
判断当前的前台的进程组pid是否和当前进程的pid是否相等,如果相等则休眠直到前台进程结束。
注意:
因为信号可能会在执行pause前到来(恰好),这样就会形成竞争关系,如果程序这个时候执行了pause的话,就会等待着一个永远不会到来的信号(可能永远不会醒过来)。所以要使用sigsuspend函数。同时,也可以使用sleep,但是同样不如使用sigsuspend函数。
void waitfg(pid_t pid)
{
sigset_t mask, prev;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, &prev);
while (fgpid(jobs) != 0){
sigsuspend(&mask);
//printf ("wait here\n");
}
sigprocmask(SIG_SETMASK, &prev, NULL);
return;
}
sigchld_handler()函数
**函数功能:**处理SIGCHILD信号
**参数设置:**参数为信号类型
实现思路:
用while循环调用waitpid直到它所有的子进程终止。
检查己回收子进程的退出状态
WIFSTOPPED::引起返回的子进程当前是被停止的
WIFSIGNALED:子进程是因为一个未被捕获的信号终止
WIFEXITED:子进程通过调用exit 或者return正常终止
然后分别用WSTOPSIG,WTERMSIG,WEXITSTATUS提取以上三个退出状态。注意如果引起返回的子进程当前是被停止的进程,那么要将其状态设置为ST。
注意:
这个函数要处理三种情况,一是处理正常中止的情况,二是收到信号中止的情况,三是被信号所暂时停止的情况,前两种情况都要显示的将进程从进程表中删除,第三种情况却不用,但是却要更改状态。为了防止del程序被中断,我们仍然需要加塞。
void sigchld_handler(int sig)
{
int status, jid;
pid_t pid;
struct job_t *job;
if(verbose)
puts("sigchld_handler: entering");
/* 以非阻塞方式等待所有子进程 waitpid 参数3: 1. 0 : 执行waitpid时, 只有在子进程终止时才会返回。 2. WNOHANG : 若子进程仍然在运行,则返回0 。 注意只有设置了这个标志,waitpid才有可能返回0 3. WUNTRACED : 如果子进程由于传递信号而停止,则马上返回。 只有设置了这个标志,waitpid返回时,其WIFSTOPPED(status)才有可能返回true */
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
// 如果当前这个子进程的job已经删除了,则表示有错误发生
if((job = getjobpid(jobs, pid)) == NULL){
printf("Lost track of (%d)\n", pid);
return;
}
jid = job->jid;
//接下来判断三种状态
// 如果这个子进程收到了一个暂停信号(还没退出)
if(WIFSTOPPED(status)){
printf("Job [%d] (%d) stopped by signal %d\n", jid, job->pid, WSTOPSIG(status));
job->state = ST; //状态设为挂起
}
// 如果子进程通过调用 exit 或者一个返回 (return) 正常终止
else if(WIFEXITED(status)){
if(deletejob(jobs, pid))
if(verbose){
printf("sigchld_handler: Job [%d] (%d) deleted\n", jid, pid);
printf("sigchld_handler: Job [%d] (%d) terminates OK (status %d)\n", jid, pid, WEXITSTATUS(status));
}
}
// 如果子进程是因为一个未被捕获的信号终止的,例如SIGKILL
else {
if(deletejob(jobs, pid)){
//清除进程
if(verbose)
printf("sigchld_handler: Job [%d] (%d) deleted\n", jid, pid);
}
printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status)); //返回导致子进程终止的信号的数量
}
}
if(verbose)
puts("sigchld_handler: exiting");
return;
}
sigint_handler函数
**函数功能:**当用户按下ctrl-c时,向前台程序发送SIGINT信号
**参数设置:**参数为信号类型
实现思路:
- 使用fgpid来获得前台进程的pid
- 当前台进程存在时,使用kill发送SIGINT给前台进程组
- 如果kill函数调用失败,则输出error
注意:
前台进程组内的进程除了当前前台进程以外,还包括前台进程的子进程。
void sigint_handler(int sig)
{
if(verbose)
puts("sigint_handler: entering");
pid_t pid = fgpid(jobs);
if(pid){
// 发送SIGINT给前台进程组里的所有进程
// 需要注意的是,前台进程组内的进程除了当前前台进程以外,还包括前台进程的子进程。
// 最多只能存在一个前台进程,但前台进程组内可以存在多个进程
if(kill(-pid, SIGINT) < 0)
unix_error("kill (sigint) error");
if(verbose){
printf("sigint_handler: Job (%d) killed\n", pid);
}
}
if(verbose)
puts("sigint_handler: exiting");
return;
}
sigtstp_handler()函数
**函数功能:**捕获SIGTSTP信号
SIGTSPT信号默认行为是停止直到下一个 SIGCONT,是来自终端的停止信号,在键盘上输入 CTR+Z会导致一个 SIGTSPT信号被发送到外壳。外壳捕获该信号,然后发送SIGTSPT信号到这个前台进程组中的每个进程。在默认情况下,结果是停止或挂起前台作业。
**参数设置:**参数为信号类型
实现思路:
- 用fgpid(jobs)获取前台进程pid,判断当前是否有前台进程,如果没有直接返回。
- 用kill(-pid,sig)函数发送SIGTSPT信号给前台进程组。
void sigtstp_handler(int sig)
{
if(verbose)
puts("sigstp_handler: entering");
pid_t pid = fgpid(jobs);
struct job_t *job = getjobpid(jobs, pid);
if(pid){
if(kill(-pid, SIGTSTP) < 0)
unix_error("kill (tstp) error");
if(verbose){
printf("sigstp_handler: Job [%d] (%d) stopped\n", job->jid, pid);
}
}
if(verbose)
puts("sigstp_handler: exiting");
return;
}
至此,七个函数全部补充完整,接下来开始测试函数功能是否正确。
测试:
首先,我们需要了解输出的是什么:
符号:
- 空格:分隔指令作用
- &:如果命令以&结尾,表示标该作业在后台运行
- #:直接打印#后一行的文本内容
- %:后接一个整数,表示job的ID号。
用户程序:
- myint程序:函数睡眠,使程序睡眠n秒,运行结束后不会自动退出,并会检测系统错误;
- myspin程序:函数睡眠,使程序睡眠n秒,在睡眠结束后就自动退出,不检测系统错误;
- mysplit程序:函数睡眠,使程序睡眠n秒,创建一个子进程进行睡眠,然后父进程等待子进程正常睡眠n秒后,继续运行;
- mystop程序:让进程暂定n秒,并发送信号。
第一关:
第一关的文件如下:
#
# trace01.txt - Properly terminate on EOF.
# CLOSE
WAIT
我们可以看到第一关调用linux命令close关闭文件并wait等待,看是否正确中止在EOF。
验证结果:
可以看到测试文件与答案输出的一致。
第二关:
第二关的文件如下:
#
# trace02.txt - Process builtin quit command.
# quit
WAIT
这里是内部命令就是当输入quit的时候,我们需要退出shell进程,这里我们需要判断命令是否是quit,如果是则我们需要退出该shell进程。也就是内置命令,就是测试builtin_cmd()函数的部分实现。
验证结果:
结果相同,所以说明builtin_cmd()函数的部分实现是正确的。
第三关:
第三关的文件如下:
#
# trace03.txt - Run a foreground job.
#
/bin/echo tsh> quit
quit
这里讲一下/bin/echo
eval函数先通过builtin_cmd查询cmdline是不是内置命令如quit,如果是则实行
如果不是则创建一个子进程,在子进程中调用 execve()函数通过 argv[0]来寻找路径,并在子进程中运行路径中的可执行文件,如果找不到则说明命令为无效命令,输出命令无效,并用 exit(0)结束该子进程
/bin/echo就是打开bin目录下的echo文件,echo可以理解为将其后面的内容当作字符串输出
打开回显或关闭请求回显功能,或显示消息。如果没有任何参数,echo 命令将显示当前回显设置。
所以这里的意思就是eval发现/bin/echo不是内置命令,查询路径找到了echo程序并且在前台执行,输出了其后面的一句话。
第三关所做的事情就是:
- 打开bin目录下的echo的可执行文件,在前台开启一个子进程运行echo这个文件,然后eval()函数使用builtin_cmd()判断这里是否是内置的命令,如果是内置指令则直接执行。
- 执行quit指令,子进程结束,接下来第二个quit执行则直接退回到了中断。
验证结果:
发现输出的结果相同。
第四关:
第四关的文件如下:
#
# trace04.txt - Run a background job.
#
/bin/echo -e tsh> ./myspin 1 \046
./myspin 1 &
根据注释我们可以知道这个文件是测试是否可以运行一个后台程序的。
看到具体指令,通过第三关我们可以知道,这里同样是运行bin目录底下的可执行文件echo,这个时候是在前台执行的echo命令,之后等待程序执行完毕回收子进程。&代表是一个后台程序,myspin睡眠1秒,然后停止。因为在后台,所以显示下面一句,如果在前台则无。(这个是自己写的程序,后台程序会打印出一句话)
验证结果:
输出的结果相同,说明后台运行程序是正确的。
第五关:
第五关的文件如下:
#
# trace05.txt - Process jobs builtin command.
#
/bin/echo -e tsh> ./myspin 2 \046
./myspin 2 &
/bin/echo -e tsh> ./myspin 3 \046
./myspin 3 &
/bin/echo tsh> jobs
jobs
这里的运行顺序如下:前台echo,后台myspin,前台echo,后台myspin,前台echo,然后使用内置指令jobs来打印进程列表中的所有信息。
验证结果:
输出的结果相同,说明eval()和builtin_cmd()函数的部分实现是正确的。
第六关:
第六关代码如下:
#
# trace06.txt - Forward SIGINT to foreground job.
#
/bin/echo -e tsh> ./myspin 4
./myspin 4
SLEEP 2
INT
根据注释我们可以知道这些命令的具体含义是将SIGINT转发给前台作业。
所以,我们应该可以知道如果接收到了中断信号SIGINT(即CTRL_C)那么结束前台进程。
验证结果:
根据输出结果,我们可以知道该进程被signal 2中止了,根据宏定义可以知道这个信号就是中断信号SIGINT。所以输出的结果是正确的。
第七关:
第七关的文件如下:
#
# trace07.txt - Forward SIGINT only to foreground job.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo -e tsh> ./myspin 5
./myspin 5
SLEEP 2
INT
/bin/echo tsh> jobs
jobs
根据注释,我们可以知道第七关测试的是只将SIGINT转发给前台作业。这里的命令行其实根据前面的就很好理解了,就是给出两个作业,一个在前台工作,另一个在后台工作,接下来传递SIGINT指令,然后调用内置指令jobs来查看此时的工作信息,来对比出是不是只将SIGINT转发给前台作业。
验证结果:
通过输出结果,我们可知道最后只有后台工作还存在,说明我们的实现是正确的。同时输出的结果相同,也说明了这一点。
第八关:
第八关的文件如下:
#
# trace08.txt - Forward SIGTSTP only to foreground job.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo -e tsh> ./myspin 5
./myspin 5
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
根据注释我们是需要将SIGTSTP转发给前台作业。根据这个信号的作用,也就是该进程会停止知道下一个SIGCONT也就是挂起,让别的程序继续运行。这里也就是运行了后台程序,然后使用jobs来打印出进程的信息。
验证结果:
我们可以发现最后的输出结果与我们分析的相同,同时也与答案输出的相同。
第九关:
第九关文件如下:
#
# trace09.txt - Process bg builtin command
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo -e tsh> ./myspin 5
./myspin 5
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> bg %2
bg %2
/bin/echo tsh> jobs
jobs
根据注释,这个测试程序是为了测试处理bg内置命令。
bg :将停止的后台作业更改为正在运行的后台作业。通过发送SIGCONT信号重新启动,然后在后台运行它。参数可以是PID,也可以是JID。ST -> BG
这里是在第八关的测试文件之上的一个更加完整的测试,这里也就是在停止后,输出进程信息之后,使用bg命令来唤醒进程2,也就是刚才被挂起的程序,接下来继续使用Jobs命令来输出结果。
验证结果:
最后,发现输出结果与分析的一致,同时与答案输出的也一致。
第十关:
第十关文件如下:
#
# trace10.txt - Process fg builtin command.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
SLEEP 1
/bin/echo tsh> fg %1
fg %1
SLEEP 1
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> fg %1
fg %1
/bin/echo tsh> jobs
jobs
根据注释我们可以知道这一关主要是测试命令fg是否正确。
fg :将已停止或正在运行的后台作业更改为前台正在运行的作业。通过发送SIGCONT信号重新启动,然后在前台运行它。参数可以是PID,也可以是JID。ST -> FG,BG -> FG
这里是将后台的进程更改为前台正在运行的程序。测试文中进程1根据&可以知道,进程1是一个后台进程。先使用fg命令将其转化为前台的一个程序,接下来停止进程1,然后打印出进程信息,这时候进程1应该是前台程序同时被挂起了,接下来使用fg命令使其继续运行,使用jobs来打印出进程信息即可验证我们的分析是否正确。
验证结果:
与分析一致,说明正确。
第十一关:
第十一关文件如下:
#
# trace11.txt - Forward SIGINT to every process in foreground process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4
SLEEP 2
INT
/bin/echo tsh> /bin/ps a
/bin/ps a
根据注释我们可以知道这里需要将SIGINT发给前台进程组中的每个进程。ps –a 显示所有进程,这里是有两个进程的,mysplit创建了一个子进程,接下来发送指令SIGINT,所以进程组中的所有进程都应该停止,接下来调用pl来查看该进程组中的每个进程是否都停止了。
验证结果:
输出的结果与分析的相同。
第十二关:
第十二关的文件如下:
#
# trace12.txt - Forward SIGTSTP to every process in foreground process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> /bin/ps a
/bin/ps a
根据注释可知该测试程序是为了测试将SIGTSTP转发给前台进程组中的每个进程。与上一关相同,只需要相应的进程被挂起即可。
验证结果:
与分析的结果一致。
第十三关:
第十三关的文件如下:
#
# trace13.txt - Restart every stopped process in process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> /bin/ps a
/bin/ps a
/bin/echo tsh> fg %1
fg %1
/bin/echo tsh> /bin/ps a
/bin/ps a
根据注释我们可以知道该程序是为了测试重新启动进程组中的每个停止的进程。这里也就是使用fg来唤醒整个工作,中间使用ps -a来查看停止整个工作和唤醒整个工作的区别。
验证结果:
与分析的一致。
第十四关:
第十四关的文件如下:
#
# trace14.txt - Simple error handling
#
/bin/echo tsh> ./bogus
./bogus
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo tsh> fg
fg
/bin/echo tsh> bg
bg
/bin/echo tsh> fg a
fg a
/bin/echo tsh> bg a
bg a
/bin/echo tsh> fg 9999999
fg 9999999
/bin/echo tsh> bg 9999999
bg 9999999
/bin/echo tsh> fg %2
fg %2
/bin/echo tsh> fg %1
fg %1
SLEEP 2
TSTP
/bin/echo tsh> bg %2
bg %2
/bin/echo tsh> bg %1
bg %1
/bin/echo tsh> jobs
jobs
根据注释可以知道这个文件是为了测试简单的错误处理。这里的测试文件,也就是测试fg和bg后面的参数,我们知道fg和bg后面需要一个JID或者是PID,其中JID是加上%的整型数。其余参数都应该报错,或是没有参数也应该报错。接下来测试的功能,都在上面的关卡测试过了。
注:这里我觉得少了一中测试情况,也就是当输入三个参数的情况。也应该报出参数数量不对的错误。
验证结果:
与分析的结果一致。
第十五关:
第十五关的文件如下:
#
# trace15.txt - Putting it all together
#
/bin/echo tsh> ./bogus
./bogus
/bin/echo tsh> ./myspin 10
./myspin 10
SLEEP 2
INT
/bin/echo -e tsh> ./myspin 3 \046
./myspin 3 &
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &
/bin/echo tsh> jobs
jobs
/bin/echo tsh> fg %1
fg %1
SLEEP 2
TSTP
/bin/echo tsh> jobs
jobs
/bin/echo tsh> bg %3
bg %3
/bin/echo tsh> bg %1
bg %1
/bin/echo tsh> jobs
jobs
/bin/echo tsh> fg %1
fg %1
/bin/echo tsh> quit
quit
根据注释这个测试文件测试的是把它们放在一起。本来还纳闷放在一起是什么意思呢?仔细阅读测试文件,可以知道他是测试了上述所有命令,如jobs,fg,bg,quit。这里就不在赘述了。
验证结果:
与分析的结果相同。
第十六关:
第十六关的文件如下:
#
# trace16.txt - Tests whether the shell can handle SIGTSTP and SIGINT
# signals that come from other processes instead of the terminal.
#
/bin/echo tsh> ./mystop 2
./mystop 2
SLEEP 3
/bin/echo tsh> jobs
jobs
/bin/echo tsh> ./myint 2
./myint 2
根据注释可以知道这个测试文件是为了测试测试shell是否可以处理SIGTSTP和SIGINT信号来自其他进程而不是终端。
这个测试文件的具体含义就是,用户程序向job 2传送了中止信号,所以最后会输出进程2被中止的信息。同时,mystop需要自己停止才能给别的进程发送信号,所以中间也会出现进程1被中止的信息。
验证结果:
与分析的结果一致。
至此,是十六关全部通过。
完整的tsh.c的代码:
/* * 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);
/* * 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) */
dup2(1, 2);
/* Parse the command line */
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = 1;
break;
case 'p': /* don't print a prompt */
emit_prompt = 0; /* handy for automatic testing */
break;
default:
usage();
}
}
/* Install the signal handlers */
/* These are the ones you will need to implement */
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
/* This one provides a clean way to kill the shell */
Signal(SIGQUIT, sigquit_handler);
/* Initialize the job list */
initjobs(jobs);
/* Execute the shell's read/eval loop */
while (1) {
/* Read command line */
if (emit_prompt) {
printf("%s", prompt);
fflush(stdout);
}
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");
if (feof(stdin)) {
/* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}
/* Evaluate the command line */
eval(cmdline);
fflush(stdout);
fflush(stdout);
}
exit(0); /* control never reaches here */
}
/* * eval - Evaluate the command line that the user has just typed in * * If the user has requested a built-in command (quit, jobs, bg or fg) * then execute it immediately. Otherwise, fork a child process and * run the job in the context of the child. If the job is running in * the foreground, wait for it to terminate and then return. Note: * each child process must have a unique process group ID so that our * background children don't receive SIGINT (SIGTSTP) from the kernel * when we type ctrl-c (ctrl-z) at the keyboard. */
void eval(char *cmdline)
{
char* argv[MAXARGS]; //execve()函数的参数
int state = UNDEF; //工作状态,FG或BG
sigset_t set;
pid_t pid; //进程id
// 处理输入的数据
if(parseline(cmdline, argv) == 1) //解析命令行,返回给argv数组
state = BG;
else
state = FG;
if(argv[0] == NULL) //命令行为空直接返回
return;
// 如果不是内置命令
if(!builtin_cmd(argv))
{
if(sigemptyset(&set) < 0)
unix_error("sigemptyset error");
if(sigaddset(&set, SIGINT) < 0 || sigaddset(&set, SIGTSTP) < 0 || sigaddset(&set, SIGCHLD) < 0)
unix_error("sigaddset error");
//在它派生子进程之前阻塞SIGCHLD信号,防止竞争
if(sigprocmask(SIG_BLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
if((pid = fork()) < 0) //fork创建子进程失败
unix_error("fork error");
else if(pid == 0) //fork创建子进程
{
// 子进程的控制流开始
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0) //解除阻塞
unix_error("sigprocmask error");
if(setpgid(0, 0) < 0) //设置子进程id
unix_error("setpgid error");
if(execve(argv[0], argv, environ) < 0){
printf("%s: command not found\n", argv[0]);
exit(0);
}
}
// 将当前进程添加进job中,无论是前台进程还是后台进程
addjob(jobs, pid, state, cmdline);
// 恢复受阻塞的信号 SIGINT SIGTSTP SIGCHLD
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
// 判断子进程类型并做处理
if(state == FG)
waitfg(pid); //前台作业等待
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); //将进程id映射到job id
}
return;
}
/* * parseline - Parse the command line and build the argv array. * * Characters enclosed in single quotes are treated as a single * argument. Return true if the user has requested a BG job, false if * the user has requested a FG job. */
int parseline(const char *cmdline, char **argv)
{
static char array[MAXLINE]; /* holds local copy of command line */
char *buf = array; /* ptr that traverses command line */
char *delim; /* points to first space delimiter */
int argc; /* number of args */
int bg; /* background job? */
strcpy(buf, cmdline);
buf[strlen(buf)-1] = ' '; /* replace trailing '\n' with space */
while (*buf && (*buf == ' ')) /* ignore leading spaces */
buf++;
/* Build the argv list */
argc = 0;
if (*buf == '\'') {
buf++;
delim = strchr(buf, '\'');
}
else {
delim = strchr(buf, ' ');
}
while (delim) {
argv[argc++] = buf;
*delim = '\0';
buf = delim + 1;
while (*buf && (*buf == ' ')) /* ignore spaces */
buf++;
if (*buf == '\'') {
buf++;
delim = strchr(buf, '\'');
}
else {
delim = strchr(buf, ' ');
}
}
argv[argc] = NULL;
if (argc == 0) /* ignore blank line */
return 1;
/* should the job run in the background? */
if ((bg = (*argv[argc-1] == '&')) != 0) {
argv[--argc] = NULL;
}
return bg;
}
/* * builtin_cmd - If the user has typed a built-in command then execute * it immediately. */
int builtin_cmd(char **argv)
{
if(!strcmp(argv[0], "quit")) //如果命令是quit,退出
exit(0);
else if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) //如果是bg或者fg命令,执行do_fgbg函数
do_bgfg(argv);
else if(!strcmp(argv[0], "jobs")) //如果命令是jobs,列出正在运行和停止的后台作业
listjobs(jobs);
else
return 0; /* not a builtin command */
return 1;
}
/* * do_bgfg - Execute the builtin bg and fg commands */
void do_bgfg(char **argv)
{
int num;
struct job_t *job;
// 没有参数的fg/bg应该被丢弃
if(!argv[1]){
//命令行为空
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return ;
}
// 检测fg/bg参数,其中%开头的数字是JobID,纯数字的是PID
if(argv[1][0] == '%'){
//解析jid
if((num = strtol(&argv[1][1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobid\n",argv[0]);//失败,打印错误消息
return;
}
if((job = getjobjid(jobs, num)) == NULL){
printf("%%%d: No such job\n", num); //没找到对应的job
return;
}
} else {
if((num = strtol(argv[1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobid\n",argv[0]);//失败,打印错误消息
return;
}
if((job = getjobpid(jobs, num)) == NULL){
printf("(%d): No such process\n", num); //没找到对应的进程
return;
}
}
if(!strcmp(argv[0], "bg")){
// bg会启动子进程,并将其放置于后台执行
job->state = BG; //设置状态
if(kill(-job->pid, SIGCONT) < 0) //采用负数发送信号到进程组
unix_error("kill error");
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
} else if(!strcmp(argv[0], "fg")) {
job->state = FG; //设置状态
if(kill(-job->pid, SIGCONT) < 0) //采用负数发送信号到进程组
unix_error("kill error");
// 当一个进程被设置为前台执行时,当前tsh应该等待该子进程结束
waitfg(job->pid);
} else {
puts("do_bgfg: Internal error");
exit(0);
}
return;
}
/* * waitfg - Block until process pid is no longer the foreground process */
void waitfg(pid_t pid)
{
struct job_t *job = getjobpid(jobs, pid);
if(!job) return;
// 如果当前子进程的状态没有发生改变,则tsh继续休眠
while(job->state == FG)
// 使用sleep的这段代码会比较慢,最好使用sigsuspend
sleep(1);
return;
}
/***************** * Signal handlers *****************/
/* * sigchld_handler - The kernel sends a SIGCHLD to the shell whenever * a child job terminates (becomes a zombie), or stops because it * received a SIGSTOP or SIGTSTP signal. The handler reaps all * available zombie children, but doesn't wait for any other * currently running children to terminate. */
void sigchld_handler(int sig)
{
int status, jid;
pid_t pid;
struct job_t *job;
if(verbose)
puts("sigchld_handler: entering");
/* 以非阻塞方式等待所有子进程 waitpid 参数3: 1. 0 : 执行waitpid时, 只有在子进程 **终止** 时才会返回。 2. WNOHANG : 若子进程仍然在运行,则返回0 。 注意只有设置了这个标志,waitpid才有可能返回0 3. WUNTRACED : 如果子进程由于传递信号而停止,则马上返回。 只有设置了这个标志,waitpid返回时,其WIFSTOPPED(status)才有可能返回true */
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
// 如果当前这个子进程的job已经删除了,则表示有错误发生
if((job = getjobpid(jobs, pid)) == NULL){
printf("Lost track of (%d)\n", pid);
return;
}
jid = job->jid;
//接下来判断三种状态
// 如果这个子进程收到了一个暂停信号(还没退出)
if(WIFSTOPPED(status)){
printf("Job [%d] (%d) stopped by signal %d\n", jid, job->pid, WSTOPSIG(status));
job->state = ST; //状态设为挂起
}
// 如果子进程通过调用 exit 或者一个返回 (return) 正常终止
else if(WIFEXITED(status)){
if(deletejob(jobs, pid))
if(verbose){
printf("sigchld_handler: Job [%d] (%d) deleted\n", jid, pid);
printf("sigchld_handler: Job [%d] (%d) terminates OK (status %d)\n", jid, pid, WEXITSTATUS(status));
}
}
// 如果子进程是因为一个未被捕获的信号终止的,例如SIGKILL
else {
if(deletejob(jobs, pid)){
//清除进程
if(verbose)
printf("sigchld_handler: Job [%d] (%d) deleted\n", jid, pid);
}
printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status)); //返回导致子进程终止的信号的数量
}
}
if(verbose)
puts("sigchld_handler: exiting");
return;
}
/* * sigint_handler - The kernel sends a SIGINT to the shell whenver the * user types ctrl-c at the keyboard. Catch it and send it along * to the foreground job. */
void sigint_handler(int sig)
{
if(verbose)
puts("sigint_handler: entering");
pid_t pid = fgpid(jobs);
if(pid){
// 发送SIGINT给前台进程组里的所有进程
// 需要注意的是,前台进程组内的进程除了当前前台进程以外,还包括前台进程的子进程。
// 最多只能存在一个前台进程,但前台进程组内可以存在多个进程
if(kill(-pid, SIGINT) < 0)
unix_error("kill (sigint) error");
if(verbose){
printf("sigint_handler: Job (%d) killed\n", pid);
}
}
if(verbose)
puts("sigint_handler: exiting");
return;
}
/* * sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever * the user types ctrl-z at the keyboard. Catch it and suspend the * foreground job by sending it a SIGTSTP. */
void sigtstp_handler(int sig)
{
if(verbose)
puts("sigstp_handler: entering");
pid_t pid = fgpid(jobs);
struct job_t *job = getjobpid(jobs, pid);
if(pid){
if(kill(-pid, SIGTSTP) < 0)
unix_error("kill (tstp) error");
if(verbose){
printf("sigstp_handler: Job [%d] (%d) stopped\n", job->jid, pid);
}
}
if(verbose)
puts("sigstp_handler: exiting");
return;
}
/********************* * End signal handlers *********************/
/*********************************************** * Helper routines that manipulate the job list **********************************************/
/* clearjob - Clear the entries in a job struct */
void clearjob(struct job_t *job) {
job->pid = 0;
job->jid = 0;
job->state = UNDEF;
job->cmdline[0] = '\0';
}
/* initjobs - Initialize the job list */
void initjobs(struct job_t *jobs) {
int i;
for (i = 0; i < MAXJOBS; i++)
clearjob(&jobs[i]);
}
/* maxjid - Returns largest allocated job ID */
int maxjid(struct job_t *jobs)
{
int i, max=0;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].jid > max)
max = jobs[i].jid;
return max;
}
/* addjob - Add a job to the job list */
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid == 0) {
jobs[i].pid = pid;
jobs[i].state = state;
jobs[i].jid = nextjid++;
if (nextjid > MAXJOBS)
nextjid = 1;
strcpy(jobs[i].cmdline, cmdline);
if(verbose){
printf("Added job [%d] %d %s\n", jobs[i].jid, jobs[i].pid, jobs[i].cmdline);
}
return 1;
}
}
printf("Tried to create too many jobs\n");
return 0;
}
/* deletejob - Delete a job whose PID=pid from the job list */
int deletejob(struct job_t *jobs, pid_t pid)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid == pid) {
clearjob(&jobs[i]);
nextjid = maxjid(jobs)+1;
return 1;
}
}
return 0;
}
/* fgpid - Return PID of current foreground job, 0 if no such job */
pid_t fgpid(struct job_t *jobs) {
int i;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].state == FG)
return jobs[i].pid;
return 0;
}
/* getjobpid - Find a job (by PID) on the job list */
struct job_t *getjobpid(struct job_t *jobs, pid_t pid) {
int i;
if (pid < 1)
return NULL;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].pid == pid)
return &jobs[i];
return NULL;
}
/* getjobjid - Find a job (by JID) on the job list */
struct job_t *getjobjid(struct job_t *jobs, int jid)
{
int i;
if (jid < 1)
return NULL;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].jid == jid)
return &jobs[i];
return NULL;
}
/* pid2jid - Map process ID to job ID */
int pid2jid(pid_t pid)
{
int i;
if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].pid == pid) {
return jobs[i].jid;
}
return 0;
}
/* listjobs - Print the job list */
void listjobs(struct job_t *jobs)
{
int i;
for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid != 0) {
printf("[%d] (%d) ", jobs[i].jid, jobs[i].pid);
switch (jobs[i].state) {
case BG:
printf("Running ");
break;
case FG:
printf("Foreground ");
break;
case ST:
printf("Stopped ");
break;
default:
printf("listjobs: Internal error: job[%d].state=%d ",
i, jobs[i].state);
}
printf("%s", jobs[i].cmdline);
}
}
}
/****************************** * end job list helper routines ******************************/
/*********************** * Other helper routines ***********************/
/* * usage - print a help message */
void usage(void)
{
printf("Usage: shell [-hvp]\n");
printf(" -h print this message\n");
printf(" -v print additional diagnostic information\n");
printf(" -p do not emit a command prompt\n");
exit(1);
}
/* * unix_error - unix-style error routine */
void unix_error(char *msg)
{
fprintf(stdout, "%s: %s\n", msg, strerror(errno));
exit(1);
}
/* * app_error - application-style error routine */
void app_error(char *msg)
{
fprintf(stdout, "%s\n", msg);
exit(1);
}
/* * Signal - wrapper for the sigaction function */
handler_t *Signal(int signum, handler_t *handler)
{
struct sigaction action, old_action;
action.sa_handler = handler;
sigemptyset(&action.sa_mask); /* block sigs of type being handled */
action.sa_flags = SA_RESTART; /* restart syscalls if possible */
if (sigaction(signum, &action, &old_action) < 0)
unix_error("Signal error");
return (old_action.sa_handler);
}
/* * sigquit_handler - The driver program can gracefully terminate the * child shell by sending it a SIGQUIT signal. */
void sigquit_handler(int sig)
{
printf("Terminating after receipt of SIGQUIT signal\n");
exit(1);
}
边栏推荐
- Should wildcard import be avoided- Should wildcard import be avoided?
- Lab 8 文件系统
- The maximum number of words in the sentence of leetcode simple question
- Stc-b learning board buzzer plays music
- Mysql database (II) DML data operation statements and basic DQL statements
- ucore lab1 系统软件启动过程 实验报告
- The minimum number of operations to convert strings in leetcode simple problem
- UCORE LaB6 scheduler experiment report
- Brief description of compiler optimization level
- Express
猜你喜欢
线程及线程池
CSAPP Shell Lab 实验报告
Opencv recognition of face in image
MySQL数据库(四)事务和函数
转行软件测试必需要知道的知识
CSAPP家庭作业答案7 8 9章
How to write the bug report of software test?
Knowledge that you need to know when changing to software testing
What if software testing is too busy to study?
[C language] twenty two steps to understand the function stack frame (pressing the stack, passing parameters, returning, bouncing the stack)
随机推荐
Video scrolling subtitle addition, easy to make with this technique
Investment should be calm
ucore lab5用户进程管理 实验报告
CSAPP homework answers chapter 789
Cc36 different subsequences
How to write the bug report of software test?
JDBC介绍
[C language] twenty two steps to understand the function stack frame (pressing the stack, passing parameters, returning, bouncing the stack)
Global and Chinese market of DVD recorders 2022-2028: Research Report on technology, participants, trends, market size and share
The minimum sum of the last four digits of the split digit of leetcode simple problem
Global and Chinese market of pinhole glossmeter 2022-2028: Research Report on technology, participants, trends, market size and share
How to use Moment. JS to check whether the current time is between 2 times
UCORE lab5 user process management experiment report
MySQL transactions
Practical cases, hand-in-hand teaching you to build e-commerce user portraits | with code
Currently, mysql5.6 is used. Which version would you like to upgrade to?
STC-B学习板蜂鸣器播放音乐
软件测试Bug报告怎么写?
Threads and thread pools
ucore lab1 系统软件启动过程 实验报告