- A+
所属分类:系统文档
目录:
☆ 原始问题
1) 模拟场景
2) vi本身的swap机制
☆ 预防措施
1) screen
2) tmux
3) screen vs tmux
☆ 救急方案
1) cryopid
2) screenify(可用)
3) retty
4) injcode
5) neercs
6) reptyr(推荐)
6.1) reptyr(1)
6.2) reptyr原理简介
6.3) "reptyr -T"原理简介
☆ 伪终端
1) pty(7)/tty(4)
2) termios(3)
3) stty(1)
4) ioctl(2)/ioctl_tty(2)
5) Sessions and Process Groups
6) Job control
7) 信号与任务控制示例
8) setsid(1)
9) jobs/disown/nohup
☆ 结束语
--------------------------------------------------------------------------
☆ 原始问题
在一个SSH会话里执行vi,后因TCP连接中断而失去控制。重新登录后发现原SSH会话
对应的伪终端还在,其中的vi进程也在。有什么办法重新获取对vi的控制?
这种情况一般是单向TCP故障所致,即服务端没有收到FIN或RST,客户端单方面中止
了TCP连接,现实中并不罕见。
1) 模拟场景
设计一个实验确保精确复现这种情况。
服务端是位于Guest中的Linux,客户端是Host中的SecureCRT。多登录几个SSH会话,
其中一个SSH会话中执行"vi some.txt"。在VMware中断开虚拟网卡,在Host中用
Tcpview切断vi进程所在SSH会话对应的TCP连接,由于虚拟网卡已断开,Guest中的
SSH会话不会收到RST或FIN,而Host中的SecureCRT会收到。在其他SSH会话中用
netstat、pstree、ps等工具确认目标SSH会话及vi进程仍在。
$ netstat -ntp | grep :22
tcp 0 52 x.x.x.x:22 y.y.y.y:1999 ESTABLISHED 1185/sshd: root@pts
tcp 0 0 x.x.x.x:22 y.y.y.y:2069 ESTABLISHED 2244/sshd: scz [pri
tcp 0 0 x.x.x.x:22 y.y.y.y:2070 ESTABLISHED 2342/sshd: scz [pri
$ pstree -npu -al 2244
sshd,2244
`-sshd,2263,scz
`-bash,2264
`-vi,2341 some.txt
$ pstree -H `pidof -s vi` -npu
systemd(1)-+-systemd-journal(223)
...
|-sshd(651)-+-sshd(1185)-+-bash(1204)
| | `-bash(2271)---pstree(2401)
| |-sshd(2244)---sshd(2263,scz)---bash(2264)---vi(2341)
| `-sshd(2342)---sshd(2349,scz)---bash(2350)
...
[scz@ /tmp]> echo $$
2350
[scz@ /tmp]> ps -f -o pid,user,args
PID USER COMMAND
2350 scz -bash
2474 scz \_ ps -f -o pid,user,args
2264 scz -bash
2341 scz \_ vi some.txt
2) vi本身的swap机制
[scz@ /tmp]> ls -l .some.txt.swp
-rw------- 1 scz scz 12288 Feb 14 11:50 .some.txt.swp
如果启动vi时没有指定-n,缺省有swap文件用于crash后的恢复。
[scz@ /tmp]> vi -r some.txt
它会自动从.some.txt.swp中恢复内容到some.txt,之后可以删除swap文件。
[scz@ /tmp]> rm .some.txt.swp
此处不考虑vi本身的这种恢复机制,考虑更普遍情形。
☆ 预防措施
1) screen
以前跑oclHashcat-plus时我就碰上过客户端单方面中止TCP连接的事,当
时周大给我推荐了screen。
$ aptitude install screen
简单演示一下screen:
[scz@ /tmp]> tty
/dev/pts/4
[scz@ /tmp]> screen -S screen.scz.pts_4
[scz@ /tmp]> tty
/dev/pts/5
[scz@ /tmp]> vi pts_4.txt
Ctrl-A D
[detached from 2647.screen.scz.pts_4]
在另一个伪终端里恢复对vi的控制:
[scz@ /tmp]> tty
/dev/pts/6
[scz@ /tmp]> screen -r screen.scz.pts_4
可以简单地"screen -r"、"screen -x"。看到vi界面,退出vi后检查当前伪终端:
[scz@ /tmp]> tty
/dev/pts/5
退出screen状态,可以exit,也可Ctrl-D。
[screen is terminating]
2) tmux
$ aptitude install tmux
简单演示一下tmux:
[scz@ /tmp]> tty
/dev/pts/4
[scz@ /tmp]> tmux
[scz@ /tmp]> tty
/dev/pts/7
[scz@ /tmp]> vi pts_4.txt
在另一个伪终端里夺取对vi的控制:
[scz@ /tmp]> tty
/dev/pts/6
[scz@ /tmp]> tmux ls
0: 1 windows (created Thu Feb 14 13:47:38 2019) [132x57] (attached)
[scz@ /tmp]> tmux attach -t 0
看到vi界面,退出vi后检查当前伪终端:
[scz@ /tmp]> tty
/dev/pts/7
后果与screen类似。
[scz@ /tmp]> tmux detach
[detached (from session 0)]
[scz@ /tmp]> tty
/dev/pts/6
退出tmux状态,可以exit,也可Ctrl-D。
[exited]
3) screen vs tmux
这是二者的比较:
http://www.wikivs.com/wiki/screen_vs_tmux
☆ 救急方案
screen、tmux要求提前考虑到风险,它们都是预防措施,非原始问题的答案。原始问
题发生时,显然没有提前进入screen、tmux状态,还有救吗?
1) cryopid
A: Bernard Blackham 2004
https://github.com/maaziz/cryopid
https://github.com/maaziz/cryopid.git
CryoPID允许捕捉正在运行中的进程状态并将之保存到文件中,将来利用该文件恢复
进程状态,甚至可以在系统重启后或迁移至另一台主机时生效。简单理解成进程级快
照,不过很怀疑它的实用性,而且没能编译成功。
[scz@ /tmp]> git clone https://github.com/maaziz/cryopid.git
[scz@ /tmp/cryopid/src]> make
2) screenify(可用)
A: Timo Lindfors 2004
http://tomaw.net/tmp/screenify
脚本编写年代过早,对于较新版本GDB,需要做点小修改,下面是我改过的:
--------------------------------------------------------------------------
#!/bin/sh
# Copyright Timo Lindfors 2004
function usage() {
echo usage: $0 pid
exit 1
}
TCGETS=0x5401
TCSETS=0x5402
SIZEOF_STRUCT_TERMIOS=60
O_RDWR=2
((FLAGS=O_RDWR))
PID=$1
if [ x`which gdb` == x ]; then
echo gdb not found in PATH. Please apt-get install gdb
exit
fi
if [ x$PID == x ]; then
usage;
fi
if [ x$2 != x ]; then
usage;
fi
MYPID=$$
MYFD0=`readlink /proc/$MYPID/fd/0`
MYFD1=`readlink /proc/$MYPID/fd/1`
MYFD2=`readlink /proc/$MYPID/fd/2`
EXE=`readlink /proc/$PID/exe`
if [ x$EXE == x ]; then
echo $0: $PID: no such pid
exit 1
fi
BATCHFILE=`mktemp -p /tmp "gdb.$$_${RANDOM}_XXXXXXXXXX"`
cat >$BATCHFILE <<EOF
file $EXE
attach $PID
call (char*)malloc($SIZEOF_STRUCT_TERMIOS)
call (char*)malloc($SIZEOF_STRUCT_TERMIOS)
call (char*)malloc($SIZEOF_STRUCT_TERMIOS)
call (void)ioctl(0, $TCGETS, \$1)
call (void)ioctl(1, $TCGETS, \$2)
call (void)ioctl(2, $TCGETS, \$3)
call (void)close(0)
call (void)close(1)
call (void)close(2)
call (int)open("$MYFD0", $FLAGS)
call (int)open("$MYFD1", $FLAGS)
call (int)open("$MYFD2", $FLAGS)
call (void)ioctl(0, $TCSETS, \$1)
call (void)ioctl(1, $TCSETS, \$2)
call (void)ioctl(2, $TCSETS, \$3)
call (void)free(\$1)
call (void)free(\$2)
call (void)free(\$3)
detach
EOF
gdb -batch -x $BATCHFILE >/dev/null 2>&1 </dev/null
rm $BATCHFILE
cat <<EOF
Process $PID should now be talking to this pty. Refresh the screen
(e.g. ESC CTRL+L) and have fun!
EOF
exec tail -f --pid=$PID /proc/$PID/stat
--------------------------------------------------------------------------
GDB调试目标进程;用TCGETS取stdin、stdout、stderr的终端属性,这个应该就是
tcgetattr();关闭原来的0/1/2号句柄;分三次打开当前SHELL所在伪终端,正常情
况下它们的句柄依次是0/1/2;用TCSETS重设stdin、stdout、stderr的终端属性,这
个应该就是tcsetattr();至此,目标进程的stdin、stdout、stderr转移到当前
SHELL所在伪终端。
参看ioctl_tty(2)。
脚本中的三次open()是在目标进程空间中执行的,受制于目标进程的权限,如果当前
SHELL与目标进程均属于同一用户,不存在问题。假设目标进程属于scz,当前SHELL
属于root,open()会失败(返回-1),这是个小坑。
这种只依赖GDB的技术方案有一些限制,参看:
《2.12 在GDB中调用被调试进程空间中的函数》
《2.19 在GDB里如何搜索内存》
《2.31 GDB中如何调用指定动态链接库中的导出函数》
《2.62 GDB条件断点中进行字符串比较》
[scz@ /tmp]> tty
/dev/pts/3
[scz@ /tmp]> vi some.txt
随便编辑点内容,然后切到另一个伪终端:
[scz@ /tmp]> tty
/dev/pts/4
[scz@ /tmp]> ps -f -o pid,user,args
PID USER COMMAND
3599 scz -bash
3608 scz \_ ps -f -o pid,user,args
3590 scz -bash
3606 scz \_ vi some.txt
夺取对vi的控制:
[scz@ /tmp]> ./screenify 3606
按ESC之后再Ctrl-L刷新屏幕,看到vi界面,可以正常操作并保存退出。
此时/dev/pts/3上的bash已经无法工作,但vi正常退出,之前编辑的内容得以保存。
3) retty
A: Petr Baudis, Jan Sembera 2006
retty attach processes running on other terminals
http://pasky.or.cz//dev/retty/
http://pasky.or.cz//dev/retty/retty-1.0.tar.bz2
retty本质上同screenify,我未实测。
4) injcode
A: Thomas Habets 2009-03-21
Moving a process to another terminal
https://blog.habets.se/2009/03/Moving-a-process-to-another-terminal.html
http://github.com/ThomasHabets/injcode
git clone git://github.com/ThomasHabets/injcode.git
2009年之后再未更新。
5) neercs
A: Sam Hocevar, Jean-Yves Lamoureux, Pascal Terjan
http://caca.zoy.org/wiki/neercs
http://caca.zoy.org/wiki/neercs?format=txt
http://caca.zoy.org/wiki/neercs/devel
git://git.zoy.org/neercs.git
http://caca.zoy.org/git/neercs.git
neercs比较复杂,我未实测。
6) reptyr(推荐)
A: Nelson Elhage 2011
reptyr: Attach a running process to a new terminal - [2011-01-21]
https://blog.nelhage.com/2011/01/reptyr-attach-a-running-process-to-a-new-terminal/
http://github.com/nelhage/reptyr
https://github.com/nelhage/reptyr.git
reptyr: Changing a process's controlling terminal - [2011-02-08]
https://blog.nelhage.com/2011/02/changing-ctty/
(介绍reptyr修改目标进程控制终端的技术原理)
New reptyr feature: TTY-stealing - [2014-08-20]
https://blog.nelhage.com/2014/08/new-reptyr-feature-tty-stealing/
Reptyr Move A Running Process From One Terminal To Another Without Closing It - [2017-02-26]
https://www.ostechnix.com/reptyr-move-running-process-new-terminal/
对less使用screenify,less仍从旧终端读取输入。对ncurses程序使用screenify,
无法调整窗口大小。对程序使用screenify,新终端上Ctrl-C无效。reptyr解决了这
些问题。
reptyr可以在i386、x86_64、ARM上运行。
常见使用方式:
a) reconnect ssh
b) screen
c) ps -a | grep <orphaned process>
d) reptyr <pid>
Debian有这个包,说明reptyr已成为业界通用工具。万一发行版不带reptyr,就自己
编译源码吧。
$ aptitude install reptyr
6.1) reptyr(1)
--------------------------------------------------------------------------
NAME
reptyr - 给正在运行中的目标进程更换控制终端(CTTY)
SYNOPSIS
reptyr PID
reptyr -l|-L [COMMAND [ARGS]]
OPTIONS
-T
reptyr不是用ptrace(2)调试目标进程,而是试图找出目标进程对应的terminal
emulator并劫持mater pty。这种模式更可靠,适用性更强。此时可以更改目标
进程所在session的所有进程的CTTY。缺点是,除非以root身份执行reptyr,否
则不能用于sshd(8)的子进程。
-l, -L [COMMAND [ARGS]]
此时没有目标进程。这种模式将创建新的pty对,在新master pty与当前终端之
间进行数据转发,显示新slave pty的名字(/dev/pts/N),新slave pty没有进程
与之关联。假设正用gdb调试某进程,新slave pty可做为"set inferior-tty"的
参数,这比被调试进程直接使用当前终端要好。
(gdb) help set inferior-tty
Set terminal for future runs of program being debugged.
Usage: set inferior-tty [TTY]
If TTY is omitted, the default behavior of using the same terminal as
GDB is restored.
如果指定了COMMAND、ARGS,将做为reptyr的子进程运行,其进程空间环境变量
REPTYR_PTY指向新slave pty。-L相比-l,前者会将子进程的0、1、2号fd指向
新slave pty,子进程会在一个新session中运行,其CTTY对应新slave pty。
-s
缺省情况下,reptyr只会让目标进程中确实与CTTY相关联的fd指向新终端。指定
-s后,reptyr死活将目标进程中的0、1、2号fd指向新终端,即使目标进程本来
没有CTTY。
一般情况下用不着-s,用reptyr时,目标进程很大可能是交互式进程。
-v
显示版本
-h
显示帮助
-V
输出冗余调试信息
NOTES
reptyr使用ptrace(2)调试目标进程。在Ubuntu Maverick及更高版本上,出于安
全考虑缺省禁止这种行为。可以临时解禁:
# echo 0 > /proc/sys/kernel/yama/ptrace_scope
也可以编辑/etc/sysctl.d/10-ptrace.conf永久解禁。
BUGS
如果目标进程的屏幕未能重绘,按Ctrl-L
假设目标进程对stdin使用epoll(),reptyr并未更新epoll()所用数据,epoll()
仍将访问原来的stdin。
--------------------------------------------------------------------------
6.2) reptyr原理简介
reptyr用ptrace(2)调试目标进程(vi),利用一些Hacking技术在vi进程空间里执行由
reptyr提供的代码,比如打开新的伪终端,利用dup(2)使之变成vi进程的stdout、
stderr。
相比screenify,reptyr更改了vi进程的控制终端(CTTY),于是支持对目标进程
Ctrl-C、Ctrl-Z。
同一session中的所有进程共用同一个CTTY。
参看ioctl_tty(2)
TIOCSCTTY int arg
修改主调进程的CTTY。主调进程必须是session leader,同时不能已经拥有CTTY。
此时这样调用:
ioctl( fd, TIOCSCTTY, 0 )
如果fd已经是某个session的CTTY,ioctl()失败(EPERM),除非主调进程拥有
CAP_SYS_ADMIN权限且arg等于1,此时会抢夺CTTY,原session中所有进程将失去
fd对应的CTTY。此时这样调用:
ioctl( fd, TIOCSCTTY, 1 )
从bash中启动vi,bash是session leader,vi是process group leader,该进程组只
包含vi进程。为了在vi中调用ioctl(TIOCSCTTY),须设法让vi成为session leader。
参看setsid(2)
EPERM
主调进程PID等于某个PGID,即主调进程是process group leader时,setsid()
失败。
vi现在是process group leader,无法调用setsid(2)。可以fork(),子进程仍在同一
session、同一进程组,但不是process group leader,该子进程可以setsid()。但
fork()后杀掉父进程的做法有潜在风险,谁知道vi有没有依赖PID的行为。看有无其
他办法更改vi的PGID,使得vi不再是process group leader。
参看setpgid(2)
setpgid( pid, pgid );
bash处理管道符时会用setpgid()将指定进程移入指定进程组。这个操作要求
pgid与pid位于同一session(参看setsid(2)、credentials(7))。
需要在vi所在session中找一个进程组,把vi移入该进程组,使得vi可以调用
setsid()。bash似乎是个候选者,但我们采用更直接的办法,创建一个新进程组。在
vi进程空间中fork(2),同时用ptrace(2)调试子进程。让子进程调用setpgid()创建
新进程组,将父进程移入该新进程组,父进程中的vi可以调用setsid()创建新
session,父进程成为session leader,父进程调用ioctl(TIOCSCTTY)指定新的CTTY。
injcode、neercs、reptyr使用同样的技术更改目标进程的CTTY。
6.3) "reptyr -T"原理简介
"reptyr -T"使用了新技术,劫持目标进程关联的master pty。
不使用-T时,reptyr更改单个目标进程的slave pty。使用-T时,reptyr尝试寻找目
标进程对应的terminal emulator,用ptrace(2)调试后者(而不是目标进程),寻找
master pty fd,利用AF_UNIX、SCM_RIGHTS将master pty fd传递到reptyr进程。
reptyr在terminal emulator进程空间中更改master pty fd,使之指向/dev/null,
最后从terminal emulator detach。
接着reptyr扮演terminal emulator的角色,从前述master pty fd读取output并写到
当前终端,从当前终端读取input并写到前述master pty fd。
假设terminal emulator是sshd(8)的子进程,sshd会调用setuid(2)以匹配登录帐号。
Linux禁止ptrace(2)调试这种调用过setuid(2)的进程,除非以root身份执行reptyr。
☆ 伪终端
单说解决原始问题,不需要深入了解伪终端内部细节,如果充满好奇心,可以继续阅
读如下四个链接:
A Brief Introduction to termios - [2009-12-22]
https://blog.nelhage.com/2009/12/a-brief-introduction-to-termios/
A Brief Introduction to termios: termios(3) and stty - [2009-12-30]
https://blog.nelhage.com/2009/12/a-brief-introduction-to-termios-termios3-and-stty/
A Brief Introduction to termios: Signaling and Job Control - [2010-01-11]
https://blog.nelhage.com/2010/01/a-brief-introduction-to-termios-signaling-and-job-control/
The TTY demystified - [2008-07-25]
http://www.linusakesson.net/programming/tty/index.php
(介绍TTY的历史及架构变迁,比如PTY如何出现)
简译了对我本人有用的部分内容,可能有错误。
1) pty(7)/tty(4)
-----------input-------------->
+--------+ +--------+ +-------+ +--------+ +--------+
|terminal|=| master |=|termios|=| slave |=|shell or|
|emulator| | pty | | | | pty | |other(s)|
+--------+ +--------+ +-------+ +--------+ +--------+
<----------output--------------
xterm /dev/ptmx /dev/pts/N bash
sshd
screen
tmux
gnome-terminal
termios负责:
Line buffering
行缓冲
Echo
回显
Line editing
退格删除
Newline translation
\n转\r\n
Signal generation
Ctrl-C SIGINT
Ctrl-Z SIGTSTP
右侧允许多个进程连接同一个slave pty,不一定是bash。
2) termios(3)
emacs、vi之类的程序使用了curses。可以通过struct termios调整termios的行为。
tcflag_t c_iflag; /* input modes */
tcflag_t c_oflag; /* output modes */
tcflag_t c_cflag; /* control modes */
tcflag_t c_lflag; /* local modes */
cc_t c_cc[NCCS]; /* control chars */
local modes
ICANON
canonical mode就是line editing mode,与之对应的是cbreak mode(raw mode)
ECHO
回显
ISIG
若未设置,Ctrl-C、Ctrl-Z不会产生信号,而是向右侧传递相应ASCII码
TOSTOP
参后
input and output modes
IXON in c_iflag
是否允许流控。缺省启用,master pty收到Ctrl-S后,slave pty不再从右
侧接收任何输出,向slave pty的write()操作将阻塞,直到master pty收到
Ctrl-Q,恢复正常。
IUTF8 in c_iflag
IUTF8告诉termios输入流是UTF-8编码过的,处理退格删除时以单个UTF-8字
符为单位进行删除。
OLCUC in c_oflag
Map展开 lowercase characters to uppercase on output
control chars
下列c_cc[i]为0时表示禁用
VINTR
c_cc[VINTR] = 0x3;
Ctrl-C产生SIGINT,要求ISIG置位
VSUSP
Ctrl-Z产生SIGTSTP,要求ISIG置位
VERASE
Ctrl-H或Ctrl-?退格删除
VEOF
Ctrl-D causes the next read call by the slave to return end-of-file.
VSTOP
Ctrl-S,要求IXON置位
VSTART
Ctrl-Q,要求IXON置位
第一次接触这些东西是1998年,当时进行curses编程,与西门子某设备进行串口通信。
3) stty(1)
stty封装了对tcgetattr()、tcsetattr()的调用。
stty -a
以人类可读方式显示struct termios
stty -isig
复位ISIG,此时无法Ctrl-C中止进程。
stty intr ^G
c_cc[VINTR] = 0x7;
Ctlr-G产生SIGINT
stty -ixon stop undef
复位IXON
c_cc[VSTOP] = 0;
stty -a -F /dev/pts/N
查看指定伪终端
bash有自己的termios设置,从bash中启动其他进程时,bash会将控制终端的termios
设置恢复回去。所以在bash中直接stty与从其他终端stty -F看到的不一致。
4) ioctl(2)/ioctl_tty(2)
tcgetattr( fd, p )
ioctl( fd, TCGETS, p )
tcsetattr( fd, p )
ioctl( fd, TCSETS, p )
5) Sessions and Process Groups
session leader (SID==PID) setsid(2)
process group leader (PGID==PID) setpgid(2)
process
process
...
process group leader
process
process
...
...
[scz@ /tmp]> cat /dev/urandom > /dev/null
# cat /proc/$(pidof -s cat)/stat
7559 (cat) R 5487 7559 5487 ...
前6项是
pid (name) state ppid pgid sid
# pstree -npu -al 5487
bash,5487,scz
└─cat,7559 /dev/urandom
# ps -o pid,args,state,ppid,pgid,sid $(pidof -s cat)
PID COMMAND S PPID PGID SID
7559 cat /dev/urandom R 5487 7559 5487
每个session有一个控制终端(CTTY)。单个进程可以打开多个终端,但只有CTTY可以
进行任务控制(job control),比如Ctrl-Z。一个终端最多只能成为一个session的
CTTY。某进程调用setsid(2)创建新session的同时,会失去原有的CTTY。若某进程没
有CTTY,当它不带O_NOCTTY标志打开某终端时,该终端自动成为其CTTY。
每个CTTY只有一个前台进程组,同一session中的其他进程属于后台进程组。
CTTY产生的控制信号不是发往单个进程,而是发往前台进程组(中的所有进程)。
前台进程组可以任意读写CTTY,可以对CTTY调用tcsetattr()。
后台进程组中的进程试图读CTTY时,该后台进程组将收到SIGTTIN。后台进程组中的
进程可以写CTTY,除非c_lflag中TOSTOP置位,此时该后台进程组将收到SIGTTOU。后
台进程组中的进程对CTTY调用tcsetattr()时,该后台进程组将收到SIGTTOU。
session中的进程可以调用tcsetpgrp()设置前台进程组,所受限制同前述tcsetattr()。
关于SIGHUP,参看:
《24.3 如何编写daemon程序》
一般来说,有两种典型的与SIGHUP信号相关的情形。
假设某session有控制终端,当session leader终止时,系统会向该session前台进程
组中所有进程及后台进程组中处于"停止"状态的每个进程分发SIGHUP信号。
如果某进程组中有一个进程,其父进程属于同一会话(session)的另一个进程组,则
该进程组不是"孤儿进程组",反之该进程组称为"孤儿进程组"。
APUE 9.10指出,当某进程的终止导致一个新的"孤儿进程组"产生,系统会向这个新
的"孤儿进程组"中处于"停止"状态的每个进程分发SIGHUP信号,然后分发SIGCONT信
号。那些未处于"停止"状态的进程不会收到这两个信号。
6) Job control
任务就是进程组的别称。bash有个内部命令jobs,"help jobs"了解细节。
假设在bash中执行"foo | bar | grep baz",bash会调用setpgid(),把这三个进程
置于同一进程组,接着调用tcsetpgrp()使之成为前台进程组,最后调用waitpid()。
此时Ctrl-C会杀死所有三个进程。按下Ctrl-Z,这三个进程都会被挂起,bash对
waitpid()的调用将返回,bash恢复自己成前台进程组,这三个进程所在进程组变成
后台进程组。
在bash中使用"bg %n"命令时,bash调用killpg(2)向指定后台进程组发送SIGCONT。
后台进程组试图读CTTY时,会收到SIGTTIN,bash的wait*()会监控到后台进程组的状
态变化。
在bash中使用"fg %n"命令时,bash调用tcsetpgrp()将指定进程组变成前台进程组。
7) 信号与任务控制示例
假设你正在用emacs编辑大文件,光标位于屏幕中部某处,此时emacs正对该文件进行
搜索、替换操作。按下Ctrl-Z,emacs所在前台进程组收到SIGTSTP。
emacs的SIGTSTP信号句柄得到执行,通过向CTTY写入相应控制序列移动光标至屏幕最
后一行。接着emacs向自己所在前台进程组发送SIGSTOP。
emacs现在被挂起,session leader收到SIGCHLD,知道emacs状态发生变化。当前台
进程组中所有进程被挂起后,session leader保存当前termios设置,以备将来恢复
用。session leader调用tcsetpgrp()将自身所在进程组设置成前台进程组,输出形
如"[1]+ Stopped"的信息,通知用户有任务被挂起。
ps(1)可以看到emacs处在停止状态(T)。可以用bash内置命令bg使emacs继续执行,也
可以用kill(1)向emacs发送SIGCONT,emacs的SIGCONT信号句柄得到执行,这将试图
重绘emacs的GUI。但是,emacs现在位于后台进程组,写CTTY导致emacs收到SIGTTOU,
emacs再次停止运行,session leader再次收到SIGCHLD,再次输出"[1]+ Stopped"。
用bash内置命令fg,bash恢复之前保存的termios设置,调用tcsetpgrp()将emacs所
在进程组设置成前台进程组,向前台进程组发送SIGCONT。emacs的SIGCONT信号句柄
得到执行,重绘emacs的GUI。
8) setsid(1)
这是个外部命令
$ dpkg -S $(which setsid)
util-linux: /usr/bin/setsid
--------------------------------------------------------------------------
NAME
setsid - 在新session中运行指定程序
SYNOPSIS
setsid [options] program [arguments]
DESCRIPTION
setsid在一个新session中执行指定程序。
如果setsid本身已经是process group leader,会调用fork(2)创建子进程,在
子进程中调用setsid(2),否则直接调用setsid(2);最后调用exec*()执行指定
程序。如果使用--fork参数,则总是创建子进程。考虑setsid不从bash启动,
而是由其他进程或脚本启动。
OPTIONS
-c, --ctty
将指定程序的CTTY设置成当前终端
一个终端最多只能成为一个session的CTTY,ioctl(TIOCSCTTY)会抢夺CTTY。
-f, --fork
总是创建新(子)进程
-w, --wait
等待指定程序执行结束,其退出码做为setsid命令的退出码
-V, --version
显示版本
-h, --help
显示帮助
SEE ALSO
setsid(2)
--------------------------------------------------------------------------
$ strace -f -ff -o /tmp/setsid.log setsid cat /tmp/some.txt
scz@nsfocus
$ ls -l /tmp/setsid.log*
-rw-r--r-- 1 scz scz 5755 Feb 19 11:21 /tmp/setsid.log.8526
$ vi /tmp/setsid.log.8526
...
getpgrp() = 8523
getpid() = 8526
setsid() = 8526
execve("/bin/cat", ["cat", "/tmp/some.txt"], 0xbfa0c2b4 /* 20 vars */) = 0
...
strace的情形下setsid已经是在fork(2)后的子进程中运行,此时setsid不是process
group leader,可以直接调用setsid(2)。
$ rm /tmp/setsid.log*
$ strace -f -ff -o /tmp/setsid.log setsid -f cat /tmp/some.txt
$ ls -l /tmp/setsid.log*
-rw-r--r-- 1 scz scz 2654 Feb 19 11:32 /tmp/setsid.log.8549
-rw-r--r-- 1 scz scz 3274 Feb 19 11:32 /tmp/setsid.log.8550
$ vi /tmp/setsid.log.8549
...
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xb7f0a168) = 8550
...
$ vi /tmp/setsid.log.8550
setsid() = 8550
execve("/bin/cat", ["cat", "/tmp/some.txt"], 0xbfb9f428 /* 20 vars */) = 0
...
"setsid -f"时死活fork(),现在libc中的fork(3)由clone(2)实现,尽管fork(2)仍
然可用。setsid.log.8549中没有getpgrp()、getpid(),因为-f参数不需要检查
setsid是否是process group leader。
9) jobs/disown/nohup
$ help jobs
jobs: jobs [-lnprs] [jobspec ...] or jobs -x command [args]
Display status of jobs.
Lists the active jobs. JOBSPEC restricts output to that job.
Without options, the status of all active jobs is displayed.
Options:
-l lists process IDs in addition to the normal information
-n lists only processes that have changed status since the last
notification
-p lists process IDs only
-r restrict output to running jobs
-s restrict output to stopped jobs
If -x is supplied, COMMAND is run after all job specifications that
appear in ARGS have been replaced with the process ID of that job's
process group leader.
Exit Status:
Returns success unless an invalid option is given or an error occurs.
If -x is used, returns the exit status of COMMAND.
$ help disown
disown: disown [-h] [-ar] [jobspec ... | pid ...]
Remove jobs from current shell.
Removes each JOBSPEC argument from the table of active jobs. Without
any JOBSPECs, the shell uses its notion of the current job.
Options:
-a remove all jobs if JOBSPEC is not supplied
-h mark each JOBSPEC so that SIGHUP is not sent to the job if the
shell receives a SIGHUP
-r remove only running jobs
Exit Status:
Returns success unless an invalid option or JOBSPEC is given.
$ type disown
disown is a shell builtin
bash(1)关于SIGHUP的内容不清晰,下面加以补充。
交互式(无论登录、非登录)bash收到SIGHUP会退出,在其结束前会向所有前后台任务
(无论状态)发SIGHUP。假设此时有停止状态的后台任务,交互式bash可能还会向其发
送SIGCONT,但我从未观察到过。
交互式(无论登录、非登录)bash主动exit时,不会向运行状态的后台任务发送任何信
号,会向停止状态的后台任务发送SIGTERM,不是SIGHUP。
$ shopt -s huponexit
$ shopt | grep hup
huponexit on
假设huponexit被启用(缺省是off),交互式登录bash在退出前(无论是收到SIGHUP还
是主动exit)会向所有前后台任务发送SIGHUP,包括运行状态的后台任务。huponexit
只影响交互式登录bash,不影响交互式非登录bash。比如SSH登录后的bash受
huponexit影响,在登录bash中新启动的bash不受huponexit影响。
这些结论可以用strace确认。
"disown %n"是bash内置命令,其作用是将后台任务从bash的任务列表中移除,之后
无法对被移除的后台任务使用fg、bg命令,同时阻止交互式bash收到SIGHUP之后向被
移除任务发送SIGHUP。
"disown -h"只对付SIGHUP,不从任务列表中移除指定任务,此时可以fg、bg。
disown不会影响PID、PPID、PGID、SID,也不剥离CTTY。
若某停止状态的后台任务事先被disown过,收到SIGHUP的交互式bash确实不会向之发
送SIGHUP,但是kernel会向之发送SIGHUP,因为此时有新的"孤儿进程组"产生。这意
味着disown过的停止状态的后台任务仍将被杀,strace可能看到:
--- stopped by SIGTSTP ---
--- SIGHUP {si_signo=SIGHUP, si_code=SI_KERNEL} ---
+++ killed by SIGHUP +++
对运行状态的任务(无论前后台)使用disown才稍有意义,disown其实十分鸡肋。
参看:
Difference between nohup, disown and & - [2010-11-09]
https://unix.stackexchange.com/questions/3886/difference-between-nohup-disown-and
对比&、nohup、disown的使用,不过有些内容术语混乱、表述不严谨,请自行修正。
nohup启动进程之前重定向stdout、对SIGHUP使用SIG_IGN(被启动进程可能更改这种
设置)。nohup并不影响session、CTTY。
&只是将进程丢入bash后台运行,不影响stdin、stdout、stderr、CTTY。
☆ 结束语
跑oclHashcat-plus时没事先丢到screen session里,这个后悔药怎么吃?
原始问题在过去经常碰上,始终没有深究过救急方案,只是从原理上知道动用llkm、
tty hijacking技术可能可能解决,因为以前搞过tty hijacking。但涉及内核态编程,
稳定性、可移植性不高。
最近有同事找过来问这个事,当时想当然地以为没有成熟工具,谁知道在用户态有
screenify、reptyr这两种成熟工具,尤其后者进了Debian发行版。
20年前搞过curses编程,当时仅限于利用各种API实现功能,没有从更深处理解伪终
端机制,这次一并学习了一点点。本文没有原创技术点,汇总从2004年至今在互联网
上能找到的关于此问题的绝大多数有价值的讨论。本文价值在于将这些零散分布的有
用信息归档一处。建议直接阅读英文原文,我是反复过了好几遍。
那些第一次听说screenify、reptyr的人,你们会感谢我的
from:http://scz.617.cn:8/unix/201902151750.txt
- 我的微信
- 这是我的微信扫一扫
- 我的微信公众号
- 我的微信公众号扫一扫