梦飞云服务器MFISP

会员中心

技术支持

登入
帮助中心

linux 信号

一、概念

信号是运载消息的工具,是消息的载体。在Linux世 界中,信号是进程间通信的方式之一,它提供了处理异步事件的一种方法。信号是软中断,一个进程收到某信号后可以执行一个信号处理函数(捕捉信号),与 CPU收到一个中断请求在原理上是一样的。进程收到信号后,也可以采用系统的默认动作,忽略信号、终止或停止进程。但是有两个信号是不能被忽略和捕 捉:SIGKILL和SIGSTOP。

早期的信号机制只是一个简单的事件通知,告知进程某事件发生了,让进程去执行相应的动作,后来信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

二、来源

         1、来自于硬件:键盘(如ctrl+C会产生一个终止信号SIGINT),硬件故障(除数为0,无效的内存访问等)。kill命令,若不带信号参数,就会发出终止信号SIGTERM(15)。

2、来自于软件:某软件条件发生时,如设置的闹钟时间超时会产生SIGALRM信号。

三、分类

         先说明两个概念:不可靠信号与可靠信号。

不可靠信号:进程可能对这些信号做出错误的反应以及信号可能丢失,同一种信号若在一段时间内发生多次,进程来不及处理,系统不支持信号排队,所以后来的信号会覆盖早先到达的信号。这是因为早期Unix系统中的信号机制比较简单和原始。

可靠信号:对信号的原始机制加以改进和扩充,由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。

在时间上,不可靠信号具有非实时性,可靠信号具有实时性,因此,不可靠信号又称为非实时信号,可靠信号又称为实时信号。

下面列出linux下的信号:在命令行输入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     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

 

前面是信号编号,后面是信号名。系统是这样定义这些信号的:

#define 信号名 信号编号

如:#define SIGINT 2

信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,SIGHUP — SIGSYS是不可靠信号。

四、信号集

信号集——能表示多个信号的数据类型。如果整型数据的一位表示一种信号,那么在32位机器上只能表示32种信号,现在信号的种类多出了32种,就需要一种数据类型来表示多个信号,那就是信号集。

信号集被定义为:

  typedef struct {

                     unsigned long sig[_NSIG_WORDS];

} sigset_t;

linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。下面是为信号集操作定义的相关函数:

#include <signal.h>

sigemptyset(sigset_t *set)初始化由set指定的信号集,信号集里面的所有信号被清空;

sigfillset(sigset_t *set)调用该函数后,set指向的信号集中将包含linux支持的64种信号;

sigaddset(sigset_t *set, int signum)在set指向的信号集中加入signum信号;

sigdelset(sigset_t *set, int signum)在set指向的信号集中删除signum信号;

sigismember(const sigset_t *set, int signum)判定信号signum是否在set指向的信号集中。

int sigaction( int sig, const struct sigaction *act,struct sigaction *oact )检查、修改和指定信号相关联的信号响应。

五、信号阻塞与信号未决

每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。下面是与信号阻塞相关的几个函数:

#include <signal.h>

int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));

int sigpending(sigset_t *set));

int sigsuspend(const sigset_t *mask));

sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:

         how                                                                                               说明

 SIG_BLOCK         该该进程新的信号屏蔽字是其当前信号屏蔽字和 set指向信号集的并集。 s e t包含了我们希望阻塞的附加信号

SIG_UNBLOCK       该该进程新的信号屏蔽字是其当前信号屏蔽字和 set所指向信号集的交集。 s e t包含了我们希望解除阻塞的信号。 

SIG_SETMASK                    该该进程新的信号屏蔽是 set指向的值

 

sigpending(sigset_t *set))获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。

sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。

         信号阻塞是一种动作,阻止信号被处理,不是阻止信号的产生。

         信号未决是信号所处的一种状态,即从信号产生到信号被处理这段时间内的状态。

有个概念与信号阻塞相关,即是进程的信号屏蔽字,它是当前阻塞而不能递送给该进程的信号集。

六、信号的生命周期

         信号的生命周期可以化分为下面几个阶段:

信号诞生—>信号在进程中注册―>信号在进程中注销―>信号处理函数执行完毕

         以上四个事件将信号的生命周期分为三个阶段,下面简述一下这四大事件。

1、信号"诞生"。信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时以及调用信号发送函数kill()或sigqueue()等)。

2、信号在目标进程中"注册";进程的task_struct结构中有关于本进程中未决信号的数据成员:

struct sigpending pending:

struct sigpending{

         struct sigqueue *head, **tail;

         sigset_t signal;

};


第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为"未决信号信息链")的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:

struct sigqueue{

         struct sigqueue *next;

         siginfo_t info;

}


信号在进程中注册指的就是信号值加入到进程的未决信号集中(sigpending结构的第二个成员sigset_t signal),并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的 存在,但还没来得及处理,或者该信号被进程阻塞。

注:
当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着 同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把 该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册);
当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一 个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构(一个非实时信号诞生后,(1)、如果发现相同的信号已经在目标结构中注册,则 不再注册,对于进程来说,相当于不知道本次信号发生,信号丢失;(2)、如果进程的未决信号中没有相同信号,则在进程中注册自己)。

3、信号在进程中的注销。在目标进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的 检查)。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程 未决信号集中删除对于实时与非实时信号是不同的。对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后, 应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用 sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完 毕)。否则,不应该在进程的未决信号集中删除该信号,只是删除相应的sigqueue结构(信号注销完毕)。
进程在执行信号相应处理函数之前,首先要把信号在进程中注销。

4、信号生命终止。进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。

注:
1)信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只 与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)。
2)在信号被注销到相应的信号处理函数执行完毕这段时间内,如果进程又收到同一信号多次,则对实时信号来说,每一次都会在进程中注册;而对于非实时信号来说,无论收到多少次信号,都会视为只收到一个信号,只在进程中注册一次。

七、信号的编程思路

大体思路是:安装信号,发送信号,处理信号。若我们对信号不采用系统默认动作,则要编写信号处理函数。

a、  安装信号:它的任务是告诉进程要捕捉哪个信号,然后对信号采取什么动作。

有两个函数来实现:signal(),sigaction().

●  signal() ,是以前的系统实现,主要针对前32种不可靠信号。

#include <signal.h>
原型:void (*signal(int signum, void (*handler))(int)))(int);
如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));
第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。
如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。

●  sigaction() ,它对signal()进行了改进,
#include <signal.h>
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,可以为除SIGKILL及 SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例 的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存 原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。

第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等。

 

b、  发送信号:主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

1、kill()
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int signo)

参数pid的值

信号的接收进程

pid>0

进程ID为pid的进程

pid=0

同一个进程组的进程

pid<0 pid!=-1

进程组ID为 -pid的所有进程

pid=-1

除发送进程自身外,所有进程ID大于1的进程

Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存 在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同 一个用户的进程发送信号)。

Kill()最常用于pid>0时的信号发送,调用成功返回 0; 否则,返回 -1。注:对于pid<0时的情况,对于哪些进程将接受信号,各种版本说法不一,其实很简单,参阅内核源码kernal/signal.c即可,上 表中的规则是参考red hat 7.2。

2、raise()
#include <signal.h>
int raise(int signo)
向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。

3、sigqueue()
#include <sys/types.h>
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval val)
调用成功返回 0;否则,返回 -1。

sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。

sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。

     typedef union sigval {

            int  sival_int;

            void *sival_ptr;

     }sigval_t;

 

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送 信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程 发送信号。

在调用sigqueue时,sigval_t指定的信息会拷贝到3参数信号处理函数(3参数信号处理函数指的是信号处理 函数由sigaction安装,并设定了sa_sigaction指针,稍后将阐述)的siginfo_t结构中,这样信号处理函数就可以处理这些信息 了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。

注:sigqueue()发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数; sigqueue()发送非实时信号时,仍然不支持排队,即在信号处理函数执行过程中到来的所有相同信号,都被合并为一个信号。

4、alarm()
#include <unistd.h>
unsigned int alarm(unsigned int seconds)
专门为SIGALRM信号而设,在指定的时间seconds秒后,将向进程本身发送SIGALRM信号,又称为闹钟时间。进程调用alarm后,任何以前的alarm()调用都将无效。如果参数seconds为零,那么进程内将不再包含任何闹钟时间。
返回值,如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。

5、setitimer()
#include <sys/time.h>
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));
setitimer()比alarm功能强大,支持3种类型的定时器:

ITIMER_REAL: 设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程;

ITIMER_VIRTUAL 设定程序执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程;

ITIMER_PROF 设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程;

Setitimer()第一个参数which指定定时器类型(上面三种之一);第二个参数是结构体itimerval。第三个参数可不做处理。

Setitimer()调用成功返回0,否则返回-1。

6、abort()
#include <stdlib.h>
void abort(void);

向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。

 

C、信号处理:若不想默认系统对信号的处理,可以编写自己的信号处理函数。

八、几个实例

1、信号屏蔽与信号集

 

  1. #include <stdio.h>  
  2. #include <signal.h>  
  3. static void    sig_quit(int);  
  4. int main(void)  
  5. {  
  6.       sigset_t  newmask, oldmask, pendmask;  
  7.       if (signal(SIGQUIT, sig_quit) == SIG_ERR)  
  8.              printf("can't catch SIGQUIT");  
  9.       //阻塞SIGQUIT信号  
  10.       sigemptyset(&newmask);  
  11.      sigaddset(&newmask, SIGQUIT);  
  12.      if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)  
  13.          printf("SIG_BLOCK error");  
  14.      sleep(5);       /* SIGQUIT阻塞,按ctrl+\进程不会退出 */  
  15.      if (sigpending(&pendmask) < 0)    //获取当前阻塞和未决的信号  
  16.           printf("sigpending error");  
  17.      if (sigismember(&pendmask, SIGQUIT))  
  18.         printf("\nSIGQUIT pending\n");  
  19.       //解除SIGQUIT信号的阻塞  
  20.       if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)  
  21.          printf("SIG_SETMASK error");  
  22.      if (!sigismember(&oldmask, SIGQUIT))  
  23.          printf("SIGQUIT unblocked\n");  
  24.      sleep(5);       /* SIGQUIT解除阻塞,按ctrl+\进程退出*/  
  25.      exit(0);  
  26.   
  27. }  
  1. static void sig_quit(int signo)  
  2. {  
  3.       printf("caught SIGQUIT\n");  
  4.       if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)  
  5.           printf("can't reset SIGQUIT");  
  6.   
  7. }  

 

/* 前5秒按ctrl+\,等打印出信息在后5秒内按ctrl+\

结果:

[lgh@localhost signals]$ ./a.out

                     (休息5秒,按下ctrl+\)

SIGQUIT pending

caught SIGQUIT

SIGQUIT unblocked

退出

*/

 

2、可靠信号与不可靠信号

         a、不可靠信号,它不支持信号排队,若阻塞了多个同一种信号,它只处理一次。

  1. #include <stdio.h>  
  2. #include <signal.h>  
  3. static void         sig_fun(int);  
  4. int main(void)  
  5. {  
  6.     if (signal(SIGUSR1, sig_fun) == SIG_ERR)  //SIGUSR1:10  
  7.         printf("can't catch SIGUSR1");  
  8.     sleep(60);  //捕捉到一个信号并从信号处理程序返回后sleep()会被终止  
  9.      printf("end main!\n");  
  10.     exit(0);  
  11. }  
  12.   
  13. static void sig_fun(int signo)  
  14. {  
  15.          printf("caught SIGUSR1 %d\n", signo);  
  16.          sleep(5);  //这里再加上,以便发送多个信号  
  17. }  


 

/*结果:

[lgh@localhost signals]$ ./a.out &

[1] 935

[lgh@localhost signals]$ kill -10 935

caught SIGUSR1 10

[lgh@localhost signals]$ kill -10 935

[lgh@localhost signals]$ kill -10 935

[lgh@localhost signals]$ kill -10 935

[lgh@localhost signals]$ caught SIGUSR1 10

end main!

*/

结果分析:进程在后台运行,在主函数休息的60秒内,发送用户自定义信号(编号为10),进程捕捉,打印caught SIGUSR1 10,然后休息5秒,在这5秒内发送用户自定义信号3次,最后进程对这3次信号只作一次处理,即打印一次caught SIGUSR1 10。

 

         b、可靠信号,它支持信号排队,若阻塞了几个同一种信号,它会处理几次。

  1. #include <stdio.h>  
  2. #include <signal.h>  
  3. static void         sig_fun(int);  
  4. int main(void)  
  5. {  
  6.          sigset_t   newmask, oldmask, pendmask;  
  7.          if (signal(SIGRTMIN+3, sig_fun) == SIG_ERR)  //SIGRTMIN+3:38  
  8.                   printf("can't catch SIGRTMIN+3");  
  9.          sleep(60);    //捕捉到一个信号并从信号处理程序返回后sleep()会被终止  
  10.            printf("end main!\n");  
  11.          exit(0);  
  12. }  
  13. static void sig_fun(int signo)  
  14. {  
  15.          printf("caught SIGRTMIN+3\n");  
  16.          sleep(5);    //这里再加上,以便发送多个信号  
  17. }  


 

/*结果:

[lgh@localhost signals]$ ./a.out &

[1] 814

[lgh@localhost signals]$ kill -38 814

caught SIGRTMIN+3

[lgh@localhost signals]$ kill -38 814

[lgh@localhost signals]$ kill -38 814

[lgh@localhost signals]$ kill -38 814

[lgh@localhost signals]$ caught SIGRTMIN+3

caught SIGRTMIN+3

caught SIGRTMIN+3

end main!

*/

结果分析:信号SIGRTMIN+3是SIGRTMIN以后的信号,它是可靠信号。

进程在后台运行,在主函数休息的60秒内,发送SIGRTMIN+3信号(编号为38),进程捕捉,打印caught SIGRTMIN+3,然后休息5秒,在这5秒内发送SIGRTMIN+3信号3次,最后进程对这3次信号只作三次处理,即打印三次caught SIGRTMIN+3。

九、注意事项

  1、信号处理函数不可以调用不可重入函数。满足下列条件的函数多数是不可再入的:(1)使用静态的数据结构,如 getlogin(),gmtime(),getgrgid(),getgrnam(),getpwuid()以及getpwnam()等等;(2)函数 实现时,调用了malloc()或者free()函数;(3)实现时使用了标准I/O函数的。另外,longjmp()以及siglongjmp()没有 被列为可再入函数,因为不能保证紧接着两个函数的其它调用是安全的。

The Open Group视下列函数为可再入的:

_exit()、access()、alarm()、cfgetispeed()、cfgetospeed()、 cfsetispeed()、cfsetospeed()、chdir()、chmod()、chown()、close()、creat()、 dup()、dup2()、execle()、execve()、fcntl()、fork()、fpathconf()、fstat()、 fsync()、getegid()、 geteuid()、getgid()、getgroups()、getpgrp()、getpid()、getppid()、getuid()、 kill()、link()、lseek()、mkdir()、mkfifo()、 open()、pathconf()、pause()、pipe()、raise()、read()、rename()、rmdir()、 setgid()、setpgid()、setsid()、setuid()、 sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、 sigismember()、signal()、sigpending()、sigprocmask()、sigsuspend()、sleep()、 stat()、sysconf()、tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、 tcsendbreak()、tcsetattr()、tcsetpgrp()、time()、times()、 umask()、uname()、unlink()、utime()、wait()、waitpid()、write()。

      2、进入处理函数时,首先要保存errno的值,结束时,再恢复原值。因为,信号处理过程中,errno值随时可能被改变。

 

 

PS:本文旨在对学习linux信号心路历程的记录,有些不错的内容来自于网络和《apue》第十章信号,如有雷同,纯 属学习引用,没有抄袭之恶意,不错的学习资料何不用之,我对信号的学习思路作了一个疏理,相信对初学者来说有不小的帮助。如有不足或错误,请指正,共勉! 至于本文内容,今后若发现不足,会即时更进。

本文最初发表于:http://dev.jizhiinfo.net/?post=41

  • 56 用户发现这很有用
这篇文章有帮助吗?

Related Articles

linux screen 命令详解

一、背景 系统管理员经常需要SSH 或者telent 远程登录到Linux 服务器,经常运行一些需要很长时间才能完成的任务,比如系统备份、ftp...

linux学习路线

很 多同学接触Linux不多,对Linux平台的开发更是一无所知。而现在的趋势越来越表明,作为一个优秀的软件开发人员,或计算机IT行业从业人员,掌握...

DirectAdmin内网安装

DirectAdmin内网安装最新版 ×××××××××××××××××××××环境配置×××××××××××××××××××××××××××××yum -y install wget...