以前的笔记里写过linux信号相关的使用,当时是读apue TLPI等书籍时记录的关于信号的一些编程知识。最近又改了一些shell脚本的bug,本篇笔记权当个人总结,主要从修改过的典型bug串起各知识点,文章延续之前想到哪写到哪的风格,如果内容有误,欢迎留言指出,不胜感激。

进程状态

D 不可中断 uninterruptible sleep (usually IO) 
R 运行 runnable (on run queue) 
S 中断 sleeping 
T(t) 停止 traced or stopped 
Z 僵死 a defunct ("zombie") process 

重点介绍下状态为T的进程,可以使用如下例子
ping www.baidu.com > output.txt 2>&1 &
然后再开一个窗口,tail -f output.txt
然后通过kill -STOP/-CONT 进程号,观察tail的输出,验证进程暂停和恢复。
实际演示例子:

[root c++]#ping www.baidu.com > output.txt 2>&1 &
[1] 22566

我们tailf output.txt
在新开一个窗口使用如下命令暂停和恢复进程

kill -STOP 22566
kill -CONT 22566

同样也可以使用如下数字形式:

[root ~]#kill -19 22566
[root ~]#kill -18 22566

这里所有的信号都可以通过命令kill -l获得:

[root singal]#kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

具体信号的含义参考Unix信号
使用如下命令查看进程状态:
ps -aux

root     22566  0.0  0.1  36132  2972 pts/0    S    00:22   0:00 ping www.baidu.com  运行状态
root     22566  0.0  0.1  36132  2972 pts/0    T    00:22   0:00 ping www.baidu.com   暂停状态

进程状态为T的另一种形式:traced,这里我所知道的一种情况是使用gdb挂进来的情况。
例如如下代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    while(1)
    {
        int a = 10;
    int b = 20;
    int c = a + b;

        sleep(1);
    }

    return 9;
}

编译运行:

gcc -g -o trace trace.c
./trace

在另一个终端里执行如下命令:

[root c++]#ps -ef | grep trace
root     19835 22529  0 23:44 pts/2    00:00:00 ./trace
root     19881  2756  0 23:45 pts/0    00:00:00 grep --color=auto trace
[root c++]#
[root c++]#gdb -p 19835

我们再开一个终端查看trace进程状态:

[root c++]#ps -aux | grep trace | grep -v grep
root     19835  0.0  0.0   4376   788 pts/2    t+   23:44   0:00 ./trace

这里也可以验证一点,如果你再开一个窗口想挂进这个进程里看发生了什么,会有如下报错提醒:

[root c++]#ps -ef | grep trace
root     19835 22529  0 23:44 pts/2    00:00:00 ./trace
root     21887 22399  0 23:56 pts/1    00:00:00 grep --color=auto trace
[root c++]#gdb -p 19835
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
Attaching to process 19835
Could not attach to process.  If your uid matches the uid of the target
process, check the setting of /proc/sys/kernel/yama/ptrace_scope, or try
again as the root user.  For more details, see /etc/sysctl.d/10-ptrace.conf
warning: process 19835 is already traced by process 20286
ptrace: Operation not permitted.
(gdb)

这里有个关键的告警信息:

warning: process 19835 is already traced by process 20286

讲到这里忽然想到之前有同事做了一件非常有趣的事情,为了检测到程序崩溃,直接调试现场环境,修改代码,使得如果崩溃直接挂到gdb,结果由于不知道在哪台机器连接的linux,导致又不能重复挂进去,这个时候除了看日志,没有别的有效的手段,pstack也不能显示程序在执行什么,最终的解决方法只能30 40台跳板机中一个一个去查看。

[root c++]#pstack 19835
Could not attach to target 19835: Operation not permitted.
detach: No such process
查看进程状态

ps -aux查看所有进程状态
ps -axjf显示进程树
ps -aux | egrep '(cron|syslog)'显示与cron和syslog相关的信息
关于PID PPID PGID SID 有几篇非常不错的文章参考:PID, PPID, PGID与SID
credentials - process identifiers

[root c++]#ps -axjf | grep sshd
    1  1530  1530  1530 ?           -1 Ss       0   0:02 /usr/sbin/sshd -D
 1530  2294  2294  2294 ?           -1 Ss       0   0:02  \_ sshd: root@pts/0
 1530 22293 22293 22293 ?           -1 Ss       0   0:00  \_ sshd: root@pts/1
22399  2865  2864 22399 pts/1     2864 S+       0   0:00  |       \_ grep --color=auto sshd
 1530 22454 22454 22454 ?           -1 Ss       0   0:00  \_ sshd: root@pts/2
[root c++]#
[root c++]#
[root c++]#ps -ef | grep sshd
root      1530     1  0 Mar16 ?        00:00:02 /usr/sbin/sshd -D
root      2294  1530  0 Mar16 ?        00:00:02 sshd: root@pts/0
root      2885 22399  0 11:07 pts/1    00:00:00 grep --color=auto sshd
root     22293  1530  0 Mar20 ?        00:00:00 sshd: root@pts/1
root     22454  1530  0 Mar20 ?        00:00:00 sshd: root@pts/2
[root c++]#
[root c++]#ps -aux | grep sshd
root      1530  0.0  0.2  72300  5584 ?        Ss   Mar16   0:02 /usr/sbin/sshd -D
root      2294  0.0  0.3 110080  7028 ?        Ss   Mar16   0:02 sshd: root@pts/0
root      2917  0.0  0.0  21536  1116 pts/1    S+   11:07   0:00 grep --color=auto sshd
root     22293  0.0  0.3 110080  7176 ?        Ss   Mar20   0:00 sshd: root@pts/1
root     22454  0.0  0.3 110080  7076 ?        Ss   Mar20   0:00 sshd: root@pts/2

[root c++]#ps -aux | egrep '(cron|syslog)'
root       656  0.0  0.1  38428  3020 ?        Ss   Mar16   0:01 /usr/sbin/cron -f
message+   664  0.3  0.2  50840  4988 ?        Ss   Mar16  27:01 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
syslog     748  0.0  0.2 263040  4272 ?        Ssl  Mar16   0:03 /usr/sbin/rsyslogd -n
root      3965  0.0  0.0  21536  1152 pts/1    S+   11:13   0:00 grep -E --color=auto (cron|syslog)

此外我个人较少使用的方式是:
指定要显示的字段
ps -eo user,stat,cmd
这里除了user stat还有如下可以指定的字段

user          用户名 
uid           用户号 
pid           进程号 
ppid          父进程号 
size          内存大小, Kbytes字节. 
vsize         总虚拟内存大小, bytes字节(包含code+data+stack) 
share         总共享页数 
nice          进程优先级(缺省为0, 最大为-20) 
priority(pri) 内核调度优先级 
pmem          进程分享的物理内存数的百分比 
trs           程序执行代码驻留大小 
rss           进程使用的总物理内存数, Kbytes字节 
time          进程执行起到现在总的CPU暂用时间 
stat          进程状态 
cmd(args)     执行命令的简单格式 

查看当前系统进程的uid,pid,stat,pri, 以uid号排序.

[root c++]#ps -eo pid,stat,pri,uid --sort uid
  PID STAT PRI   UID
    1 Ss    19     0
    2 S     19     0
    4 I<    39     0
    6 I<    39     0
    7 S     19     0
    8 I     19     0
    9 I     19     0
   10 S    139     0
   11 S    139     0
   12 S     19     0
   13 S     19     0
   14 S    139     0
   15 S    139     0
   16 S     19     0
   18 I<    39     0
   19 S     19     0
   20 I<    39     0
   21 S     19     0
   22 S     19     0
   25 S     19     0
   26 S     19     0
   27 I<    39     0
   28 S     19     0
   29 SN    14     0
   30 SN     0     0
   31 I<    39     0

查看当前系统进程的user,pid,stat,rss,args, 以rss排序.

[root c++]#ps -eo user,pid,stat,rss,args --sort rss
USER       PID STAT   RSS COMMAND
root         2 S        0 [kthreadd]
root         4 I<       0 [kworker/0:0H]
root         6 I<       0 [mm_percpu_wq]
root         7 S        0 [ksoftirqd/0]
root         8 I        0 [rcu_sched]
root         9 I        0 [rcu_bh]
root        10 S        0 [migration/0]
root        11 S        0 [watchdog/0]
root        12 S        0 [cpuhp/0]

linux下还有一种查看进程信息的方法,就是通过虚拟文件系统/proc,top命令等应该也是从/proc中获取的数据。例如我们验证通过ps看到的进程状态和从/proc中看到的状态是否一致:

[root c++]#ps -aux | grep trace
root      9406  0.0  0.0  21536  1100 pts/1    S+   11:45   0:00 grep --color=auto trace
root     19835  0.0  0.0   4376   788 pts/2    t+   Mar20   0:00 ./trace
[root c++]#
[root c++]#cat /proc/19835/status
Name:   trace
Umask:  0022
State:  t (tracing stop)
Tgid:   19835
Ngid:   0
Pid:    19835
PPid:   22529
TracerPid:  20286
Uid:    0   0   0   0
Gid:    0   0   0   0

在编程中也经常使用/proc/pid/cmdline,下面是一个简单的例子:

/* 获取进程命令行参数 */
void get_cmd_by_pid(pid_t pid, char *cmd)
{
    char buf[TMP_BUF_SIZE];
    int i = 0;

    snprintf(buf, TMP_BUF_SIZE, "/proc/%d/cmdline", pid);
    FILE* fp = fopen(buf, "r");
    if(fp == NULL)
    {
        return;
    }
    //敏感信息使用完内存中清空 
    memset(buf, 0, TMP_BUF_SIZE);
    size_t ret = fread(cmd, 1, TMP_BUF_SIZE - 1, fp);
    
    /*
    *需要下面for循环的原因是
    *man手册资料
    *This  holds  the  complete command line for the process, unless the process is a zombie.  
    *In the latter case,there is nothing in this file: that is, a read on this file will return 0 
    *characters.  The command-line arguments appear in this file as a set of strings separated by 
    *null bytes ('\0'), with a further null byte after the last string.
    */

    for (i = 0; ret != 0 && i < ret - 1; i++)
    {
        if (cmd[i] == '\0')
        {
            cmd[i] = ' ';
        }
    }

    fclose(fp);
    cmd[TMP_BUF_SIZE - 1] = '\0';
}
程序后台运行

nohup是一个POSIX命令,用于忽略SIGHUP(挂断信号),SIGHUP是终端注销时所发送至程序的一个信号。nohup在默认情况下(没有使用重定向时)会输出一个名叫nohup.out的文件到终端上。
下例中,nohup用于忽略SIGHUP信号,&使命令于后台执行,因此终端退出后命令仍旧执行。

nohup 命令名 &

值得注意,这种方法防止命令在注销时被忽略SIGHUP信号,但,如果该命令对标准I/O文件(stdin,stdout,或stderr)进行输入/输出,那么该命令仍旧可能被终端挂起。[1] 详情请看下文的 阻止挂起 。 另外,nohup 命令常常和nice命令一起执行,以调整命令/程序的优先级。

nohup nice 命令 &

验证例子如下:

#正如上面讲的,如果nohup命令没有处理标准输入 输出 错误,可能会导致终端挂起时命令挂起。
[root ~]#nohup ping www.baidu.com </dev/null >nohup.out 2>nohup.err &
[1] 30378
[root ~]#

iTerm2中打开新的tab标签,查看进程信息如下:

[root ~]#ps -ef | grep 30378
root     30378 28012  0 16:12 pts/1    00:00:00 ping www.baidu.com
root     30689 28012  0 16:14 pts/1    00:00:00 grep --color=auto 30378

我们关闭第一个终端,再继续查看ping进程依旧存在

[root ~]#ps -ef | grep 30378
root     30378 28012  0 16:12 pts/1    00:00:00 ping www.baidu.com
root     30689 28012  0 16:14 pts/1    00:00:00 grep --color=auto 30378

详细解释nohup: ignoring input - what does it mean?

io重定向

上面nohup中的例子中有重定向标准输入输出错误,这里不展开讲,只讲shell中另一种常见的形式。
I/O Redirection

n<&-
Close input file descriptor n.

0<&-, <&-
Close stdin.

n>&-
Close output file descriptor n.

1>&-, >&-
Close stdout.

由于子进程继承父进程打开的文件描述符,因此在脚本中经常使用上面的方法关闭文件描述符。
如果我们使用cat显示文件时把标准输出关闭会报错:

[root ~]#cat /etc/passwd >&-
cat: standard output: Bad file descriptor
[root ~]#

使用lsof可以查看文件描述符打开情况,一篇非常好的介绍文章lsof command
一般的套路是与flock一起使用:
parent.sh

#!/bin/bash

set -e

#实现单例
LOCK_FILE=${0}.lock

[ -f $LOCK_FILE ] || touch $LOCK_FILE
exec 9<> $LOCK_FILE
flock -no 9 || exit 0

trap "rm -f $LOCK_FILE" EXIT

SERVICE_PATH=/root/shell/child.sh

$SERVICE_PATH 9>&- &

#查找打开文件的pid
output=`lsof -t $LOCK_FILE`

pid=$$

echo $pid > parent
echo $output >> parent

child.sh

#!/bin/bash

output=`lsof -t  parent.sh.lock`
pid=$$

echo $pid > child
echo $output >> child

运行./parent.sh之后,可以根据parent和child中的内容判定,child.sh中没有继承打开的文件描述符。
这里用到了一个有争议的命令set -e,作用是set -e之后出现的代码,一旦执行返回值不等于0脚本立刻退出。这里之前改过一个bug,在使用了set -e的脚本中新增了一个函数,这个函数可能返回0 1 2三个值,于是问题出现了,这个调用逻辑不是主流程必现调用,因此这个问题过了几个月才发现。