shlab实验

计算机系统原理实验报告

一、实验目的及内容

1. 实验目的:通过编写完善一个支持作业控制的简单 Unix 外壳 Shell 程序(tsh),

掌握 shell 的基本功能原理和实现方法;掌握 Unix 进程控制、特别是信号的应用,

包括信号产生和信号接收(捕获处理)、及并发冲突及解决方法,熟悉相关系统函数

的应用。深入理解计算机系统中进程、并发、异常控制流,逻辑空间与物理空间等应

用的基本原理。

2. 试验任务及内容:按提供的 tsh 框架,利用相关系统调用库函数,完成编写并测试一

个带作业控制的Unix外壳程序,分步实现的功能包括 quit、bg、fg、jobs内部命令

及其他外部作业控制,实现sigchild、sigint、sigtstp信号处理。

二、实验原理及方案

1.编程实现shell的功能及方法

  tsh.c中所给主要函数如下:

需要自己编写完善的函数如下:

void eval(char *cmdline)      -- 分析命令,并派生子进程执行

int builtin_cmd(char **argv) -- 执行内置命令

void do_bgfg(char **argv)     -- 执行bg和fg命令

void waitfg(pid_t pid)        -- 阻塞,直到一个前台进程运行完

以及4个信号处理函数中3个待完善的信号处理函数:

void sigchld_handler(int sig) -- SIGCHID信号处理函数

void sigint_handler(int sig) -- SIGINT信号处理函数

void sigtstp_handler(int sig) -- SIGTSTP信号处理函数

在信号处理函数中需要注意的是:

在进入和退出信号处理函数的时候保存和还原errno变量;

当试图访问全局结构变量的时候暂时block所有的信号,然后还原;

全局变量的声明为volatile;

其中对于主函数,我们只需了解它的框架、功能等,其流程图如下:

然后要一一实现tsh.c中的待完善函数:

  1. eval(char *cmdline) 函数

函数功能:对用户输入的命令参数进行解析计算并求值运行;

参数:char *cmdline -- 传入的是从命令行标准缓冲区读入的命令行;

处理流程:

首先调用parseline函数将cmdline解析生成参数数组argv[](返回值为1代表后台作业,为0代表前台作业);

如果argv[0]==NULL即命令行为空,直接返回;

否则,调用builtin_cmd函数,若用户输入为内置命令行则其返回1,否则返回0;

若builtin_cmd返回0,阻塞SIGCHLD,防止子进程在父进程之间结束,fork一个新的子进程,在子进程中解除对SIGCHLD阻塞,并且通过setpgid函数修改子进程的进程组;

/*

*因为tsh调用fork()创建的子进程与父进程同属于一个进程组,所以所有的子进程与当前的tsh shell进程为同一个进程组,发送信号到tsh shell进程组时,前后台的子进程均会收到,假如我们键入了ctrl+c,这时会让所有子进程全被杀死(包括后台进程); 所以我们需要在fork()子进程之后通过setpgid(0,0)设置新的进程组,与tsh shell进程组分开,比如当我们输入ctrl+c,内核会发送一个SIGINT信号到前台进程组的每个进程,前台进程tsh shell应该捕获sigint,然后将其转发到 setpgid新设置的前台作业进程组,这样就可以只杀死所有的前台进程了

*/

然后利用execve函数加载该任务,在子进程的上下文中运行,如果该任务是前台任务,那么需要等其运行终止并回收才返回;

创建完成子进程后,通过addjob将创建的子进程添加到jobs,即子进程应添加到前台进程组,以便接收发到前台进程组的信号,添加时要注意传入此进程的状态(前台/后台作业);

addjob函数执行期间,必须保证不能被中断,因此,要阻塞所有信号;

然后判断作业为前台还是后台,如果是前台作业,调用waitfg函数等待前台作业运行完成,如果是后台作业,打印消息即可。

在该函数中调用其他函数,需要设置调用失败的情况。

 

(2) builtin_cmd(char **argv) 函数

函数功能:判断用户是否键入一个内置命令,若是则返回1,不是则返回0;

参数:char **argv -- parseline函数已解析好的命令;

处理流程:根据参数通过分支结构执行操作:

如果argv[0]==NULL                      à 命令为空,立刻返回1;

如果argv[0]==“quit”                 à 终止 tsh,用exit(0)函数退出程序;

如果argv[0]==“jobs”                 à 调用listjobs,返回1;

如果argv[0]==“bg”|| argv[0]==“fg”  à 调用do_bgfg函数,返回1;

其它                                  à 返回0,返回eval函数中执行外部命令。

 

(3)信号SIGCHLD处理函数 sigchld_handler(int sig)

函数功能:子进程终止或者停止的时候会向父进程发送这个信号,

然后父进程进入sigchld_handler信号处理函数进行回收或者提示。

参数:char **argv -- parseline函数已解析好的命令;

处理流程:

对于信号处理函数中的细节,书上讲得很清楚,我们要做的就是学以致用,把多种情况结合起来使用;

首先,访问jobs之前阻塞所有信号,

然后,通过waitpid函数来循环等待子进程中某进程结束,

若正常结束,直接从jobs队列中删掉;

若捕获某个信号导致进程终止,deletejobs并打印相关信息;

若是个停止进程,不需要回收,打印相关信息并将其状态设置为st(stopped);

最后,假如waitpid发生错误,其返回-1,判断是否发生错误以及是否打印错误信息。

 

(4)信号SIGINT处理函数与信号SIGTSTP处理函数

a. sigint_handler(int sig)

  函数功能:处理sigint信号,接受此信号时杀死前台进程所在的进程组中的所有进程

b. sigtstp_handler(int sig)

  函数功能:处理sigtstp信号,接受此信号时挂起前台进程组中的所有进程

参数:int sig -- 信号类型

/*

* Linux信号及信号处理

  

*/

处理流程:

1.获取前台进程,判断当前是否有前台进程,如果没有直接返回,有则步骤2;

2.使用kill函数,发送SIGINT/SIGTSTP信号给前台进程组

(发送给进程组即在第一个参数前加个负号)

 

(5)waitfg(pid_t pid)函数

函数功能:等待前台程序运行结束;

处理流程:

使用while循环等待,访问jobs之前阻塞所有信号,通过sigsuspend等待信号中断并返回,保证原子性,直到没有前台作业(只等待不回收),结束循环。

 

(6) do_bgfg(char **argv)函数

函数功能:执行bg和fg命令;

参数:char **argv -- parseline函数已解析好的命令;

处理流程:

首先判断参数个数是否正确;

然后判断输入的第二个参数是否为符合标准(大于0的数字)的pid/jid (jid用法为%jid,所以可用是否包含%进行判断),若为进程号pid,使用pid2jid函数将其转换为作业号jid,便于后续改变作业状态(前台/后台/停止);

然后判断命令为bg 还是 fg,按照所给要求:

 

首先通过getjobjid函数判断输入的pid/jid对应的进程是否存在;

无论是bg还是fg,都需要让其继续运行(不论为停止状态还是运行状态);

如果是bg,将作业状态设置为BG表示后台运行;

如果是fg,将作业状态设置为FG表示前台运行,并等待前台作业终止。

 

主要使用的相关系统函数库函数:

1.应用程序加载execve

  execve(执行文件)在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。

函数定义:

int execve(const char *filename,char *const argv[],char *const envp[]);

返回值:

函数执行成功时没有返回值,执行失败时的返回值为-1.

函数说明:

execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用数组指针来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。

linux fork()和execve()的区别:

fork是分身,execve是变身;

exec系列的系统调用是把当前程序替换成要执行的程序,而fork用来产生一个和当前进程一样的进程(虽然通常执行不同的代码流)。通常运行另一个程序,而同时保留原程序运行的方法是,fork + exec。

 

2.进程组设置 -- setpgid(pid_t pid,pid_t pgid)

使用原因:

tsh调用fork()创建的子进程与父进程同属于一个进程组,所以所有的子进程与当前的tsh shell进程为同一个进程组,发送信号到tsh shell进程组时,前后台的子进程均会收到,假如我们键入了ctrl+c,这时会让所有子进程全被杀死(包括后台进程);所以在fork()子进程之后需要通过setpgid(0,0)设置子进程为新的进程组,与tsh shell进程组分开,这样当我们输入ctrl+c,内核会发送一个SIGINT信号到前台进程组的每个进程,然后让前台进程tsh shell捕获sigint,再将其转发到 setpgid新设置的前台作业进程组,这样就可以只杀死所有的前台进程了

函数:int setpgid(pid_t pid,pid_t pgid) -- 将进程pid 的进程组改为pgid;

如果参数pid为0,那么使用当前进程的pid;

如果参数pgid为0,那么就用pid指定的进程的pid作为进程组id;

例如,如果进程15213是调用进程,那么setpgid(0,0)会创建一个新的进程组,

其进程组id是15213,并且把进程15213加入到这个新的进程组中。

函数:int setpgrp(void)

setpgrp()将目前进程所属的组ID设为目前进程的进程ID,相当于调用setpgid(0,0)。

 

3.信号阻塞/解除信号阻塞

使用原因:

比如在fork()新进程前要阻塞SIGCHLD信号,需要防止出现同步错误,

如果不阻塞,会出现子进程先结束,从jobs中删除作业deletejob,然后再执行到主进程增加作业addjob的竞争问题,保证先addjob,后deletejob.

这时需要通过系统调用sigprocmask实现信号阻塞或解除信号阻塞:

函数:sigprocmask(int how, const sigset_t *set, sigset_t *oldset)  

其中各参数分别为:  

how:

SIG_BLOCK :加入信号到进程屏蔽(blocked);               

SIG_UNBLOCK :从进程屏蔽里将信号删除;          

SIG_SETMASK :将set的值设定为新的进程屏蔽;

set:指向信号集的指针,专指新设的信号集,如果仅想读取现在的屏蔽值,可将其置为NULL;

sigemptyset:清空信号集;

sigfillset:信号集填充全部信号;

sigaddset:信号集增加信号;

sigdelset:信号集中删除信号;

sigismember:判断信号集是否包含某信号;

oldset:也是指向信号集的指针,在此存放原来的信号集。  

 

4.回收僵尸进程waitpid()

 

其中option:

1.WNOHANG:如果等待集合中的任何子进程都还没有终止,那么立马返回0,

           默认行为是挂起调用进程,直到有子进程终止;

2.WUNTRACED:挂起调用进程的执行,直到等待集合中的一个进程变成已终止或被停止,

              返回的pid为导致返回的已终止或被停止子进程的pid,

              默认行为是只返回已终止的子进程;

其中status参数如果非空,那么waitpid就会在status中放上关于导致返回的子进程的状态信息(终止或暂停原因):

1.wifexited:正常终止,返回true;

2.wifsignaled:如果子进程是因为一个未被捕获的信号终止的,返回true;

3.wtermsig:返回导致子进程终止的信号的数量(wifsignaled返回true时才有此状态)

4.wifstopped:如果引起返回的子进程是被停止的,返回true;

5.wstopsig:返回引起子进程停止的信号的数量(wifstopped返回true时才有此状态)

当子进程状态改变时,将向其父进程发出SIGCHLD信号:

(1)子进程(前后台)终止时发出SIGCHLD信号

子进程终止时发出SIGCHLD信号后,父进程在信号 sigchld_handler处理中,回收僵尸进程并从jobs中删除该进程(父进程创建子进程加载用户程序完成用户作业,将作业按作业 结构加入作业列表,如果是前台,调用waitfg(sleep或sigsuspend)函数来等待jobs列 表中不存在前台进程返回);

(2)子进程进入停止状态

此时只改变jobs中进程状态,打印引起进程停止的信号;

(3)子进程从stopped变到running(收到SIGCONT)

这时信号处理函数会一直等待它执行完毕,在shell中显示的情况就是卡住了;

解决方案是设置一个全局变量st_run_child记录需要从stopped到running的进程ID,

在进入信号处理函数后首先检查这个变量,如果是就直接退出不做处理。

 

重要知识点:

1.两种Shell命令     

a.内置命令:Shell自带的命令,在当前进程中执行该命令;

(1)quit:终止正在运行的tinyshell

(2)jobs:查看当前有多少在后台运行的命令

(3)fg %jobnu 或fg PID(ST->FG  BG->FG):

将后台中的命令调至前台继续运行;

如果后台中有多个命令,可以用fg %jobnumber将选中的命令调出,%jobnumber是通过jobs命令查到的后台正在执行的命令的序号(不是pid)

(4)bg %jobnu 或 bg PID(BG->FG):

将一个在后台暂停的命令, 变成继续执行;

如果后台中有多个命令,可以用bg %jobnumber将选中的命令调出,%jobnumber是通过jobs命令查到的后台正在执行的命令的序号(不是pid)

b.外部命令:可执行文件的路径名,shell创建一个子进程,然后在子进程的上下文中加载和运行应用程序;

 

2.作业、前台、后台

(1)作业

输入命令行执行的进程称为作业,一个作业可以包含一个或多个进程,尤其是当使用了管道和重定向命令。

Shell定义一个 job_struct 作业结构记录相关作业的控制信息描述:

作业操作管理函数: clearjob、initjob、maxjid、fgpid(jobs)、addjob、deletejob等;

(2)前台作业与后台作业

输入可执行程序的路径名及其参数:

在前台运行作业,作业终止或停止后,打印提示符,可输入下一作业命令。

输入可执行程序的路径名及其参数 + &:

在后台运行命令行作业, 可运行多个后台作业。

 

3.信号发送, 处理, 阻塞

ctrl+\ -- 发送 SIGQUIT 信号给前台进程组中的所有进程, 终止运行

ctrl+c -- 发送 SIGINT 信号给前台进程组中的所有进程, 终止运行

ctrl+z(FG->ST)-- 发送 SIGTSTP 信号给前台进程组中的所有进程, 挂起进程

ctrl+D -- 表示命令行输入一个特殊的二进制值 EOF(非信号),退出

 

2.测试方案

tshref

用来给我们参考的二进制执行文件,通过tshref,将自己tsh的输出与标准tshref的输出对比,其执行输出应相同,但他们的输出的PID可以不同,这样就可以知道自己tsh的功能是否正确,根据这个测试结果判断自己的代码实现是否正确;

Makefile

make目标 

all:(包括tsh.c共5个C程序的编译) 

handin: 提交上传文件及地址挃示 

clean: 清除原文件 

test01-16: test16共16个测试目标挃示(tsh) 

rtest01-16: rtest16共16个测试目标挃示(tshref)  

如:

·test01:

 $(DRIVER) -t trace01.txt -s $(TSH) -a $(TSHARGS)

·键入命令make来链接一些测试例程:

make test01

./sdriver.pl -t trace01.txt -s tsh -a “-p”

                    tracexx.txt    tshref 

利用sdriver及trace文件测试原理:

(1) sdriver.pl(shell测试驱动文件)

sdriver.pl程序以子进程的形式执行tsh,按照跟踪文件的指示发送命令和信号,并捕获和显示tsh的输出。

通过分别运行设计的程序tsh和标准参考程序tshref来进行测试,输入相同命令参数,观察是否运行及输出相同;

   测试方法有两种:

输入命令./tsh(tshref)运行tsh(tshref)后,直接输入测试命令,观测结果及输出;或者运用shell驱动程序sdriver.pl自动测试;

(2) tracexx.txt(可供sdriver用的)跟踪文件

跟踪文件指令由sdriver.pl读入后将其中shell指令输入被检测的子程序并将子程序的执行结果输出。tsh与tshref的输出相同则判为正确,如不同则给出原因分析;

每个文件根据不同测验功能的需要被设计成包含对应的注释行、驱动程序命令、shell命令,需要注意的是,对于使用同一跟踪文件的检测tsh与tshref的执行输出应相同,但他们的输出的PID可以不同。

 

三、实验步骤

1.源程序代码的编写(或修改)

(1)builtin_cmd函数

这个函数逻辑很简单,就是判断是哪个内置命令、跳转到相应的函数,然后return 1;否则,就不是内置命令,直接返回0。就是通过if-else判断parseline函数解析得到的命令行参数并对不同参数执行不同操作:

 

(2)eval函数

首先声明各变量,然后通过parseline解析命令行,得到是否是后台命令,置位state;然后判断是否是内置命令,如果不是内置命令,阻塞SIGCHLD,防止子进程在父进程之间结束,创建新的子进程,先解除对SIGCHLD阻塞,改变进程的进程组,不要跟tsh进程在一个进程组,然后调用exevce函数执行相关的文件,然后exit(0),否则当execve函数无法执行的时候,子进程开始运行主进程的代码,出现不可预知的错误,创建完成子进程后,父进程addjob,整个函数执行期间,必须保证不能被中断,因此,要阻塞所有信号, 然后判断是否是bg,如果是前台作业,调用waitpid函数等待前台运行完成,如果是后台作业,打印消息即可,最后解除所有的阻塞。而且在eval函数功能说明中,要求在该函数中调用其他函数,需要设置调用失败的情况(程序中用 unix_error封装函数提示错误),所以还需要判断如果fork()失败,输出错误信息。

 

 

2.调试方法及步骤

A.解压shlab-handout.tar文件后先进行编译tsh.c等生成tsh等执行文件;

B.在shell输入命令./tsh –hvp显示参数:

测试tsh的输入EOF及quit命令响应功能,直接在shell终端输入命令./tsh 运行tsh测试:

(1)测试tsh(对比tshref)的命令行读入Ctrl+D(EOF)终止:

  

(2)测试tsh(对比tshref)的quit命令执行,tshref终止功能正常,tsh不响应:

  

shell终端通过Makefile运行sdrive按跟踪文件trace自动测试tsh:

(1)按trace01.txt测试,对比tshref的运行输出;

(2)按trace02.txt测试,对比tshref的运行输出;

   

C.按要求完成eval命令求值代码:实现内置命令quit处理(并添加到builtin_cmd 函数),加载执行外部作业,调试编译成tsh:

  

D.测试quit命令及前后台作业加载执行及作业ID进程PID显示功能,

先按跟踪文件测试:

1.trace02.txt测试tsh(对比tshref)的quit终止命令执行功能,正常:

  

相较于补齐eval函数前:

 

可以看到补齐了eval函数后,make test02时已经可以正常直接终止了。

2.trace03.txt测试tsh(对比tshref)外部作业命令及前台作业功能,正常:

  

3.trace04.txt测试tsh(对比tshref)外部作业命令及后台(命令加&)作业功能,作业能加载执行,后台作业号为0:

  

4.trace05.txt测试tsh无jobs命令,显示command  not found:

  

E.直接运行tsh测试quit命令、前后台作业加载执行情况:

  

(3)信号SIGCHLD处理函数

对于信号处理函数中的细节,其实书上讲得很清楚了,我们要做的就是学以致用,把多种情况结合起来使用;

首先,访问jobs之前阻塞所有信号,

然后,通过waitpid函数来循环等待子进程中某进程结束,

若正常结束,直接从jobs队列中删掉;

若捕获某个信号导致进程终止,deletejobs并打印相关信息;

若是个停止进程,不需要回收,打印相关信息并将其状态设置为st(stopped);

最后,假如waitpid发生错误,其返回-1,判断是否发生错误以及是否打印错误信息。  

 

 

(4)信号SIGINT处理函数与信号SIGTSTP处理函数

这两个函数的思路简单相似:

1.获取前台进程,判断当前是否有前台进程,如果没有直接返回,有则步骤2;

2.使用kill函数,发送SIGINT/SIGTSTP信号给前台进程组;

 

(5) waitfg函数

需要实现等待前台程序运行结束,访问jobs之前阻塞所有信号,使用while循环等待,直到没有前台作业(只等待不回收),通过sigsuspend等待信号中断并返回,保证原子性。

对于waitfg函数:

以 ./myspin 4 为例,子进程前台执行,主进程进入pause INT #

(1)CTRL+C发送INT信号给主进程,中断pause状态,进入INT处理函数,该函数把INT

信号转发给前台子进程,处理完成后返回上次中断的位置(2)。

(2)INT中断处理函数返回后,继续执行下一条指令,发现while循环条件依然成立又

进入pause.

(3)前台子进程被INT信号终止结束生命发送CHLD信号给主进程,中断pause状态,

进入CHLD处理函数,该函数回收僵尸进程,删除对应的job,处理完成后返回上次

中断的位置。

(4)CHLD中断返回后,继续执行下一条指令,发现while循环条件不再成立,

跳出whlile循环。

假若(3)的中断位置在while条件执行后,pause执行前,那么中断处理函数返回后会一直pause,sigsuspend可以避免这种情况。

 

2.调试方法及步骤

 (1)test04 / trace04

 

对于后台作业的运行,在实验部分1中已经完成。

 

(2)test07 / trace07

  

   未添加本次实验中要求功能之前:

  

   对于前台作业,在完成本部分的任务之前,不会打印出前台作业停止的相关信息,说明

前台作业停止后未将其状态修改为st,并且未实现jobs命令。

   

 

(6)do_bgfg函数

  首先判断参数个数是否正确;

然后判断输入的第二个参数是否为符合标准(大于0的数字)的pid/jid (jid用法为%jid,所以可用是否包含%进行判断),若为进程号pid,使用pid2jid函数将其转换为作业号jid,便于后续改变作业状态(前台/后台/停止);

然后判断命令为bg 还是 fg,按照所给要求:

 

首先通过getjobjid函数判断输入的pid/jid对应的进程是否存在;

无论是bg还是fg,都需要让其继续运行(不论为停止状态还是运行状态);

如果是bg,将作业状态设置为BG表示后台运行;

如果是fg,将作业状态设置为FG表示前台运行,并等待前台作业终止;

 

 

2.调试方法及步骤

 

使用./tsh命令进入tsh,先运行了一个33秒的前台作业,然后使用ctrl+z挂起前台所有进程,可以看到输出,进程号为4380的进程被信号20终止了,然后使用bg命令将此进程调到后台执行(%1即代表此进程的作业号),然后使用jobs命令打印当前正在进行进程,可以看到挂起的进程正在运行,再使用fg 4380(4380代表此进程的进程号)将进程重新调回前台运行,运行完之后通过ctrl+d退出./tsh。

然后通过make test10测试跟踪文件trace10.txt,这个文件中先运行了一个4秒的后台作业,然后使用fg命令将此进程调到前台执行(%1即代表此进程的作业号),应该是使用了ctrl+z挂起前台进程,然后使用jobs命令打印当前正在进行的进程,可以看到此进程正处于停止状态,再使用fg %1将进程重新调回前台运行,运行完之后使用jobs命令打印当前正在进行进程,所有进程已经结束。

 

四、实验总结及心得体会

1.分析实验效果

  如部分三中记录,虽然我的实验代码中可能还有很多未完善之处,但已经完成了本次实验的所有基本要求,通过与所给参照tshref的对比可以看到我的实验测试结果与所要求结果均相同,证明编写的各函数功能已经正确实现。

完成所有代码后,完整调试过程如下:

 

2.实验收获及实验心得体会

通过实践的方法,我不仅对之前所学知识进行了复习,而且在运用所学知识的过程中加深了自己对所学计算机系统的第八章-异常、进程、信号等知识的理解与掌握,并且学习到了之前未考虑到及未接触到的新知识,比如用volatile声明全局变量,强迫编译器从内存中读取引用的值;sigemptyset:清空信号集,sigfillset:信号集填充全部信号……而且对内置外壳命令有了更深入地了解,学会了如何编写代码去实现信号与进程处理相关的内容、以及对前后台作业状态的修改。

总而言之,本次实验不仅让我掌握了更多关于进程与信号方面的理论知识,而且帮助我锻炼了动手能力与实践测试能力,收获甚多!

额外收获:

1.在shell命令中,命令最后加&,就可以在后台运行了

2.parseline中是将一串字符串以空格为分界符,拆分成若干个字符串的算法(类似于Python中str.strip(' ') ),就是巧妙利用指针以及结束符‘\0’实现的,可以具体看一下源代码

3.回收进程时,如果使用阻塞回收,可以这样写:
 

总结:
1.以后要向老师这种模式学习,边测试边添加,这样心里有底,bug少,也便于  

找到错误的地方;

2.通过本次实验,复杂的系统可以分解为一个个小的功能模块,通过实现这些小

功能模块再将其拼接起来就可以完成一个复杂的系统,在这个过程中,一定要保证每个函数的健壮性,然后测试通过,这样微小的一个个函数堆积起来就是复杂庞大的系统。

 

3.遇到的问题

问题1:

刚开始未注意到

 

在addjobs时直接把state(fg-0,bg-1)作为作业状态了(如注释)

导致结果如下:

 

所有进程状态全为0.

解决:

把 state 改为 state ? BG:FG .

 

问题2:

用waitpid(-1,NULL,0)导致前台程序很快运行并返回后shell发生阻塞,

(因为前台和后台都算父进程的子进程),造成父进程等待后台子进程,而影响了之后语句的输入。

解决:

将waitpid的默认行为变成WNOHANG|WUNTRACED,回收子进程然后立即返回,不做任何停留。

 

问题3:

处理Ctrl C(前台进程全部关闭)和Ctrl Z(前台进程全部暂停)在joblist中的行为时,要放入sigchld_handler中,而不是sigint_handler或sigtstp_handler。因为最终对终止/暂停进程的处理还是要通过sigchld_handler来完成。

解决:

将waitpid(-1,NULL,WNOHANG|WUNTRACED) 变为

waitpid(-1,&state,WNOHANG|WUNTRACED),根据返回的state来确定这是“因为中断信号造成的退出”还是“正常退出”还是“暂停”.

 

代码:
 

/* 
 * 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 在execv函数中用到了(应该是本机的环境变量,可以借鉴)*/
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 */
    /* 在运行./tsh时,可以在./tsh后面加-h或者-v或者-p,可以实现不同的功能*/
    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 */
    //个人理解job结构体就是一个执行命令的子进程,jobs就是存放已有的执行任务信息的数组
    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),检测标准输入是否有结束符,只有按ctrl+d时,输入一个EOF符,就会终止 */
	    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];			//存放拆分的cmdline参数
    char buf[MAXARGS];				//存放cmdline的拷贝
    int state;						//为 1表示后台作业,为 0表示前台作业 
    pid_t pid;
    
    //设置阻塞集合 
    sigset_t mask_all,mask_child,prev_child;
    sigfillset(&mask_all);
    sigemptyset(&mask_child);
    sigaddset(&mask_child, SIGCHLD);
    memset(argv, 0, sizeof(argv));
    
    strcpy(buf, cmdline);
    state = parseline(buf, argv);	//解析命令行,得到是否为后台命令 
    
    if (argv[0]==NULL)				//忽略空行 
    {
        return;
    }
 
 	//不是内置命令 
    if (!builtin_cmd(argv))
    {
        sigprocmask(SIG_BLOCK, &mask_child, &prev_child);	//在开始进程时不接受子进程结束的信号
         
        if ((pid=fork())==0)
        {
            sigprocmask(SIG_UNBLOCK, &prev_child, NULL);	//在子进程中解除对sigchld阻塞,接受子进程的信号
            if(setpgid(0,0) < 0)							//修改子进程的组ID变成自己的进程ID,这样当接收SIGINT信号时就不会被一起杀死了
            {
				unix_error("setpgid error");
			}
            if (execve(argv[0],argv,environ)<0)
            {
            	//没找到相关可执行文件 
                printf("%s:command not found\n",argv[0]);
                exit(0);
            }
 
        }
 
        sigprocmask(SIG_BLOCK, &mask_all, NULL);			//在修改jobs时,阻塞所有信号,结束后再开启
        addjob(jobs, pid, state ? BG:FG, cmdline);	
        sigprocmask(SIG_SETMASK, &prev_child, NULL); 		 
 
        if (!state)			
        {
            //前台运行
            waitfg(pid);
        }
        else
        {
            //后台运行
            printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
        }
          
    }
    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. 
 * 一方面可以判断cmdline是后台还是前台工作,另一方面也可以将cmdline切分成argv数组 
 */
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 将\n符变成空格 */
    while (*buf && (*buf == ' ')) /* ignore leading spaces 忽视命令开始的空格*/
	buf++;
 
    /* Build the argv list 建立argv数组*/
    /*
    *以一个例子详细说明,假设cmdline="/bin/ls -a"
    *在1处是进行预处理,将开始的空格去除,并且delim定位到第一处分隔符处,在例子中也就是cmdline[8](ls后面的空格)
    *在2处,注意argv[0]是一个字符串指针,指向buf指向的地址,
    *在第一次循环时指向cmdline[0],此时printf(argv[0])结果是“/bin/ls -a”然后把delim变为\0。也就是把cmdline[8]变为终止符,此时printf(argv[0])结果是“/bin/ls”,这样就通过空格分开了命令
    */
    //1
    argc = 0;
    if (*buf == '\'') {
	buf++;
	delim = strchr(buf, '\'');
    }
    else {
	delim = strchr(buf, ' ');
    }
 
    while (delim) {
        //2
        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) 
{
	char *cmd = argv[0];
	if(cmd == NULL)
	{
		return 1;
	} 
    else if (!strcmp(cmd,"quit"))
    {
        exit(0);
	}
    else if (!strcmp(cmd,"jobs"))
	{
        listjobs(jobs);
        return 1;
    }
    else if (!strcmp(cmd,"bg") || !strcmp(cmd,"fg"))
    {
        do_bgfg(argv);
        return 1;
    }
    return 0;     /* not a builtin command */
}
 
/* 
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv) 
{
    int pid,jid;
    
    if (argv[1]==NULL)
    {
        printf("the command requires PID or %%job argument\n");
        return;
    }
    
    char* tempcmd=argv[1];
    char* ch;
    
    if ((ch = strchr(tempcmd,'%')) == NULL)							//第二个参数非 %%jid 
    {
        if ((pid = atoi(tempcmd))==0 && strcmp(tempcmd, "0"))		//输入不是符合标准的数字 
        {
            printf("the argument of command must be number\n");
            return;
        }
        //将输入的pid转换为 jib,便于后续操作 
        jid = pid2jid(pid);
    }
    else
    {
        tempcmd=ch+1;
        if( (jid = atoi(tempcmd))==0 && strcmp(tempcmd, "0") )		//输入不是符合标准的数字 
        {
            printf("the argument of command must be number\n");	
            return;
        }
    }
    
    // bg 命令 
    if (!strcmp(argv[0],"bg"))
    {
        if (getjobjid(jobs, jid)==NULL)
        {
            printf("No such job\n");
            return;
        }
        getjobjid(jobs, jid)->state = BG;		//设置作业状态为 bg 
        pid = getjobjid(jobs, jid)->pid;
        kill(-pid, SIGCONT); 					//发送 SIGCONT 给停止状态进程使其运行 
        printf("Job [%d] (%d) %s", jid, pid, getjobjid(jobs, jid)->cmdline);
    }
    // fg 命令 
    else
    {
        if (getjobjid(jobs, jid)==NULL)
        {
            printf("No such job\n");
            return;
        }
        getjobjid(jobs, jid)->state = FG;		//设置作业状态为 fg 
        pid = getjobjid(jobs, jid)->pid;
        kill(-pid, SIGCONT); 					//发送 SIGCONT 给停止状态进程使其运行 
        waitfg(pid);							//等待前台作业终止 
    }
    
    return;
}
/*
  if(!strcmp(cmd, "bg")){
        //bg 命令 
        switch(curr_job->state){
        case ST:									//bg命令,改变该任务的运行状态,ST->BG,同时发送信号给对应的子进程
            curr_job->state = BG;
            kill(-(curr_job->pid), SIGCONT);
            printf("[%d] (%d) %s", curr_job->jid, curr_job->pid, curr_job->cmdline);
            break;
        case BG:									//该任务已经是后台运行了
        case UNDEF:									//如果bg前台作业或者undef,那么肯定哪里出错了
        case FG:
            unix_error("bg 出现undef或者FG的进程\n");
            break;
        }
    }
    else{
        //fg 命令
        switch(curr_job->state)
		{
        	case ST:								//如果fg挂起的进程,那么重启它,并且挂起主进程等待它回收终止
	            curr_job->state = FG;
	            kill(-(curr_job->pid), SIGCONT);
	            waitfg(curr_job->pid);
	            break;
	        case BG:								//如果fg后台进程,那么将它的状态转为前台进程,然后等待它终止
	            curr_job->state = FG;
	            waitfg(curr_job->pid);
	            break;
	        case FG:        						//如果本身就是前台进程
	        case UNDEF:
	            unix_error("fg 出现undef或者FG的进程\n");
	            break;
        }
    }
*/ 
 
/* 
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
    sigset_t mask_empty, mask_all, prev_all;;
    Sigemptyset(&mask_empty);
    Sigfillset(&mask_all);
    //等待直到没有前台作业(只等待不回收)
    while(true)
	{
        Sigsuspend(&mask_empty); //保证原子性,等待信号中断并返回
        //访问jobs之前阻塞所有信号
        Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
        if(fgpid(jobs)==0)		 
		{
            Sigprocmask(SIG_SETMASK, &prev_all, NULL);
            break;
        }
        else
		{
            Sigprocmask(SIG_SETMASK, &prev_all, NULL);
        }
    }
}
 
/*****************
 * 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 olderrno = errno;
    pid_t pid;
    int status;
    
    sigset_t mask_all,prev_all;
    sigfillset(&mask_all);
 
 	/*
	 * 尽可能的回收子进程,同时使用 WNOHANG选项使得如果当前进程都没有终止时,直接返回而非挂起该回收进程
	 * 这样可能会阻碍无法两个短时间结束的后台进程 
	 */ 
    while ( (pid=waitpid(-1, &status, WNOHANG | WUNTRACED | WCONTINUED)) > 0)
    {
        sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
        //防止出现该进程不是被父进程杀死,如果不加WUNTRACED标志位,就会出现不执行sigchld_handler这一函数
        //但是jobs中一直没有删除这个job,如果是前台运行的程序,就会一直等待
        
        if (WIFEXITED(status))			//进程正常结束
        {
        	deletejob(jobs,pid);
		}   
        else if (WIFSIGNALED(status))	//捕获某个信号导致进程终止 
		{
            printf("Job [%d] (%d) terminated by signal 2\n", pid2jid(pid), pid);  
            deletejob(jobs,pid);
        } 
        else if(WIFSTOPPED(status))		//是个停止进程,不需要回收 
		{
            printf("Job [%d] (%d) stopped by signal 20\n", pid2jid(pid), pid);
            struct  job_t *job = getjobpid(jobs,pid);
            if(job != NULL)
			{
				job->state = ST;		//设置状态为 ST 
			}
        }
        sigprocmask(SIG_SETMASK, &prev_all, NULL);
    }
    
    if (pid == -1 && errno != ECHILD)
    {
    	printf("waitpid error\n");
	}    
    errno = olderrno;
    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.  
 * 如果接受到SIGINT信号,就会杀死前台进程所在的进程组中的所有进程,实现就是靠kill函数,并且第一个参数为负数
 */
void sigint_handler(int sig) 
{
	int olderrno = errno;
    pid_t curr_fg_pid = fgpid(jobs);
    
    if(curr_fg_pid != 0)
    {
    	kill(-curr_fg_pid, SIGINT); 
	}        
	
	errno = olderrno;                                                                                                                                
    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) 
{
	int olderrno = errno;
    pid_t curr_fg_pid = fgpid(jobs);
    
    if(curr_fg_pid != 0)
    {
    	kill(-curr_fg_pid, SIGTSTP); 
	}        
	
	errno = olderrno;                                                                                                                                
    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
 * 开启signum的信号捕获,handler是信号捕获后的处理函数,返回的是上一个处理函数(没什么用)
 */
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 */
                                  /*如果信号打断了系统调用,就会重新启动原来的系统调用,一般会被信号打断的系统调用有读写终端,网络,磁盘,wait,pause等 */
 
    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);
}

 

  • 9
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值