C语言实现Shell时,向execvp创建的子进程发送信号无效问题求助
我正在用C语言实现一个Shell,需要处理SIGINT和SIGTSTP信号,确保Shell自身不终止,而是将信号传递给子进程。尝试过两种方案:一是在信号处理函数中调用kill(child_pid, SIGINT)(child_pid为fork()返回的子进程ID);二是在子进程调用execvp()前设置新的组ID,再使用killpg()发送信号。但两种方案都无效:信号处理函数能通过write()正常输出提示信息,却无法终止子进程。
// global variable __pid_t child_pid = 0; int main(){ // init variables pid_t childpid; char *commandp; while(1){ ignore(); //do stuff that read input // do stuff that end up using fork to make child process if ( ( childpid = fork() ) == -1 ){ perror ( " Fork fail " ); exit (2); // exit value 2 means fork fail inside the shell } if (childpid == 0){ setpg(getpid(), getpid()); child_pid = getpgrp(); commandp = argv[0]; execvp(commandp, argv); } else { ignore_child(); waitpid(childpid, &status, WUNTRACED); } } } //signal handling functions void ignore(){ struct sigaction act; act.sa_handler=SIG_IGN; // sigaction() sigaction(SIGINT, &act, NULL); sigaction(SIGTSTP, &act, NULL); } void ignore_child(){ struct sigaction act; act.sa_handler = &handle_signal; sigaction(SIGINT, &act, NULL); sigaction(SIGTSTP, &act, NULL); } void handle_signal(int sig){ if (child_pid != 0) if (sig == SIGINT){ write(STDERR_FILENO, "Received SIGINT (CTRL-C) signal. Exiting...\n", 46); killpg(child_pid, SIGINT); } else if (sig == SIGTSTP){ write(STDERR_FILENO, "\nReceived SIGSTP (CTRL-Z) signal. Stopping...\n", 47); killpg(child_pid, SIGTSTP); } }
核心问题分析
全局变量
child_pid赋值完全无效
fork后父子进程拥有独立的地址空间,子进程里的child_pid = getpgrp();只会修改子进程自己的全局变量,父进程的child_pid始终是初始值0。这直接导致信号处理函数里的killpg逻辑根本不会执行。信号结构体未正确初始化
你的sigaction结构体没有用memset初始化,也没设置信号掩码,可能存在脏数据干扰信号注册,或者在处理信号时出现意外的信号阻塞。子进程未成为前台进程组
终端发送的SIGINT/SIGTSTP默认只会传递给前台进程组。如果Shell本身还是前台进程组,信号会先被Shell捕获,而子进程在后台进程组无法收到终端信号。
修复步骤
修正
child_pid的赋值逻辑
直接在父进程中把fork得到的childpid赋值给全局变量,子进程里的赋值代码可以完全删除:else { child_pid = childpid; // 父进程中赋值全局变量 ignore_child(); waitpid(childpid, &status, WUNTRACED); child_pid = 0; // 子进程结束后重置,避免后续误操作 }规范初始化
sigaction结构体
每次注册信号前清空结构体和信号掩码,避免脏数据:#include <string.h> // 需包含memset的头文件 void ignore(){ struct sigaction act; memset(&act, 0, sizeof(act)); act.sa_handler = SIG_IGN; sigemptyset(&act.sa_mask); sigaction(SIGINT, &act, NULL); sigaction(SIGTSTP, &act, NULL); } void ignore_child(){ struct sigaction act; memset(&act, 0, sizeof(act)); act.sa_handler = handle_signal; sigemptyset(&act.sa_mask); // 处理信号时阻塞同类信号,避免并发问题 sigaddset(&act.sa_mask, SIGINT); sigaddset(&act.sa_mask, SIGTSTP); sigaction(SIGINT, &act, NULL); sigaction(SIGTSTP, &act, NULL); }将子进程设为前台进程组
父进程fork后,调用tcsetpgrp把终端的前台进程组切换为子进程组,确保终端信号能直接发送给子进程:#include <termios.h> // 需包含tcsetpgrp的头文件 else { child_pid = childpid; // 将子进程组设为终端前台进程组 tcsetpgrp(STDIN_FILENO, childpid); ignore_child(); waitpid(childpid, &status, WUNTRACED); // 子进程结束后,Shell重新成为前台进程组 tcsetpgrp(STDIN_FILENO, getpid()); child_pid = 0; }简化信号发送逻辑
因为你已经给子进程单独设置了进程组(组ID等于子进程PID),直接用kill(child_pid, sig)即可,效果和killpg一致,代码更简洁。
内容的提问来源于stack exchange,提问作者stefnto

