微信
手机版
网站地图

合肥论坛,比例-王者荣耀中的骚走位,游戏走位教程

2019-07-15 14:59:13 投稿人 : admin 围观 : 145 次 0 评论
学习在 Linux 中进程是怎么与其他进程进行同步的。
-- Marty Kalin

本篇是 Linux 下进程间通讯(IPC)系列的榜首篇文章。这个系列将运用 C 言语代码示例来说明以下 IPC 机制:

◈ 同享文件
◈ 同享内存(运用信号量)
◈ 管道(命名的或非命名的管道)
◈ 音讯行列
◈ 套接字
◈ 信号

在聚集上面说到的同享文件和同享内存这两个机制之前,这篇文章将带你回忆一些中心的概念。

中心概念

进程是运转着的程序,每个进程都有着它自己的地址空间,这些空间由进程被答应拜访的内存地址组成。进程有一个或多个履行线程,而线程是一系列履行指令的调集:单线程进程就只要一个线程,而多线程的进程则有多个线程。一个进程中的线程同享各种资源,特别是地址空间。别的,一个进程中的线程可以直接经过同享内存来进行通讯,虽然某些现代言语(例如 Go)鼓舞一种更有序的办法,例如运用线程安全的通道。当然关于不同的进程,默许情况下,它们能同享内存。

有多种办法发动之后要进行通讯的进程,下面所郭源朝举的比如中首要运用了下面的两种办法:

◈ 一个终端被用来发动一个进程,别的一个不同的终端被用来发动另一个。
◈ 在一个进程(父进程)中调用体系函数 fork,以此生发另一个进程(子进程)。

榜首个比如采用了上面运用终端的办法。这些代码示例的 ZIP 压缩包可以从我的网站下载到。

同享文件

程序员对文件拜访应该都现已很熟识了,包含许多坑(不存在的文件、文件权限损坏等等),这些问题困扰着程序对文件的运用。虽然如此,同享文件或许是最为根底的 IPC 机制了。考虑一下下面这样一个相对简略的比如,其间一个进程(生产者 producer)创立和写入一个文件,然后另一个进程(顾客 consumer)从这个相同的文件中进行读取:

  1. writes +-----------+ reads
  2. producer-------->| disk file |<-------consumer
  3. +-----------+

在运用这个 IPC 机制时最显着的应战是竞赛条件或许会发作:生产者和顾客或许恰好在同一时间拜访该文件,然后使得输出成果不确定。为了防止竞赛条件的发作,该文件在处于状况时有必要以某种办法处于被锁状况,然后阻挠在操作履行时和其他操作的抵触。在标准体系库中与锁相关的 API 可以被总结如下:

◈&nbs合肥论坛,份额-王者荣耀中的骚走位,游戏走位教程p;生产者应该在写入文件时取得一个文件的排挤锁。一个排挤锁最多被一个进程所具有。这样就可以排除去竞赛条件的发作,由于在锁被开释之前没有其他的进程可以拜访这个文件。
◈ 顾客应该在从文件中读取内容时得到至少一个同享锁。多个读取者可以一起保有一个同享锁,可是没有写入者可以获取到文件内容,乃至在当只要一个读取者保有一个同享锁时。

同享锁可以提高功率。假设一个进程仅仅读入一个文件的内容,而不去改动它的内容,就没有什么原因阻挠其他进程来做相同的事。但假设需求写入内容,则很显然需求文件有排挤锁。

标准的 I/O 库中包含一个名为 fcntl 的有用函数,它可以被用来查看或许操作一个文件上的排挤锁和同享锁。该函数经过一个文件描述符(一个在进程中的非负整数值)来符号一个文件(在不同的进程中不同的文件描述符或许符号同一个物理文件)。关于文件的确定, Linux 供给了名为 flock 的库函数,它是 fcntl 合肥论坛,份额-王者荣耀中的骚走位,游戏走位教程的一个精简包装。榜首个比如中运用 fcntl 函数来露出这些 API 细节。

示例 1. 生产者程序

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <fcntl.h>
  4. #include <unistd.h>
  5. #define FileName "data.dat"
  6. void report_and_exit(const char* msg) {
  7. [perror][4](msg);
  8. [exit][5](-1); /* EXIT_FAILURE */
  9. }
  10. int main() {
  11. struct flock lock;
  12. lock.l_type = F_WRLCK; /* read/write (exclusive) lock */
  13. lock.l_whence = SEEK_SET; /* base for seek offsets */
  14. lock.l_start = 0合肥论坛,份额-王者荣耀中的骚走位,游戏走位教程; /* 1st byte in file */
  15. lock.l_len = 0; /* 0 here means 'until EOF' */
  16. lock.l_pid = getpid(); /* process id *nh962/
  17. i二战之狂野战兵nt fd; /* file descriptor to identify a file within a process */
  18. if ((fd = open(FileName, O_RDONLY)) < 0) /* -1 signals an error */
  19. report_and_exit("open to rea隐世大神医d failed...");
  20. /* If the file is write-locked, we can't continue. */
  21. fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */
  22. if (lock.l_type != F_UNLCK)
  23. report_and_exit("file is still write locked...");
  24. lock.l_type = F_RDLCK; /* prevents any writing during the reading */
  25. if (fcntl(fd, F_SETLK, &lock) < 0)
  26. report_and_exit("can't get a read-only lock...");
  27. /* Read the bytes (they happen to be ASCII codes) one at a time. */
  28. int c; /* buffer for read bytes */
  29. while (read(fd, &c, 1) > 0) /* 0 signals EOF */
  30. write(STDOUT_FILENO, &c, 1); /* write one byte to the standard output */
  31. /* Release the lock explicitly. */
  32. lock.l_type = F_UNLCK;
  33. if (fcntl(fd, F_SETLK, &lock) < 0)
  34. report_and_exit("explicit unlocking failed...");
  35. close(fd);
  36. return 0;
  37. }

上面生产者程序的首要过程可以总结如下:

◈ 这个程序首要声明晰一个类型为 struct flock 的变量,它代表一个锁,并对它的 5 个域做了初始化。榜首个初始化
  1. lock.l_type = F_WRLCK; /* exclusive lock */

使得这个锁为排挤锁(read-write)而不是一个同享锁(read-only)。假设生产者取得了这个锁,则其他的进程将不可以对文件做读或许写操作,直到生产者开释了这个锁,或许显式地调用 fcntl,又或许隐式地封闭这个文件。(当进程中止时,一切被它翻开的文件都会被主动封闭,然后开释了锁)

◈ 上面的程序接着初始化其他的域。首要的作用是整个文件都将被锁上。可是,有关锁的 API 答应特别指定的字节被上锁。例如,假设文件包含多个文本记载,则单个记载(或许乃至一个记载的一部分)可以被锁,而其余部分不被锁。
◈ 榜首次调用 fcntl
  1. if (fcntl(fd, F_SETLK, &lock) < 0)

测验排挤性地将文件锁住,并查看调用是否成功。一般来说, fcntl 函数回来 -1 (因而小于 0)意味着失利。第二个参数 F_SETLK 意味着 fcntl 的调用不是堵塞的;函数当即做回来,要么取得锁,要么显现失利了。假设替换地运用 F_SETLKW(结尾的 W代指等候),那么对 fcntl 的调用将是堵塞的,直到有或许取得锁的时分。在调用抗战之虎头山大队 fcntl 函数时,它的榜首个参数 fd 指的是文件描述符,第二个参数指定了即将采纳的动作(在这个比如中,F_SETLK 指代设置锁),第三个参数为锁结构的地址(在本例中,指的是javbuy &lock)。

◈ 假设生产者取得了锁,这个程序将向文件写入两个文本记载。
◈ 在向文件写入内容后,生产者改动锁结构中的 l_type 域为 unlock 值:
  1. lock.l_type = F_UNLCK;

并调用 fcntl 来履行解锁操作。最终程序封闭了文件并退出。

示例 2. 顾客程序

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <fcntl.h>
  4. #include <unistd.h>
  5. #define FileName "data.dat"
  6. void report_and_exit(const char* msg) {
  7. [perror][4](msg);
  8. [exit][5](-1); /* EXIT_FAILURE */
  9. }
  10. int main() {
  11. struct flock lock;
  12. lock.l_type = F_WRLCK; /* read/write (exclusive) lock */
  13. lock.l_whence = SEEK_SET; /* base for seek offsets */
  14. lock.l_start = 0; /* 1st byte in file */
  15. lock.l_len = 0; /* 0 here means 'until EOF' */
  16. lock.l_pi飞向甲子园d = getpid(); /* process id */
  17. int fd; /* file descriptor to identify a file within a process */
  18. if ((fd = open(FileName, O_RDONLY)) < 0) /* -1 signals an error */
  19. report_and_exit("open to read failed...");
  20. /* If the file is write-locked, we can't continue. */
  21. fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */
  22. if (lock.l_type != F_UNLCK)
  23. report_and_exit("file is still write locked...");
  24. lock.l_type = F_RDLCK; /* prevents any writing during the reading */
  25. if (fcntl(fd, F_SETLK, &lock) < 0)
  26. report_and_exit("can't get a read-only lock...");
  27. /* Read the bytes (they happen to be ASCII codes) one at a time. */
  28. int c; /* buffer for read bytes */
  29. while (read(fd, &c, 1) > 0) /* 0 signals EOF */
  30. write(STDOUT_FILENO, &合肥论坛,份额-王者荣耀中的骚走位,游戏走位教程amp;c, 1); /* write one byte to the standard output */
  31. /* Release the lock explicitly. */
  32. lock.l_type = F_UNLCK;
  33. if (fcntl(fd, F_SETLK, &lock) < 0)
  34. report_and_exit("explicit unlocking failed...");
  35. close(fd);
  36. return 0;
  37. }

比较于锁的 API,顾客程序会相对凌乱一点儿。特别的,顾客程序首要查看文件是否被排挤性的被锁,然后才测验去取得一个同享锁。相关的代码为:

  1. lock.l_type = F_WRLCK;
  2. ...
  3. fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_卡加加UNLCK if no write lock */
  4. if (lock.l_type != F_UNLCK)
  5. report_and_exit("file is still write locked...");

在 fcntl 调用中的 F_GETLK 操作指定查看一个锁,在本例中,上面代码的声明中给了一个 F_WRLCK 的排挤锁。假设特指的锁不存在,那么 fcntl 调用将会主动地改动锁类型域为 F_UNLCK 以此来显现当时的状况。假设文件是排挤性地被锁,那么顾客将会中止。(一个更强健的程序版别或许应该让顾客会儿,然后再测验几回。)

假设当时文件没有被锁,那么顾客将测验获取一个同享(read-only)锁(F_RDLCK)。为了缩短程序,fcntl 中的 F_GETLK 调用可以丢掉,由于假设其他进程现已保有一个读写锁,F_RDLCK 的调用就或许会失利。从头调用一个只读锁可以阻挠其他进程向文件进行写的操作,但可以答应其他进程凶恶魔咒对文件进行读取。简而言之,同享锁可以被多个进程所保有。在获取了一个同享锁后,顾客程序将当即从文件中读取字节数据,然后在标准输出中打印这些字节的内容,接着开释锁,封闭文件并中止。

下面的 % 为命令行提示符,下面展现的是从相同终端敞开这两个程序的输出:

  1. % ./producer
  2. Process 29255 has written to data file...
  3. % ./consumer
  4. Now is the winter of our discontent
  5. Made glorious summer by this sun of York

在本次的代码示例中,经过 IPC 传输的数据是文本:它们来自莎士比亚的戏曲《理查三世》中的两行台词。可是,同享文件的内容还可以是纷繁凌乱的,恣意的字节数据(例如一个电影)都可以,这使得文件同享变成了一个十分灵敏的 IPC 机制。但它的缺陷是文件获取速度较慢,由于文件的获取涉及到读或许写。同往常相同,编程总是伴随着折中。下面的比如将经过同享内存来做 IPC,而不是经过同享文件,在功能上相应的有极大的提高。

同享内存

关于同享内存,Linux 体系供给了两类不同的 API:传统的 System V API 和更新一点的 POSIX API。在单个运用中,这些 API 不能混用。可是,POSIX 办法的一个害处是它的特性仍在发展中,并且依赖于装置的内核版别,这十分影响代码的可移植性。例如,默许情况下,POSIX API 用内存映射文件来完成同享内存:关于一个同享的内存段,体系为相应的内容保护一个备份文件。在 POSIX 标准下同享内存可以被装备为不需求备份文件,但这或许会影响可移植性。我的比如中运用的是带有备份文件的 POSIX API,这既结合了内存获取的速度优势,又取得了文件存储的耐久性。

下面的同享内存比如中包含两个程序,别离名为 memwriter 和 memreader,并运用信号量来调整它们对同享内存的获取。在任何时分当同享内存进入一个写入者场景时,无论是多进程仍是多线程,都有遇到根据内存的竞赛条件的危险,所以,需求引进信号量来和谐(同步)对同享内存的获取。

memwriter 程序应当在它自己所在的终端首要发动,然后 memreader 程序才可以在它自己所在的终端发动(在接着的十几秒内)。memreader 的输出如下:

  1. This is the way the world ends...

在每个源程序的最上方注释部分都解说了在编译它们时需求添加的链接参数。

首要让咱们温习一下信号量是怎么作为一个同步机制作业的。一般的信号量也被叫做一个计数信号量,由于带有一个可以添加的值(一般初始化为 0)。考虑一家租借自行车的商铺,在它的库存中有 100 辆自行车,还有一个供职工用于租借的程序。每逢一辆自行车被租出去,信号量就添加 1;当一辆自行车被还回来,信号量就减 1。在信号量的值为 100 之前都还可以进行租借事务,但假设等于 100 时,就有必要中止事务,直到至少有一辆自行车被还回来,然后信号量减为 99。

二元信号量是一个特例,它只要两个值:0 和 1。在这种情况下,信号量的表现为互斥量(一个互斥的结构)。下面的同享内存示例将把信号量用作互斥量。当信号量的值为 0 时,只要 memwriter 可以获取同享内存,在写操作完成后,这个进程将添加信号量的值,然后答应 memreader 来读取同享内存。

示例 3. memwriter 进程的源程序

  1. /** Compilation: gcc -o memwriter memwriter.c -lrt -lpthread **/
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <sys/mman.h>
  5. #include <sys/stat.h>
  6. #include <fcntl.h>
  7. #include <unistd.h>
  8. #include <semaphore.h>
  9. #include <string.h>
  10. #include "shmem.h"
  11. void report_and_exit(const char* msg) {
  12. [p女生体罚error][4](msg);
  13. [exit][5](-1);
  14. }
  15. int main() {
  16. int fd = shm_open(BackingFile, /* name from smem.h */
  17. O_RDWR | O_CREAT, /* read/write, create if needed */
  18. dj热舞 AccessPerms); /* access permissions (0644) */
  19. if (fd < 0) report_and_exit("Can't open shared mem segment...");
  20. ftruncate(fd, ByteSize); /* get the bytes */
  21. caddr_t memptr = mmap(NULL, /* let system pick where to put segment */
  22. ByteSize, /* how many bytes */
  23. PROT_READ | PROT_WRITE, /* access protections */
  24. MAP_SHARED, /* mapping visible to other processes */
  25. fd, /* file descriptor */
  26. 0); /* offset: start at 1st byte */
  27. if ((caddr_t) -1 == memptr) report_and_exit("Can't get segment...");
  28. [fprintf][7](stderr, "shared mem address: %p [0..%d]\n", memptr, ByteSize - 1);
  29. [fprintf][7](stderr, "backing file: /dev/shm%s\n", BackingFile );
  30. /* semahore code to lock the shared mem */
  31. sem_t* semptr = sem_open(SemaphoreName, /* name */
  32. O_CREAT, /* create the semaphore */
  33. AccessPerms, /* protection perms */
  34. 0); /* initial value */
  35. if (semptr == (void*) -1) report_and_exit("sem_open");
  36. [strcpy][8](memptr, MemContents); /* copy some ASCII bytes to the segment */周杰伦女儿姓名
  37. /* increment the semaphore so that memreader can read */
  38. if (sem_post(semptr) < 0) report_and_exit("sem_post");
  39. sleep(12); /* give reader a chance */
  40. /* clean up */
  41. munmap(memptr, ByteSize); /* unmap the storage */
  42. close(fd);
  43. sem_close(semptr);
  44. shm_unlink(BackingFile); /* unlink from the backing file */
  45. return 0;
  46. }

下面是 memwriter 和 memreader 程序怎么经过同享内存来通讯的一个总结:

◈ 上面展现的 memwriter 程序调用 shm_open 函数来得到作为体系和谐同享内存的备份文件的文件描述符。此刻,并没有内存被分配。接下来调用的是令人误解的名为 ftruncate 的函数
  1. ftrSaivianuncate(fd, ByteSize); /* get the bytes */

它将分配 ByteSize 字节的内存,在该情况下,一般为巨细适中的 512 字节。memwriter 和 memreader 程序都只从同享内存中获取数据,而不是从备份文件。体系将担任同享内存和备份文件之间数据的同步。

◈ 接着 memwriter 调用 mmap 函数:
  1. caddr_t memptr = mmap(NULL, /* let system pick where to put segment */
  2. ByteSize, /* how many bytes */
  3. PROT_READ | PROT_WRITE, /* access protections */
  4. MAP_SHARED, /* mapping visible to other processes */
  5. fd, /* file descriptor */
  6. 0); /* offset: start at 1st byte */

来取得同享内存的指针。(memreader 也做一次相似的调用。) 指针类型 caddr_t以 c 最初,它代表 calloc,而这是动态初始化分配的内存为 0 的一个体系函数。memwriter 经过库函数 strcpy(字符串仿制)来获取后续操作的 memptr

◈ 到现在为止,memwriter 现已准备好进行写操作了,但首要它要创立一个信号量来保证同享内存的排挤性。假设 mem六合游身尺writer 正在履行写操作而一起 memre无敌牧场主ader 在履行读操作,则有或许呈现竞赛条件。假设调用 sem_open 成功了:
  1. sem_t* semptr = sem_open(SemaphoreName, /* name */
  2. O_CREAT, /* create the semaphore */
  3. 骚医师 AccessPerms, /* protection perms */
  4. 0); /* initial value */

那么,接着写操作便可以履行。上面的 SemaphoreName(恣意一个仅有的非空称号)用来在 memwriter 和 memreader 辨认信号量。初始值 0 将会传递给信号量的创立者,在这个比如中指的是 memwriter 赋予它履行操作的权力。

◈ 在写操作完成后,memwriter* 经过调用sem_post` 函数将信号量的值添加到 1:
  1. if (sem_post(semptr) < 0) ..

添加信号了将开释互斥锁,使得 memreader 可以履行它的操作。为了更好地丈量,memwriter 也将从它自己的地址空间中撤销映射,

  1. munmap(memptr, ByteSize); /* unmap the storage *

这将使得 memwriter 不能进一步地拜访同享内存。

示例 4. memreader 进程的源代码

  1. /** Compilation: gcc -o memreader memreader.c -lrt -lpthread **/
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <sys/mman.h>
  5. #include <sys/stat.h>
  6. #include <fcntl.h>
  7. #include <unistd.h>
  8. #include <semaphore.h>
  9. #include <string.h>
  10. #include "shmem.h"
  11. void report_and_exit(const char* msg) {
  12. [perror][4](msg);
  13. [exit][5](-1);
  14. }
  15. int main() {
  16. int fd = shm_open(BackingFile, O_RDWR, AccessPerms); /* empty to begin */
  17. if (fd < 0) report_and_exit("Can't get file descriptor...");
  18. /* get a pointer to memory */
  19. caddr_t memptr = mmap(NULL, /* let system pick where to put segment */
  20. ByteSize, /* how many bytes */
  21. PROT_READ | PROT_WRITE, /* access protections */
  22. MAP_SHARED, /* mapping visible to other processes */
  23. fd, /* file descriptor */
  24. 0); /* offset: start at 1st byte */
  25. if ((caddr_t) -1 == memptr) report_and_exit("Can't access segment...");
  26. /* create a semaphore for mutual exclusion */
  27. sem_t* semptr = sem_open(SemaphoreName, /* name */
  28. O_CREAT, /* create the semaphore */
  29. AccessPerms, /* protection perms */
  30. 0); /* initial value */
  31. if (semptr == (void*) -1) report_and_exit("sem_open");
  32. /* use semaphore as a mutex (lock) by waiting for岩台县 writer to increment it */
  33. if (!sem_wait(semptr)) { /* wait until semaphore != 0 */
  34. int i;
  35. for (i = 0; i < [strlen][6](MemContents); i++)
  36. write(STDOUT_F浅笑28猜测ILENO, memptr + i, 1); /* one byte at a time */
  37. sem_post(semptr);
  38. }
  39. /* cleanup */
  40. munmap(memptr, ByteSize);
  41. close(fd);
  42. sem_close(semptr)李小幼;
  43. unlink(BackingFile);
  44. return 0;
  45. }

memwriter 和 memreader 程序中,同享内存的首要着重点都在 shm_open 和 mmap 函数上:在成功时,榜首个调用回来一个备份文件的文件描述符,而第二个调用则运用这个文件描述符从同享内存段中获取一个指针。它们对 shm_open 的调用都很相似,除了 memwriter 程序创立同享内存,而 `memreader 只获取这个现已创立的内存:

  1. int fd = shm_open(BackingFile, O_RDWR | O_CREAT, AccessPerms); /* memwriter */
  2. int fd = shm_open(BackingFile, O_RDWR, AccessPerms); /* memreader */

有了文件描述符,接着对 mmap 的调用便是相似的了:

  1. caddr_t memptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

mmap 的榜首个参数为 NULL,这意味着让体系自己决定在虚拟内存地址的哪个当地分配内存,当然也可以指定一个地址(但很有技巧性)。MAP_SHARED 标志着被分配的内存在进程中是同享的,最终一个参数(在这个比如中为 0 ) 意味着同享内存的偏移量应该为榜首个字节。size 参数特别指定了即将分配的字节数目(在这个比如中是 512);别的的保护参数(AccessPerms)暗示着同享内存是可读可写的。

当 memwriter 程序履行成功后,体系将创立并保护备份文件,在我的体系中,该文件为 /dev/shm/shMemEx,其间的 shMemEx 是我为同享存储命名的(在头文件 shmem.h中给定)。在当时版别的 memwriter 和 memreader 程序中,下面的句子

  1. shm_unlink(BackingFile); /* removes backing file */

将会移除备份文件。假设没有 unlink 这个句子,则备份文件在程序中止后依然耐久地保存着。

memreader 和 memwriter 相同,在调用&nb合肥论坛,份额-王者荣耀中的骚走位,游戏走位教程sp;sem_open 函数时,经过信号量的姓名来获取信号量。但 memreader 随后将进入等候状况,直到 memwriter&合肥论坛,份额-王者荣耀中的骚走位,游戏走位教程nbsp;将初始值为 0 的信号量的值添加。

  1. if (!sem_wait(semptr)) { /* wait until semaphore != 0 */

一旦等候完毕,memreader 将从同享内存中读取 ASCII 数据,然后做些整理作业并中止。

同享内存 API 包含显式地同步同享内存段和备份文件。在这次的示例中,这些操作都被省掉了,避免文章显得凌乱,好让咱们专心于内存同享和信号量的代码。

即便在信号量代码被移除的情况下,memwriter 和 memreader 程序很大几率也可以正常履行而不会引进竞赛条件:memwriter 创立了同享内存段,然后当即向它写入;memreader 不能拜访同享内存,直到同享内存段被创立好。可是,当一个写操作处于混合状况时,最佳实践需求同享内存被同步。信号量 API 满足重要,值得在代码示例中着重强调。

总结

上面同享文件和同享内存的比如展现了进程是怎样经过同享存储来进行通讯的,前者经过文件而后者经过内存块。这两种办法的 API 相对来说都很直接。这两种办法有什么一起的缺陷吗?现代的运用常常需求处理流数据,并且是十分大规模的数据流。同享文件或许同享内存的办法都不能很好地处理大规模的流数据。依照类型运用管道会愈加适宜一些。所以这个系列的第二部分将会介绍管道和音讯行列,相同的,咱们将运用 C 言语写的代码示例来辅佐解说。


via: https://opensource.com/article/19/4/interprocess-communication-linux-storage

作者:Marty Kalin 选题:lujun9972 译者:FSSlc 校正:wxy

本文由 LCTT&nbs合肥论坛,份额-王者荣耀中的骚走位,游戏走位教程p;原创编译,Linux我国 荣誉推出


相关文章

标签列表