`

Linux C 编程实战--阅读笔记(原创)

 
阅读更多

           LinuxC编程实战--阅读笔记(原创)


由  王宇 原创并发布


第一篇Linux和C编程基础 (参考C相关书籍 )

   第1章Linux系统概述

    第2章C编程基础、Vi和Emacs编辑器

        2.1C程序的结构

        2.2C语言的基本数据类型

        2.3运算符和表达式

        2.4标准输入输出函数

        2.5VI编辑器的使用

        2.6Emacs编辑器的使用

        2.7命名规范

            2.7.1标识符

                在程序设计中,变量名、函数名、数组名等统称为标识符

                标识符的第一个字符必须是字母或者下划线,不能是数字

            2.7.2关键字

            2.7.3命名规范

                1、变量名必须有一定的意义,并且意义准确。例如:number_of_book

                2、不建议大小写混用,例如:不建议int nCount

                3、在失去意义的情况下,尽量使用较短的变量名

                4、不采用匈牙利命名法表示变量的类型

                5、函数名应该以动词开头,因为函数是一组具有特定功能的语句块

                6、尽量避免使用全局变量

        2.8面试题选与实践精讲

    第3章C程序控制结构和gcc编辑器

        3.1C程序的控制结构

        3.2分支控制结构

        3.3循环控制结构

        3.4gcc编译器

        3.5面试题选与实践精讲

    第4章C函数、数组、指针和调试器gdb

        4.1函数

        4.2数组

        4.3指针

        4.4字符串函数

        4.5调试器gdb

    第5章C语言预处理、结构体和make的使用

        5.1C语言处理命令

            5.1.1宏定义

                1、无参数宏

                2、有参数宏,注意:

                    宏名与形参表的圆括号之间不能有空格,否则会导致错误

                    在宏定义中,字符串内的形式参数最好用括号括起来,以避免错误

                    有参数宏的形式参数不是变量,不分配内存空间,无需说明数据类型

                    预处理器程序认为有参数宏的实参是字符串,并用它去替换形参

                    使用宏的次数较多时,宏替换后源程序一般会变成。而函数调用不会使程序变长。宏替换不会占用运行时间,只是编译的时间稍微变长一点。而函数调用则会占用运行时间。一般用宏来代表一些较为简单的表达式比较合适

            5.1.2文件包含

            5.1.3条件编译

                实际上,不用条件编译而直接使用if...else语句也可以达到要求。采用条件编译,可以减少被编译的语句,从而减少可执行程序的长度,缩短程序运行时间。当条件编译的程序段比较多时,可执行程序的长度可以大大减少

        5.2结构体和共用体

        5.3位运算

        5.4make的使用和Makefile的编写

第二篇Linux系统编程

    第6章文件操作

        6.1系统编程概述

            Linux下的系统编程是指程序员使用系统调用或C语言本身所携带的函数库来设计和编写具有某一特定功能的程序

            实际上,C语言的函数库也是通过系统调用来实现的,他封装了系统调用,并在此基础上为方便程序员使用而增加了一些功能

            这些系统调用都是以函数库的方式提供的

            对于gcc不会自动连接的库,则在编译程序是需要制定所使用的库(编译程序时使用-l<库名>-L<库所在的目录>选项)

            shell命令行下使用man命令来查看函数原型

                有些函数名如mkdir既是Linux的命令,也是系统调用,这时可以通过输入man2mkdir获得该函数的原型

                对于库函数,输入man3<库函数名>可以获得帮助信息

        6.2Linux的文件结构

            文件主要包含两方面的内容:

                一是文件本身所包含的数据

                二是文件的属性,也称为元数据

            目录也是一种文件,称为目录文件

            文件结构:

                /bin用于存放普通用户可执行的命令

                /boot Linux的内核级启动系统时所需要的文件

                /dev设备文件的存储目录,如硬盘、光驱等

                /etc用于存放系统的配置文件

                /home普通用户的主目录

                /lib用于存放各种库文件

                /proc该目录是一个虚拟文件系统,只有在系统运行时才存在

                /root超级用户root的主目录

                /sbin存放的适用于管理系统的命令

                /tmp临时文件目录

                /usr用于存放系统应用程序及相关文件,如说明文档、帮助文档等

                /var用于存放系统中经常变化的文件,如日志文件,用户邮件等

            6.2.1Linux的文件系统模型

                对于物理磁盘的访问则有两种途径:

                    一种是通过系统旭东本身提供的接口(带来很大的不稳定性)

                    另一种是通过虚拟文件系统(Virtual Filesystem,VFS)

                        VFS是虚拟的,不存在,他和前面都是只存在于内存而不存在于磁盘之中的提到的/proc文件系统一样,都是只存在于内存而不存在于磁盘之中的,即只有在系统运行起来以后才存在

                        VFS提供一种机制,它将各种不同的文件系统整合在一起,并提供统一的应用程序编程接口(API)供上层的应用程序使用

                        VFS的使用体现了Linux文件系统最大的特点---支持多种不同的文件系统,如:ext2、ext3,也支持windowsvfat

            6.2.2文件的分类

                普通文件“-”表示

                目录文件“d”表示

                字符特殊文件

                块特殊文件“b”

                FIFO:这种类型文件用于进程间的通信,也称为命名管道

                套接字(soket):主要用于网络通信

                符号连接:指向另一个文件,是另一个文件的引用

            6.2.3文件的访问权限控制:r=4 w=2 x=1     4+2+1=7,777

        6.3文件的输入输出

            6.3.1文件的创建、打开与关闭

                open函数

                create函数

                close函数


            6.3.2文件的读写

                read函数

                write函数


            6.3.3文件读写指针的移动**

                lseek函数

            6.3.4dup、dup2、fcntl、ioctl系统调用

                1、dup、dup2函数:用来复制文件描述符

                2、fcntl函数:用来对已打开的文件描述符进行各种控制操作以打开文件的各种属性件描述符

                3、ioctl函数:用来控制设备

        6.4文件属性操作

            6.4.1获取文件属性

                函数:stat fstat lstat通过man2stat查看详细信息

                structs tat*but结构体,说明文件状态信息

            6.4.2设置文件属性

        6.5文件的移动和删除

            6.5.1文件的移动 :rename()

            6.5.2文件的删除 :unlink()remove()

        6.6目录操作

            6.6.1目录的创建和删除

                1、目录的创建:mkdir()

                2、目录的删除:rmdir()

            6.6.2获取当前目录

                getcwd()  get_current_dir_name()  getwd()

            6.6.3设置工作目录

                chdir()  fhdir()

            6.6.64获取目录信息

                1、opendir

                2、readdir

                3、closedir

        6.7编程实践:实现自己的ls命令

    第7章进程控制

        7.1进程概述

            7.1.1Linux进程

                1、进程概念

                2、进程标识:ID

                3、进程的结构:代码段;数据段;堆栈段

                4、Linux进程状态

                    运行状态

                    可中断等待状态

                    不可中断等待状态

                    僵死状态

                    停止状态

            7.1.2进程控制:创建进程、执行新程序、推出进程以及改变进程优先级等

                fork:创建进程

                exit:终止进程

                exec:执行一个应用程序

                wait:将父进程挂起,等待子进程终止

                getid:获取当前进程的进程ID

                nice:改变进程的优先级


            7.1.3进程的内存映像

                1、Linux下程序转化进程

                    C程序的生成分为4各阶段:预编译,编译,汇编,连接

                    程序转化为进程的步骤:

                        内核将程序读入内存,为程序分配内存空间

                        内核为该进程分配进程标识符和其他所需资源

                        内核为该进程保存PID及相应的状态信息,把进程放到运行队列中等待执行。程序转化为进城后就可以被操作系统的调度程序调度执行了。

                2、进程的内存映像

                    代码段

                    数据段

                    未初始化数据段

                    堆:程序运行中动态分配的变量

                    栈:用于函数调用,保存函数的返回地址、函数的参数、函数内部定义的局部变量


        7.2进程操作

            7.2.1创建进程

                1、fork函数

                2、孤儿进程

                    如果一个子进程的父进程先于子进程结束,子进程就成为一个孤儿进程,它由init进程收养,称为init进程的子进程

            3、vfork函数

            7.2.2创建守护进程

                编写创建守护进程程序要点:

                    让进程在后台执行:方法是调用fork产生一个子进程,然后使得父进程退出

                    调用setid创建一个新对话期。控制终端,登陆会话和进程组通常师父进程继承下来的,守护进程要拜托它们,不受它们的映像,其方法是调用setid使进程成为一个会话组长

                    禁止进程重新打开控制终端

                    关闭不再需要的文件描述符

                    将当前目录更改为根目录

                    将文件创建时使用的屏蔽字设置为0

                    处理SIGCHLD

            7.2.3进程退出

                正常退出:exit_exit

                异常退出:about函数或是进程收到某个信号,而该信号使程序终止

            7.2.4执行新程序

                execv() execve() execl() execle() execvp() execlp()

            7.2.5等待进程结束

        7.3进程的其他操作

            7.3.1获得进程ID

            7.3.2setuid和setgid

            7.3.3改变进程的优先级


        7.4编程实践:实现自己的myshell

    第8章线程控制

        8.1线程和进程关系
      

         1 地址空间 :每个进程有独立的地址空间,线程共享进程的地址空间。因此创建进程需要的资源多于线程

 

         2 数据空间 :每个进程间有独立的数据空间,线程共享进程的数据空间,所以线程间的通信更加方便和省时

 

         3 系统调用 :由于进程地址空间独立而线程共享地址空间,所以线程间的切换速度远快于进程。


        8.2创建线程

            8.2.1线程创建函数 pthread_create()

            8.2.2线程属性

       8.3线程终止 :phread_exit()


      8.4私有数据:一键多值


                     pthead_key_create(); 创建一个键

                     pthead_setspecific(); 为一个键设置线程私有数据

                     pthead_getspecific(); 从一个键读取线程私有数据

                     pthead_key_delete(); 删除一个键


       8.5线程同步

            线程同步问题:互斥锁、条件变量和异步信号

            8.5.1互斥锁

                互斥锁通过锁机制来实现线程间的同步。在同一时刻它通常只允许一个线程执行一个关键部分的代码

                使用互斥锁前必须先进行初始化操作。两种方法:

                    静态赋值法,将宏结构常量PTHREAD_MUTEX_INITIALIZER赋值互斥锁

                    通过pthread_mutex_init()初始化

                加锁有两个函数:

                    pthread_mutex_lock() 如果mutex已经被锁住,当前尝试加锁的线程就会阻塞

                    phtread_mutex_trylock() mutex已经被加锁,它将立即返回错误码

                用pthread_mutex_unlock() 函数解锁时,要满足两个条件:

                    互斥锁必须处于加锁状态

                    调用本函数的线程必须是给互斥锁加锁的线程,解锁后如果有其他线程在等待互斥锁,等待队列中的第一个线程将获得互斥锁

                当一个互斥锁使用完毕后,不需进行清除:pthread_mutex_destory()

                临界区的两种机制:互斥锁、自旋锁(互斥锁、自旋锁是现实临界区得方法)

                    与自旋锁不同的是,互斥体在进入一个被占用的临界区之前不会原地打转,而是使当前线程进入睡眠状态。

                    .

            8.5.2条件变量

                条件变量是利用线程间共享的全局变量进行同步的一种机制。

            8.5.3异步信号

                一个进程通过信号通知另一个进程发生了某事件,比如该进程所需要的输入数据已经能够就绪。线程同进程一样也可以接受和处理信号,信号也是一种线程间同步的手段。

                多线程中有三个函数处理异步信号:

                    pthread_kill()

                    pthread_sigmask()

                    sigwait();


        8.6出错处理

            软件开发中,如果忽略了出错处理,往往会给软件产品带来毁灭性的后果,因此,在设计和编写程序时,要时刻注意检查错误发生的各种可能

            8.6.1错误检查

            8.6.2错误码

            8.6.3错误的提示信息:strerror()perror()


    第9章信号及信号处理

        9.1Linxu信号介绍

            9.1.1信号的来源

                1、硬件方式

                    当用户在终端上按下某些键时,例如:Ctrl+C

                    硬件异常产生信号

            2、软件方式

                用户在终端下调用kill命令向进程发送任意信号

                进程调用Kill或sigqueue函数发送信号

                当检测到某种软件条件已经具备时发出信号,如alermsettimer

            9.1.2 信号的种类

                一共有64种,详见表9-1

                1、可靠信号与不可靠信号

                    (1号)到(31)号之间的信号都是继承UNIX系统,是不可靠信号;(33号)到(64号)之间的信号是可靠信号,也称为实时信号

                        信号的可靠性是指信号是否会丢失,或者说该信号是否支持排队

                        如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,那么如果信号被递送多次(即信号在未决信号队列里面排队),则称之为可靠信号;只被递送一次的信号称为不可靠信号

                2、信号的优先级

            9.1.3进程对信号的响应

                捕捉信号

                忽略信号

                按照系统默认方式处理

        9.2信号处理

                  9.2.1信号的捕捉和处理

                         1 signal函数

                                     signal函数用来设置进程在接收到信号时的动作

                         2 sigaction函数

                                      sigaction函数可以用来检查或设置进程在接收到信号时的动作

                         3 pause

                                       pause函数使调用进程挂起直至捕捉到一个信号

                  9.2.2信号处理函数的返回

                          1 setjmp/longjmp

                                   使用longjmp可以跳转到setjmp设置的位置

                          2 sigsetjmp/siglongjmp

                                  在信号处理期间自动屏蔽了正在被处理的信号,而使用setjmp/longjmp跳出信号处理程序时又

不会自动将信号屏蔽码,从而引起该信号被永久屏蔽。可以使用sigsetjmp/siglongjmp来解决这个一个问题。

                  9.2.3信号的发送

                              1 kill函数

                                          kill函数用来发送信号给指定的进程

                               2 raise函数

                                           raise函数是ANSIC而非POSIX标准定义的,用来给调用它的进程发送信号

                                3 sigqueue函数

                                          它支持信号带有参数,从而可以与函数sigaction配合使用

                               4 alarm函数

                                            alarm函数可以用来设置定时器

                               5 getitimer/setitimer

                               6 abort函数


                                           abort函数用来向进程发送SIGABRT信号。

                9.2.4信号的屏蔽

                                1信号集

                                           mansigsetops查看原型

                                           函数sigemptyset用来初始化一个信号集,使其不包括任何信号

                                           函数sigfillset用来初始化一个信号集,使其包括所有信号

                                           函数sigaddset用来向set指定的信号集中添加由signum指定的信号

                                            函数sigdelset用来从set指定的信号集中删除由signum指定的信号

                                            函数sigismember用来测试信号signum是否包含在set指定的信号集中

                                  2 信号屏蔽

                                             mansigprocmask查看原型

                                             信号屏蔽又称为信号阻塞

                                             (1)sigprocmask函数

                                                       每个进程都有一个信号屏蔽码,它规定了当前阻塞而不能递送给该进程的信号集。调用函数sigprocmask可以检测或更改进程的信号屏蔽码

                                               (2)sigpending函数

                                                        用来获取调用进程因被阻塞而不能递送和当前未决的信号集。

                                              (3)siguspend函数

                                                       将进程的信号屏蔽码设置为mask,然后与pause函数一样等待信号的发生并执行完信号处理函数   

 

        9.3编程中如何获得帮助

        9.4编程实践:

            实例一:信号的发送与处理

            实例二:信号应用于事件通知

    第10章进程间通信

   10.1进程间通信概述

 

    10.2管道

        10.2.1管道的概念

            管道是一种两个进程间进行单向通信的机制

            数据只能由一个进程流向另一个进程(其中一个写管道,另一个读管道)如果要进行全双工通信,需要

建立两个管道

            管道只能用于父子进程或者兄弟进程间的通信,也就是说管道只能用于具有亲缘关系的进程间的通信,

无亲缘关系的进程不能使用管道

        10.2.2管道的创建与读写

            1、管道的创建

                int pipe(intfd[2])

            2、从管道中读数据

                read()

            3、向管道中写数据

                write()

            4、dup()和dup2() 复制文件描述符的功能

    10.3有名管道

        10.3.1有名管道的概念

            FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储与文件系统中,有名管道

是一个设备文件,因此即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。FIFO按照先

进先出的原则工作。

        10.3.2有名管道的创建与读写

            <sys/stat.h>

            int mknod()

            int mkfifo()

            open()

            read()

            write()


        10.3.2有名管道的应用实例

    10.4消息队列

        10.4.1消息队列的基本概念

            消息队列是一个存放在内核中的消息链表,每个消息队列由消息队列标示符

            消息队列需要用到一些数据结构,熟悉这些数据结构是掌握消息队列的关键

            1消息缓冲结构

                <linux/msg.h>

                struct msgbuf{

                        longmtype;

                        charmtext[1];

                    }

            2msqid_ds内核数据结构

                struct msqid_ds{} 详见:P257

        10.4.2消息队列的创建与读写

            1创建消息队列

                消息队列是随着内核的存在而存在的,每个消息队列在系统范围内对应惟一的键值。要获得一

个消息队列的描述符,只需提供该消息队列的键值即可,该键值通常由函数ftok返回。

                <sys/ipc.h>

                key_t ftok()


                ftok返回值提供给msgget,它根据键值创建一个新的消息队列或者访问一个已存在的消息队列。

                <sys/msg.h>

                int msgget()

            2写消息队列

                函数msgsnd用于向消息队列发送(写)数据

                <sys/msg.h>

                int msgsnd()

            3读消息队列

                <sys/msg.h>

                int msgrcv()


        10.4.3获取和设置消息队列的属性

            消息队列的属性保存在系统维护的数据结构mspid_ds中,用户可以通过函数msgctl设置消息队列的属性   

                <sys/msg.h>

                int msgctl()   

        10.4.4消息队列的应用实例

    10.5信号量

        10.5.1信号量的基本概念

            信号量是一个计数器,常用于处理进程或线程的同步问题,特别是对临界资源访问的同步。临界资源可

以简单地理解为在某一时刻只能由一个进程或线程进行操作的资源,这里的资源可以是一段代码、一个变量或某种硬件资源。信号

量的值大于或等于0时表示可供并发进程使用的资源实体数:小于0时代表正在等待使用临界资源的进程数

        10.5.2信号量的创建与使用

            1信号集的创建或打开

                <sys/sem.h>

                int semget()

            2信号量的操作

                <sys/sem.h>

                PV操作通过调用函数intsemop()

                struct sembuf{}

            3信号集的控制

                <sys/sem.h>

                int semctl();          


        10.5.3信号量的应用实例

    10.6共享内存

        10.6.1共享内存的数据结构

            共享内存就是分配一块能被其他进程访问的内存

                <linux/shm.h>

                struct shmid_ds{}
详见P270

        10.6.2共享内存的创建与操作

            1共享内存区的创建

                <linux/shm.h>

                int shmget()


            2共享内存区的操作

                <linux/shm.h>

                void * shmat()


            3共享内存区的控制

                <linux/shm.h>

                int shmctl()


        10.6.3共享内存的应用实例

    10.7库的创建和使用

        10.7.1Linux库的概念

            库是一种软件组件技术,库里面封装了数据和函数,提供给用户程序调用

            Windows系统本身提供并使用了大量的库,包括静态链接库(.lib文件)和动态链接库(.dll文件)

            Linux库文件名由前缀lib、库名以及后缀3部分组成,其中动态库以.so作为后缀,而静态库通常以.a作

为后缀。

            在程序中使用静态库和动态库时,它们的载入顺序是不一样的。静态库的代码在编译时就拷贝到应用程序中,因此当多个应用程序同时引用一个静态函数时,内存中将会有调用函数的多个副本。动态库是在程序开始运行后调用库函数时才被载入,被调函数在内存中只有一个副本,并且动态库可以在程序运行期间释放动态库所占用的内存,腾出空间供其他程序使用


        10.7.2静态库的创建和使用

            步骤:

                1、在一个头文件中声明静态库所导出的函数

                2、在一个源文件中实现静态库所导出的函数

                3、编译源文件,生成可执行代码

                4、将可执行代码所在目标文件加入到某个静态库中,并将静态库拷贝到系统默认的存放库文件

的目录下

                参见:P276

            Linux下,可以使用ar命令 来创建和修改静态库

        10.7.3动态库的创建和使用

            在Linux环境下,可以很方便地创建和使用动态链接库。只要在编译函数库源程序时加上-shared 选项即可

    10.8进一步学习建议

        《UNIX环境高级编程》《Solaris系统编程》《深入理解Linux内核》《Linux设备驱动程序》



第三篇Linux网络和图形界面编程(参考QT以及TCP/IP相关书籍

第四篇Linux项目实践

分享到:
评论

相关推荐

    Linux笔记全套-最新版.zip

    Linux笔记全套-最新版.zipLinux笔记全套-最新版Linux笔记全套-最新版Linux笔记全套-最新版Linux笔记全套-最新版Linux笔记全套-最新版Linux笔记全套-最新版Linux笔记全套-最新版Linux笔记全套-最新版Linux笔记全套-...

    Linux开发-Artificial-Intelligence-Pb笔记

    Linux开发-Artificial-Intelligence-Pb笔记

    Linux开发-learning-homewo笔记

    Linux开发-learning-homewo笔记

    Linux-韩顺平学习笔记 - PDF-PPT

    Linux课程学习笔记 -韩顺平 包含c/c++/python/java 专项 面试题 PDF PPT 笔记 面试题 (百度网盘链接 永久有效) 自学,做笔记,复习可用

    Java并发编程实战-读书笔记

    《Java并发编程实战》个人读书笔记,非常详细: 1 简介 2 线程安全性 3 对象的共享 4 对象的组合 5 基础构建模块 6 任务执行 7 取消与关闭 8 线程池的使用 9 图形用户界面应用程序 10 避免活跃性危险 11 性能与可...

    S7-200 Smart入门笔记1-8 程序合集

    S7-200 Smart入门笔记1-8 程序合集 S7-200 Smart入门笔记1——流水灯 按钮 S7-200 Smart入门笔记1——流水灯 定时器 S7-200 Smart入门笔记2——读时钟 S7-200 Smart入门笔记3——呼吸灯 S7-200 Smart入门笔记4——...

    兄弟连linux教程1-16章学习笔记(全)

    《兄弟连Linux教程1-16章学习笔记》是一份全面涵盖Linux基础知识的学习资料,特别适合初学者和希望快速提升Linux技能的人群。这个压缩包包含了一系列文本文件和辅助图片,旨在帮助用户深入理解Linux系统的基本操作和...

    《Java 多线程编程核心技术》-- 高洪岩著 阅读笔记

    《Java 多线程编程核心技术》-- 高洪岩著 阅读笔记

    Linux开发ge-Retrieval-m笔记

    Linux开发ge-Retrieval-m笔记

    Linux开发311-Artificial-Intelligence-Project-ma笔记

    Linux开发311-Artificial-Intelligence-Project-ma笔记

    【笔记】谭浩强C语言程序设计--简明笔记.pdf

    《谭浩强C语言程序设计--简明笔记》是一份针对C语言学习者的详细学习资料,涵盖了C语言的基础知识、程序结构、数据类型、运算符和表达式等关键概念。以下是从这份资料中提炼出的重要知识点: ### C语言标准与历史 ...

    Linux及Arm-Linux程序开发笔记

    Linux及Arm-Linux程序开发笔记的知识点涵盖了从基础设置到高级开发过程的方方面面,是专为初学者准备的指南。以下是该笔记中的主要知识点: 一、Arm-Linux程序开发平台简要介绍: 1.1 程序开发所需系统及开发语言:...

    C语言学习整理--翁恺教授慕课笔记

    在C语言的学习中,翁恺教授的慕课是许多初学者和进阶者的重要资源。以下是一些基于标题和描述中的关键知识点的详细说明: 1. **运算符和运算优先级**(第二周-运算优先级):C语言中有多种运算符,包括算术运算符...

    linux及linux-arm程序开发笔记.pdf

    开发语言通常选择C/C++,因为大多数Arm-Linux嵌入式板支持这些编译库。 接下来,文档详细讲解了Linux开发平台的搭建,包括安装虚拟工作站、设置Linux虚拟机、虚拟机的基本配置如共享目录和桌面分辨率的调整,以及...

    Multi-feature-linuxdemo笔记

    Multi-feature-linuxdemo笔记Multi-feature-linuxdemo笔记Multi-feature-linuxdemo笔记Multi-feature-linuxdemo笔记

    Linux系统设计-Linux系统编程笔记

    Linux系统具有高效的网络功能和稳定的性能,因此被广泛应用于服务器领域,Linux是云计算的核心组成部分,被广泛用于构建云平台和云服务。许多知名的云计算服务提供商都采用Linux系统作为其基础架构,一些游戏平台和...

Global site tag (gtag.js) - Google Analytics