Linux同TTY下后台进程触发前台TUI应用的技术问题
问题背景与代码文件
tui.c:简单的ncurses Hello World TUI应用
// gcc -o tui tui.c -lncurses #include <ncurses.h> int main() { initscr(); // 启动curses模式 printw("Hello, World!"); // 输出Hello, World refresh(); // 刷新到真实屏幕 getch(); // 等待用户输入 endwin(); // 结束curses模式 return 0; }
test.c:需要后台运行的C程序(避免阻塞终端其他操作)
// gcc -o test test.c -lpthread #include <stdio.h> #include <signal.h> #include <unistd.h> #include <pthread.h> #include <sys/types.h> #include <sys/wait.h> int countdown = 2; void start_tui(){ system("./start.sh"); } void sigusr1_handle(int signum){ system("uptime"); } void sigtstp_handle(int signum){ _exit(0); } void* func2(void* callback){ signal(SIGUSR1, sigusr1_handle); // 捕获SIGUSR1信号 signal(SIGTSTP, sigtstp_handle); // 捕获SIGTSTP信号 printf("this is func2 running..."); sleep(3); int status; start_tui(); // 启动TUI应用 void (*reset_timer)() = callback; // 设置回调函数 reset_timer(); // 执行回调 return NULL; } void reset(){ // 回调函数定义 countdown = 2; // 重置倒计时 } void func1(){ while(1){ while(countdown > 0){ // 3秒倒计时 printf("count down: %d\n", countdown); sleep(1); countdown--; } printf("count down: %d\n", countdown); if(!countdown){ // 分离进程逻辑 pthread_t thread; pthread_create(&thread, NULL, func2, reset); // 执行func2 pthread_join(thread, NULL); // 等待线程结束 printf("func2 has stopped.\n"); } } } int main(){ func1(); return 0; }
start.sh:被C程序调用,用于启动TUI窗口
#!/bin/sh set -m # 启用作业控制 ./tui # 调用ncurses TUI应用
预期行为
$ ./test & # 程序后台运行,不阻塞当前TTY的其他任务 count down 2 count down 1 count down 0 hello world! # TUI窗口输出在前台 <按任意键退出ncurses TUI窗口,中断TUI线程> count down 2 count down 1 count down 0
实际行为
$ ./test & # 程序后台运行,不阻塞当前TTY的其他任务 count down 2 count down 1 count down 0 hello world! # TUI窗口输出在前台 <尝试按任意键退出ncurses TUI窗口,中断TUI线程> <意外:SSH终端登出,会话断开> <重新登录后,tui hello world进程仍在运行... <按键无法传递消息给后台进程的内部线程>
技术问题
- 若
test二进制文件前台运行(执行$ ./test),功能正常但终端会被阻塞,无法处理其他用户输入。
- 若
- 如何让后台运行的任务调用前台TUI应用,确保终端输入能被前台应用正常接收?
- 如何将前台TUI应用转为后台运行,同时让它接收当前终端的stdin输入并输出到stdout?
解决方案
针对问题1:后台任务调用前台TUI并正常接收输入
后台进程默认脱离终端控制,要让它启动的TUI能抢占前台并接收输入,需调整进程的终端关联和作业控制:
修改
start.sh,显式绑定终端
后台进程启动的子进程默认继承后台状态,强制让tui绑定当前终端:#!/bin/sh # 直接将tui的输入输出绑定到当前终端 ./tui < /dev/tty > /dev/tty 2>&1解释:
/dev/tty是当前终端的设备文件,强制让tui的标准输入输出与当前终端关联,不受父进程后台状态影响。在C程序中替换
system()为fork()+exec(),增强可控性system()会启动shell,容易继承后台状态,改用直接执行方式:
修改start_tui()函数:void start_tui(){ pid_t pid = fork(); if(pid == 0){ // 子进程绑定当前终端 dup2(STDIN_FILENO, fileno(stdin)); dup2(STDOUT_FILENO, fileno(stdout)); execl("./tui", "tui", NULL); perror("execl failed"); _exit(1); } else if(pid > 0){ waitpid(pid, NULL, 0); // 等待TUI退出 } }子进程直接关联当前终端,启动后会自动抢占前台,正常接收用户输入。
针对问题2:TUI后台运行但仍接收当前终端输入输出
ncurses应用依赖终端环境变量和直接终端访问,要实现后台交互需做以下调整:
启动时明确绑定终端设备
即使后台运行,也要让tui绑定当前终端:
在C程序中调用时,用exec()方式指定输入输出:void start_tui(){ pid_t pid = fork(); if(pid == 0){ // 后台运行但绑定终端 freopen("/dev/tty", "r", stdin); freopen("/dev/tty", "w", stdout); freopen("/dev/tty", "w", stderr); setsid(); // 创建新会话避免进程组冲突 execl("./tui", "tui", NULL); perror("execl failed"); _exit(1); } }忽略后台进程的终端信号
后台进程尝试读写终端时会收到SIGTTIN/SIGTTOU信号,需在tui.c中忽略:int main() { signal(SIGTTIN, SIG_IGN); signal(SIGTTOU, SIG_IGN); initscr(); printw("Hello, World!"); refresh(); getch(); endwin(); return 0; }
额外优化:解决SSH会话断开后残留进程问题
SSH会话断开时,终端会发送SIGHUP信号给关联进程,让tui捕获该信号正常退出:
在tui.c中添加:
void sig_hup(int signum){ endwin(); exit(0); } int main() { signal(SIGHUP, sig_hup); // 捕获SIGHUP信号,正常退出 initscr(); printw("Hello, World!"); refresh(); getch(); endwin(); return 0; }
内容的提问来源于stack exchange,提问作者melon
相关产品推荐
相关产品推荐

