Posted on 2015-12-01 11:30:27 os
在本文的叙述中,会交替使使用进程地址空间、虚拟内存空间、虚拟内存,这些都代表了进程的地址空间。在一般意义上,它是逻辑上存在的,实际会因为触发缺页异常,导致内核为进程在物理地址空间中分配实际的存储空间。
在unix/linux平台下读写文件,一般有两种方式。第一种是首先open文件,接着使用read系统调用读取文件的全部或一部分。于是内核将文件的内容从磁盘上读取到内核页高速缓冲,再从内核高速缓冲读取到用户进程的地址空间。这么做需要在内核和用户空间之间做四次数据拷贝。而且当多个进程同时读取一个文件时,则每一个进程在自己的地址空间都有这个文件的副本,这样也造成了物理内存的浪费。如下图所示:
第二种方式是使用内存映射的方式。首先open文件,接着调用mmap系统调用,将文件的内容的全部或一部分直接映射到进程的地址空间,映射完成后,进程可以像访问普通内存一样做memcpy等操作,不必再调用read/write等操作。mmap并不分配物理地址空间,它只是会占用进程的虚拟内存空间。而第一种方式则需要进程预先分配好物理内存,内核才能将页高速缓冲中的文件数据拷贝到用户进程指定的内存空间中。
使用第二种方式,当多个进程同时访问读取一个文件时,每个进程都将文件内容在内核中的页高速缓冲映射到自己的地址空间。当第一个进程访问内核中的页缓冲时,进程的机器指令会触发一个缺页中断。内核将文件的这一页数据读入到页高速缓冲,并更新进程的页表,使页表指向内核缓冲中的这个页。之后有其他进程再次访问同一页时,该页已经在内存中,内核只需要将进程的页表登记并指向内核中的页高速缓冲即可。如下图所示:
相对于mmap,malloc并不是系统调用,而是glibc的库函数。通俗的说,malloc会动态调整进程地址空间中,数据段的brk的指针。
如下图:
具体实现上,malloc有一些优化,它会根据申请内存的大小,使用不同的策略:
分配一块小型内存(小于或等于128kb),malloc()会调用brk()调高断点(brk是将数据段(.data)的最高地址指针_edata往高地址推),分配的内存在堆区域
分配一块大型内存(大于128kb),malloc()会调用mmap2()分配一块内存(mmap是在进程的虚拟地址空间中(一般是堆和栈中间)找一块空闲的)(一般是堆和栈中间区域)。
malloc分配的内存只分配了虚拟地址空间,当在第一次访问的时候,发生缺页中断,操作系统负责分配物理内存,并建立虚拟内存和物理内存之间的映射关系。
下图是IA32平台下进程虚拟地址空间示意图:
其中,brk(program break)的地址即是malloc调整的指针。
mmap函数是unix/linux下的系统调用,函数原型如下:
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *start, size_t length);
参数:
msync函数原型:
#include <sys/mman.h>
int msync ( void * addr , size_t len, int flags)
一般来说,进程在映射空间的对共享内容的更改并不能直接写回到磁盘文件,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。
一般使用mmap实现内存映射有两种方法,第一种是映射文件到进程的虚拟地址空间。第二种是使用匿名内存映射,用于在进程内部使用或者在父子进程之间共享内存。
这个例子是使用mmap映射普通文件,调用mmap完毕后返回映射后的地址空间,读写该内存范围内的地址相当于相应的文件位置:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define PAGE_SIZE 4096
int main(int argc, char**argv) {
int fd;
int i;
unsigned char *p_map;
if (argv[1] == NULL) {
printf("command line is empty!\n");
exit(1);
}
fd = open("test.dat", O_RDWR | O_APPEND | O_CREAT);
if (fd<0) {
printf("open failed!\n");
exit(1);
}
p_map = (unsigned char *)mmap(0, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p_map == MAP_FAILED) {
printf("mmap failed!\n");
goto end;
}
// 读取有效数据
for (i=0;i<PAGE_SIZE;i++) {
if (p_map[i] == 0) {
break;
}
}
printf("length: [%d]\n%s\n", i, p_map);
char * line = argv[1];
printf("line length: %lu\n", strlen(line));
// 复制数据
memcpy(p_map, line, strlen(line));
msync(p_map, strlen(line), MS_SYNC);
close(fd);
end:
munmap(p_map, PAGE_SIZE);
return 0;
}
下面这个例子是使用mmap分配了匿名映射方式,分配了一块虚拟地址空间。调用mmap结束后并不会直接分配内存,而是等到用户代码访问该内存空间的地址时,发生缺页异常,内核捕获到缺页异常后分配页目录和页表,并将页表和实际的物理page相关联,最后更新进程的页表:
#define _GNU_SOURCE /* or _BSD_SOURCE or _SVID_SOURCE */
#include <unistd.h>
#include <sys/syscall.h> /* For SYS_xxx definitions */
#include <sys/types.h>
#include <sched.h>
#include <sys/mman.h>
#include <signal.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>
#define STACK_SIZE 1024*1024*1 //1M
int thread_func(void *lparam) {
char * str = "this is temp string";
char * str1 = "this is temp string";
printf("thread id %d \n", (int)syscall(SYS_gettid));
printf("thread get param : %d \n", (*(int*)lparam));
printf("stack address: %p\n", str);
sleep(30);
return 0;
}
void child_handler(int sig) {
printf("I got a SIGCHLD\n");
}
int main(int argc, char **argv) {
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGCHLD, child_handler);
//signal(SIGUSR1, SIG_IGN);
void *pstack = (void *)mmap(NULL,
STACK_SIZE,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_ANON | MAP_GROWSDOWN ,
-1,
0);
if (MAP_FAILED != pstack) {
int ret;
//printf("stack addr : 0x%X\n", (int)pstack);
printf("stack addr : %p\n", pstack);
/*
CLONE_VM (0x100) - tells the kernel to let the original process and the clone in the same memory space;
CLONE_FS (0x200) - both get the same file system information;
CLONE_FILES (0x400) - share file descriptors;
CLONE_SIGHAND (0x800) - both processes share the same signal handlers;
CLONE_THREAD (0x10000) - this tells the kernel, that both processes would belong to the same thread group (be threads within the same process);
*/
int arg = 1234;
ret = clone(thread_func,
(void *)((unsigned char *)pstack + STACK_SIZE),
CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND |CLONE_THREAD |SIGCHLD,
//CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND |SIGCHLD,
(void *)&arg);
if (-1 != ret)
{
printf("PID: %d\n", getpid());
pid_t pid = 0;
printf("start thread %d \n", ret);
sleep(60);
//pid = waitpid(-1, NULL, __WCLONE | __WALL);
printf("child : %d exit %s\n", pid,strerror(errno));
} else {
printf("clone failed %s\n", strerror(errno) );
}
} else {
printf("mmap() failed %s\n", strerror(errno));
}
return 0;
}
程序输出内容为:
其中stack addr为主线程栈空间的地址,我们在/proc/1250/smaps中可以发现下面的内容:
stack addr的地址0x7ffc86015df8正好在这个范围内。
stack address 1是子线程的虚拟内存空间,大小为1M:
代码中出现的clone是经过glibc封装的,glibc中clone实现如下:
#include <sysdep.h>
#define _ERRNO_H 1
#include <bits/errno.h>
#include <asm-syntax.h>
/* int clone(int (*fn)(void *arg), void *child_stack, int flags, void *arg); */
.text
ENTRY(__clone)
/* Sanity check arguments. */
/* 参数检查 */
movl $-EINVAL,%eax
// 保存fn参数到ecx
movl 4(%esp),%ecx /* no NULL function pointers */
// exc为0则跳转
jecxz SYSCALL_ERROR_LABEL
// 保存child_stack参数到ecx
movl 8(%esp),%ecx /* no NULL stack pointers */
// exc为0则跳转
jecxz SYSCALL_ERROR_LABEL
/* Insert the argument onto the new stack. */
// 将参数插入到child_stack中
// ecx减8
subl $8,%ecx
// 保存arg参数到eax
movl 16(%esp),%eax /* no negative argument counts */
// 移动eax到child_stack+4
movl %eax,4(%ecx)
/* Save the function pointer as the zeroth argument.
It will be popped off in the child in the ebx frobbing below. */
// 保存fn参数到eax
movl 4(%esp),%eax
// 移动eax到child_stack
movl %eax,0(%ecx)
/* Do the system call */
// 执行系统调用
// eax保存的是系统调用号
// ebx保存的是flags参数
// exc保存的是child_stack地址
// 以及压入child_stack地址中的fn和arg参数
pushl %ebx // ebx入栈
movl 16(%esp),%ebx
movl $SYS_ify(clone),%eax
int $0x80
popl %ebx // ebx出栈
// 判断系统调用返回值,为0是子线程
// 内核生成子线程后
// 由子线程在用户态调用fn指定的函数地址
test %eax,%eax
// 小于0说明有错误发生
jl SYSCALL_ERROR_LABEL
jz thread_start
L(pseudo_end):
ret
thread_start:
subl %ebp,%ebp /* terminate the stack frame */
// ebx中的值是ENTRY宏保存的第一个参数即fn的地址
call *%ebx
#ifdef PIC
call L(here)
L(here):
popl %ebx
addl $_GLOBAL_OFFSET_TABLE_+[.-L(here)], %ebx
#endif
pushl %eax
// 子线程执行完毕后退出
call JUMPTARGET (_exit)
PSEUDO_END (__clone)
weak_alias (__clone, clone)
在内核空间中申请内存,由于内核代码优先级最高并且内核信任自身,所以会被立刻执行。而相对于内核来说,进程中的内存申请总是被尽量推出,并且内核不信任用户进程,内核需要捕获用户进程中对内存地址访问产生的异常,例如读写权限不足,以及请求的页不存在。
举例来说,当内核载入用户可执行文件时,用户进程并不一定立刻访问所有的代码页。类似的,当用户进程执行malloc()获取内存时,并不意味着进程很快就会访问所有获取的内存。
在x86下,进程的线性地址空间由两部分构成,0~3G为用户空间,3~4G为内核态空间。由于内核为每个进程分配了不同的页目录表,所以每个进程的用户线性地址空间相同。而所有进程的3~4G内核态空间都相同。并且内核可以通过增加或删除线性地址区间来动态的修改进程的线性地址空间。
下面的情况会进程会获得新的线性地址空间:
下面是一些和创建、删除线性区相关的系统调用:
用户进程经过编译、链接后形成的映象文件有一个代码段和数据段(包括data段和bss段),其中代码段在下,数据段在上。数据段中包括了所有静态分配的数据空间,即全局变量和所有申明为static的局部变量,这些空间是进程所必需的基本要求,这些空间是在建立一个进程的运行映像时就分配好的。除此之外,堆栈使用的空间也属于基本要求,所以也是在建立进程时就分配好的,如下图:
在内核中,这样每个区域用一个结构struct vm_area_struct
来表示.它描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍。可以使用cat /proc/maps
来查看一个进程的内存使用情况,pid是进程号.其中显示的每一行对应进程的一个vm_area_struct结构.
下面是struct vm_area_struct结构体的定义:
#include <linux/mm_types.h>
/* This struct defines a memory VMM memory area. */
struct vm_area_struct {
struct mm_struct * vm_mm; /* VM area parameters */
unsigned long vm_start;
unsigned long vm_end;
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned long vm_flags;
/* AVL tree of VM areas per task, sorted by address */
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
/* For areas with an address space and backing store,
vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
struct vm_operations_struct * vm_ops;
unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */
struct file * vm_file;
unsigned long vm_raend;
void * vm_private_data; /* was vm_pte (shared mem) */
};
通常,进程所使用到的虚存空间不连续,且各部分虚存空间的访问属性也可能不同。所以一个进程的虚存空间需要多个vm_area_struct
结构来描述。在vm_area_struct
结构的数目较少的时候,各个vm_area_struct
按照升序排序,以单链表的形式组织数据(通过vm_next指针指向下一个vm_area_struct
结构)。但是当vm_area_struct
结构的数据较多的时候,仍然采用链表组织的化,势必会影响到它的搜索速度。针对这个问题,vm_area_struct
还添加了vm_avl_hight(树高)、vm_avl_left(左子节点)、vm_avl_right(右子节点)三个成员来实现AVL树,以提高vm_area_struct
的搜索速度。
假如该vm_area_struct
描述的是一个文件映射的虚存空间,成员vm_file便指向被映射的文件的file结构,vm_pgoff是该虚存空间起始地址在vm_file文件里面的文件偏移,单位为物理页面。
下图是用户进程线性地址空间示意图:
因此,mmap系统调用所完成的工作就是准备这样一段虚存空间,并建立vm_area_struct
结构体,将其传给具体的设备驱动程序.
建立文件映射的第二步就是建立虚拟地址和具体的物理地址之间的映射,这是通过修改进程页表来实现的.mmap方法是file_opeartions结构的成员:
int (*mmap)(struct file *,struct vm_area_struct *);
linux有2个方法建立页表:
(1). 使用remap_pfn_range一次建立所有页表.
int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);
返回值: 成功返回 0, 失败返回一个负的错误值
参数说明: vma 用户进程创建一个vma区域
virt_addr 重新映射应当开始的用户虚拟地址. 这个函数建立页表为这个虚拟地址范围从 virt_addr 到 virt_addr_size.
pfn 页帧号, 对应虚拟地址应当被映射的物理地址. 这个页帧号简单地是物理地址右移 PAGE_SHIFT 位. 对大部分使用, VMA 结构的 vm_paoff 成员正好包含你需要的值.
(2). 使用nopage VMA方法每次建立一个页表项.
struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);
返回值:成功则返回一个有效映射页,失败返回NULL.
参数说明: address 代表从用户空间传过来的用户空间虚拟地址.
(3) 使用方面的限制:
remap_pfn_range不能映射常规内存,只存取保留页和在物理内存顶之上的物理地址。因为保留页和在物理内存顶之上的物理地址内存管理系统的各个子模块管理不到。640 KB 和 1MB 是保留页可能映射,设备I/O内存也可以映射。如果想把kmalloc()申请的内存映射到用户空间,则可以通过mem_map_reserve()把相应的内存设置为保留后就可以。
(1) page cache及swap
cache中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cache或swap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的。
(2) 文件与 address_space结构的对应:
一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个 address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。
(3) 进程调用mmap()时:
只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。
(4) 对于共享内存映射情况
缺页异常处理程序首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区 (swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。
注:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新.
(5) 所有进程在映射同一个共享内存区域:
在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。
可以在/proc/PID/smaps
文件,查看进程的内存映射区域信息。但是使用pmap
可以用更易读的方式输出:
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 2800 1612 0 r-x-- python2.7
00000000008bb000 4 4 4 r---- python2.7
00000000008bc000 468 328 232 rw--- python2.7
0000000000931000 72 68 68 rw--- [ anon ]
00000000018a2000 1796 1684 1684 rw--- [ anon ]
00007fca1bb9a000 212 4 4 r--s- hosts
00007fca1bbcf000 1284 1144 1144 rw--- [ anon ]
00007fca1bd10000 92 48 0 r-x-- datetime.x86_64-linux-gnu.so
00007fca1bd27000 2044 0 0 ----- datetime.x86_64-linux-gnu.so
00007fca1bf26000 4 4 4 r---- datetime.x86_64-linux-gnu.so
00007fca1bf27000 16 16 16 rw--- datetime.x86_64-linux-gnu.so
00007fca1bf2b000 256 256 256 rw--- [ anon ]
00007fca1bf6b000 16 12 0 r-x-- _hashlib.x86_64-linux-gnu.so
00007fca1bf6f000 2044 0 0 ----- _hashlib.x86_64-linux-gnu.so
00007fca1c16e000 4 4 4 r---- _hashlib.x86_64-linux-gnu.so
00007fca1c16f000 4 4 4 rw--- _hashlib.x86_64-linux-gnu.so
00007fca1c170000 1736 924 0 r-x-- libcrypto.so.1.0.0
00007fca1c322000 2044 0 0 ----- libcrypto.so.1.0.0
00007fca1c521000 108 108 108 r---- libcrypto.so.1.0.0
00007fca1c53c000 44 44 44 rw--- libcrypto.so.1.0.0
00007fca1c547000 16 16 16 rw--- [ anon ]
00007fca1c54b000 336 224 0 r-x-- libssl.so.1.0.0
00007fca1c59f000 2048 0 0 ----- libssl.so.1.0.0
00007fca1c79f000 12 12 12 r---- libssl.so.1.0.0
00007fca1c7a2000 28 28 28 rw--- libssl.so.1.0.0
00007fca1c7a9000 32 32 0 r-x-- _ssl.x86_64-linux-gnu.so
00007fca1c7b1000 2044 0 0 ----- _ssl.x86_64-linux-gnu.so
00007fca1c9b0000 4 4 4 r---- _ssl.x86_64-linux-gnu.so
00007fca1c9b1000 4 4 4 rw--- _ssl.x86_64-linux-gnu.so
00007fca1c9b2000 44 12 0 r-x-- libnss_files-2.19.so
00007fca1c9bd000 2044 0 0 ----- libnss_files-2.19.so
00007fca1cbbc000 4 4 4 r---- libnss_files-2.19.so
00007fca1cbbd000 4 4 4 rw--- libnss_files-2.19.so
00007fca1cbbe000 44 16 0 r-x-- libnss_nis-2.19.so
00007fca1cbc9000 2044 0 0 ----- libnss_nis-2.19.so
00007fca1cdc8000 4 4 4 r---- libnss_nis-2.19.so
00007fca1cdc9000 4 4 4 rw--- libnss_nis-2.19.so
00007fca1cdca000 92 20 0 r-x-- libnsl-2.19.so
00007fca1cde1000 2044 0 0 ----- libnsl-2.19.so
00007fca1cfe0000 4 4 4 r---- libnsl-2.19.so
00007fca1cfe1000 4 4 4 rw--- libnsl-2.19.so
00007fca1cfe2000 8 0 0 rw--- [ anon ]
00007fca1cfe4000 36 24 0 r-x-- libnss_compat-2.19.so
00007fca1cfed000 2044 0 0 ----- libnss_compat-2.19.so
00007fca1d1ec000 4 4 4 r---- libnss_compat-2.19.so
00007fca1d1ed000 4 4 4 rw--- libnss_compat-2.19.so
00007fca1d1ee000 1044 116 0 r-x-- libm-2.19.so
00007fca1d2f3000 2044 0 0 ----- libm-2.19.so
00007fca1d4f2000 4 4 4 r---- libm-2.19.so
00007fca1d4f3000 4 4 4 rw--- libm-2.19.so
00007fca1d4f4000 96 8 0 r-x-- libz.so.1.2.8
00007fca1d50c000 2044 0 0 ----- libz.so.1.2.8
00007fca1d70b000 4 4 4 r---- libz.so.1.2.8
00007fca1d70c000 4 4 4 rw--- libz.so.1.2.8
00007fca1d70d000 8 4 0 r-x-- libutil-2.19.so
00007fca1d70f000 2044 0 0 ----- libutil-2.19.so
00007fca1d90e000 4 4 4 r---- libutil-2.19.so
00007fca1d90f000 4 4 4 rw--- libutil-2.19.so
00007fca1d910000 12 8 0 r-x-- libdl-2.19.so
00007fca1d913000 2044 0 0 ----- libdl-2.19.so
00007fca1db12000 4 4 4 r---- libdl-2.19.so
00007fca1db13000 4 4 4 rw--- libdl-2.19.so
00007fca1db14000 1772 684 0 r-x-- libc-2.19.so
00007fca1dccf000 2044 0 0 ----- libc-2.19.so
00007fca1dece000 16 16 16 r---- libc-2.19.so
00007fca1ded2000 8 8 8 rw--- libc-2.19.so
00007fca1ded4000 20 16 16 rw--- [ anon ]
00007fca1ded9000 100 68 0 r-x-- libpthread-2.19.so
00007fca1def2000 2044 0 0 ----- libpthread-2.19.so
00007fca1e0f1000 4 4 4 r---- libpthread-2.19.so
00007fca1e0f2000 4 4 4 rw--- libpthread-2.19.so
00007fca1e0f3000 16 4 4 rw--- [ anon ]
00007fca1e0f7000 140 120 0 r-x-- ld-2.19.so
00007fca1e11a000 1024 1024 1024 rw--- [ anon ]
00007fca1e24b000 788 788 788 rw--- [ anon ]
00007fca1e315000 16 16 16 rw--- [ anon ]
00007fca1e319000 4 4 4 r---- ld-2.19.so
00007fca1e31a000 4 4 4 rw--- ld-2.19.so
00007fca1e31b000 4 4 4 rw--- [ anon ]
00007ffdf51c3000 132 32 32 rw--- [ stack ]
00007ffdf51ef000 8 4 0 r-x-- [ anon ]
ffffffffff600000 4 0 0 r-x-- [ anon ]
---------------- ------- ------- -------
total kB 45736 9656 5624
[anon]
表示匿名内存映射[stack]
表示进程的栈Resident Set Size
的缩写,是真实使用的物理内存大小r-x--
表示内存可读可执行,r---
表示只读,一般是代码页的映射,rw---
一般是数据段的映射。พบกับ pgslot auto เกมสล็อตที่หลากหลายทั้งกราฟิกสวยงามและฟีเจอร์ที่น่าตื่นเต้น เล่นง่าย ทำกำไรได้จริง! สมัครฟรี เครดิต100%
ทางเข้าเล่นสล็อตออนไลน์ ใหม่มาแรง แนะนำเว็บเดิมพันอันดับหนึ่งมาแรงที่สุดในตอนนี้ situs judi slot สมัครเล่นได้อย่างจุใจ รวมเกมสล็อต มากกว่า 1,000 เกม ที่นี่เท่านั้น
ทดลองเล่นฟรี pg ไม่ต้องลงทุนก่อนก็สามารถทดลองเล่นเกมสล็อตสุดมันส์ได้ทันที คลิกที่นี่ wq888.live สัมผัสความสนุกและฝึกฝนทักษะของคุณก่อนลงเดิมพันจริง!
เข้าร่วมสนุกกันเลย! ลงทะเบียนตอนนี้และเริ่มเล่น เว็บแทงบอล กับเรา! โอกาสรอคุณอยู่! เข้าร่วมเป็นส่วนหนึ่งของเราวันนี้! สมัครสมาชิกง่าย ๆ และเริ่มแทงบอลกับเรา รับรองว่าคุณจะไม่ผิดหวัง
คลิกที่นี่ https://pgsumo.com/ทดลองเล่นสล็อต/ เว็บสล็อตมาแรง ใหม่ล่าสุด2024 พร้อมให้บริการแล้ววันนี้ เล่นสนุก เล่นง่าย ได้เงินจริง อย่ารอช้า สมัครสมาชิกวันนี้ ลุ้นรับเงินรางวัลก้อนโตมากมาย
คลิกที่นี่ https://pgsumo.com/ทดลองเล่นสล็อต/ เว็บสล็อตมาแรง ใหม่ล่าสุด2024 พร้อมให้บริการแล้ววันนี้ เล่นสนุก เล่นง่าย ได้เงินจริง อย่ารอช้า สมัครสมาชิกวันนี้ ลุ้นรับเงินรางวัลก้อนโตมากมาย
สล็อต pg เว็บตรง แตกหนัก No.1 เว็บเดิมพันมาแรง รวมทุกค่ายในเว็บเดียว ทดลองเล่นฟรีทุกค่ายเกม มาพร้อมกับระบบฝาก-ถอน ไม่มีขั้นต่ำ สามารถวางเดิมพันได้อย่างไม่มีขีดจำกัด
สล็อต pg เว็บตรง แตกหนัก No.1 เว็บเดิมพันมาแรง รวมทุกค่ายในเว็บเดียว ทดลองเล่นฟรีทุกค่ายเกม มาพร้อมกับระบบฝาก-ถอน ไม่มีขั้นต่ำ สามารถวางเดิมพันได้อย่างไม่มีขีดจำกัด
CREATE BLACK LINKSs FOR ( GAM-BLING!! ) AND NUMBER 1 SEO… TELEGRAM - @pheonnixxx sexy baccarat