当前位置:网站首页>csapp shell lab
csapp shell lab
2022-07-06 15:25:00 【Hu Da jinshengyu】
The complete code will be placed at the end of the article .
The experiment purpose :
modify tsh.c The file completes the following seven functions :
eval: Parse the command line statement and run the process
builtin_cmd: Determine whether it is a built-in instruction
do_bgfg: perform bg < job > and fg < job > Built in commands
waitfg: wait for pid The process is no longer a foreground process
sigchld_handler: Recycle all available dead child processes
sigtstp_handler: Respond to SIGTSTP(ctrl-z) The signal
sigint_handler: Respond to SIGINT(ctrl-c) The signal
Experimental content :
By reading the experimental instruction, we know that this experiment requires us to complete tsh.c In order to achieve a simple shell, Be able to handle the front and background running programs 、 Able to handle ctrl+z、ctrl+c Equal signal .
We know that seven functions are in tsh.c To realize , We see tsh.c The concrete realization inside , We found that some auxiliary functions are defined as follows :
int parseline(const char *cmdline, char **argv); // Get parameter list , Returns whether to run the command in the background
void sigquit_handler(int sig); // Handle SIGQUIT The signal
void clearjob(struct job_t *job); // eliminate job Structure
void initjobs(struct job_t *jobs); // Initialization task jobs[]
int maxjid(struct job_t *jobs); // return jobs The largest in the linked list jid Number .
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline); // towards jobs[] Add a task
int deletejob(struct job_t *jobs, pid_t pid); // stay jobs[] Delete in pid Of job
pid_t fgpid(struct job_t *jobs); // Return to the current foreground operation job Of pid Number
struct job_t *getjobpid(struct job_t *jobs, pid_t pid); // according to pid Find the corresponding job
struct job_t *getjobjid(struct job_t *jobs, int jid); // according to jid Find the corresponding job
int pid2jid(pid_t pid); // according to pid find jid
void listjobs(struct job_t *jobs); // Print jobs
Then there is mian function , The function is to get commands line by line in the file , And judge whether the file ends (EOF), Will command cmdline Send in eval Function to parse . What we need to do is gradually improve this process .
Then start the experiment :
1. Learn to compile tsh.c, call tsh file traceXX.txt The function verification method uses make Command compilation tsh.c file ( If the file changes, you need to use it first make clean Command clear );
2. Use make testXX Command comparison traceXX.txt The document is under preparation shell and reference shell Results of operation ; Or you can use it ”./sdriver.pl -t traceXX.txt -s ./tsh -a “-p”;
3. If you add r, Is the implementation of Standards tshref, Or will tsh Turn into tshref. By comparing Standards tshref And self-control tsh The execution result of , You can observe tsh Whether the function of is correct . If tsh The results of the implementation and tshref Consistent result , It shows that the result is correct .
Realization
eval() function :
The functionality : If the user has requested a built-in command ( sign out 、 Homework 、bg or fg) Then execute it immediately . otherwise , Derive a child process and run the job in the context of the child object . If the job is running in the foreground , Wait for it to end , Then return . notes : Each child process must have a unique process group ID, So that the backend child does not receive from the kernel SIGINT(SIGTSTP) When we type ctrl-c(ctrl-z) when .
The prototype of function is in the book void eval(char *cmdline);
This function keeps looping in the loop body Parse command line We passed what we have given parseline Function to parse into a character pointer If found to be built-in Then directly execute the built-in command
Realize the idea :
- First initialize variables , Next call parseline Function to parse into a character pointer , because builtin_cmd Function implements the execution of built-in instructions , So we just need to judge whether the input instruction is a built-in instruction , return 0 Description is not a built-in command .
- If not built-in instructions , Then use fork Function create subprocess , At the same time, in order to avoid the synchronization problem caused by the competition between parent and child processes , We need to mask out before creating sub processes SIGCHLD The signal .
- In the subprocess , First, unblock , And then use setpgid(0,0) Set up process groups , Make the process group number of the child process different from that of the parent process , And then call execve Function to execute job.
- The parent process determines whether the job is running in the background , Yes, call addjob Function will subprocess job Join in job In the list , unblocked , And then call waifg Function waits for the foreground to complete . If you are not working in the background, print the process group jid And subprocesses pid And command line string .
Be careful :
The key point of this function is to be careful about the competition in synchronous concurrent streams , Besides ,setpgid It's also necessary , It combines sub process groups with tsh Separate process groups , avoid tsh Stop after receiving an inexplicable signal .
The child process inherits the blocked set of the parent process , Calling execve Before , First, be careful to remove the blocking in the subprocess SIGCHID The signal .
stay fork() The new process should be blocked before and after SIGCHLD The signal , Prevent competitive synchronization errors . otherwise , There may be sub processes running first and ending from jobs Delete in , and deletejob Don't do anything? , Because the parent process has not added the child process to the list ; Then execute the parent process , call addjob Add non-existent subprocess to the job list by mistake , It will never be deleted .
Each child process must have its own process group id, In order to send signals to the sub process group . otherwise , All subprocesses are related to tsh shell The process is the same process group , When sending a signal , The subprocesses of the front and back stations will receive .—— Realization : stay fork() In the next sub process setpgid(0,0)
setpgid(pid_t pid, pid_t pgid): Process pid The process group of is changed to pgid. If pid yes 0, Then it is the current process pid, If pgid yes 0, Then use pid.
void eval(char *cmdline)
{
char* argv[MAXARGS]; //execve() The parameters of the function
int state = UNDEF; // Working state ,FG or BG
sigset_t set;
pid_t pid; // process id
// Process the input data
if(parseline(cmdline, argv) == 1) // Parse command line , Return to argv Array
state = BG;
else
state = FG;
if(argv[0] == NULL) // The command line returns null directly
return;
// If it's not a built-in command
if(!builtin_cmd(argv))//builtin The command is used to execute the specified shell Internal orders , And return the return value of the internal command .
{
if(sigemptyset(&set) < 0)// This function initializes the signal set to null .
unix_error("sigemptyset error");
if(sigaddset(&set, SIGINT) < 0 || sigaddset(&set, SIGTSTP) < 0 || sigaddset(&set, SIGCHLD) < 0)// The purpose of this function is to put the signal signo Add to signal set set in , Return on success 0, Return... On failure -1.
unix_error("sigaddset error");
// Block before it spawns a child process SIGCHLD The signal , Prevent competition
if(sigprocmask(SIG_BLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
if((pid = fork()) < 0) //fork Failed to create child process
unix_error("fork error");
else if(pid == 0) //fork Create child process
{
// The control flow of the child process begins
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0) // unblocked
unix_error("sigprocmask error");
if(setpgid(0, 0) < 0) // Set up child processes id, Put it in the same process group
unix_error("setpgid error");
if(execve(argv[0], argv, environ) < 0){
printf("%s: command not found\n", argv[0]);
exit(0);
}
}
// Add the current process to job in , Whether it's a foreground process or a background process
addjob(jobs, pid, state, cmdline);
// Recover the blocked signal SIGINT SIGTSTP SIGCHLD
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
// Determine the sub process type and handle it
if(state == FG)
waitfg(pid); // Front desk job waiting
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); // Process id Mapping to job id
}
return;
}
builtin_cmd() function
** The functionality :**builtin The command is used to execute the specified shell Internal orders , And return the return value of the internal command . This is to recognize and execute the built-in commands : quit, fg, bg, and jobs.
** Parameter setting :** Parameter is argv parameter list
Realize the idea :
- jobs List tasks in all States ( Except that it's over The suspended ones should also be listed )
- bg <jid/pid> Make a background task ( Paused or running ) Restart in the background
- fg <jid/pid> Make a task Rerun And run in the foreground
- quit Exit the program directly
- Return if it is not a built-in command 0
int builtin_cmd(char **argv)
{
if(!strcmp(argv[0], "quit")) // If the order is quit, sign out
exit(0);
else if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) // If it is bg perhaps fg command , perform do_fgbg function
do_bgfg(argv);
else if(!strcmp(argv[0], "jobs")) // If the order is jobs, List running and stopped background jobs
listjobs(jobs);
else
return 0; /* not a builtin command */
return 1;
}
do_bgfg() function
** The functionality :** Realization bg and fg Two built-in commands
bg : Change the stopped background job to the running background job . By sending SIGCONT Signal restart , Then run it in the background . Parameters can be PID, It can also be JID.ST -> BG
fg : Change the background job that has been stopped or is running to the job that is running in the foreground . By sending SIGCONT The signal starts again , Then run it in the foreground . Parameters can be PID, It can also be JID.ST -> FG,BG -> FG
Realize the idea :
Through the first argv[0] Judge the front and back
id=argv[1] probability :
1.null
2.%jid( Judge whether it exists )
3.pid( Judge whether it exists )
4. neither %jid Neither pidIf the process exists : A signal sent to the process to continue execution
And then use strcmp Function judgment is bg Order or fg Instructions :
If it's a foreground process , Set state to FG, Wait for it to finish executing
If it's a background process , Set state to BG, Output prompt message .
Be careful :
distinguish bg and fg command , And the introduction pid perhaps jid The state of the process corresponding to the parameter .
Pay attention to user input error handling , For example, the number of parameters is not enough or the parameters are imported incorrectly
void do_bgfg(char **argv)
{
int num;
struct job_t *job;
// Without parameters fg/bg Should be discarded
if(!argv[1]){
// The command line is empty
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return ;
}
// testing fg/bg Parameters , among % The first number is JobID, Pure numbers are PID
if(argv[1][0] == '%'){
// analysis jid
if((num = strtol(&argv[1][1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobid\n",argv[0]);// Failure , Print error messages
return;
}
if((job = getjobjid(jobs, num)) == NULL){
printf("%%%d: No such job\n", num); // No corresponding job
return;
}
} else {
if((num = strtol(argv[1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobid\n",argv[0]);// Failure , Print error messages
return;
}
if((job = getjobpid(jobs, num)) == NULL){
printf("(%d): No such process\n", num); // No corresponding process was found
return;
}
}
if(!strcmp(argv[0], "bg")){
// bg Will start the child process , And put it in the background for execution
job->state = BG; // Set the state of
if(kill(-job->pid, SIGCONT) < 0) // Send a negative signal to the process group
unix_error("kill error");
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
} else if(!strcmp(argv[0], "fg")) {
job->state = FG; // Set the state of
if(kill(-job->pid, SIGCONT) < 0) // Send a negative signal to the process group
unix_error("kill error");
// When a process is set to execute in the foreground , At present tsh You should wait for the subprocess to end
waitfg(job->pid);
} else {
puts("do_bgfg: Internal error");
exit(0);
}
return;
}
waitfg() function
** The functionality :** Wait for a foreground job to finish , Or block a foreground process until it becomes a background process
** Parameter setting :** There is only one parameter and the parameter is set to process ID
Realize the idea :
Determine the current foreground process group pid Whether it is related to the current process pid Whether it is equal or not , If equal, sleep until the foreground process ends .
Be careful :
Because the signal may be executing pause Come before ( just ), This will form a competitive relationship , If the program is executed at this time pause Words , Will wait for a signal that will never come ( May never wake up ). So use sigsuspend function . meanwhile , You can also use sleep, But it's also better to use sigsuspend function .
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() function
** The functionality :** Handle SIGCHILD The signal
** Parameter setting :** The parameter is the signal type
Realize the idea :
use while Cycle call waitpid Until all its child processes terminate .
Check the exit status of the recycled child process
WIFSTOPPED:: The child process that caused the return is currently stopped
WIFSIGNALED: The subprocess terminates because of an uncapped signal
WIFEXITED: Subprocesses call exit perhaps return Normal termination
And then separately WSTOPSIG,WTERMSIG,WEXITSTATUS Extract the above three exit States . Note that if the child process causing the return is currently the stopped process , Then set its status to ST.
Be careful :
This function deals with three cases , One is to deal with normal suspension , The second is when the signal is received and stopped , Third, it is temporarily stopped by the signal , In the first two cases, delete the process from the process table , The third case is not used , But you have to change the state . In order to prevent del The program was interrupted , We still need to plug .
void sigchld_handler(int sig)
{
int status, jid;
pid_t pid;
struct job_t *job;
if(verbose)
puts("sigchld_handler: entering");
/* Wait for all child processes in a non blocking manner waitpid Parameters 3: 1. 0 : perform waitpid when , Only when the child process terminates will it return . 2. WNOHANG : If child process is still running , Then return to 0 . Note that only this flag is set ,waitpid It's possible to return 0 3. WUNTRACED : If the child process stops because of a signal , Then we will return immediately . Only when this flag is set ,waitpid return , Its WIFSTOPPED(status) It's possible to return true */
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
// If the current subprocess job Has deleted , It means there is an error
if((job = getjobpid(jobs, pid)) == NULL){
printf("Lost track of (%d)\n", pid);
return;
}
jid = job->jid;
// Next, judge three states
// If the subprocess receives a pause signal ( I haven't quit yet )
if(WIFSTOPPED(status)){
printf("Job [%d] (%d) stopped by signal %d\n", jid, job->pid, WSTOPSIG(status));
job->state = ST; // The status is set to pending
}
// If the child process calls exit Or a return (return) Normal termination
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));
}
}
// If the child process is terminated because of an uncapped signal , for example SIGKILL
else {
if(deletejob(jobs, pid)){
// Clear process
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)); // Returns the number of signals that cause the child process to terminate
}
}
if(verbose)
puts("sigchld_handler: exiting");
return;
}
sigint_handler function
** The functionality :** When the user presses ctrl-c when , Send to the front program SIGINT The signal
** Parameter setting :** The parameter is the signal type
Realize the idea :
- Use fgpid To get the foreground process pid
- When the current process exists , Use kill send out SIGINT To the foreground process group
- If kill Function call failed , The output error
Be careful :
The processes in the foreground process group are in addition to the current foreground process , It also includes sub processes of the foreground process .
void sigint_handler(int sig)
{
if(verbose)
puts("sigint_handler: entering");
pid_t pid = fgpid(jobs);
if(pid){
// send out SIGINT To all processes in the foreground process group
// It should be noted that , The processes in the foreground process group are in addition to the current foreground process , It also includes sub processes of the foreground process .
// At most one foreground process can exist , But there can be multiple processes in the foreground process group
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() function
** The functionality :** Capture SIGTSTP The signal
SIGTSPT The default behavior of the signal is to stop until the next SIGCONT, Is the stop signal from the terminal , Type... On the keyboard CTR+Z Will lead to a SIGTSPT The signal is sent to the housing . The enclosure captures this signal , And then send SIGTSPT Signal to each process in the foreground process group . By default , The result is to stop or suspend the foreground job .
** Parameter setting :** The parameter is the signal type
Realize the idea :
- use fgpid(jobs) Get foreground process pid, Determine whether there is currently a foreground process , If there is no direct return .
- use kill(-pid,sig) Function send SIGTSPT Signal to the foreground process group .
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;
}
thus , All seven functions are completed , Next, start to test whether the function function is correct .
test :
First , We need to know what the output is :
Symbol :
- Space : Separate instruction function
- &: If the command is in & ending , Indicates that the job is running in the background
- #: Print directly # The text content of the next line
- %: Followed by an integer , Express job Of ID Number .
User programs :
- myint Program : Functional sleep , Make the program sleep n second , It will not exit automatically after running , And will detect system errors ;
- myspin Program : Functional sleep , Make the program sleep n second , Automatically quit after sleep , Do not detect system errors ;
- mysplit Program : Functional sleep , Make the program sleep n second , Create a sub process to sleep , Then the parent process waits for the child process to sleep normally n Seconds later , Continue operation ;
- mystop Program : Let the process be tentative n second , And send signals .
The first level :
The documents of the first level are as follows :
#
# trace01.txt - Properly terminate on EOF.
# CLOSE
WAIT
We can see that the first level calls linux command close Close the file and wait wait for , See if it stops correctly in EOF.
The verification results :
You can see that the test file is consistent with the answer output .
The second level :
The documents of the second level are as follows :
#
# trace02.txt - Process builtin quit command.
# quit
WAIT
Here is the internal command when inputting quit When , We need to quit shell process , Here we need to judge whether the order is quit, If so, we need to exit this shell process . That is, built-in commands , It's a test builtin_cmd() Partial implementation of function .
The verification results :
The result is the same , So explain builtin_cmd() The partial implementation of the function is correct .
The third level :
The documents of the third level are as follows :
#
# trace03.txt - Run a foreground job.
#
/bin/echo tsh> quit
quit
So let's talk about that /bin/echo
eval The function passes first builtin_cmd Inquire about cmdline Is it built-in commands such as quit, If so, implement
If not, create a child process , Called in a child process execve() Function by argv[0] To find the path , And run the executable file in the path in the subprocess , If not found, the command is invalid , Invalid output command , And use exit(0) End the subprocess
/bin/echo Open up bin In the catalog echo file ,echo It can be understood that the content behind it is output as a string
Turn on echo or turn off request echo , Or display a message . If there are no parameters ,echo The command will display the current echo settings .
So what this means is eval Find out /bin/echo It's not a built-in command , Query path found echo Program and execute in the foreground , Output the following sentence .
What the third level does is :
- open bin In the catalog echo The executable of , Start a sub process in the foreground to run echo This file , then eval() Function USES builtin_cmd() Determine whether this is a built-in command , If it is a built-in instruction, execute it directly .
- perform quit Instructions , End of subprocess , Next, the second one quit The execution is directly returned to the interrupt .
The verification results :
It is found that the output results are the same .
The fourth level :
The documents of the fourth level are as follows :
#
# trace04.txt - Run a background job.
#
/bin/echo -e tsh> ./myspin 1 \046
./myspin 1 &
According to the comments, we can know that this file is used to test whether a background program can be run .
See specific instructions , Through the third level, we can know , Here is also running bin The executable file under the directory echo, At this time, it is executed at the front desk echo command , Then wait for the execution of the program to complete and recycle the sub process .& Represents a background program ,myspin sleep 1 second , Then stop . Because backstage , So show the following sentence , If you are at the front desk, there is no .( This is a program written by myself , The background program will print out a sentence )
The verification results :
The output is the same , It shows that the background running program is correct .
The fifth level :
The documents of the fifth level are as follows :
#
# 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
The running sequence here is as follows : The front desk echo, backstage myspin, The front desk echo, backstage myspin, The front desk echo, Then use the built-in instructions jobs To print all the information in the process list .
The verification results :
The output is the same , explain eval() and builtin_cmd() The partial implementation of the function is correct .
The sixth level :
The code of the sixth level is as follows :
#
# trace06.txt - Forward SIGINT to foreground job.
#
/bin/echo -e tsh> ./myspin 4
./myspin 4
SLEEP 2
INT
According to the comments, we can know the specific meaning of these commands is to SIGINT Forward to the foreground job .
therefore , We should know if we receive an interrupt signal SIGINT( namely CTRL_C) Then end the foreground process .
The verification results :
According to the output , We can know that the process is signal 2 Aborted , According to the macro definition, this signal is an interrupt signal SIGINT. So the output result is correct .
The seventh level :
The documents of the seventh level are as follows :
#
# 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
According to the notes , We can know that the seventh level tests only SIGINT Forward to the foreground job . The command line here is actually well understood according to the previous , Is to give two assignments , One works at the front desk , The other one works in the background , Next pass SIGINT Instructions , Then call the built-in instructions jobs To view the work information at this time , To compare whether it is only SIGINT Forward to the foreground job .
The verification results :
By output , We know that in the end, only the backstage work still exists , It shows that our implementation is correct . At the same time, the output results are the same , It also shows that .
The eighth level :
The documents of the eighth level are as follows :
#
# 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
According to the notes, we need to SIGTSTP Forward to the foreground job . According to the function of this signal , That is, the process will stop until the next SIGCONT That is, to suspend , Let other programs continue . Here is the background program , And then use jobs To print out the process information .
The verification results :
We can find that the final output is the same as what we analyzed , It is also the same as the answer output .
The Ninth level :
The ninth document is as follows :
#
# 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
According to the notes , This test program is to test processing bg Built in commands .
bg : Change the stopped background job to the running background job . By sending SIGCONT Signal restart , Then run it in the background . Parameters can be PID, It can also be JID.ST -> BG
Here is a more complete test on the test file of the eighth level , Here is after the stop , After outputting process information , Use bg Command to wake up the process 2, That is, the program just suspended , Next, continue to use Jobs Command to output results .
The verification results :
Last , It is found that the output results are consistent with the analysis , At the same time, it is also consistent with the output of the answer .
The tenth level :
The tenth document is as follows :
#
# 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
According to the notes, we can know that this level is mainly for testing commands fg Whether it is right .
fg : Change the background job that has been stopped or is running to the job that is running in the foreground . By sending SIGCONT Signal restart , Then run it in the foreground . Parameters can be PID, It can also be JID.ST -> FG,BG -> FG
Here is to change the background process to the foreground running program . Test the process in the text 1 according to & You can know , process 1 It's a background process . First use fg Command to convert it into a program in the foreground , Next, stop the process 1, Then print out the process information , This is the process 1 The foreground program should be suspended at the same time , Next use fg Command to continue running , Use jobs To print out the process information to verify whether our analysis is correct .
The verification results :
Consistent with the analysis , The explanation is correct .
The eleventh level :
The eleventh document is as follows :
#
# 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
According to the notes, we can know that we need to SIGINT Send it to each process in the foreground process group .ps –a Show all processes , There are two processes here ,mysplit Created a subprocess , Next, send instructions SIGINT, So all processes in the process group should be stopped , Next call pl To see whether each process in the process group has stopped .
The verification results :
The output result is the same as the analysis .
Pass 12 :
The documents of the twelfth level are as follows :
#
# 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
According to the notes, the test program is to test SIGTSTP Forward to each process in the foreground process group . Same as the previous level , Only need the corresponding process to be suspended .
The verification results :
Consistent with the results of the analysis .
Level 13 :
The documents of the thirteenth level are as follows :
#
# 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
According to the comments, we can know that the program is to test every stopped process in the restart process group . Here is to use fg To wake up the whole work , In the middle ps -a To see the difference between stopping the whole work and waking up the whole work .
The verification results :
Consistent with the analysis .
The fourteenth pass :
The documents of the 14th level are as follows :
#
# 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
According to the comments, you can know that this file is for testing simple error handling . Here's the test file , That's testing fg and bg Later parameters , We know fg and bg I need a JID Or is it PID, among JID Plus % The integer number of . Other parameters should report errors , Or if there is no parameter, it should also report an error . Next, test the function , All of them have been tested at the above level .
notes : Here I think there is one missing test , That is, when three parameters are input . You should also report an error with the wrong number of parameters .
The verification results :
Consistent with the results of the analysis .
15 :
The documents of the fifteenth level are as follows :
#
# 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
According to the notes, the test of this test file is to put them together . I was still wondering what it means to put them together ? Read the test file carefully , You can know that he tested all the above commands , Such as jobs,fg,bg,quit. I'm not going to repeat it here .
The verification results :
The result is the same as that of the analysis .
16 :
The documents of the 16th level are as follows :
#
# 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
According to the comments, you can know that this test file is for testing shell Is it possible to handle SIGTSTP and SIGINT Signals come from other processes rather than terminals .
The specific meaning of this test file is , User program to job 2 Send a stop signal , So the process will be output finally 2 Suspended information . meanwhile ,mystop You need to stop yourself to send signals to other processes , So there will also be processes in the middle 1 Suspended information .
The verification results :
Consistent with the results of the analysis .
thus , Pass all the sixteen levels .
complete tsh.c Code for :
/* * 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() The parameters of the function
int state = UNDEF; // Working state ,FG or BG
sigset_t set;
pid_t pid; // process id
// Process the input data
if(parseline(cmdline, argv) == 1) // Parse command line , Return to argv Array
state = BG;
else
state = FG;
if(argv[0] == NULL) // The command line returns null directly
return;
// If it's not a built-in command
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");
// Block before it spawns a child process SIGCHLD The signal , Prevent competition
if(sigprocmask(SIG_BLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
if((pid = fork()) < 0) //fork Failed to create child process
unix_error("fork error");
else if(pid == 0) //fork Create child process
{
// The control flow of the child process begins
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0) // unblocked
unix_error("sigprocmask error");
if(setpgid(0, 0) < 0) // Set up child processes id
unix_error("setpgid error");
if(execve(argv[0], argv, environ) < 0){
printf("%s: command not found\n", argv[0]);
exit(0);
}
}
// Add the current process to job in , Whether it's a foreground process or a background process
addjob(jobs, pid, state, cmdline);
// Recover the blocked signal SIGINT SIGTSTP SIGCHLD
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
// Determine the sub process type and handle it
if(state == FG)
waitfg(pid); // Front desk job waiting
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); // Process id Mapping to 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")) // If the order is quit, sign out
exit(0);
else if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) // If it is bg perhaps fg command , perform do_fgbg function
do_bgfg(argv);
else if(!strcmp(argv[0], "jobs")) // If the order is jobs, List running and stopped background 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;
// Without parameters fg/bg Should be discarded
if(!argv[1]){
// The command line is empty
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return ;
}
// testing fg/bg Parameters , among % The first number is JobID, Pure numbers are PID
if(argv[1][0] == '%'){
// analysis jid
if((num = strtol(&argv[1][1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobid\n",argv[0]);// Failure , Print error messages
return;
}
if((job = getjobjid(jobs, num)) == NULL){
printf("%%%d: No such job\n", num); // No corresponding job
return;
}
} else {
if((num = strtol(argv[1], NULL, 10)) <= 0){
printf("%s: argument must be a PID or %%jobid\n",argv[0]);// Failure , Print error messages
return;
}
if((job = getjobpid(jobs, num)) == NULL){
printf("(%d): No such process\n", num); // No corresponding process was found
return;
}
}
if(!strcmp(argv[0], "bg")){
// bg Will start the child process , And put it in the background for execution
job->state = BG; // Set the state of
if(kill(-job->pid, SIGCONT) < 0) // Send a negative signal to the process group
unix_error("kill error");
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
} else if(!strcmp(argv[0], "fg")) {
job->state = FG; // Set the state of
if(kill(-job->pid, SIGCONT) < 0) // Send a negative signal to the process group
unix_error("kill error");
// When a process is set to execute in the foreground , At present tsh You should wait for the subprocess to end
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;
// If the status of the current child process has not changed , be tsh Keep sleeping
while(job->state == FG)
// Use sleep This code of will be slow , Best use 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");
/* Wait for all child processes in a non blocking manner waitpid Parameters 3: 1. 0 : perform waitpid when , Only in child processes ** End ** Only when . 2. WNOHANG : If child process is still running , Then return to 0 . Note that only this flag is set ,waitpid It's possible to return 0 3. WUNTRACED : If the child process stops because of a signal , Then we will return immediately . Only when this flag is set ,waitpid return , Its WIFSTOPPED(status) It's possible to return true */
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
// If the current subprocess job Has deleted , It means there is an error
if((job = getjobpid(jobs, pid)) == NULL){
printf("Lost track of (%d)\n", pid);
return;
}
jid = job->jid;
// Next, judge three states
// If the subprocess receives a pause signal ( I haven't quit yet )
if(WIFSTOPPED(status)){
printf("Job [%d] (%d) stopped by signal %d\n", jid, job->pid, WSTOPSIG(status));
job->state = ST; // The status is set to pending
}
// If the child process calls exit Or a return (return) Normal termination
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));
}
}
// If the child process is terminated because of an uncapped signal , for example SIGKILL
else {
if(deletejob(jobs, pid)){
// Clear process
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)); // Returns the number of signals that cause the child process to terminate
}
}
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){
// send out SIGINT To all processes in the foreground process group
// It should be noted that , The processes in the foreground process group are in addition to the current foreground process , It also includes sub processes of the foreground process .
// At most one foreground process can exist , But there can be multiple processes in the foreground process group
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);
}
边栏推荐
- 软件测试方法有哪些?带你看点不一样的东西
- MySQL数据库(五)视 图 、 存 储 过 程 和 触 发 器
- Crawling cat's eye movie review, data visualization analysis source code operation instructions
- JS --- BOM details of JS (V)
- The minimum number of operations to convert strings in leetcode simple problem
- Mysql database (I)
- Your wechat nickname may be betraying you
- 基于485总线的评分系统双机实验报告
- 线程及线程池
- LeetCode#2062. Count vowel substrings in strings
猜你喜欢
Unpleasant error typeerror: cannot perform 'ROR_‘ with a dtyped [float64] array and scalar of type [bool]
Interview answering skills for software testing
LeetCode#62. Different paths
Future trend and planning of software testing industry
几款开源自动化测试框架优缺点对比你知道吗?
Maximum nesting depth of parentheses in leetcode simple questions
软件测试Bug报告怎么写?
Practical cases, hand-in-hand teaching you to build e-commerce user portraits | with code
C4D quick start tutorial - Introduction to software interface
Leetcode notes - dynamic planning -day6
随机推荐
软件测试需求分析之什么是“试纸测试”
基于485总线的评分系统双机实验报告
Jupyter installation and use tutorial
软件测试方法有哪些?带你看点不一样的东西
ucore lab5用户进程管理 实验报告
12306: mom, don't worry about me getting the ticket any more (1)
C4D quick start tutorial - creating models
Capitalize the title of leetcode simple question
How to solve the poor sound quality of Vos?
Nest and merge new videos, and preset new video titles
The number of reversing twice in leetcode simple question
What is "test paper test" in software testing requirements analysis
Mysql database (I)
Emqtt distribution cluster and node bridge construction
Servlet
Contest3145 - the 37th game of 2021 freshman individual training match_ A: Prizes
学习记录:TIM—电容按键检测
CSAPP shell lab experiment report
几款开源自动化测试框架优缺点对比你知道吗?
MySQL数据库(一)