sudo 是为当它是 tty 上其他进程的子进程时获得的 SIGHUP 语义而设计的。在这种情况下,当父进程退出时,所有进程都会从内核获得自己的 SIGHUP。
xterm -e sudo cmd
直接在伪终端上运行 sudo。这会产生与 sudo 预期不同的 SIGHUP 语义。只有 sudo 从内核接收 SIGHUP,并且不会转发它,因为它期望仅当其子进程也获得自己的 SIGHUP 时才从内核获取 SIGHUP(因为 sudo 的父进程(例如 bash)所做的事情)。
I 向上游报告了该问题 http://bugzilla.sudo.ws/show_bug.cgi?id=719, and 它现在在 sudo 1.8.15 及更高版本中标记为已修复.
解决方法:
xterm -e 'sudo ./sig-counter; true'
# or for uses that don't implicitly use a shell:
xterm -e sh -c 'sudo some-cmd; true'
If your -c
argument 是一个命令,bash 通过执行它来优化。跟踪另一个命令(微不足道的true
在这种情况下),让 bash 继续运行并作为一个孩子运行 sudo 。我测试过,使用这种方法,当您关闭 xterm 时,sig-counter 会从内核收到一个 SIGHUP。 (对于任何其他终端模拟器来说应该是相同的。)
我已经测试过了,它可以与 bash 和 dash 一起使用。包含一个方便的无需退出的信号接收程序的源代码,您可以跟踪它以查看它接收到的所有信号。
这个答案的其余部分的某些部分可能稍微不同步。在弄清楚 sudo 作为控制进程与 sudo 作为 shell 子进程的区别之前,我经历了一些理论和测试方法。
POSIX 说 http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html that close()
在伪终端的主端会导致这样的情况:“应将 SIGHUP 信号发送到控制进程(如果有),其中伪终端的从机端是控制终端。”
POSIX 的写法为close()
意味着只能有一个以 pty 作为其控制终端的处理进程。
当 bash 是 pty 从属端的控制进程时,它会执行一些操作,导致所有其他进程接收 SIGHUP。这是 sudo 所期望的语义。
ssh localhost
,然后中止连接~.
或者杀死你的 ssh 客户端。
$ ssh localhost
ssh$ sudo ~/.../sig-counter # without exec
# on session close: gets a SIGHUP and a SIGCONT from the kernel
$ ssh localhost
ssh$ exec sudo ~/src/experiments-sys/sig-counter
# on session close: gets only a SIGCONT SI_USER relayed from sudo
$ ssh -t localhost sudo ~/src/experiments-sys/sig-counter
# on session close: gets only a SIGCONT SI_USER relayed from sudo
$ xterm -e sudo ./sig-counter
# on close: gets only a SIGCONT SI_USER relayed from sudo
测试这个很棘手,因为xterm
在退出并关闭 pty 之前,也会自行发送 SIGHUP。其他终端模拟器(gnome-terminal、konsole)可能会也可能不会这样做。我必须自己编写一个信号测试程序,以免在第一次 SIGHUP 后就死掉。
除非 xterm 以 root 身份运行,否则它无法向 sudo 发送信号,因此 sudo 只能从内核获取信号。 (因为它是 tty 的控制进程,而 sudo 下运行的进程不是。)
The sudo
手册页说:
除非该命令正在新的 pty 中运行,
SIGHUP、SIGINT 和 SIGQUIT 信号不会被中继,除非它们是由用户进程而不是内核发送的。否则,
命令将收到 SIGINT
每次用户输入 control-C 两次。
在我看来,sudo 的 SIGHUP 双信号避免逻辑是为作为交互式 shell 的子进程运行而设计的。当不涉及交互式 shell 时(之后exec sudo
从交互式 shell 中,或者当一开始就没有涉及 shell 时),只有父进程 (sudo) 会收到 SIGHUP。
sudo 的行为对于 SIGINT 和 SIGQUIT 来说是有好处的,即使在不涉及 shell 的 xterm 中:在 xterm 中按 ^C 或 ^\ 后,sig-counter
恰好收到一个 SIGINT 或 SIGQUIT。sudo
收到一个并且不转发它。si_code=SI_KERNEL
在这两个过程中。
在 Ubuntu 15.04 上测试,sudo --version
:1.8.9p5。xterm -v
:
XTerm(312)。
###### No sudo
$ pkill sig-counter; xterm -e ./sig-counter &
$ strace -p $(pidof sig-counter)
Process 19446 attached
quit xterm (ctrl-left click -> quit)
rt_sigtimedwait(~[TERM RTMIN RT_1], {si_signo=SIGHUP, si_code=SI_USER, si_pid=19444, si_uid=1000}, NULL, 8) = 1 # from xterm
rt_sigtimedwait(~[TERM RTMIN RT_1], {si_signo=SIGHUP, si_code=SI_KERNEL}, NULL, 8) = 1 # from the kernel
rt_sigtimedwait(~[TERM RTMIN RT_1], {si_signo=SIGCONT, si_code=SI_KERNEL}, NULL, 8) = 18 # from the kernel
sig-counter is still running, because it only exits on SIGTERM
#### with sudo, attaching to sudo and sig-counter after the fact
# Then send SIGUSR1 to sudo
# Then quit xterm
$ sudo pkill sig-counter; xterm -e sudo ./sig-counter &
$ sudo strace -p 20398 # sudo's pid
restart_syscall(<... resuming interrupted call ...>) = ?
ERESTART_RESTARTBLOCK (Interrupted by signal)
--- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=20540, si_uid=0} ---
write(7, "\n", 1) = 1 # FD 7 is the write end of a pipe. sudo's FD 6 is the other end. Some kind of deadlock-avoidance?
rt_sigreturn() = -1 EINTR (Interrupted system call)
poll([{fd=6, events=POLLIN}], 1, 4294967295) = 1 ([{fd=6, revents=POLLIN}])
read(6, "\n", 1) = 1
kill(20399, SIGUSR1) = 0 ##### Passes it on to child
read(6, 0x7fff67d916ab, 1) = -1 EAGAIN (Resource temporarily unavailable)
poll([{fd=6, events=POLLIN}], 1, 4294967295
####### close xterm
--- SIGHUP {si_signo=SIGHUP, si_code=SI_KERNEL} ---
rt_sigreturn() = -1 EINTR (Interrupted system call)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_KERNEL} --- ### sudo gets both SIGHUP and SIGCONT
write(7, "\22", 1) = 1
rt_sigreturn() = -1 EINTR (Interrupted system call)
poll([{fd=6, events=POLLIN}], 1, 4294967295) = 1 ([{fd=6, revents=POLLIN}])
read(6, "\22", 1) = 1
kill(20399, SIGCONT) = 0 ## but only passes on SIGCONT
read(6, 0x7fff67d916ab, 1) = -1 EAGAIN (Resource temporarily unavailable)
poll([{fd=6, events=POLLIN}], 1, 4294967295
## keeps running after xterm closes
$ sudo strace -p $(pidof sig-counter) # in another window
rt_sigtimedwait(~[RTMIN RT_1], {si_signo=SIGUSR1, si_code=SI_USER, si_pid=20398, si_uid=0}, NULL, 8) = 10
rt_sigtimedwait(~[RTMIN RT_1], {si_signo=SIGCONT, si_code=SI_USER, si_pid=20398, si_uid=0}, NULL, 8) = 18
## keeps running after xterm closes
下运行的命令sudo
仅当 xterm 关闭时才会看到 SIGCONT。
请注意,单击 xterm 标题栏上的窗口管理器的关闭按钮只会使 xterm 手动发送 SIGHUP。通常这会导致 xterm 内部的进程关闭,在这种情况下 xterm 随后退出。再次强调,这只是 xterm 的行为。
这是什么bash
当它收到 SIGHUP 时执行,产生行为sudo
期望:
Process 26121 attached
wait4(-1, 0x7ffc9b8c78c0, WSTOPPED|WCONTINUED, NULL) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGHUP {si_signo=SIGHUP, si_code=SI_KERNEL} ---
--- SIGCONT {si_signo=SIGCONT, si_code=SI_KERNEL} ---
... write .bash history ...
kill(4294941137, SIGHUP) = -1 EPERM (Operation not permitted) # This is kill(-26159), which signals all processes in that process group
rt_sigprocmask(SIG_BLOCK, [CHLD TSTP TTIN TTOU], [CHLD], 8) = 0
ioctl(255, SNDRV_TIMER_IOCTL_SELECT or TIOCSPGRP, [26121]) = -1 ENOTTY (Inappropriate ioctl for device) # tcsetpgrp()
rt_sigprocmask(SIG_SETMASK, [CHLD], NULL, 8) = 0
setpgid(0, 26121) = -1 EPERM (Operation not permitted)
rt_sigaction(SIGHUP, {SIG_DFL, [], SA_RESTORER, 0x7f3b25ebf2f0}, {0x45dec0, [HUP INT ILL TRAP ABRT BUS FPE USR1 SEGV USR2 PIPE ALRM TERM XCPU XFSZ VTALRM SYS], SA_RESTORER, 0x7f3b25ebf2f0}, 8) = 0
kill(26121, SIGHUP) = 0 ## exit in a way that lets bash's parent see that SIGHUP killed it.
--- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=26121, si_uid=1000} ---
+++ killed by SIGHUP +++
我不确定哪一部分可以完成工作。可能实际的退出是技巧,或者是它在启动命令之前所做的事情,因为kill
and tcsetpgrp()
都失败了。
我自己的第一次尝试是:
xterm -e sudo strace -o /dev/pts/11 sleep 60
(其中 pts/11 是另一个终端。)sleep
在第一个 SIGHUP 后退出,因此不使用 sudo 的测试仅显示 xterm 手动发送的 SIGHUP。
sig-counter.c:
// sig-counter.c.
// http://stackoverflow.com/questions/32511170/terminate-sudo-python-script-when-the-terminal-closes
// gcc -Wall -Os -std=gnu11 sig-counter.c -o sig-counter
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#define min(x, y) ({ \
typeof(x) _min1 = (x); \
typeof(y) _min2 = (y); \
(void) (&_min1 == &_min2); \
_min1 < _min2 ? _min1 : _min2; })
int sigcounts[64];
static const int sigcount_size = sizeof(sigcounts)/sizeof(sigcounts[0]);
void handler(int sig_num)
{
sig_num = min(sig_num, sigcount_size);
sigcounts[sig_num]++;
}
int main(void)
{
sigset_t sigset;
sigfillset(&sigset);
// sigdelset(&sigset, SIGTERM);
if (sigprocmask(SIG_BLOCK, &sigset, NULL))
perror("sigprocmask: ");
const struct timespec timeout = { .tv_sec = 60 };
int sig;
do {
// synchronously receive signals, instead of installing a handler
siginfo_t siginfo;
int ret = sigtimedwait(&sigset, &siginfo, &timeout);
if (-1 == ret) {
if (errno == EAGAIN) break; // exit after 60 secs with no signals
else continue;
}
sig = siginfo.si_signo;
// switch(siginfo.si_code) {
// case SI_USER: // printf some stuff about the signal... just use strace
handler(sig);
} while (sig != SIGTERM );
//sigaction(handler, ...);
//sleep(60);
for (int i=0; i<sigcount_size ; i++) {
if (sigcounts[i]) {
printf("counts[%d] = %d\n", i, sigcounts[i]);
}
}
}
我的第一次尝试是 perl,但是安装信号处理程序并不能阻止 perl 在信号处理程序返回后在 SIGHUP 上退出。我在 xterm 关闭之前看到该消息出现。
cmd=perl\ -e\ \''use strict; use warnings; use sigtrap qw/handler signal_handler normal-signals/; sleep(60); sub signal_handler { print "Caught a signal $!"; }'\';
xterm -e "$cmd" &
显然 perl 信号处理相当复杂,因为 perl 必须推迟它们,直到它不在无法正确锁定的事物中间 http://perldoc.perl.org/perlipc.html#Deferred-Signals-%28Safe-Signals%29.
C 中的 Unix 系统调用是进行系统编程的“默认”方式,因此可以消除任何可能的混乱。 strace 通常是一种廉价的方法,可以避免实际编写日志/打印代码来玩弄东西。 :P