`

Linux进程间通信(二)——信号signal【转】

 
阅读更多

参考文档:http://blog.csdn.net/mylxiaoyi/article/details/4258508


一、线程 
      Linux进程可以协作,可以发送消息,也可以中断另一个进程。他们甚至可以在彼此之间共享内存段,但是在操作系统内部他们却是完全不同的实体。他们并不能共享变量。
      在许多Unix系统与Linux系统还有另一类名为线程的进程。线程在某些程序中具有巨大的价值,例如多线程数据库服务器,然而线程很难进行编程。Linux上的线程编程并不如使用多进程那样常见,因为Linux的进程是轻量级的,而且多个协作进程的编程要比线程编程容易得多。我们会在第12章讨论线程。
 
二、信号 
      信号是由Unix与Linux系统响应某些条件,依据哪一个进程按顺序执行某些动作而产生的事件。我们使用术语"raise"来表示一个信号的生成,而使用术语"catch"来表示信号的接收。信号是由一些错误条件所产生的,例如内存段错误,浮点处理器错误,或是非法指令。
      他们是由shell或是终端处理器所产生来引起中断,也可以显示的由一个进程发住另一个进程作为传递信息或是修改行为的一种方法。在所有这些情况下,编程接口是相同的。信号可以产生,捕获,响应或是忽略。
      信号的名字是由所包含的头文件signal.h来定义的。他们以"SIG"开头,如下表所示:
 
信号名        描述
SIGABORT    处理失败
SIGALRM        报警时钟
SIGFPE        浮点异常
SIGHUP        挂起
SIGILL        非法指令
SIGINT        终端中断
SIGKILL        Kill(不能被捕获或是忽略)
SIGPIPE        写入没有读端的管道
SIGQUIT        终端退出
SIGSEGV        非法的内存段访问
SIGTERM        终止
SIGUSR1        用户定义的信号1
SIGUSR2        用户定义的信号2
 
      如果进程接收一个这样的信号,但是却并没有在第一时间捕获信号,进程就会立即终止。通常,会创建一个核心映像文件(core dump)。这个文件名为core,并且位于当前目录下,这是一个进程映像文件,并且在调试中是非常有用的
      其他的信号包含在下表中。
 
信号        描述
SIGCHLD        子进程已停止或退出
SIGCONT        如果停止,继续执行
SIGSTOP        停止执行(不能被捕获或是忽略)
SIGTSTP        终止停止信号
SIGTTIN        后台进程尝试读
SIGTTOU        后台进程尝试写
 
      SIGCHLD对于管理子进程十分有用。默认情况下这个信号会被忽略。其余的信号会使得接收到他们的进程停止,除了SIGCONT,这个信号会使得进程重新运行。他们被shell程序用于工作控制,而很少为其他的程序所使用。
 
      我们会在稍后详细的讨论第一组进程。就现在,我们只需要知道如果shell与终端驱动器被正常配置,在键盘上输入中断字符会产生SIGINT信号发送到前台进程,这会使得程序终止,除非已经设定动作来捕获这个信号。
      如果我们希望向一个进程而不是当前前端任务发送信号,我们使用kill命令。这个命令会以一个可选的信号标识号或是名字,以及要发住的PID作为参数。例如,如果向运行在另一个终端上的,PID为512的shell发送一个"hangup"信号,我们可以使用下面的命令:
      kill –HUP 512
 
      另一个有用的kill命令的变体就是killall命令,这会允许我们向过行一个指定命令的所有进程发送一个信号。但不是所有的Unix都会支持这个命令,而Linux通常都会支持。当我们不知道PID,或是当我们希望向执行相同命令的多个不同进程发送信号时是十分有用的。一个通常的用法就是通知inetd程序重新读取其配置选项。要这样做,我们可以使用下面的命令:
      killall –HUP inetd
 
      程序可以使用信号库函数来处理信号。
      #include <signal.h>
      void (*signal(int sig, void (*func)(int)))(int);
 
      这个复杂的声明一signal是一个需要两个参数的函数,sig与func。要捕获或是忽略的信号是由sig参数来指定的。当接收到指定的信号时要调用的函数是由func来指定的。这个函数必须是带有一个int作为参数(接收到的信号),并且其类型为void。信号函数本身返回一个相同类型的函数,这是函数设置的要处理的信号标识号,或是下面两个特殊值中的一个:
      SIG_IGN        忽略这个信号
      SIG_DFL        重新载入默认行为
      例子会使得我们的解释更为清晰。下面我们来编写一个程序,ctrlc.c,这会输出一条合适的信息而是结束来响应Ctrl+C。第二次按下Ctrl+C会结束这个程序。
 
三、试验--信号处理 
      函数ouch会响应作为参数sig传递的信号。这个函数会在信号发生时调用。他会输出一条信息,然而重新设置SIGINT的信号处理为默认行为。
 
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void ouch(int sig) {
    printf(“OUCH! - I got signal %d/n”, sig);
    (void) signal(SIGINT, SIG_DFL);
}
 
      main函数必须解释当我们输入Ctrl+C时所产生的SIGINT信号。在其余时间,他只是无限循环,每一秒输出一条信息。
 
int main() {
    (void) signal(SIGINT, ouch); 
    while(1) {
        printf(“Hello World!/n”);
        sleep(1);
    }
}
 
      第一次输入Ctrl+C会使得程序重新响应,然后继续执行。当我们再次输入Ctrl+C时,程序会结束,因为SIGINT的行为已经恢复为默认行为,从而使得程序退出。
$ ./ctrlc1
Hello World!
Hello World!
Hello World!
Hello World!
^C
OUCH! - I got signal 2
Hello World!
Hello World!
Hello World!
Hello World!
^C
$
      正如我们在这个例子中所看到的,信号处理函数带有一个整数参数,使得函数被调用的信号标识号。这对于使用相同的函数来处理多个信号的情况是十分用的。在这里我们输出SIGINT的值,在这个系统上这个值恰好为2。我们不要依赖于信号的传统值;在新的程序中总是使用信号名。
      在信号处理器的内部并不是调用所有的函数都是安全的,例如printf。一个有用的技巧就是使用信号处理器来设置一个标记,然后在主程序中检测这个标记并输出所需要的信息。在本章的结束处,我们会看到一个在信号处理器内部可以安全调用的函数列表。
 
工作原理 
      当我们通过Ctrl+C来传送SIGINT信号时,程序会调用ouch函数。在中断函数ouch结束之后,程序会继续执行,但是信号的行为已经恢复为默认行为。当他接收到第二个SIGINT信号时,程序会采用默认行为,这个行为会终止程序。
      如果我们希望重新获得信息处理器并且继续响应Ctrl+C,我们需要通过再次调用signal函数来重新建立。这会造成一小段信号没有被处理的时间,由中断函数开始到重新建立信号处理器。在这段时间也许会接收到第二个信号,而程序就会并非如我们所愿的终止。
      注:我们并不推荐使用signal接口。我们在这里介绍这个函数是我们会发现在许多老的程序中会用到这个函数。我们会在后面看到sigaction函数定义了一个更为清晰与可靠的接口,我们应在所有的新程序中使用这个接口。
      signal函数会返回一个用于特定信号的信号处理器的值,否则会返回SIG_ERR,在这种情况下,errno会被设置为一个负值。如果并没有指定一个正确的值或是尝试生成一个不会被捕获或是忽略的信号时,例如SIGKILL,errno会被设置为EINVAL。
 
发送信号 
      一个进程会通过调用kill向其他进程,包括其本身发送一个信号。如果程序并没有权限向另一个进程发送信号时,调用就会失败,这通常是因为目标进程是为另一个用户所拥有的。这与具有相同名字的shell命令等同。
 
      #include <sys/types.h>
      #include <signal.h>
      int kill(pid_t pid, int sig);
 
      kill函数会具有pid进程标识符的进程发送一个指定的信号sig
      如果成功,会返回0。要发送信号,发送进程必须具有相应的权限。通常,这意味着两个进程必须具有相同的用户ID(也就是说,我们只能向我们所拥有的进程发送信号,尽管超级用户可以任何进程发送信号)。
      如果kill失败则会返回-1并且设置errno。这通常是因为所指定的信号并不是一个可用的信号(errno设置为EINVAL),或是没有相应的权限(EPERM),或是指定的进程不存在(ESRCH)。
      信号为我们提供了一个非常有用的报警时钟程序。alarm函数可以为一个进程所调用来在将来的某个时间调度一个SIGALRM信号。
      #include <unistd.h>
      unsigned int alarm(unsigned int seconds);
 
      alarm函数会延时SIGALRM信号的传递seconds秒。事实上,报警会因为处理时延与调度的不确定性,会在稍后传输。值0会关闭所有的外部报警请求。在一个信号接收之间调用alarm会使得报警重新调度。每一个进程只能有一个外部警报。alarm函数会返回在外部警报可以发送之是所剩余的秒数,如果调用失败则会返回-1。
      要了解alarm是如何工作的,我们可以使用fork,sleep与signal来模拟其效果。一个程序可以在某段时间之后为发送信号的角色目的重新启动一个进程。
 
四、试验--警报时钟 
      在alarm.c中,第一个函数ding模拟一个警报时钟。
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h> 

/* TIPS:
        1. 如何找到定义SIGALRM宏的signal.h在哪里:
        find / -name signal.h -print > tmp.txt   #每行为一个文件路径 
        awk '{print $0, system("grep 'SIGALRM' "$0)}' tmp.txt   #在每个文件路径中搜出现过SIGALRM的行
        
        可以看到:
        ...
        /usr/include/signal.h 1
        #define SIGALRM 14 
        ...
        
        2. #include <unistd.h>
        unistd.h 是 C 和 C++ 程序设计语言中提供对 POSIX 操作系统 API 的访问功能的头文件的名称。
        
        该头文件由 POSIX.1 标准(单一UNIX规范的基础)提出,故所有遵循该标准的操作系统和编译器均应提供
        该头文件(如 Unix 的所有官方版本,包括 Mac OS X、Linux 等)。
        
        对于类 Unix 系统,unistd.h 中所定义的接口通常都是大量针对系统调用的封装(wrapper functions),
        如 fork、pipe 以及 I/O 原语(read、write、close 等)。类似于 Cygwin 和 MinGW 的 Unix 兼容层也
        提供相应版本的 unistd.h。
        
        本函数中用到: 
        fork();
        getppid();
        pause();
        
        
        3. #include <signal.h>
        
        本函数中用到: 
        signal(int, __sighandler_t)
        kill(pid_t, int)
        
*/

static int alarm_fired = 0;
void ding(int sig){
      printf("ding received signal [%d]\n", sig);
      alarm_fired = 1;
}

int main() {    
    pid_t pid;
    pid = fork();
    
    switch(pid) {
    case -1:
         /* Failure */
         perror("fork failed");
         exit(1);
    case 0:
         //在main函数中,我们通知子进程在向其父进程发送SIGALRM信号之间等待5秒
         
         /* child */
        sleep(5);
        kill(getppid(), SIGALRM);
        exit(0);
    }
 
    //父进程安排一个函数来捕获SIGALRM信号,并且进行必须的等待
 
    /* if we get here we are the parent process */
    (void) signal(SIGALRM, ding); 
    pause();
    
    if (alarm_fired) 
        printf("ding!\n");
    
    exit(0);
}
 
      当我们运行这个程序时,当他等待模拟的警报时钟时会等待5秒。
$gcc -o alarm.o alarm.c
$./alarm.o
ding received signal [14]
ding!
 
      这个程序引入一个新的函数,pause,他只是简单的使得程序执行挂起,直到产生一个信号。当他接收到一个信号时,就会运行所建立的处理器函数,并且如通常一样继续执行。其声明如下:
      #include <unistd.h>
      int pause(void);
      如果被一个信号中断时,此函数会返回-1并且设置errno为EINTR。通常当等待信号时,更为常见的用法是使用sigaction,我们会在本章的稍后进行讨论。
 
工作原理 
      警报时钟模拟程序通过fork启动一个新进程。子进程会休眠5秒,然后向其父进程发送SIGALRM信号。父进程会捕获SIGALRM信号,并且暂停直到接收信号。我们并没有在信号处理函数内部直接调用printf;相反,我们设置一个标识,并且在后面检测这个标记。
      使用信号与挂起执行是Linux编程的一个重要部分。他意味着一个程序并不需要全时段运行。他可以等待一个事件的发生,而不是在一个循环中运行来检测一个事件是否发生了。这在多用户环境中是十分重要的,因为多用户环境的处理共享一个处理器,而这种频繁的等待对于系统的性能有巨大的影响。信号的一个特殊问题就是我们绝不会知道"在一个系统调用中间如果一个信号发生会发生什么?"。通常情况下,我们只需要担心"缓慢"的系统调用,例如终端读取,在这种情况下,当他等待时,发生一个信号,系统调用就会返回一个错误。如果我们开始在我们的程序中使用信号,那么我们必须清楚如果信号接收到一个在添加信号处理之间我们并没有考虑到的错误条件,系统调用就会失败。
      我们必须小心的编写我们的信号程序,因为在使用信号的程序中会有许多"竞争条件"发生。例如,如果我们调用pause来等待一个信号,而那个信号却是在调用pause之前发生,那么我们的程序就会等待一个不会发生的事件。这些竞争条件,严格的时间问题,困扰着程序员。所以我们总是要小心的检测信号代码。
 
五、更健壮的信号接口sigaction
      我们已经讨论了使用signal来发出与捕获信号,因为他们在较为旧的Unix程序中很常见。然而,X/Open与Unix规范推荐了一个更为健壮的用于信号处理的新的编程接口:sigaction。
      #include <signal.h>
      int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
      sigaction结构定义在signal.h中,这个结构用于定义依据由sig所指定的信号而所要采取的动作,而且这个结构至少有以下几个成员:
      void (*) (int) sa_handler /* function, SIG_DFL or SIG_IGN
      sigset_t sa_mask          /* signals to block in sa_handler
      int sa_flags              /* signal action modifiers
      sigaction函数设置与sig信号相关联的动作。如果oact不为空,sigaction就会将前一个信号动作写入他所指向的位置。如果act为空,这就是sigaction所做的所有事情。如果act不为空,就会设置指定信号的动作。
      作为信号,如果成功,sigaction就会返回0,否则返回-1。如果指定的信号不存在或是尝试捕获或忽略不能捕获或是忽略的信号时,错误变量errno就会被设置EINVAL。
      在参数act所指向的sigaction结构内部,sa_handler是一个指针,指向当接收到sig信号时所调用的函数。这与我们在前面所见到过的传递给signal的函数func相类似。我们可以在sa_handler域使用特殊值SIG_IGN与SIG_DFL分别表示要忽略此信号或是重新载入默认动作
      sa_mask域指定了一个在sa_handler函数调用之前要添加到进程的信号掩码中信号集合。有一些被屏蔽或是不会传递给进程的信号集合。这就会阻止了我们在前面所看到的在处理器函数运行完成之前接收到信号的情况。使用sa_mask域可以减少竞争条件。
      然而,由sigaction所设置的处理器捕获的信号默认情况下并不会重新设置,而如要我们希望获得我们在前面所看到的信号行为,那么就必须设置sa_flags域来包含SA_RESETHAND值。在我们详细深入了解sigaction之前,让我们来使用sigaction来替换信号重新编写ctrlc.c程序。
试验--sigaction
      进行如下的修改,从而SIGINT可以被sigaction所解释。我们将这个新程序称之为ctrlc2.c
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void ouch(int sig){
       printf(“OUCH! - I got signal %d/n”, sig);
}
int main(){
    struct sigaction act;
    act.sa_handler = ouch;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    sigaction(SIGINT, &act, 0);

    while(1) {
      printf(“Hello World!/n”);
      sleep(1);
    }
}
 
      当我们运行这个版本的程序时,当我们按下Ctrl+C时我们总会得到一条消息,因为SIGINT信号反复的被sigaction处理。要结束这个程序,我们必须按下Ctrl+/,这在默认情况下会产生SIGQUIT信号。
$ ./ctrlc2
Hello World!
Hello World!
Hello World!
^C
OUCH! - I got signal 2
Hello World!
Hello World!
^C
OUCH! - I got signal 2
Hello World!
Hello World!
^/
Quit
$
 
工作原理
      这个程序调用sigaction而不是signal来将Ctrl+C(SIGINT)的信号处理器设置为函数ouch。首先需要设置一个包含处理器,信号掩码与标志的sigaction结构。在这个例子中,我们并不需要任何标志,并且使用一个新函数sigemptyset设置一个空的信号掩码。
信号集合
      头文件signal.h定义了类型sigset_t以及用来操作信号集合的函数。这个集合用在sigaction结构与其他的函数中,用来依据信号修改进程行为。
      #include <signal.h>

      int sigaddset(sigset_t *set, int signo);
      int sigemptyset(sigset_t *set);
      int sigfillset(sigset_t *set);
      int sigdelset(sigset_t *set, int signo);
      这些函数的执行他们的名字所显示的操作。sigemptyset将一个信号集合初始化为一个空集合。sigfillset将一个信号集合初始化为一个包含所有定义的信号的集合。sigaddset与sigdelset由一个信号集合中添加和删除一个指定的信号(signo)。这些函数都会在成功时返回0,而失败时返回-1,并且设置errno变量。如果指定的信号不可用,那么唯一的错误就是EINVAL。
      #include <signal.h>
      int sigismember(sigset_t *set, int signo);
      函数sigismember确定一个指定的信号是否是一个信号集合中的成员。如果此信号是信号集合中的一员,函数就会返回1,如果不是则会返回0,而如果信号不可用,则返回-1,并且将errno设置为EINVAL。
      #include <signal.h>
      int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
      进程信号掩码是通过调用函数sigprocmask来设置或是检测的。信号掩码是当前被屏蔽掉的信号集合,因而并不会当前的进程接收到。  sigprocmask函数会根据how参数以多种方式来改变进程信号掩码。如果参数set不为空,那么新的信号掩码值可以通过set传入,而前一个信号掩码值被写入信号集合oset。
how参数如下:
      SIG_BLOCK    set中的信号都被加入到信号掩码中
      SIG_SETMASK    由set中设置信号掩码
      SIG_UNBLOCK    set中的信号由信号掩码中移除
      如果set参数是一个空指,那么how参数并不会被使用,而这个调用的唯一作用就是获取当前的信号掩码值并写入oset。
如果函数调用成功,sigprocmask会返回0,如果how参数不可用则返回-1,并且将errno设置为EINVAL。
如果一个信号被一个进程屏蔽,那么他就不会被传输,但是仍保持等待状态。一个程序可以通过调用函数sigpending来他的被屏蔽的信号哪些处理等待状态。
#include <signal.h>
int sigpending(sigset_t *set);
这会输出一个被阻止传输的信号集合并且添加到由set所指向的信号集合中。如果成功会返回0,否则会返回-1,并且设置errno来表示错误。当一个程序需要处理信号并且要控制何时调用处理函数时,这个函数会很有用。
一个进程可以挂起执行直到通过调用sigsuspend传输一个信号。这就是我们在前面常见到的暂停功能的常见形式:
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
sigsuspend函数使用由sigmask信号集合来替换进程信号掩码,然后挂起执行。他会在信号处理函数执行之后恢复执行。如果接收到的信号终止程序,sigsuspend函数就不会再返回。如果接收的信号没有终止程序,sigsuspend函数会返回-1,并且将errno设置为EINTR。
sigaction标记
用于sigaction中的sigaction结构的sa_flags域也许会包含下列值来修改信号行为:
SA_NOCLDSTOP    当子进程停止时不要生成SIGCHLD信号
SA_RESETHAND    当收到信号时将信号动作重设为SIG_DFL
SA_RESTART    重新启动可中断函数而不是以EINTR报错
SA_NODEFER    当捕获时不要将信号添加到信号掩码中
SA_RESETHAND可以用于当捕获一个信号时自动清除一个信号函数,正如我们在前面所看到的。
程序所用的许多系统调用都是可中断的;也就是说,当他们接收到的一个信号时,他们会返回一个错误,并且将errno设置为EINTR表明函数是因为信号而返回的。这个行为对于使用信号的程序要格外小心。如果在sigaction调用中的sa_flags域被设置为SA_RESTART,那么这个函数就不会被中断,而在信号处理函数被执行时重新启动。
通常而言,当一个信号处理函数正在执行时,所接收到的信号会在信号处理函数执行期间添加到进程的信号掩码中。这是为了阻止后面发生了同样的信号,从而使得信号处理函数再次运行。如果函数不是可重入的,在他完成处理之前为另一个信号的发生所调用,那么前一个就会出现问题。然而,如果调用了SA_NODEFER标记,当接收到这个信号时,信号掩码并不会提示。
信号处理函数可以在执行期间被中断,并且在其他的函数再次调用。当我们返回到的前一个调用时,他可正常运行是很重要的。他不仅是递归(调用自身),而是重入(无问题的进入并再次运行)。在内核中同时处理多个设备的中断服务例程需要是可重入的,因为在相同代码的执行期间也许会有一个更高优先级的中断进入。
下面列出的是X/Open规范认为在一个信号处理器内部是可以安全调用的,他们或者是可重入的或者是自身不会发出信号。
access      alarm       cfgetispeed cfgetospeed
cfsetispeed cfsetospeed chdir       chmod
chown       close       creat       dup2
dup         execle      execve      _exit
fcntl       fork        fstat       getegid
geteuid     getgid      getgroups   getpgrp
getpid      getppid     getuid      kill
link        lseek       mkdir       mkfifo
open        pathconf    pause       pipe
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
通用信号参考
在这一部分,我们会列出Linux与Unix程序默认行为通常需要的信号。
下表所列出的信号默认动作都是使用_exit的进程非正常终止(类似于exit,但是在返回内核之前并没有执行任何清理)。然而,状态可以用于wait,而waitpid可以通过特定的信号来表明非正常的终止。
信号名        描述
SIGALRM        由alarm函数所调用的计时器生成的信号
SIGHUP        通过非连接终端发往控制进程,或是通过终端控制进程发往前台进程
SIGINIT        通常是在终端由Ctrl+C或是配置的中断字符生成的信号
SIGKILL        通常由shell使用来强制终止一个不确定进程,因为这个信号是不能被捕获或是忽略的
SIGPIPE        尝试写入一个没有相应读端管道时生成的信号
SIGTERM        作为一个请求进程完成的请求发送。Unix用于停机的情况来请求系统服务停止。这是由kill命令所发送的默认信号。
SIGUSR1,SIGUSR2    也许会被进程用于彼此通信,可能使得他们报告状态信息
默认情况下,下表中的信号也许会引起非正常终止。另外,与实现相关的动作,例如也许会发生创建核心文件的情况。
信号名        描述
SIGFPE        由浮点算术异常生成的信号
SIGILL        处理器执行了一条非法指令。通常是由一个恶意程序或是不正确的共享内存模块引起的
SIGQUIT        通常是在终端输入Ctrl+/或是配置的退出字符发出的信号
SIGSEGV        段错误,通常是由读写非法内存地址(或者是超出数组边界或是引用非法指针)引起的。覆盖一个局部数组变量并破坏堆栈,从而使得函数返回非法地址,引起SIGSEGV信号。
进程默认情况下接收到下表中的信号时会挂起执行。
信号名        描述
SIGSTOP        停止执行(不能被捕获或是忽略)
SIGTSTP        终端停止信号,通常是由Ctrl+Z引起的
SIGTTIN,SIGTTOU    shell用来表明后台作业已经停止,因为他们需要由终端读取或是产生输出
SIGCONT可以使得一个停止进程重新启动,如果被一个并没有停止的进程接收到,则会被忽略。SIGCHLD默认会被忽略。
信号名        描述
      SIGCONT        如果停止,继续执行
      SIGCHLD        当一个子进程停止或是退出时生成信号
小结
在这一章,我们了解了进程如何成为Linux操作系统的基础部分。我们了解了他们如何启动,终止,查看,并且我们如何使用他们来解决编程问题。我们也了解了可以用于控制运行程序动作的信号,事件。我们了解到所有的Linux进程,包括init,使用与程序可用的相同的系统调用集合。

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics