远距离的欣赏
近距离的迷惘
谁说太阳会找到月亮
——修炼爱情
完整代码见:CSAPP/shlab-handout at main · SnowLegend-star/CSAPP (github.com)
上来就遇到了些小问题:①本来想看看“tshref”支持的命令,结果命令居然被拒绝执行了,显示“-bash: ./tshref: Permission denied”。然后又试了下“./sdriver.pl -h”这个命令,也是报错“-bash: ./sdriver.pl: Permission denied”。我还以为是用户权限的问题,试了下切到root,结果还是不能执行。最后查看了下文件,发现除了自己编译的文件,其他文件都是“rw-r--r--”。下面是GPT对文件默认权限问题的回答。
②书上P239,关于子进程和父进程对各自内存里的内容进行操作后的结果。
③在正式进行实验之前,还得正式了解一番waitpid函数。
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid :
Pid=-1, 等待任一个子进程。与 wait 等效。
Pid>0. 等待其进程 ID 与 pid 相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID 。
使用的注意事项:
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
- //pid_t ret=wait(NULL);
- //pid_t ret=waitpid(id,NULL,0);//等待指定为id的子进程。
- //pid_t ret=waitpid(-1,NULL,0);//设置成-1代表等待任意一个子进程,可能我们有10个子进程,这里只等待任意一个,等价于wait
④在运行“make testxx”时,注意是在linux的shell中运行,而不是在自己编写的tsh中运行。
eval(char *cmdline)
首先解析命令行,调用builtin_cmd判断是否是内置命令。如果是内置命令,执行对应的内置命令。否则,fork一个子进程,execv 对应的代码,将其添加到 job 列表中。
2023/12/15 23:08
书上的P525那份样例代码可以直接拿来用,以便于我们把代码的框架先搭建起来。
有一点我没有理解,为什么在调用fork()之前要先屏蔽SIGCHLD信号,而在fork()结束之后可以立即对SIGCHLD信号解除屏蔽?
sigprocmask(SIG_BLOCK,&mask_one,&prev_one);
if((pid=fork())==0){
sigprocmask(SIG_SETMASK,&prev_one,NULL);
}
我忽略了最重要的一点,解除对SIGCHLD信号的屏蔽只是作用在子进程,而父进程依然保持对SIGCHLD信号的屏蔽。只有当
sigprocmask(SIG_SETMASK,&prev_one,NULL);
父进程才会解除对SIGCHLD信号以及其他信号的屏蔽,进而可以着手处理待处理信号队列中的SIGCHLD,从而避免了竞争的问题。
而之所以子进程要解除对SIGCHLD信号的屏蔽,原因可以分为两部分来讲:①子进程 会继承父进程 的被阻塞集合②子进程 可能自己也要创建自己的子进程 ,但是没有屏蔽SIGCHLD信号的需求,所以 清空从 继承过来的被阻塞集合是比较合理的。
void eval(char *cmdline)
{
char *argv[MAXARGS];
char buf[MAXLINE];
int bg;
pid_t pid;
sigset_t mask_all,mask_one,prev_one;
sigfillset(&mask_all); //这一步的作用是什么?
sigemptyset(&mask_one); //把一会儿要阻塞的信号集清空
sigaddset(&mask_one,SIGCHLD); //把子进程发送的SIGCHLD阻塞,避免父子进程的竞争现象
// signal(SIGCHLD,sigchld_handler);
// signal(SIGTSTP,sigtstp_handler);
// signal(SIGINT,sigint_handler);
strcpy(buf,cmdline);
bg=parseline(buf,argv);
if(argv[0]==NULL)
return ; //ignore empty lines
if(!builtin_cmd(argv)){
sigprocmask(SIG_BLOCK,&mask_one,&prev_one); //把SIGCHLD阻塞之
if((pid=fork())==0){ //子进程运行user job
sigprocmask(SIG_SETMASK,&prev_one,NULL); //取消对SIGCHLD的阻塞,确保子进程结束后父进程可以收到
setpgid(0,0); //做到一个进程一组,不然父进程一会儿调用kill函数的时候也会给自己发送消息
if(execve(argv[0],argv,environ)<0){ //execve函数成功不返回,失败才返回-1
printf("%s: Command not found.\n",argv[0]);
exit(0);
}
}
//父进程添加jobs
sigprocmask(SIG_BLOCK,&mask_all,NULL);
int job_status;
if(bg==0)
job_status=FG;
else
job_status=BG;
addjob(jobs,pid,job_status,cmdline);
sigprocmask(SIG_SETMASK,&prev_one,NULL);
//父进程等待前台job终止
if(!bg){ //是前台进程
waitfg(pid);
}
else{ //是后台进程
printf("[%d] (%d) %s",pid2jid(pid),pid,cmdline);
}
}
return;
}
builtin_cmd(char **argv)
按实验要求,我们的tsh需要支持4个内置命令
-quit 终止命令
-jobs 列出所有的后台作业
-bg <job> 重新唤醒<job>,并把该作业运行在后台
-fg <job> 重新唤醒<job>,并把该作业运行在前台
没什么技术含量,就是把命令行的第一个参数取出来与几个内置命令进行对比,每个内置命令的处理函数已经提供了,直接调用即可。
实现代码如下:
/*
* 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);
if(!strcmp(argv[0],"&")) //如果只有一个“&”,不用理会
return 1;
if(!strcmp(argv[0],"bg")){ //处理bg命令
do_bgfg(argv);
return 1;
}
if(!strcmp(argv[0],"fg")){ //处理fg命令
do_bgfg(argv);
return 1;
}
if(!strcmp(argv[0],"jobs")){ //处理jobs命令
listjobs(jobs);
return 1;
}
return 0; /* not a builtin command */
}
void waitfg(pid_t pid);
在这个函数体内调用sleep()函数就行,很粗糙的一个函数。
/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
while(fgpid(jobs)) //如果检测到这个前台进程还没结束,系统就保持休眠1s,轮询执行直到该前台进程结束
sleep(1);
return;
}
void do_bgfg(char **argv)
这个函数要求我们做好“fg”与“bg”命令的区分,同时“%num”表示用job号来进行查找,“num”则表示用process号来查找。这里有一个比较麻烦的东西,就是如果第二个命令行参数是“%123”,我们现在只需要提取出“123”,一般想法是创建一个新的数组tmp,再把“123”赋值过去。但是,由于我想用atoi()函数,所以又要把tmp数组装换为字符串形式,总之很是麻烦。
看到CSDN一个博主的妙招之后,我直接被这记妙手所折服。
job_tmp=getjobjid(jobs,atoi(&argv[1][1])); //很妙的操作
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{
//做好bg、fg、pid、jid的区分就行
struct job_t *job_tmp;
if(argv[1]==NULL){
printf("%s command requires PID or %%jobid argument\n",argv[0]);
return ;
}
if(argv[1][0]!='%'&&!isdigit(argv[1][0])){ //如果命令行第二个参数的首字符不是数字也不是%
printf("%s: argument must be a PID or %%jobid\n",argv[0]);
return ;
}
if(argv[1][0]!='%'){ //如果是用pid找
job_tmp=getjobpid(jobs,atoi(argv[1]));
if(job_tmp==NULL){
printf("(%d): NO such job\n",atoi(argv[1]));
return ;
}
}
else{ //用jid找
job_tmp=getjobjid(jobs,atoi(&argv[1][1])); //很妙的操作
if(job_tmp==NULL){
printf("%%%d: NO such job\n",atoi(&argv[1][1]));
return ;
}
}
kill(-job_tmp->pid,SIGCONT);
if(!strcmp(argv[0],"fg")){ //如果是前台进程
job_tmp->state=FG;
waitfg(job_tmp->pid);
}
else{
job_tmp->state=BG;
printf("[%d] (%d) %s",pid2jid(job_tmp->pid),job_tmp->pid,job_tmp->cmdline);
}
return;
}
void sigchld_handler(int sig);
为了消除竞争问题,势必要对SIGCHLD信号做一定的处理。我们可以参照书上的P542,其中的关键一招就是在进行deletejob()的时候屏蔽所有信号。
while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0)
在这个 while 循环中,waitpid 以非阻塞方式检查所有子进程的状态。如果有子进程的状态发生变化,waitpid 将返回该子进程的进程ID。如果没有子进程的状态发生变化,waitpid 将返回0,这时循环结束。
这种方式的使用通常用于轮询(polling)子进程的状态,而不是阻塞等待它们。这允许主程序在等待子进程的同时继续执行其他任务。如果没有设置 WNOHANG 标志,waitpid 将阻塞,直到有子进程的状态发生变化。
void sigchld_handler(int sig)
{
int olderrno=errno;
sigset_t mask_all,prev_all;
pid_t pid;
int status;
sigfillset(&mask_all);
while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0){ //回收僵尸进程 waitpid(-1,NULL,0)有问题
if(WIFEXITED(status)){ //如果这个子进程是正常终止的 WEXITSTATUS(status)
sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
deletejob(jobs,pid); //从job list里面删除该结束的子进程
sigprocmask(SIG_SETMASK,&prev_all,NULL);
}
else if(WIFSIGNALED(status)){ //如果这个子进程是因为收到某个信号而终止的(SIGINT)
sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
printf("Job [%d] (%d) terminated by signal %d\n",pid2jid(pid),pid,WTERMSIG(status));
deletejob(jobs,pid); //从job list里面删除该结束的子进程
sigprocmask(SIG_SETMASK,&prev_all,NULL);
}
else{ //如果是SIGSTP信号
struct job_t *job_running=getjobpid(jobs,pid);
sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
printf("Job [%d] (%d) stopped by signal %d\n",pid2jid(pid),pid,WTERMSIG(status));
job_running->state=ST;
sigprocmask(SIG_SETMASK,&prev_all,NULL);
}
}
// if(errno!=ECHILD)
// printf("waitpid error");
errno=olderrno;
return;
}
void sigtstp_handler(int sig);
由于SIGSTP信号是针对于前台进程的,所以在这个函数内部只需要用kill()发送一个信号给前台进程就好,其他的操作可以放到SIGCHLD信号的处理函数里面去。
这里可能会有一个令人费解的地方,明明SIGSTP的处理函数并没有和SIGCHLD的处理函数进行交互,怎么后者还可以处理前者应该处理的部分呢?
其中原因就在于sigchld_handler ()内部的waitpid()函数,一旦子进程接收到了SIGSTP信号,则该子进程的状态会立即发生改变从而导致waitpid()收到对应的pid,从而代码的实际执行部分就跳转到了sigchld_handler()函数内部。
void sigtstp_handler(int sig)
{
int olderrno = errno;
pid_t fg_pid = fgpid(jobs);
if(fg_pid){
kill(-fg_pid,sig);
}
errno = olderrno;
return;
}
void sigint_handler(int sig);
同上。
void sigint_handler(int sig)
{
int olderrno=errno;
pid_t fg_pid=fgpid(jobs); ;//获取当前正在运行的前台进程的pid,如果没有就返回0
if(fg_pid)
kill(-fg_pid,sig);
errno=olderrno;
return;
}
Test01~04
完成eval()和bulitin_cmd()后,尝试执行了上面前三个test,发现输出都和参考输出一致。只有test04出现了输出格式的问题,修改下输出格式即可。
Test05
执行这个测试的时候,发现jobs输出为空。
猜测是出现了父子竞争的问题,开始着手修改eval()、waitfg()、sigchld_handler()。eval()和sigchld_handler()可以按照书上P543来修改,但是得注意sigchld_handler()内部有一处需要修改的地方。
while((pid=waitpid(-1,NULL,0))>0)
这条命令是等待任意进程结束,但如果进程结束的话我们就会从job list中删除这个进程,此时就不存在这个进程的相关信息了。按提示的意思,是一旦发现后台进程存在,waitpid()就立即返回,顺便打印出该后台进程的相关信息。所以waitpid(pid_t pid,int *status,int options)的options部分应改为“WNOHANG|WUNTRACED”才是正确的。
Test06~08
①为什么父进程中有signal(SIGCHLD,sigchld_handler);这种处理SIGCHLD的函数,而并没有处理SIGSTP和SIGINT的函数呢?
signal(SIGCHLD,sigchld_handler);
// signal(SIGTSTP,sigtstp_handler);
// signal(SIGINT,sigint_handler);
我的理解是处理下面两个信号的函数在main()中已经调用了,所以不用在eval()函数中进行重复调用。
/* 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 */
之所以要在eval()中重新调用signal(SIGCHLD,sigchld_handler),是因为我们在eval()函数中阻塞了SIGCHLD信号,在对这个信号解阻塞后要重新对SIGCHLD信号进行处理,所以得再次调用signal(SIGCHLD,sigchld_handler)。
Md,刚才我把signal(SIGCHLD,sigchld_handler)删掉了再对样例进行测试,结果发现输出并没有丝毫变化。事实证明signal(SIGCHLD,sigchld_handler)只需要调用一次就好,在eval()函数中并不需要重新调用这个函数。怪不得连我自己都觉得解释显得很牵强附会,原来我压根儿就在胡说八道啊。
②很奇怪的问题
熄灯之后借着电脑屏幕的光瞄了眼书,结果看差了。应该调用的是WIFEXITED()函数而不是WEXITSTATUS()函数。调用了WEXITSTATUS()后之所以会出现上图结果,是因为子进程因为接收到了SIGINT信号而异常终止。对于非正常终止的进程,WEXITSTATUS()返回的值是未定义的,这可能就导致了进程出现后续一连串的问题。
Test09~10
这里开始要求处理“fg”“bg”命令,完成do_bgfg()即可。
Test11~13
输出的东西有点难看懂,粗略对比下,如果前10个都没问题的话这个应该也没问题。
Test14
测试各种输入错误的处理,简单更加错误处理部分即可。
Test15
将前面所有的测试内容都测试一遍,基本不用再修改代码。
Test16
是测试tsh能否处理不是来自终端而是来自其他进程的SIGSTP和SIGINT信号,顺利通过。