code review的时候看新来的同事重复造了个轮子,使用open read write函数实现了与windows平台对应的函数CopyFile。不过他造的轮子有缺陷,没有处理信号EINTR。这篇文章通过EINTR为引子,来总结linux下与之相关的知识点,本篇文章将继续秉承想到哪写到哪的原则,由于平时大都写一些固定套路代码,如果在本文中有任何错误,欢迎指出。
从信号说起这篇文章中重点介绍了进程状态T(停止 traced or stoped),本篇开头先讲下进程状态D。

进程状态D

man ps手册中关于进程状态的描述:

PROCESS STATE CODES
       Here are the different values that the s, stat and state output specifiers (header "STAT" or "S") will display to
       describe the state of a process:

           D    uninterruptible sleep (usually IO)
           R    running or runnable (on run queue)
           S    interruptible sleep (waiting for an event to complete)
           T    stopped by job control signal(旧版本将t归类到T中)
           t    stopped by debugger during the tracing
           W    paging (not valid since the 2.6.xx kernel)
           X    dead (should never be seen)
           Z    defunct ("zombie") process, terminated but not reaped by its parent

与进程状态相关联的还有一些表示优先级等相关的符号:

< high-priority (not nice to other users)
N low-priority (nice to other users)
L has pages locked into memory (for real-time and custom IO)
s is a session leader
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
+ is in the foreground process group 

之前的一篇文章我们重点说了状态T(t),这篇文章我们从状态D开始,分析在日常开发中常用的小技巧。
TASK_INTERRUPTIBLE(S):进程处于睡眠状态,正在等待某些事件发生。进程可以被信号中断。接收到信号或被显式的唤醒呼叫唤醒之后,进程将转变为 TASK_RUNNING 状态。
TASK_UNINTERRUPTIBLE(D):此进程状态类似于 TASK_INTERRUPTIBLE,只是它不会处理信号。中断处于这种状态的进程是不合适的,因为它可能正在完成某些重要的任务。 当它所等待的事件发生时,进程将被显式的唤醒呼叫唤醒。
我曾经以为kill -9天下无敌,配合root用户权限可以大杀四方,17年还在做存储相关开发时,有一天对一个进程状态D+的进程执行kill操作,“神奇”的一幕出现了,本应被杀掉的进程居然还在。
进程状态TASK_UNINTERRUPTIBLE用于某些不能被打断的流程或者处理信号非常困难的情形,比如在设备I/O期间。进程状态TASK_UNINTERRUPTIBLE一般无法被杀死,目前唯一有效的方法是重启系统。
2.6.25内核版本引入了一种新的进程睡眠状态:TASK_KILLABLE。当进程处于这种可以终止的新睡眠状态中,它的运行原理类似于 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号。
在内核代码中搜索(/usr/src/linux-headers-4.15.0-60/include/linux/)可以看到宏中的注释部分说明TASK_KILLABLE状态可以处理信号。

/**
 * wait_event_killable - sleep until a condition gets true
 * @wq_head: the waitqueue to wait on
 * @condition: a C expression for the event to wait for
 *
 * The process is put to sleep (TASK_KILLABLE) until the
 * @condition evaluates to true or a signal is received.
 * The @condition is checked each time the waitqueue @wq_head is woken up.
 *
 * wake_up() has to be called after changing any variable that could
 * change the result of the wait condition.
 *
 * The function will return -ERESTARTSYS if it was interrupted by a
 * signal and 0 if @condition evaluated to true.
 */
#define wait_event_killable(wq_head, condition)                                 \
({                                                                              \
        int __ret = 0;                                                          \
        might_sleep();                                                          \
        if (!(condition))                                                       \
                __ret = __wait_event_killable(wq_head, condition);              \
        __ret;                                                                  \
})

目前我所了解到的能够方便构造进程状态D的方法是使用vfork。

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    int n =10;
    pid_t pid = vfork(); //creating the child process
    if (pid == 0)          //if this is a chile process
    {
        printf("Child process started\n");
    sleep(30);
    _exit(0);
    }
    else//parent process execution
    {
        printf("Now i am coming back to parent process\n");
      wait(NULL);
    }
    
    printf("value of n: %d \n",n); //sample printing to check "n" value
   //观察打印之后父进程的状态
   sleep(30)
    return 0;
}

编译执行:

[root c++]#gcc -o vfork vfork.c
[root c++]#
[root c++]#./vfork
Child process started
Now i am coming back to parent process
value of n: 10

在重新打开一个终端观察进程状态:

[root c++]#ps -aux | grep vfork
root     24003  0.0  0.0   4508   716 pts/0    D+   13:10   0:00 ./vfork
root     24004  0.0  0.0   4508   716 pts/0    S+   13:10   0:00 ./vfork
root     24240  0.0  0.0  21536  1048 pts/1    S+   13:11   0:00 grep --color=auto vfork

可以看在子进程(24004)执行期间,父进程(24003)状态是D.可以参考fork和vfork的区别对比图(来源于学习apue时记录的笔记)

vfork和fork不同点之一便是fork后父进程和子进程同时运行,而vfork后,父进程处于挂起状态直到子进程执行结束。这一点可以通过上面的程序加以验证。

EINTR信号

早期UNIX系统的一个特性是:如果进程在执行一个低速系统调用而阻塞期间捕获到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR。这样处理是因为一个信号发生了,进程捕获到它,这意味着已经发生了某种事情,所以是个好机会应当唤醒阻塞的系统调用。
当捕获到某个信号时,被中断的是内核中执行的系统调用。
与被中断的系统调用相关的问题是必须显示地处理出错返回。典型的代码序列(假定进行一个读操作,它被中断,我们希望重新启动它):

again:
    if(n = read(fd,buf,BUFFSIZE)) < 0) {
        if(errno == EINTR)
             goto again; /*just an interrupted system call*/
         /*handle other errors*/ 
    }

低速系统调用是可能会使进程永远阻塞的一类系统调用,APUE 10.5章节里介绍了哪些行为可以归类到低速系统调用。

信号捕获

/* Example of using sigaction() to setup a signal handler with 3 arguments
 * including siginfo_t.
 */
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

#define TMP_BUF_SIZE 1024

/* 获取进程命令行参数 */
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';
}

static void hdl (int sig, siginfo_t *siginfo, void *context)
{
    char buf_des[TMP_BUF_SIZE] = {0};
    get_cmd_by_pid(siginfo->si_pid,buf_des);
    printf ("Sending PID: %ld, UID: %ld,cmdline:%s\n",
           (long)siginfo->si_pid, (long)siginfo->si_uid,buf_des);
}

int main (int argc, char *argv[])
{
    struct sigaction act;
    memset (&act, '\0', sizeof(act));

    /* Use the sa_sigaction field because the handles has two additional parameters */
    act.sa_sigaction = &hdl;

    /* The SA_SIGINFO flag tells sigaction() to use the sa_sigaction field, not sa_handler. */
    act.sa_flags = SA_SIGINFO;

    if (sigaction(SIGTERM, &act, NULL) < 0) {
        perror ("sigaction");
    return 1;
    }

    while (1)
    sleep (10);

    return 0;
}

编译运行(./a.out)后在重新开一个终端执行如下操作:

[root c++]#ps -ef | grep a.out
root     10583 11644  0 00:00 pts/1    00:00:00 ./a.out
root     10626  2828  0 00:01 pts/0    00:00:00 grep --color=auto a.out
[root c++]#
[root c++]#kill 10583
#下面这个操作是看到另一个终端窗口的输出后执行的
[root c++]#ps -ef | grep 2828
root      2828  2708  0 Apr01 pts/0    00:00:00 -bash
root     11131  2828  0 00:04 pts/0    00:00:00 ps -ef
root     11132  2828  0 00:04 pts/0    00:00:00 grep --color=auto 2828

我们切换到运行a.out的终端可以可以看到如下信息:

[root c++]#gcc sigaction.c
[root c++]#./a.out
Sending PID: 11644, UID: 0,cmdline:-bash

这个例子和网上烂大街例子不同的是sa_flags指定选项SA_SIGINFO,信号捕获函数可以使用3个参数的版本_sa_sigaction,默认是一个参数的版本_sa_handler。
这种写法有个好处,能够知道进程是被谁杀死的,巧合的是昨天还用这个方法定位了进程被莫名其妙杀死的问题。

struct sigaction {
  union {
    //addr of signal handler or SIG_IGN, or SIG_DFL 
    void (*_sa_handler)(int signo); 
    //alternate(替代的) handler
    void (*_sa_sigaction)(int signo, struct siginfo *info, void *context);
  } _u;
  sigset_t sa_mask; //additional signals to block
  unsigned long sa_flags; //signal options
  void (*sa_restorer)(void); 
};

这里需要注意的是SIGTERM程序结束信号,shell命令kill缺省发送这个信号。可以通过kill -l命令查看这个信号对应的值,同样可以使用kill -15 pid向进程发送信号。SIGTERM和SIGKILL两个信号都可以停止进程,对进程来说SIGKILL信号无法忽略和捕获,就是传说中的强杀。和SIGKILL一样顽固的还有SIGSTOP信号,可以使用如下程序验证SIGSTOP和SIGKILL无法忽略和捕获。
验证SIGSTOP和SIGKILL信号无法忽略:

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

int main()
{
    signal(SIGINT,SIG_IGN);
    signal(SIGSTOP,SIG_IGN);
    signal(SIGKILL,SIG_IGN);

    sleep(100);
    return 0;
}

执行过程:编译运行上面的代码,我们使用ctrl+c发现无法停止程序运行,因为程序忽略了信号SIGINT,但是我们使用ctrl+z(SIGSTOP)发现程序被停止了,然后使用SIGCONT恢复程序运行(仍然在后台运行,可以使用fg恢复到前台)。接着使用kill -9强杀进程成功。验证了SIGSTOP和SIGKILL信号无法忽略。

[root c++]#./a.out
^C^C^C
重新打开一个终端,在另一个终端中执行 kill -19 $(pidof a.out)
[1]+  Stopped                 ./a.out
[root c++]#
[root c++]#ps -aux | grep a.out
root      8275  0.0  0.0   4376   716 pts/0    T    11:25   0:00 ./a.out
root      8310  0.0  0.0  21536  1052 pts/0    S+   11:25   0:00 grep --color=auto a.out
[root c++]#
[root c++]#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
[root c++]#
[root c++]#kill -18 8275
[root c++]#
[root c++]#ps -aux | grep a.out
root      8275  0.0  0.0   4376   716 pts/0    S    11:25   0:00 ./a.out
root      8424  0.0  0.0  21536  1020 pts/0    S+   11:26   0:00 grep --color=auto a.out
[root c++]#
[root c++]#
[root c++]#kill -9 8275
[1]+  Killed                  ./a.out
[root c++]#
[root c++]#ps -aux | grep a.out
root      8465  0.0  0.0  21536  1076 pts/0    S+   11:26   0:00 grep --color=auto a.out

验证信号SIGSTOP和SIGKILL无法捕获的代码如下:

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

void sig_handler(int signo)
{
    if (signo == SIGUSR1)
        printf("received SIGUSR1\n");
    else if (signo == SIGKILL)
        printf("received SIGKILL\n");
    else if (signo == SIGSTOP)
        printf("received SIGSTOP\n");
}

int main(void)
{
    if (signal(SIGUSR1, sig_handler) == SIG_ERR)
        printf("\ncan't catch SIGUSR1\n");
    if (signal(SIGKILL, sig_handler) == SIG_ERR)
        printf("\ncan't catch SIGKILL\n");
    if (signal(SIGSTOP, sig_handler) == SIG_ERR)
        printf("\ncan't catch SIGSTOP\n");
    // A long long wait so that we can easily issue a signal to this process
    while(1) 
        sleep(1);
    return 0;
}

运行程序:

[root c++]#./a.out

can't catch SIGKILL

can't catch SIGSTOP
重新打开一个终端输入kill -USR1 $(pidof a.out)
received SIGUSR1

我们可以看到这里如果捕获SIGKILL和SIGSTOP将会报错,如果使用sigaction也会有相同的错误,可以验证,SIGKILL和SIGSTOP无法捕获。
参考文章:
Linux 信号(三)—— sigaction 函数
TASK_KILLABLE:Linux 中的新进程状态
killing linux process in status D
Linux Signals – Example C Program to Catch Signals (SIGINT, SIGKILL, SIGSTOP, etc.)
Learn and use fork(), vfork(), wait() and exec() system calls across Linux Systems