Linux进程间通讯

Linux进程间通讯

学习内容:在前面的学习中,咱们学习了如何建立进程以及如何对进程进行基本的控制,而这些都只是停留在父子进程之间的控制,本次将要学习不一样的进程间进行通讯的方法。html

————————CONTENTS————————

Linux进程间通讯概述

在以前的学习中咱们了解到,进程是一个程序的一次执行。这里所说的进程通常是指运行在用户态的进程,而因为处于用户态的不一样进程之间是彼此隔离的,它们必须经过某种方式来进行通讯,就像现在不一样地域的人们使用手机联系同样。接下来咱们将学习如何为不一样的进程间创建通讯方式。node

Linux下的进程通讯手段基本上是从UNIX 平台上的进程通讯手段继承而来的。而对UNIX发展作出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间的通讯方面的􀗗重点有所不一样。前者是对UNIX早期的进程间通讯手段进行了系统的改进和扩充,造成了“system V IPC”,其通讯进程主要局限在单个计算机内;后者则跳过了该限制,造成了基于套接口(socket)的进程间通讯机制。而Linux则把二者的优点都继承了下来,以下图所示:linux

  • UNIX进程间通讯(IPC)方式包括管道、FIFO以及信号;
  • System V进程间通讯(IPC)包括System V消息队列、System V信号量以及System V共享内存区;
  • Posix进程间通讯(IPC)包括Posix消息队列、Posix信号量以及Posix共享内存区。

如今Linux中经常使用的进程间通讯方式主要有如下几种:程序员

  • 管道(Pipe)及有名管道(named pipe):管道可用于具备亲缘关系进程间的通讯,有名管道,除具备管道所具备的功能外,它还容许无亲缘关系进程间的通讯。
  • 信号(Signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通讯方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上能够说是同样的。
  • 信号量(Semaphore):主要做为进程之间以及同一进程的不一样线程之间的同步和互斥手段。
  • 共享内存(Shared memory):能够说这是最有用的进程间通讯方式。它使得多个进程能够访问同一块内存空间,不一样进程能够及时看到对方进程中对共享内存中数据的更新。这种通讯方式须要依赖某种同步机制,如互斥锁和信号量等。
  • 消息队列(Messge Queue):消息队列是消息的连接表,包括Posix消息队列System V消息队列。它克服了前两种通讯方式中信息量有限的缺点,具备写权限的进程能够按照必定的规则向消息队列中添加新消息;对消息队列有读权限的进程则能够从消息队列中读取消息。
  • 套接字(Socket):这是一种更为通常的进程间通讯机制,它可用于网络中不一样机器之间的进程间通讯,应用很是普遍。

接下来将详细介绍前5种进程间通讯方式,套接字在“网络编程”部分重点研究。编程

返回目录

1、管道

『1.管道概述』数组

管道是Linux中一种很重要的通讯方式,它能够把一个程序的输出直接链接到另外一个程序的输入。还记得咱们以前使用man -k process | grep create命令搜索与建立进程相关的函数网络

这就是管道的一种使用方式,即把man -k process命令的输出当作grep create命令的输入,进行二次检索。数据结构

管道是Linux中进程间通讯的一种方式。这里所说的管道主要指无名管道,它具备如下特色:异步

  • 它只能用于具备亲缘关系的进程之间的通讯(也就是父子进程或者兄弟进程之间)。
  • 它是一个半双工的通讯模式,具备固定的读端和写端。须要双方通讯时,须要创建起两个管道。
  • 管道也能够当作是一种特殊的文件,对于它的读写也可使用普通的read()和write()等函数。可是它不是普通的文件,并不属于其余任何文件系统,而且只存在于内核的内存空间中。
  • 数据的读出和写入:一个进程向管道中写的内容被管道另外一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,而且每次都是从缓冲区的头部读出数据。

『2.管道系统调用』socket

(1)管道建立与关闭说明:

管道是基于文件􁧿述符的通讯方式,当一个管道创建时,它会建立两个文件􁧿述符fds[0]和fds[1],其中fds[0]固定用于读管道,而fd[1]固定用于写管道,这样就构成了一个半双工的通道。

管道关闭时只需将这两个文件描述符关闭便可,可以使用普通的close()函数逐个关闭各个文件描述符。

注意:当一个管道共享多对文件描述符时,若将其中一对读写文件描述符都删除,则该管道就失效。

(2)管道建立函数:

建立管道能够经过调用pipe()实现,如下列出了pipe()函数的语法要点:

(3)管道读写说明:

通常状况下使用管道时,先建立一个管道,再经过fork()函数建立一子进程,该子进程会继承父进程所建立的管道。为了实现父子进程之间的读写,只需把无关的读端或写端的文件描述符关闭便可。例如在下图中将父进程的写端fd[1]和子进程的读端fd[0]关闭。此时,父子进程之间就创建起了一条“子进程写入父进程读取”的通道。

一样,也能够关闭父进程的fd[0]和子进程的fd[1],这样就能够创建一条“父进程写入子进程读取”的通道。另外,父进程还能够建立多个子进程,各个子进程都继承了相应的fd[0]和fd[1],这时,只须要关闭相应端口就能够创建其各子进程之间的通道。

(4)管道使用实例:

在下面的测试代码中,首先建立管道,以后父进程使用fork()函数建立子进程,以后经过关闭父进程的读描述符和子进程的写描述符,创建起父子进程之间的管道通讯。

/* pipe.c */
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX_DATA_LEN 256
#define DELAY_TIME 1
int main()
{
    pid_t pid;
    int pipe_fd[2];
    char buf[MAX_DATA_LEN];
    const char data[] = "Pipe Test Program";
    int real_read, real_write;
    memset((void*)buf, 0, sizeof(buf));
    /* 建立管道 */
    if (pipe(pipe_fd) < 0)
    {
        printf("pipe create error\n");
        exit(1);
    }
    /* 建立一子进程 */
    if ((pid = fork()) == 0)
    {
        /* 子进程关闭写描述符,并经过使子进程暂停1s等待父进程关闭相应的读描述符*/
        close(pipe_fd[1]);
        sleep(DELAY_TIME * 3);
        /* 子进程读取管道内容 */
        if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0)
        {
            printf("%d bytes read from the pipe is '%s'\n", real_read, buf);
        }
        /* 关闭子进程读描述符 */
        close(pipe_fd[0]);
        exit(0);
    }
    else if (pid > 0)
    {
        /* 父进程关闭读描述符,并经过使父进程暂停1s等待子进程关闭相应的写描述符*/
        close(pipe_fd[0]);
        sleep(DELAY_TIME);
        if((real_write = write(pipe_fd[1], data, strlen(data))) != -1)
        {
            printf("Parent wrote %d bytes : '%s'\n", real_write, data);
        }
        /* 关闭父进程写描述符 */
        close(pipe_fd[1]);
        /* 收集子进程退出信息 */
        waitpid(pid, NULL, 0);
        exit(0);
    }
}

运行结果以下图所示:

『3.标准流管道』

(1)标准流管道函数说明:

与Linux的文件操做中有基于文件流的标准I/O操做同样,管道的操做也支持基于文件流的模式。这种基于文件流的管道主要是用来建立一个链接到另外一个进程的管道,这里的“另外一个进程”也就是一个能够进行必定操做的可执行文件,例如,用户执行“ls -l”或者本身编写的程序“./pipe”等。因为这一类操做很经常使用,所以标准流管道就将一系列的建立过程合并到一个函数popen()中完成。它所完成的工做有如下几步。

  • 建立一个管道
  • fork()一个子进程
  • 在父子进程中关闭不须要的文件描述符
  • 执行exec函数族调用
  • 执行函数中所指定的命令

这个函数的使用能够大大减小代码的编写量,但同时也有一些不利之处,例如,它不如前面管道建立的函数那样灵活多样,而且用popen()建立的管道必须使用标准I/O函数进行操做,但不能使用前面的read()、write()一类不带缓冲的I/O函数。

与之相对应,关闭用popen()建立的流管道必须使用函数pclose()来关闭该管道流。该函数关闭标准I/O流,并等待命令执行结束。

(2)函数格式:

(3)函数使用实例:

下面的程序使用popen()来执行“ps -ef”命令,能够看出popen()函数使程序变得短小精悍:

/* standard_pipe.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#define BUFSIZE 1024
int main()
{
    FILE *fp;
    char *cmd = "ps -ef";
    char buf[BUFSIZE];

    if ((fp = popen(cmd, "r")) == NULL)
    {
        printf("Popen error\n");
        exit(1);
    }
    while ((fgets(buf, BUFSIZE, fp)) != NULL)
    {
        printf("%s",buf);
    }
    pclose(fp);
    exit(0);
}

运行结果以下:

『4.FIFO』

(1)有名管道说明

前面介绍的管道是无名管道,它只能用于具备亲缘关系的进程之间,这就大大地限制了管道的使用。有名管道的出现突破了这种限制,它可使互不相关的两个进程实现彼此通讯。该管道能够经过路径名来指出,而且在文件系统中是可见的。在创建了管道以后,两个进程就能够把它看成普通文件同样进行读写操做,使用很是方便。不过值得注意的是,FIFO是严格地遵循先进先出规则的,对管道及FIFO的读老是从开始处返回数据,对它们的写则把数据添加到末尾,它们不支持如lseek()等文件定位操做。

有名管道的建立可使用函数mkfifo(),该函数相似文件中的open()操做,能够指定管道的路径和打开的模式。

在建立管道成功以后,就可使用open()、read()和write()这些函数了。与普通文件的开发设置同样,对于为读而打开的管道可在open()中设置O_RDONLY,对于为写而打开的管道可在open()中设置O_WRONLY,在这里与普通文件不一样的是阻塞问题。因为普通文件的读写时不会出现阻塞问题,而在管道的读写中却有阻塞的可能,这里的非阻塞标志能够在open()函数中设定为O_NONBLOCK。

(2)mkfifo()函数格式

(3)使用实例

下面的实例包含了两个程序,一个用于读管道,另外一个用于写管道。其中在读管道的程序里建立管道,而且做为main()函数里的参数由用户输入要写入的内容。读管道的程序会读出用户写入到管道的内容,这两个程序采用的是阻塞式读写管道模式。

/* fifo_write.c 写管道*/
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MYFIFO "/tmp/myfifo"  //有名管道文件名
#define MAX_BUFFER_SIZE PIPE_BUF  

int main(int argc, char * argv[]) //参数为即将写入的字符串
{

    int fd;
    char buff[MAX_BUFFER_SIZE];
    int nwrite;

    if(argc <= 1)
    {
        printf("Usage: ./fifo_write string\n");
        exit(1);
    }
    sscanf(argv[1], "%s", buff);

    /*以只写阻塞方式打开FIFO管道*/
    fd = open(MYFIFO, O_WRONLY);
    if (fd == -1)
    {
        printf("Open fifo file error\n");
        exit(1);
    }

    /*向管道中写入字符串*/
    if ((nwrite = write(fd, buff, MAX_BUFFER_SIZE)) > 0)
    {
        printf("Write '%s' to FIFO\n", buff);
    }
    close(fd);
    exit(0);
}
/*fifo_read.c 读管道程序*/
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define MYFIFO "/tmp/myfifo" 
#define MAX_BUFFER_SIZE PIPE_BUF
int main()
{
    char buff[MAX_BUFFER_SIZE];
    int fd;
    int nread;

    /*判断有名管道是否已存在,若还没有建立,则以相应的权限建立*/
    if (access(MYFIFO, F_OK) == -1)
    {
        if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST))
        {
            printf("Cannot create fifo file\n");
            exit(1);
        }
    }

    /*以只读阻塞方式打开有名管道*/
    fd = open(MYFIFO, O_RDONLY);
    if (fd == -1)
    {
        printf("Open fifo file error\n");
        exit(1);
    }

    while (1)
    {
        memset(buff, 0, sizeof(buff));
        if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0)
        {
            printf("Read '%s' from FIFO\n", buff);
        }
    }
    close(fd);
    exit(0);
}

为了可以较好地观􁈏运行结果,须要把这两个程序分别在两个终端里运行,在这里首先启动读管道程序。读管道进程在创建管道以后就开始循环地从管道里读出内容,若是没有数据可读,则一直􄱫􀺎到写管道进程向管道写入数据。在启动了写管道程序后,读进程可以从管道里读出用户的􄗃入内容,程序运行结果以下所示:

终端一:

终端二:

返回目录


2、信号

『1.信号概述』

信号是UNIX中所使用的进程通讯的一种最古老的方法。它是在软件层次上对中断机制的一种模拟,是一种异步通讯方式。

使用kill -l命令能够列出该系统所支持的全部信号的列表:

在上图所示系统中,,信号值在32 以前的则有不一样的名称,而信号值在32 之后的都是用“SIGRTMIN”或“SIGRTMAX”开头的,这就是两类..型的信号。前者是从UNIX 系统中继承下来的信号,为不可靠信号(也称为非实时信号);后者是为了解决前面“不可靠信号”的问题而进行了更改和扩充的信号,称为“可靠信号”(也称为实时信号)。

那么,为何以前的信号不可靠呢?这里咱们首先介绍一下信号的生命周期。

一个完整的信号生命周期能够分为3个重要阶段,这3个阶段由4个重要事件来刻画:信号产生、信号在进程中注册、信号在进程中注销、执行信号处理函数,相邻两个事件的时间间隔构成信号生命周期的一个阶段,以下图所示:

信号处理有多种方式,通常是由内核完成的,固然也能够由用户进程来完成,故在此没有明确画出。

  • 不可靠信号的处理过程:若是发现该信号已经在进程中注册,那么就忽略该信号。所以,若前一个信号还未注销又产生了相同的信号就会产生信号丢失。
  • 可靠信号的处理过程:无论该信号是否已经在进程中注册,都会被再注册一次,所以信号不会丢失。

用户进程对信号的响应能够有3种方式:

  • 忽略信号:即对信号不作任何处理,可是有两个信号不能忽略,即SIGKILL 及SIGSTOP。
  • 捕捉信号:定义信号处理函数,当信号发生时,执行相应的自定义处理函数。
  • 执行缺省操做:Linux对每种信号都规定了默认操做。

Linux中的大多数信号是提供给内核的,使用man 7 signal能够查看信号的定义及其默认操做。下图展现了一部分:

『2.信号的发送与捕捉』

如今咱们想了解与信号有关的函数都有哪些,使用man -k signal可查询到全部与signal有关的函数,咱们重点研究如下几个:kill()、raise()、alarm()以及pause()。

(1)kill()和raise()

  • 函数说明:

kill()函数同读者熟知的kill系统命令同样,能够发送信号给进程或进程组(实际上,kill系统命令只是kill()函数的一个用户接口)。这里须要注意的是,它不只能够停止进程(实际上发出SIGKILL信号),也能够向进程发􄘱其余信号。

与kill()函数所不一样的是,raise()函数容许进程向自身发送信号。

  • 函数格式:

使用man 2 kill/man 3 raise查询函数相关信息,以下:

pid取值:①pid为正数:要发送信号的进程号;②pid=0:信号被发送到全部和当前进程在同一个进程组的进程;③pid=-1:,信号发给全部的进程表中的进程(除了进程号最大的进程外);④pid<-1时,信号发送给进程组号为-pid的每个进程。

返回值:①成功:0,②失败:-1。

返回值:①成功:0,②失败:-1。

  • 函数实例:

下面这个示例首先使用fork()建立了一个子进程,接着为了保证子进程不在父进程调用kill()以前退出,在子进程中使用raise()函数向自身发送SIGSTOP 信号,使子进程暂停。接下来再在父进程中调用kill()向子进程发送信号,如本示例中使用的是SIGKILL。

/* kill_raise.c */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t pid;
    int ret;

    if ((pid = fork()) < 0)
    {
        printf("Fork error\n");
        exit(1);
    }
    if (pid == 0)
    {
        
        printf("Child(pid : %d) is waiting for any signal\n", getpid());
        raise(SIGSTOP);
        exit(0);
    }
    sleep(3);
    if ((waitpid(pid, NULL, WNOHANG)) == 0)
    {
        if ((ret = kill(pid, SIGKILL)) == 0)
        {
                
            printf("Parent kill %d\n",pid);
        }
    }
    waitpid(pid, NULL, 0);
    exit(0);
}

运行结果以下图所示:

(2)alarm()和pause()

  • 函数说明:

alarm()也称为闹钟函数,它能够在进程中设置一个定时器,当定时器指定的时间到时,它就向进程发送SIGALARM信号。要注意的是,一个进程只能有一个闹钟时间,若是在调用alarm()以前已设置过闹钟时间,则任何之前的闹钟时间都被新值所代替。

pause()函数是用于将调用进程挂起直至捕捉到信号为止。这个函数很经常使用,一般能够用于判断信号是否已到。

  • 函数格式:

  • 函数实例:

该实例实际上已完成了一个简单的sleep()函数的功能,因为SIGALARM 默认的系统动做为终止该进程,所以程序在打􀦠信息以前,就会被结束了。

/* alarm_pause.c */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
    int ret = alarm(5);
    pause();
    printf("I have been waken up.\n",ret);//此语句不会被执行
}

『3.信号的处理』

一个进程能够决定在该进程中须要对哪些信号进行什么样的处理。例如,一个进程能够选择忽略某些信号而只处理其余一些信号,另外,一个进程还能够选择如何处理信号。总之,这些都是与特定的进程相联系的。所以,首先就要创建进程与其信号之间的对应关系,这就是信号的处理。

信号处理的主要方法有两种,一种是使用简单的signal()函数,另外一种是使用信号集函数组。下面分别介绍这两种处理方式。

(1)信号处理函数

  • 函数说明:

使用signal()函数处理时,只须要指出要处理的信号和处理函数便可。它主要是用于前32种非实时信号的处理,不支持信号传递信息,可是因为使用简单、易于理解,所以也受到不少程序员的欢迎。

Linux还支持一个更健壮、更新的信号处理函数sigaction(),推荐使用该函数。

  • 函数格式:

这里对函数原型进行简要说明:可先用以下的typedef进行替换说明:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

第一个参数signum:指明了所要处理的信号类型,它能够取除了SIGKILL和SIGSTOP外的任何一种信号。  

第二个参数handler:描述了与信号关联的动做,它能够取如下三种值:

①SIG_IGN:忽略该信号,②SIG_DFL:恢复对信号的系统默认处理,③sighandler_t类型的函数指针:用户自定义的处理函数。

这里着重讲解sigaction()函数中第2个和第3个参数用到的sigaction结构。可以使用man -k sigaction查看定义:

sa_handler 是一个函数指针,指定信号处理函数,取值与signal()函数相同;

sa_mask 是一个信号集,它能够指定在信号处理程序执行过程当中哪些信号应当被屏蔽,在调用信号捕获函数以前,该信号集要加入到信号的信号屏蔽字中;

sa_flags 中包含了许多标志位,是对信号进行处理的各个选择项。

  • 使用实例:

第一个实例代表了如何使用signal()函数捕捉相应信号,并作出给定的处理。这里,my_func就是信号处理的函数指针。第二个实例是用sigaction()函数
实现一样的功能。如下是使用signal()函数的示例:

/* signal.c */
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

//自定义信号处理函数
void my_func(int sign_no)
{
    if (sign_no == SIGINT)
    {
        printf("I have get SIGINT\n");
    }
    else if (sign_no == SIGQUIT)
    {
        printf("I have get SIGQUIT\n");
    }
}
int main()
{
    printf("Waiting for signal SIGINT or SIGQUIT...\n");

    //发出相应的信号,并跳转到信号处理函数处
    signal(SIGINT, my_func);
    signal(SIGQUIT, my_func);
    pause();
    exit(0);
}

运行结果以下所示:

若使用sigaction()函数实现一样功能,main()函数作以下修改:

//sigaction.c
//前部分省略
int main()
{
    struct sigaction action;
    printf("Waiting for signal SIGINT or SIGQUIT...\n");
    
    /* sigaction结构初始化 */
    action.sa_handler = my_func;
    sigemptyset(&action.sa_mask);
    action.sa_flags = 0;
    
    /* 发出相应的信号,并跳转到信号处理函数处 */
    sigaction(SIGINT, &action, 0);
    sigaction(SIGQUIT, &action, 0);
    pause();
    exit(0);
}

(2)信号集函数组

咱们已经知道,咱们能够经过信号来终止进程,也能够经过信号来在进程间进行通讯,程序也能够经过指定信号的关联处理函数来改变信号的默认处理方式,也能够屏蔽某些信号,使其不能传递给进程。那么咱们应该如何设定咱们须要处理的信号,咱们不须要处理哪些信号等问题呢?信号集函数就是帮助咱们解决这些问题的。

如下为信号集函数:

int sigemptyset(sigset_t *set);
//该函数的做用是将信号集初始化为空。

int sigfillset(sigset_t *set);
//该函数的做用是把信号集初始化包含全部已定义的信号。

int sigaddset(sigset_t *set, int signo);
//该函数的做用是把信号signo添加到信号集set中,成功时返回0,失败时返回-1。

int sigdelset(sigset_t *set, int signo);
//该函数的做用是把信号signo从信号集set中删除,成功时返回0,失败时返回-1.

int sigismember(sigset_t *set, int signo);
//该函数的做用是判断给定的信号signo是不是信号集中的一个成员,若是是返回1,若是不是,返回0,若是给定的信号无效,返回-1;

int sigpromask(int how, const sigset_t *set, sigset_t *oset);
//该函数能够根据参数指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由参数set(非空)指定,而原先的信号屏蔽字将保存在oset(非空)中。若是set为空,则how没有意义,但此时调用该函数,若是oset不为空,则把当前信号屏蔽字保存到oset中。

int sigpending(sigset_t *set);
//该函数的做用是将被阻塞的信号中停留在待处理状态的一组信号写到参数set指向的信号集中,成功调用返回0,不然返回-1,并设置errno代表错误缘由。

int sigsuspend(const sigset_t *sigmask);
//该函数经过将进程的屏蔽字替换为由参数sigmask给出的信号集,而后挂起进程的执行。注意操做的前后顺序,是先替换再挂起程序的执行。程序将在信号处理函数执行完毕后继续执行。若是接收到信号终止了程序,sigsuspend()就不会返回,若是接收到的信号没有终止程序,sigsuspend()就返回-1,并将errno设置为EINTR。

注意:若是一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态,当进程解除对待处理信号的阻塞时,待处理信号就会马上被处理。

如下面的程序为例,介绍上述函数的用法:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
 
void handler(int sig)
{
    printf("Handle the signal %d\n", sig);
}
 
int main(int argc, char **argv)
{
    sigset_t sigset;    // 用于记录屏蔽字
    sigset_t ign;       // 用于记录被阻塞(屏蔽)的信号集
    struct sigaction act;
 
    // 清空信号集
    sigemptyset(&sigset);
    sigemptyset(&ign);
 
    // 向信号集中添加 SIGINT
    sigaddset(&sigset, SIGINT);
 
    // 设置处理函数 和 信号集
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGINT, &act, 0);
 
    printf("Wait the signal SIGNAL...\n");
    pause();
 
    // 设置进程屏蔽字, 在本例中为屏蔽 SIGINT
    sigprocmask(SIG_SETMASK, &sigset, 0);
    printf("Please press Ctrl + C in 10 seconds...\n");
    sleep(10);
 
    // 测试 SIGINT 是否被屏蔽
    sigpending(&ign);
    if (sigismember(&ign, SIGINT))
    {
        printf("The SIGINT signal has ignored\n");
    }
 
    // 从信号集中删除信号 SIGINT
    sigdelset(&sigset, SIGINT);
    printf("Wait the signal SIGINT...\n");
 
    // 将进程的屏蔽字从新设置, 即取消对 SIGINT 的屏蔽, 并挂起进程
    sigsuspend(&sigset);
 
    printf("The process will exit in 5 seconds!\n");
    sleep(5);
 
    return 0;
}

运行结果以下图所示:

  • 首先,咱们能过sigaction()函数改变了SIGINT信号的默认行为,使之执行指定的函数handler,因此输出了语句:Handle the signal 2。
  • 而后,经过sigprocmask()设置进程的信号屏蔽字,把SIGINT信号屏蔽起来,因此过了10秒以后,用sigpending()函数去获取被阻塞的信号集时,检测到了被阻塞的信号SIGINT,输出The SIGINT signal has ignored。
  • 最后,用函数sigdelset()函数去除先前用sigaddset()函数加在sigset上的信号SIGINT,再调用函数sigsuspend(),把进程的屏蔽字再次修改成sigset(不包含SIGINT),并挂起进程。
  • 因为先前的SIGINT信号停留在待处理状态,而如今进程已经再也不阻塞该信号,因此进程立刻对该信号进行处理,从而在最后不须要输入 Ctrl+C 也会出现后面的处理语句,过了5秒程序就成功退出了。

返回目录


3、信号量

『1.信号量概述』

在多任务操做系统环境下,多个进程会同时运行,而且一些进程之间可能存在必定的关联。多个进程可能为了完成同一个任务会相互协做,这样造成进程之间的同步关系。并且在不一样进程之间,为了争夺有限的系统资源(硬件或软件资源)会进入竞争状态,这就是进程之间的互斥关系。

进程之间的互斥与同步关系存在的根源在于临界资源。临界资源是在同一个时刻只容许有限个(一般只有一个)进程能够访问(读)或修改(写)的资源,一般包括硬件资源(处理器、内存、存储器以及其余外围设备等)和软件资源(共享代码段,共享结构和变量等)。访问临界资源的代码叫作临界区,临界区自己也会成为临界资源。

信号量是用来解决进程之间的同步与互斥问题的一种进程之间通讯机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操做(PV操做)。其中信号量对应于某一种资源,取一个非负的整型值。信号量值指的是当前可用的该资源的数量,若它等于0 则意味着目前没有可用的资源。PV原子操做的具体定义以下:

  • P 操做:若是有可用的资源(信号量值>0),则占用一个资源(给信号量值减去一,进入临界区代码);若是没有可用的资源(信号量值等于0),则被阻塞到,直到系统将资源分配给该进程(进入等待队列,一直等到资源轮到该进程)。
  • V 操做:若是在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程。若是没有进程等待它,则释放一个资源(给信号量值加一)。

最简单的信号量是只能取0和1两种值,这种信号量被叫作二维信号量。在这里,咱们主要讨论二维信号量。

『2.信号量的应用』

(1)函数说明:

在Linux系统中,使用信号量一般分为如下几个步骤:

  • 建立信号量或得到在系统已存在的信号量,此时须要调用semget()函数。不一样进程经过使用同一个信号量键值来得到同一个信号量。
  • 初始化信号量,此时使用semctl()函数的SETVAL操做。当使用二维信号量时,一般将信号量初始化为1。
  • 进行信号量的PV 操做,此时􄈳用semop()函数。这一步是实现进程之间的同步和互斥的核心工做部分。
  • 若是不须要信号量,则从系统中删除它,此时使用semclt()函数的IPC_RMID 操做。此时须要注意,在程序中不该该出现对已经被删除的信号量的操做。

(2)函数格式:

  • semget():

  • semctl():

  • semop():

(3)使用实例:

下面的程序展现了信号量的概念及其基本用法。在示例程序中,使用信号量来控制父子进程之间的执行顺序。

方便起见,咱们将信号量相关的函数封装成二维单个信号量的几个基本函数。它们分别为为信号量初始化函数(或者信号量赋值函数)init_sem()、P 操做􀠭数sem_p()、V操做函数sem_v()以及删除信号量的函数del_sem()等,具体实现以下所示:

/* sem_com.c */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};
/*信号量初始化(赋值)函数*/
int init_sem(int sem_id, int init_value)
{
    union semun sem_union;
    sem_union.val = init_value; //init_value为初始值
    if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
    {
        perror("Initialize semaphore");
        return -1;
    }
    return 0;
}

/*从系统中删除信号量的函数*/
int del_sem(int sem_id)
{
    union semun sem_union;
    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
    {
        perror("Delete semaphore");
        return -1;
    }
}

/*P操做函数*/
int sem_p(int sem_id)
{
    struct sembuf sem_b; /*单个信号量的编号应该为0 */
    sem_b.sem_num = 0;  /*表示P操做*/
    sem_b.sem_op = -1;  /*系统自动释放将会在系统中残留的信号量*/
    sem_b.sem_flg = SEM_UNDO;
    if (semop(sem_id, &sem_b, 1) == -1)
    {
        perror("P operation");
        return -1;
    }
    return 0;
}

/*V操做函数*/
int sem_v(int sem_id)
{
    struct sembuf sem_b;
    sem_b.sem_num = 0; 
    sem_b.sem_op = 1; 
    sem_b.sem_flg = SEM_UNDO;
    if (semop(sem_id, &sem_b, 1) == -1)
    {
        perror("V operation");
        return -1;
    }
    return 0;
}

下面编写一个测试程序,调用这些简单易用的接口,从而解决控制两个进程之间的执行顺序的同步问题。代码以下:

/* fork.c */
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define DELAY_TIME 3 /*为了突出演示效果,等待几秒钟*/
int main(void)
{
    pid_t result;
    int sem_id;

    sem_id = semget(ftok(".", 'a'), 1, 0666|IPC_CREAT); /*建立一个信号量*/
    init_sem(sem_id, 0);

    /*调用fork()函数*/
    result = fork();
    if(result == -1)
    {
        perror("Fork\n");
    }
    else if (result == 0) //返回值为0表明子进程
    {
        printf("Child process will wait for some seconds...\n");
        sleep(DELAY_TIME);
        printf("The returned value is %d in the child process(PID = %d)\n",
        result, getpid());
        sem_v(sem_id);
    }
    else //返回值大于0表明父进程
    {
        sem_p(sem_id);
        printf("The returned value is %d in the father process(PID = %d)\n",
        result, getpid());
        sem_v(sem_id);
        del_sem(sem_id);
    }
    exit(0);
}

能够先从fork.c中删去信号量相关的代码部分查看运行结果:

再添加信号量的控制部分并运行结果:

返回目录


4、共享内存

『1.共享内存概述』

采用共享内存通讯的一个显而易见的好处是效率高,由于进程能够直接读写内存,而不须要任何数据的拷贝。对于像管道和消息队列等通讯方式,则须要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另外一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不老是读写少许数据后就解除映射,有新的通讯时,再从新创建共享内存区域。而是保持共享区域,直到通讯完毕为止,这样,数据内容一直保存在共享内存中,并无写回文件。共享内存中的内容每每是在解除映射时才写回文件的。所以,采用共享内存的通讯方式效率是很是高的。

Linux的2.2.x内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及系统V共享内存。linux发行版本如Redhat 8.0支持mmap()系统调用及系统V共享内存,但还没实现Posix共享内存,接下来将主要介绍mmap()系统调用及系统V共享内存API的原理及应用。

『2.mmap()系统调用』

(1)函数说明:

mmap()系统调用使得进程之间经过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程能够向访问普通内存同样对文件进行访问,没必要再调用read(),write()等操做。

注:实际上,mmap()系统调用并非彻底为了用于共享内存而设计的。它自己提供了不一样于通常对普通文件的访问方式,进程能够像读写内存同样对普通文件的操做。而Posix或系统V的共享内存IPC则纯粹用于共享目的,固然mmap()实现共享内存也是其主要应用之一。

(2)函数格式:

  • mmap():

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

参数含义:

  • addr:指向欲映射的内存初始地址,一般设为NULL,表明让系统自动选定地址,映射成功后返回该地址。
  • len: 表明将文件中的多大的部分映射到内存。
  • prot: 映射区域的保护方式
    • PROT_EXEC映射区域可被执行
    • PROT_READ映射区域可被读取
    • PROT_WRITE映射区域可被写入
    • PROT_NONE映射区域不能存取
  • flags:影响映射区域的各类属性。在调用mmap()时必须指定
    • MAP_SHARED或MAP_PRIVATE
    • MAP_FIXED若是参数start所指的地址没法成功创建映射时,则放弃映射,不对地址作修正。
    • MAP_SHARED对映射区域的写入数据会复制会文件内,并且容许其它映射文件的进程共享。
    • MAP_PRIVATE对映射区域的写入操做会产生一个映射文件的复制,即私人的“写时复制”对此区域的任何修改都不会写回原来的文件内容。
    • MAP_ANONYMOUS创建匿名映射。此时会忽略参数fd,不涉及文件,并且映射区域没法和其余进程共享。
    • MAP_DENYWRITE只容许对映射区域的写入操做,其余对文件直接写入的操做将会被拒绝。
    • MAP_LOCKED将映射区域锁定住,这表示该区域不会被置换(swap)。
  • fd:要映射到内存中的文件描述符。若是使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可使用fopen打开/dev/zero文件,而后对该文件进行映射,能够一样达到匿名内存映射的效果。
  • offset:文件映射的偏移量,一般设置为0,表明从文件最前方开始对应,offset必须是分页大小的整数倍。
  • 返回值:若映射成功则返回映射区的内存起始地址,不然返回MAP_FAILED(-1),错误缘由存于errno 中。

  • munmap():

int munmap( void * addr, size_t len )

该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将致使段错误发生。

  • msync():

int msync ( void * addr , size_t len, int flags)

通常说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,每每在调用munmap()后才执行该操做。能够经过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

(3)使用实例:

下面给出两个进程经过映射普通文件实现共享内存通讯。

示例包含两个子程序:map_normalfile1.c及map_normalfile2.c。编译两个程序,可执行文件分别为map_normalfile1及map_normalfile2。两个程序经过命令行参数指定同一个文件来实现共享内存方式的进程间通讯。

map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操做。

map_normalfile1把命令行参数指定的文件映射到进程地址空间,而后对映射后的地址空间执行读操做。这样,两个进程经过命令行参数指定同一个文件来实现共享内存方式的进程间通讯。

/*-------------map_normalfile1.c-----------*/
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{
  char name[4];
  int  age;
}people;
main(int argc, char** argv) // map a normal file as shared mem:
{
  int fd,i;
  people *p_map;
  char temp;
  
  fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
  lseek(fd,sizeof(people)*5-1,SEEK_SET);
  write(fd,"",1);
  
  p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
        MAP_SHARED,fd,0 );
  close( fd );
  temp = 'a';
  for(i=0; i<10; i++)
  {
    temp += 1;
    memcpy( ( *(p_map+i) ).name, &temp,2 );
    ( *(p_map+i) ).age = 20+i;
  }
  printf(" initialize over \n ");
  sleep(10);
  munmap( p_map, sizeof(people)*10 );
  printf( "umap ok \n" );
}
/*-------------map_normalfile2.c-----------*/
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{
  char name[4];
  int  age;
}people;
main(int argc, char** argv)  // map a normal file as shared mem:
{
  int fd,i;
  people *p_map;
  fd=open( argv[1],O_CREAT|O_RDWR,00777 );
  p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
       MAP_SHARED,fd,0);
  for(i = 0;i<10;i++)
  {
  printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age );
  }
  munmap( p_map,sizeof(people)*10 );
}

map_normalfile1.c首先定义了一个people数据结构,(在这里采用数据结构的方式是由于,共享内存区的数据每每是有固定格式的,这由通讯的各个进程决定,采用结构的方式有广泛表明性)。map_normfile1首先打开或建立一个文件,并把文件的长度设置为5个people结构大小。而后从mmap()的返回地址开始,设置了10个people结构。而后,进程睡眠10秒钟,等待其余进程映射同一个文件,最后解除映射。

map_normfile2.c只是简单的映射一个文件,并以people数据结构的格式从mmap()返回的地址处读取10个people结构,并输出读取的值,而后解除映射。

分别把两个程序编译成可执行文件map_normalfile1和map_normalfile2后,在一个终端上先运行./map_normalfile1 ./test,程序输出结果以下:

在map_normalfile1输出initialize over以后,输出umap ok以前,在另外一个终端上运行map_normalfile2 /tmp/test_shm,将会产生以下输出(为了节省空间,输出结果为稍做整理后的结果):

name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24;
name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;

在map_normalfile1 输出umap ok后,运行map_normalfile2则输出以下结果:

name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24;
name:   age 0;  name:   age 0;  name:   age 0;  name:   age 0;  name:   age 0;

『3.System V共享内存』

(1)函数说明:

共享内存的实现分为两个步骤,第一步是建立共享内存,这里用到的函数是shmget(),也就是从内存中得到一段共享内存区域,第二步映射共享内存,也就是把这段建立的共享内存映射到具体的进程空间中,这里使用的函数是shmat()。到这里,就可使用这段共享内存了,也就是可使用不带缓冲的I/O 读写命令对其进行操做。除此以外,固然还有撤销映射的操做,其函数为shmdt()。这里就主要介绍这3个函数。

(2)函数格式:

  • shmget()

  • shmat()

  • shmdt()

(3)使用实例:

下面的程序将给出System V共享内存API的使用方法,并对比分析System V共享机制与mmap()映射普通文件实现共享内存之间的差别。首先给出两个进程经过System V共享内存通讯的范例:

/***** testwrite.c *******/
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct{
    char name[4];
    int age;
} people;
main(int argc, char** argv)
{
    int shm_id,i;
    key_t key;
    char temp;
    people *p_map;
    char* name = "./test";
    key = ftok(name,0);
    if(key==-1)
        perror("ftok error");
    shm_id=shmget(key,4096,IPC_CREAT);  
    if(shm_id==-1)
    {
        perror("shmget error");
        return;
    }
    p_map=(people*)shmat(shm_id,NULL,0);
    temp='a';
    for(i = 0;i<10;i++)
    {
        temp+=1;
        memcpy((*(p_map+i)).name,&temp,1);
        (*(p_map+i)).age=20+i;
    }
    if(shmdt(p_map)==-1)
        perror(" detach error ");
}
/********** testread.c ************/
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct{
    char name[4];
    int age;
} people;
main(int argc, char** argv)
{
    int shm_id,i;
    key_t key;
    people *p_map;
    char* name = "./test";
    key = ftok(name,0);
    if(key == -1)
        perror("ftok error");
    shm_id = shmget(key,4096,IPC_CREAT);    
    if(shm_id == -1)
    {
        perror("shmget error");
        return;
    }
    p_map = (people*)shmat(shm_id,NULL,0);
    for(i = 0;i<10;i++)
    {
    printf( "name:%s\n",(*(p_map+i)).name );
    printf( "age %d\n",(*(p_map+i)).age );
    }
    if(shmdt(p_map) == -1)
        perror(" detach error ");
}

testwrite.c建立一个系统V共享内存区,并在其中写入格式化数据;testread.c访问同一个系统V共享内存区,读出其中的格式化数据。分别把两个程序编译为testwrite及testread,前后执行./testwrite及./testread则./testread运行结果以下:

经过对试验结果分析,对比系统V与mmap()映射普通文件实现共享内存通讯,能够得出以下结论:

  • 一、 系统V共享内存中的数据,历来不写入到实际磁盘文件中去;而经过mmap()映射普通文件实现的共享内存通讯能够指定什么时候将数据写入磁盘文件中。注:前面讲到,系统V共享内存机制实际是经过映射特殊文件系统shm中的文件实现的,文件系统shm的安装点在交换分区上,系统从新引导后,全部的内容都丢失。
  • 二、 系统V共享内存是随内核持续的,即便全部访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核从新引导以前,对该共享内存区域的任何改写操做都将一直保留。
  • 三、 经过调用mmap()映射普通文件进行进程间通讯时,必定要注意考虑进程什么时候终止对通讯的影响。而经过系统V共享内存实现通讯的进程则否则。

返回目录


5、消息队列

『1.消息队列概述』

消息队列,即一些消息的列表。用户能够从消息队列中添加消息和读取消息等。从这点上看消息队列具备必定的FIFO特性,可是它能够实现消息的随机查询,比FIFO具备更大的优点。同时,消息又是存在于内核中的,由“队列ID”来标识。

『2.消息队列的应用』

(1)函数说明:

消息队列的实现包括建立或打开消息队列、添加消息、读取消息和􁧗制消息队列这4种操做。

  • 建立或打开消息队列:msgget(),这里建立的消息队列的数量会受到系统消息队列数量的限制;
  • 添加消息:msgsnd()函数,它把消息添加到已打开的消息队列;
  • 读取消息:msgrcv(),它把消息从消息队列中取走,与FIFO不一样的是,这里能够指定取走某一条消息;
  • 控制消息队列:msgctl(),它能够完成多项功能。

(2)函数格式:

  • msgget():

  • msgsnd():

  • msgrcv():

  • msgctl():

(3)使用实例:

这里首先介绍一个函数ftok(),它能够根据不一样的路径和关键字产生标准的key。共享内存,消息队列,信号量它们三个都是找一个中间介质,来进行通讯的,而使用ftok()产生一个号,就能够惟一区分这个介质了。

ftok()函数的具体形式以下:

key_t ftok(const char *pathname, int proj_id);

其中参数fname是指定的文件名,这个文件必须是存在的并且能够访问的。id是子序号,它是一个8bit的整数。即范围是0~255。当函数执行成功,则会返回key_t键值,不然返回-1。在通常的UNIX中,一般是将文件的索引节点取出,而后在前面加上子序号就获得key_t的值。

对于该函数,还有如下几点补充说明:

  • pathname是目录仍是文件的具体路径,是否能够随便设置?
    • ftok根据路径名,提取文件信息,再根据这些文件信息及project ID合成key,该路径能够随便设置。
  • pathname指定的目录或文件的权限是否有要求?
    • 该路径是必须存在的,ftok只是根据文件inode在系统内的惟一性来取一个数值,和文件的权限无关。
  • proj_id是否能够随便设定,有什么限制条件?
    • proj_id是能够根据本身的约定,随意设置。这个数字,有的称之为project ID; 在UNIX系统上,它的取值是1到255。

如下是一个简单的验证程序:

#include <stdio.h>    
    #include <sys/sem.h>    
    #include <stdlib.h>   
 
    int main()    
    {    
        key_t semkey;    
        if((semkey = ftok("./test", 1))<0)    
        {    
            printf("ftok failed\n");    
            exit(EXIT_FAILURE);    
        }       
        printf("ftok ok ,semkey = %d\n", semkey);    
        return 0;    
    }

运行结果以下:

言归正传。下面实例体现了如何使用消息队列进行两个进程(发送端和接收端)之间的通讯,包括消息队列的建立、消息发送与读取、消息队列的撤销和删除等多种操做。

/* msgsnd.c 消息队列发送端*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 512
struct message
{
    long msg_type;
    char msg_text[BUFFER_SIZE];
};
int main()
{
    int qid;
    key_t key;
    struct message msg;

    /*根据不一样的路径和关键字产生标准的key*/
    if ((key = ftok(".", 'a')) == -1)
    {
        perror("ftok");
        exit(1);
    }

    /*建立消息队列*/
    if ((qid = msgget(key, IPC_CREAT|0666)) == -1)
    {
        perror("msgget");
        exit(1);
    }
    printf("Open queue %d\n",qid);

    while(1)
    {
        printf("Enter some message to the queue:");
        if ((fgets(msg.msg_text, BUFFER_SIZE, stdin)) == NULL)
        {
            puts("no message");
            exit(1);
        }
        msg.msg_type = getpid();

        /*添加消息到消息队列*/
        if ((msgsnd(qid, &msg, strlen(msg.msg_text), 0)) < 0)
        {
            perror("message posted");
            exit(1);
        }

        if (strncmp(msg.msg_text, "quit", 4) == 0)
        {
            break;
        }
    }
    exit(0);
}
/* msgrcv.c  消息队列接收端*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 512
struct message
{
    long msg_type;
    char msg_text[BUFFER_SIZE];
};
int main()
{
    int qid;
    key_t key;
    struct message msg;

    /*根据不一样的路径和关键字产生标准的key*/
    if ((key = ftok(".", 'a')) == -1)
    {
        perror("ftok");
        exit(1);
    }
    
    /*建立消息队列*/
    if ((qid = msgget(key, IPC_CREAT|0666)) == -1)
    {
        perror("msgget");
        exit(1);
    }
    printf("Open queue %d\n", qid);

    do
    {

        /*读取消息队列*/
        memset(msg.msg_text, 0, BUFFER_SIZE);
        if (msgrcv(qid, (void*)&msg, BUFFER_SIZE, 0, 0) < 0)
        {
            perror("msgrcv");
            exit(1);
        }
        printf("The message from process %d : %s", msg.msg_type, msg.msg_text);
    } while(strncmp(msg.msg_text, "quit", 4));

    /*从系统内核中移走消息队列*/
    if ((msgctl(qid, IPC_RMID, NULL)) < 0)
    {
        perror("msgctl");
        exit(1);
    }
    exit(0);
}

如下是程序的运行结果,输入“quit”则两个进程都结束。

返回目录


参考资料

相关文章
相关标签/搜索