5

linux之信号操作(九千字长文详解)

 8 months ago
source link: https://blog.51cto.com/u_15835985/9195041
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

linux之信号操作

[TOC]

sigset_t

这是信号在内核中的表示

linux之信号操作(九千字长文详解)_位图

block和pending都是位图——即用bit位来表示信号编号!

每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态(在软件层面上就是将bit为从0置为1,或者从1置为0,硬件上就是从充放电来表示)

在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。

同一种位图我们可以用不同的解释来改变它的含义

==阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),==这里的“屏蔽”应该理解为阻塞而不是忽略。

信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释(如用printf直接打印sigset_t变量是没有意义的)

虽然本质都是位图,但是每个操作系统的实现方式是不一样的!不要自己使用逻辑与或非去进行判断,操作

#include <signal.h>
int sigemptyset (sigset_t *set);
int sigfillset (sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset (sigset_t *set, int signo);
int sigismember (const sigset_t *set, int signo); 

参数set就是——信号集,参数signo——就是那个信号(那个比特位)

函数sigemptyset——初始化set所指向的信号集,使其中所有信号的对应bit都置为0。表示该信号集不包含任何有效信号。

函数sigfillset——初始化set所指向的信号集,使其中所有信号的对应bit置为1,表示该信号集的有效信号包括系统支持的所有信号。

注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

函数sigaddset——将一个特定的信号添加进这个信号集里面!

函数sigdelset——将特定的信号从信号集里面删除

函数sigismember——判断该信号存不存在信号集里面

前4个函数都是成功返回0,出错返回-1。

sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask

这个函数的作用就是用来更该进程的block表!——那个进程调用就修改那个进程!

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 

返回值:若成功则为0,若出错则为-1

how参数——表示如何修改这个block表,有三个不同的选项,SIG_BLOCK,SIG_UNBLOCK,SIG_SETMASK

BIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号(新增信号屏蔽字),相当于mask=mask|set
BIG_UNBLOCK st包含了我们希望从当前信号屏蔽字中解除阻塞的信号(删除信号屏蔽字),相当于mask=mask&~set
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值(重置当前的信号屏蔽字重置为set),相当于mask=set

如何理解三个选项呢?——就像有个老师布置作业,说布置800字的作文

老师在在800字作文的基础上,继续增加作业,例如多三道数学题——这就是SIG_BLOCK(增加)

老师将800字作文和三道数学题,变成800字作文和一道数学题——这既是BIG_UNBLOCK(减少)

老师收到通知将800字作文和三道数学题,变成了一个物理实验报告!重新布置作业,其他就不用做了——这就是SIG_SETMASK(即重置)

set参数——这个参数和how参数强相关!

假如我们想要对该进程的所有信号进程屏蔽!

那么我们就可以传入一个全1的set信号集!——使用sigfilset函数设置!

然后选择SIG_SETMASK操作(重置操作),就会将我们传进的set的位图结构,设置进进程的block位图里面!

oset参数——是个输出参数!

如果我们想要对信号屏蔽字做恢复呢?——所以我们就得将老的信号屏蔽字保存起来!

表示,当我们对信号屏蔽字做修改的时候,老的信号屏蔽字就会被返回!

sigpengding

 #include <signal.h>
int sigpending(sigset_t *set);

读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

我们接下来可与用上面的函数做一些实验!

我们知道,一般情况:我们所有的信号都是不被阻塞的!如果一个信号被阻塞,那么该信号就不会被抵达!如果收到了为阻塞的信号,那么就会被存入pending位图里面!

#include<iostream>
#include <signal.h>
#include<string>
#include<vector>
#include<unistd.h>
// #define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31

static std::vector<int> sigv = {2};//想要屏蔽那些信号往这个数组加就可以了!
static void show_pending(const sigset_t& pending)
{
       std::string show;
       for(int i = MAX_SIGNUM;i>=1;i--)
       {
           if(sigismember(&pending,i) == 1)
               show +='1';
           else 
               show +='0';
       }
       std::cout << show << std::endl;
}
int main()
{
       //首先尝试屏蔽2号信号!
       sigset_t set,oset,pending;
       //1.1初始化!
       sigemptyset(&set);//全部设为0
       sigemptyset(&oset);
       sigemptyset(&pending);
       //1.2添加要屏蔽的信号
       for(auto e:sigv)
       {
           sigaddset(&set, e); // 在set信号集将e号信号色设置为1
       }
       //前面这一堆动作是没有影响该进程信号屏蔽字的!——只是在用户层构建一个信号集!
       //1.3屏蔽信号!
       sigprocmask(SIG_BLOCK,&set,&oset);//将信号集设置进内核!

       //遍历打印所有的pending信号集!
       while(true)
       {
           //2.1获取
           sigpending(&pending);//获取该进程的pending表
           //2.2打印pending表!
           show_pending(pending);
           sleep(1);
       }
       return 0;
}
linux之信号操作(九千字长文详解)_位图_02

我们发现当我们使用ctrl+c发送2号信号后,2号信号就被阻塞!然后pending表里面的2更好信号位置就变了1!

9号信号是绝不能被屏蔽,绝不能被捕抓!

如果我们想要接触信号屏蔽!

#include<iostream>
#include <signal.h>
#include<string>
#include<vector>
#include<unistd.h>
#define MAX_SIGNUM 31

static std::vector<int> sigv = {2};
static void show_pending(const sigset_t& pending)
{
    std::string show;
    for(int i = MAX_SIGNUM;i>=1;i--)
    {
        if(sigismember(&pending,i) == 1)
            show +='1';
        else 
            show +='0';
    }
    std::cout << show << std::endl;
}
int main()
{
    sigset_t set,oset,pending;

    sigemptyset(&set);//全部设为0
    sigemptyset(&oset);
    sigemptyset(&pending);

    for(auto sig:sigv)
    {
        sigaddset(&set, sig);
    }

    sigprocmask(SIG_BLOCK,&set,&oset);

    int cnt = 5;
    while(true)
    {
        //2.1获取
        sigpending(&pending);//获取该进程的pending表
        //2.2打印pending表!
        show_pending(pending);
        sleep(1);
        //cnt秒之后接触屏蔽!
        if(cnt-- == 0)
        {
            std::cout << "Restore all signals" << std::endl;
              
            sigprocmask(SIG_SETMASK,&oset,&set);
            //sigprocmask(SIG_UNBLOCK,&set,&oset);//这样子也可以!
            
            //这里无法打印!
            std::cout << "Restore all signals" << std::endl;
        }
    }
    return 0;
}
linux之信号操作(九千字长文详解)_f5_03

我们会发现5s之后信号确实被解除了阻塞,直接让进程被终止了!但是为什么没有打印两个语句出来呢??

一旦对特定信号接触屏蔽,那么操作系统至少要抵达一个信号!

那么从内核态返回用户态之后就把进程终止了,压根不会回到进程的里面继续执行后续代码,所以无法打印!!!

如果想要信号在被接触阻塞之后能打印!——我们可以对信号进行捕抓!

#include<iostream>
#include <signal.h>
#include<string>
#include<vector>
#include<unistd.h>
#define MAX_SIGNUM 31

static void myhandler(int signo)
{
    std::cout << "The signal has been arrested" << std::endl;
    std::cout << "Restore all signals" << std::endl;
}
int main()
{
    for(auto sig:sigv)//信号捕抓!
    {
        signal(sig,myhandler);
    }
    //....
    return 0;
}
linux之信号操作(九千字长文详解)_#include_04

sigaction

#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

和signal一样这也是一个信号捕抓函数!功能也是一样的!即对特定信号设定特定的回调方法!——但是sigaction有更详细的选项设置

首先我们就要认识一下struct sigaction这个结构体

//The sigaction structure is defined as something like:
struct sigaction {
    void     (*sa_handler)(int);//设置普通信号的回调函数
    void     (*sa_sigaction)(int, siginfo_t *, void *);//设置实时信号的回调函数
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

这个函数可以用来处理实时信号和普通信号!——不过我们这里只关心普通信号!

我们主要设置该结构体的sa_handler这个成员变量!,sa_sigaction一般设置为nullptr,sa_flags也设置为0即可,sa_restorer也是和实时信号相关(一般也不使用)——设置为nullptr即可(一般sa_handler和sasa_sigaction是最好不要同时同时使用!)

我们后面主要讨论的是sa_mask这个成员变量!

参数signum——就是信号编号!

参数act——是一个输入信参数,对我们当前进程的信号捕抓逻辑进行影响或者修改!

信号oldact——是一个输出型参数!获取特定信号老的处理方法!

返回值——成功返回0,不成功返回-1

我们可以试用一下

#include<signal.h>
#include<iostream>
#include<unistd.h>
using namespace std;

void handler(int signo)
{
    cout << "get a signo :" << signo << endl;
}
int main()
{
    struct sigaction act,oact;
    act.sa_flags = 0;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT,&act,&oact);

    while(true)
    {
        sleep(1);
    }
    return 0;
}
linux之信号操作(九千字长文详解)_f5_05

我们可以看到和我们使用signal函数其实是一样的!那么sigaction函数和signal函数的区别究竟是什么?

#include<signal.h>
#include<iostream>
#include<unistd.h>
#include<cstdio>
using namespace std;
void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt: %d\r",cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n") ;
}
void handler(int signo)
{
    cout << "get a signo :" << signo << "is running"<< endl;
    Count(20);//我们使用Count函数来模拟handler处理时间长的场景!
}
int main()
{
    struct sigaction act,oact;
    act.sa_flags = 0;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT,&act,&oact);

    while(true)
    {
        sleep(1);
    }
    return 0;
}

**当handler函数比较废时间的时候,如果多次收到相同的信号会发生什么呢?**会不会在handler函数的内部递归式的调用handler呢?——也就是说进程在运行的时候可能会收到大量的同类型的信号!如果收到同类型的信号,当前又正在处理某一个信号时,会发生什么呢?操作系统会不会允许我们进行那么多的信号提交呢?

linux之信号操作(九千字长文详解)_f5_06

我们可以发现在第一次收到信号信后,在信号处理期间,信号都不会再抵达!后面无论是收到了多少个信号都只会处理一个信号!

当我们正在抵达某一个信号的期间!同类型信号无法被抵达!——因为当前信号被捕抓,系统会自动将当前信号加入到信号的信号屏蔽字(block表里面!)!而当信号完成捕抓动作!系统又会自动解除该信号的屏蔽!

如果我们该handler里面尝试的屏蔽正在抵达的信号!例如:2号信号!

那么就会出现虽然我们屏蔽了!——但是屏蔽完过后又被操作系统给自动的解除了!

还能捕抓一次是因为,在首次收到信号的时候,pending位图会被由1置0,然后后续收到信号的时候,就会再次将0置为1!——但是因为只有一个位图所以只能改一次!

一般一个信号被解除屏蔽的时候,会自动进行抵达!(至少要抵达一次!)如果这个信号已经在pending表里面了!没有就不进行任何动作!

处理信号信号的原则是串行的处理同类信号!不允许递归的进行处理!(只有处理完一个,才能处理下一个!)

上面我们说过,信号处理的时候是不能去屏蔽同信号(因为会被自动的接触!)那么如果我们想要在处理本信号的时候,顺便屏蔽其他类型的信号呢?——可以的,我们可以添加进sa_mask里面,这样就是这个成员变量的作用!

#include<signal.h>
#include<iostream>
#include<unistd.h>
#include<cstdio>
using namespace std;
void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt:%d",cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}
void handler(int signo)
{
    cout << "get a signo :" << signo << "is running"<< endl;
    Count(20);
}
int main()
{
    struct sigaction act,oact;
    act.sa_flags = 0;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);    
    //我们直接向
    sigaddset(&act.sa_mask,3);//将三号信号设置进mask里面!
    //这样子在除了屏蔽2号信号本身,还会将3号信号给屏蔽!
    sigaction(SIGINT,&act,&oact);

    while(true)
    {
        sleep(1);
    }
    return 0;
}
linux之信号操作(九千字长文详解)_#include_07

在处理2号信号的时候,2,3号信号就都被屏蔽了!

linux之信号操作(九千字长文详解)_位图_08

只有处理完2号信号,操作系统才会将2号信号和3号信号都解除屏蔽!——此时3号信号就可以抵达了!这样子进程就被终止了!

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。

如果 在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字

可重入函数

假设有一个链表,我们要对其进行头插!

linux之信号操作(九千字长文详解)_#include_09

正常看起来是这样没错!

但是假如出现了一些情况!——插入的第一步的时候因为某些原因触发了信号!而信号handler是我们的自定义行为,里面一个也有插入会发生什么呢?

linux之信号操作(九千字长文详解)_位图_10

我们发现,虽然都执行成功了!——但是插入顺序却出现问题了!导致了我们内存节点丢失!

之所以会出现这个问题,是因为main执行流和信号捕抓执行流,两套不同的执行流,重复进入了同一个函数!导致了代码结果出现了未定义或者出错的情况!——我们将这种函数称之为不可重入函数!

一般我们认为,main执行流和信号捕抓执行流是两套不同的执行流!——虽然在代码里面我们来看也是串行了!main想要后续继续执行,也依旧要等待信号捕抓执行流执行完!但是我们可以看到main执行流其实压根没有直接调用果handler函数!是通过信号过来后才回调过去的!所以我们才说是两套执行流!

如果在main中或者handler中该函数被重复进入后出现了问题——那就是不可重入函数!(不能重复进入)

如果重复进入后没有出现问题!——那么就是可重入函数!

我们平时用的大部分接口都是不可重入函数!——函数可不可被重入是一个特性!不是一个问题!我们不需要解决

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的
  • 调用了标准I/O库函数。标准标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile

这是一个C语言的的关键字!——不过我们一般很少用!

这个关键字的作用是保持内存的可见性!——这个说法听起来很抽象!

我们下面的例子来说明这个

#include <stdio.h>
#include<signal.h>
int quit = 0;

void handler(int signo)
{
    printf("%d 号信号,正在被捕抓\n",signo);
    printf("quit : %d",quit);
    quit = 1;
    printf("-> %d\n", quit);
}
int main()
{
    signal(2,handler);    
    while(!quit);
    printf("注意!该进程是正常退出的!\n");
    return 0;
}

linux之信号操作(九千字长文详解)_#include_11

代码运行的结果不出我们所料!

这里我们要说一点就是关于编译器的优化!当我们编译代码的时候,编译器都会进行优化,一般是O1或者O2,编译器的优化都是取决于编译器

我们可以自己手动的将优化级别调高

当我们手动的提升编译器的优化级别!

linux之信号操作(九千字长文详解)_位图_12

为什么会出现这个现象呢?

数据保存的无法就两个地方——一个是内存,一个是寄存器!那么优化其实也就是将数据从内存优化到寄存器里面!(因为寄存器比内存更快!)

linux之信号操作(九千字长文详解)_#include_13

所以这就是为什么明明quit被改了!但是循环却依旧不进行终止的原因!因为寄存器中quit的存在,遮蔽了物理内存中quit变量存在的事实!

在while循环只看到了寄存器!而看不到内存!

这种情况就会导致,我们代码没有问题!但是因为编译器的优化从而让代码没有按照预期来进行工作!

为了解决这个问题!我们就要用到volatile关键字!

在C/C++的里面,它的作用官方说法就是保持内存可见性!——用简单的说法就是,让这个数据不要优化到寄存器里面!而是一直从内存里面进行读取!(这就是所谓的保持内存可见性)

#include <stdio.h>
#include<signal.h>
volatile int quit = 0;

void handler(int signo)
{
    printf("%d 号信号,正在被捕抓\n",signo);
    printf("quit : %d",quit);
    quit = 1;
    printf("-> %d\n", quit);
}
int main()
{
    signal(2,handler);    
    while(!quit);
    printf("注意!该进程是正常退出的!\n");
    return 0;
}
linux之信号操作(九千字长文详解)_f5_14

我们依旧进行O3级别的优化!但是此时quit就不会被放进寄存器里面了!代码逻辑也就正常了!

如果出现了信号捕抓执行流和main函数执行流里面有个要修改的值的时候!这时候我们就要注意了!

SIGCHLD

这个信号是和进程等待有关系

如果一个子进程退出了!——那么这个子进程就会变成僵尸状态,然后让父进程读取它,获取子进程的退出码和退出信号!

如果一个子进程没有退出——那么父进程就要阻塞或者非阻塞的等待子进程!

当子进程退出的时候,它不会直接就退出,然后进入僵尸状态,而是它在进入僵尸状态的时候!它会告诉父进程它的状态!

那么它是怎么告诉父进程的呢?——通过发送信号的方式!发送SIGCHLD信号!(17号信号)

linux之信号操作(九千字长文详解)_#include_15

我们可以看到SIGCHLD的行为是Ign(ignore the signal)忽略该信号!这个忽略是内核级别的忽略

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
void Count(int cnt)
{
while(cnt)
{
printf("cnt:%2d\r",cnt);
fflush(stdout);
cnt--;
sleep(1);
}
printf("\n");
}
void handler(int signo)
{
printf("pid: %d , %d 号信号,正在被捕抓\n",getpid(),signo);
}
int main()
{
signal(SIGCHLD,handler);
printf("我是父进程!pid : %d,ppid: %d\n", getpid(), getppid());

pid_t id = fork();
while(id == 0)
{
printf("我是子进程!我要退出了!pid : %d,ppid: %d\n",getpid(),getppid());
Count(5);
exit(1);
} 
while(1) sleep(1);

return 0;
}
linux之信号操作(九千字长文详解)_f5_16

用上面断点我们可以看出来!——子进程退出之后!父进程确实会捕抓子进程发出的信号!

那么如果知道这一点的话有什么用处呢?

如果不知道这一点,我们想要知道子进程退出什么时候退出!我们只能主动的去调用waitpid和wait这样的函数!——无论是阻塞等待还是非阻塞等待

但是现在我们知道了!我们其实可以不用去主动关心子进程!等子进程退出了!那么它就会自己给父进程发送信号!这时候父进程再去回收子进程!

void handler(int signo)
{
//我们是可以在handler里面直接调用wait或者waitpid的!
}

但是这样子代码的健壮性不好!

情况1:假如我们有很多的子进程!(例如:10个)在同一时刻同时退出

那么就会向父进程同时发送10个SIGCHLD信号!但是上面我们讲过当正在处理一个信号的时候,操作系统会自动屏蔽同类型的信号!而且因为pending表只有一份!后续的9个信号也只能被保存一份!——所以在handler里面直接调用wait或者waitpid是不好的!

所以最好的方式是通过循环等待!——因为我们并不知道有几个进程退出了!所以只能while(1) --> waipid式的等待

waitpid的第一个参数,我们一般都是设置为指定进程的pid!但是也可以设置为-1!即等待任意子进程的进程!

那么这个死循环什么终止呢?——waitpid再也等待不到的时候!说明底层的子进程都全部退出完毕了!

情况2:我们有很多的子进程(10个),只有一部分退出了!

这种情况下我们也是进行循环式的等待,假如有5个退出了,当waitpid已经回收了这5个的时候!第六个要不要进程waitpid呢?——要的!因为进程压根不知道!到底退出了多少个子进程!我们说5个退出了是我们站在上帝视角来看待这件事情的!进程是不知道到底退出了多少个!所以即使将5个子进程都回收!也还是要进行waitpid!

如果此时是阻塞式等待!那么就出现问题了!——在handler方法里面出现了阻塞式调用!那么就无法返回主进程了!——所以在循环式等待的时候不能进行阻塞式等待!要进行非阻塞式等待!——非阻塞式等待时,如果没有等待成功waitpid是会返回0的!

void handler(int signo)
{
    pid_t id;
    while(1)
    {
        id = waitpid(-1,NULL,WNOHANG);
        if(id <= 0)//出错或者等待失败
            break;
    }
}

这就是通过信号来完成进程等待!

上面我们都是在说要么在main执行流要么在信号捕抓执行流里面调用wait’/waitpid来回收子进程,我们也可通过不调用wait/waitpid的方式来回收子进程!

要想不产生僵尸进程还有另外一种办法==:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN(忽略)==

这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的

但这是一个特例此方法对于Linux可用,但不保证 在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt:%2d\r",cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}
int main()
{
    signal(SIGCHLD,SIG_IGN);//显示的设置!对SIGCHLD进行忽略
    printf("我是父进程!pid : %d,ppid: %d\n", getpid(), getppid());

    pid_t id = fork();
    while(id == 0)
    {
        printf("我是子进程!我要退出了!pid : %d,ppid: %d\n",getpid(),getppid());
        Count(3);
        exit(1);
    } 
    while(1) sleep(1);

    return 0;
}
linux之信号操作(九千字长文详解)_#include_17

但是为什么要我们手动去显示将SIGCHLD设置为SIG_IGN?——我们上面看到过、

linux之信号操作(九千字长文详解)_#include_18

该信号的默认行为不就是IGN吗?——**默认的IGN和我们手动设置出来的是不一样的!**默认IGN行为就是我们看到的,如果没有被父进程回收那么就进入僵尸!

但是我们的自己设置的IGN就是不进入僵尸,也不等待,直接回收!

这两个值在操作系统内部肯定是不一样的!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK