Chinaunix首页 | 论坛 | 博客
  • 博客访问: 959488
  • 博文数量: 173
  • 博客积分: 3436
  • 博客等级: 中校
  • 技术积分: 1886
  • 用 户 组: 普通用户
  • 注册时间: 2009-01-07 09:29
文章分类

全部博文(173)

文章存档

2016年(6)

2015年(10)

2014年(14)

2013年(8)

2012年(36)

2011年(63)

2010年(19)

2009年(17)

分类:

2011-05-14 21:41:53

25、Windows驱动程序的同步处理(1)

驱动程序的同步处理

可重入,是指函数的执行结果不和执行顺序有关。同步机制很大程度上依赖于中断请求级。

IRQ编号

设备名称

用途

IRQ0

Tine

计算机系统计时器

IRQ1

KeyBoard

键盘

IRQ2

RedirectI RQ9

IRQ9相接,MPU-401 MDI使用该IRQ

IRQ3

COM2

串口设备

IRQ4

COM1

串口设备

IRQ5

LPT2

建议声卡使用该IRQ

IRQ6

FDD

软驱传输控制用

IRQ7

LPT1

打印机传输控制用

IRQ8

CMOSAlert

即时时钟

IRQ9

RedirectI RQ2

IRQ2相接。可设定给其他硬件使用

IRQ10

Reversed

建议保留给网卡使用该IRQ

IRQ11

Reversed

建议保留给AGP显卡使用

IRQ12

PS/2 Mouse

PS/2鼠标,若无也可以设定给其他硬件使用

IRQ13

FPU

协处理器用,例如FPU(浮点运算器)

IRQ14

Primary IDE

主硬盘传输控制用

IRQ15

Secondary lde

从硬盘传输控制用

PIC中断向量 P220

APIC 高级PIC(Programmable interrupt controller),共24个中断。

1、中断请求级

1)基本概念

Windows 把中断请求级IRQL分成了32个,02级别为软件中断,331级为硬件中断。031优先级别逐步递增。硬件中断请求级别称为设备中断请求级 DIRQLWindows大部分时间运行在软件中断请求级中。当设备中断来临时,操作系统提升IRQLDIRQL,并运行中断处理函数。当中断处理函数结束后,操作系统把IRQL降到原来的级别。

clip_image001

IRQL P222

用户模式的代码是运行在最低优先级的passive_level;驱动程序的DriveEntry函数,派遣函数,AddDevice等函数一般都运行在PASSIVE_LEVEL级别,在必要时申请进入DISPATCH_LEVEL级别。

Windows 负责线程调度的组件运行在DISPATCH_LEVEL级别。当前线程运行完毕后,系统自动从PASSIVE_LEVEL提升到 DISPATCH_LEVEL级别,当切换完毕后,操作系统又从DISPATCH_LEVEL降回到PASSIVE_LEVEL级别。

驱动程序的StartIO函数和DPC(deferred procedure call)函数,中断服务例程也运行在DISPATCH_LEVEL级别。

线程运行在PASSIVE_LEVEL级别。如果提升到DISPATCH_LEVEL级别,则不会发生线程的切换,这是一种很常见的同步处理机制。

页故障允许在PASSIVE_LEVEL级别(出现页故障时,调用缺页机制,进行物理内存和磁盘文件进行切换),如果在DISPATCH_LEVEL或更高级别,则系统崩溃。对于DISPATCH_LEVEL或更高级别程序必须使用非分页内存。

2)控制IRQL提升与降低

主要用几个函数:

KeGetCurrentIrql

KeRaiseIrql

KeLowerIrql

1 VOID RasieIRQL_Test()
2 {
3 KIRQL oldirql;
4 ASSERT(keGetCurrentIrql() <= DISPATCH_LEVEL);
5 keRaiseIrql(DISPATCH_LEVEL, &oldirql);
6 //...
7   kelowrIrql(oldirql);
8 }

 

示例程序 P224

2、自旋锁

同步处理机制。不同于线程中的等待事件;操作系统会把等待某一个事件的线程处于休眠状态,CPU运行其它线程;而自旋锁不会切换到其它线程,而是让这个线程一直自旋等待。所谓自旋,就是一直不停的询问:是否可以获取自旋锁。

在单CPU中,获取自旋锁仅仅是将当前的IRQLPASSIVE_LEVEL级别升到DISPATCH_LEVEL级别,多CPU中要复杂一点。驱动程序必须在不大于DISPATCH_LEVEL级别中使用自旋锁。

自旋锁一般作用是为各派遣函数实现间同步。尽量不要把自旋锁放在全局变量中,而应当把自旋锁放在设备扩展里。

示例 参见http://www.cnblogs.com/mydomain/archive/2010/10/18/1855118.html

KeAcquireSpinLock

KeInitializeSpinLock

KeAcquireSpinLockAtDpcLevel

3、用户模式下的同步对象

同步对象包括事件,互斥体,信号灯等。用户模式下的同步对象是对内核模式下的同步对象的再封装。

1)等待

WaitForSingleObject

WaitForMultipleObjects

2)开启多线程

CreateThread

[1]中推荐_beginthread

%28VS.80%29.aspx

3)事件

典型的同步对象。

CreateEvent

所有形如CreateXXXWin32 API,如果第一个参数是lpEventAttributes,则这种API内部都会创建一个相应的内核对象;这种API返回一个句柄(32位无符号整数),OS通过这个句柄找到具体的内核对象。

示例代码 P228

4)信号灯

常见的同步对象。

一般同步对象有两种状态:激发状态和未激发状态。

信号灯有个计数器,代表N个灯,只要有一个灯亮着,就说明处于激发状态。

HANDLE CreateSemaphore(

LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // SD

LONG lInitialCount, // initial count

LONG lMaximumCount, // maximum count

LPCTSTR lpName // object name

);

lInitialCount ,如果初始值大于0,则处理激发状态。

ReleaseSemaphore

对信号灯执行一次等待操作,减少一个读数,相当于熄灭一个灯泡。

代码
1 #include "windows.h"
2 #include "process.h"
3 #include "stdio.h"
4
5 UINT WINAPI Thread1(LPVOID para)
6 {
7 printf("Enter Thread1\n");
8 HANDLE *hSemaphore = (HANDLE*)para;
9 Sleep(5000);
10 printf("Leave Thread1\n");
11 ReleaseSemaphore(*hSemaphore, 1, NULL);
12 return 0;
13 }
14  int main()
15 {
16 HANDLE hSemaphore = CreateSemaphore(NULL, 2, 2, NULL);
17 WaitForSingleObject(hSemaphore, INFINITE);
18 WaitForSingleObject(hSemaphore, INFINITE);
19 HANDLE hThread1 = (HANDLE)_beginthreadex(NULL, 0, Thread1, &hSemaphore, 0, NULL);
20 WaitForSingleObject(hSemaphore, INFINITE);
21 }

 

示例代码 P229

5)互斥体

用互斥体来避免多个线程使用一个资源。互斥体类似于同步事件;不同的是同一个线程可以递归获得互斥体,而同步事件不能。

如果线程获得互斥体时,互斥体此时的状态是未激发(nonsignaled),而释放互斥体时,互斥体的状态为激发态。

激发有点拗口,就是signaled,也就是接到了信号了。激发与未激发,都是相对于其它线程而言的。

CreateMutex

代码
1 #include "windows.h"
2 #include "process.h"
3 #include "stdio.h"
4
5 UINT WINAPI Thread1(LPVOID para)
6 {
7 HANDLE *phMutex = (HANDLE*)para;
8 WaitForSingleObject(phMutex, INFINITE);
9 WaitForSingleObject(phMutex, INFINITE);//对于同一个线程,可以获得多次
10  
11 printf("Enter Thread1\n");
12 Sleep(5000);
13 printf("Leave Thread1\n");
14 ReleaseMutex(*phMutex);
15 return 0;
16 }
17
18 UINT WINAPI Thread2(LPVOID para)
19 {
20 HANDLE *phMutex = (HANDLE*)para;
21 WaitForSingleObject(phMutex, INFINITE);
22 WaitForSingleObject(phMutex, INFINITE);//对于同一个线程,可以获得多次
23  
24 printf("Enter Thread2\n");
25 Sleep(5000);
26 printf("Leave Thread2\n");
27 ReleaseMutex(*phMutex);
28 return 0;
29 }
30
31 int main()
32 {
33 HANDLE hMutex = CreateMutex(NULL, NULL, NULL);
34 WaitForSingleObject(hSemaphore, INFINITE);
35 WaitForSingleObject(hSemaphore, INFINITE);
36 HANDLE hThread1 = (HANDLE)_beginthreadex(NULL, 0, Thread1, &hMutex, 0, NULL);
37 HANDLE hThread2 = (HANDLE)_beginthreadex(NULL, 0, Thread2, &hMutex, 0, NULL);
38 Sleep(5000);
39 return 0;
40 }

 

示例代码 P230

6)等待线程完成

线程对象。每个对象同样有两种状态:运行之中时为未激发,当终止时为激发状态。

代码
1 #include "windows.h"
2 #include "process.h"
3 #include "stdio.h"
4
5 UINT WINAPI Thread(LPVOID para)
6 {
7 printf("Enter Thread2\n");
8 Sleep(5000);
9 return 0;
10 }
11
12 int main()
13 {
14 HANDLE hThread[2];
15 WaitForSingleObject(hSemaphore, INFINITE);
16 WaitForSingleObject(hSemaphore, INFINITE);
17 hThread[0] = (HANDLE)_beginthreadex(NULL, 0, Thread, &hMutex, 0, NULL);
18 hThread[1] = (HANDLE)_beginthreadex(NULL, 0, Thread, &hMutex, 0, NULL);
19 WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
20 return 0;
21 }

 

示例代码 P232

 

26、Windows驱动程序的同步处理(2)

4、内核模式下的同步对象 

用户模式下用句柄来操作同步对象,而内核模式下可以获得同步对象的指针。每种同步对象在内核中均对应一种数据结构。 

1)等待 

KeWaitForMultipleObjects

KeWaitForSingleObject

如果超时则返回STATUS_TIMEOUT。如果是因为数组中其一个同步对象变为激发态,则函数的返回值减去STATUS_WAIT_0,就是激发的同步对象在数组中的索引号。

2)多线程

PsCreateSystemThread 包括用户线程与系统线程。用户线程属于当前进程(当前IO操作的发起者;如在IRP_MJ_READ的派遣函数中调用 PsCreateSystemThread创建用户线程,新线程属于调用ReadFile的进程)中的线程。系统线程一般是System线程。

创建的线程必须手动用PsTerminateSystemThread结束。PEPROCESS记录进程信息。

代码
1 #include "ntddk.h"
2 VOID SystemThread(IN PVOID pContext)
3 {
4 KdPrint(("Enter SystemThread\n"));
5 PEPROCESS pEProcess = IoGetCurrentProcess();
6 PTSTR ProcessName = (PTSTR)((ULONG)pEProcess + 0x174);
7 KdPrint(("this thread is run in %s process!\n", ProcessName));
8 KdPrint(("Leave SystemThread\n"));
9 PsTerminateSystemThread (NT_STATUS);
10 }
11
12 VOID MyProcessThread(IN PVOID pContext)
13 {
14 KdPrint(("Enter MyProcessThread\n"));
15 PEPROCESS pEProcess = IoGetCurrentProcess();
16 PTSTR ProcessName = (PTSTR)((ULONG)pEProcess + 0x174);
17 KdPrint(("this thread is run in %s process!\n", ProcessName));
18 KdPrint(("Leave MyProcessThread\n"));
19 PsTerminateSystemThread (NT_STATUS);
20 }
21
22  void CreateThread_Test()
23 {
24 HANDLE hSystemThread, hMyThread;
25 NTSTATUS status = PsCreateSystemThread(&hSystemThread, 0, NULL, NULL, NULL, SystemThread, NULL);
26 NTSTATUS status = PsCreateSystemThread(&hMyThread, 0, NULL, NULL, NULL, MyProcessThread, NULL);
27 }

示例代码 P236

3)内核模式下的事件对象

KEVENT

KeInitializeEvent

代码
1 void MyProcessThread(IN VOID pContext)
2 {
3 PKEVENT pEvent = (PKEVENT)pContext;
4 kdPrint(("Enter MyProcessThread\n"));
5 KeSetEvent(pEvent, IO_NO_INCREMENT, FALSE);
6 kdPrint(("Leave MyProcessThread\n"));
7 PsTerminateSystemThread (NT_STATUS);
8 }
9  void Test()
10 {
11 HANDLE hMyThread;
12 KEVENT kEvent;
13 KeInitializeEvent(&kEvent, NotificationEvent, FALSE);
14 NTSTATUS status = PsCreateSystemThread(&hMyThread, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread, &kEvent);
15 keWaitforSingleobject(&kEvent, Executive, KernalMode, FALSE, NULL);
16 }

示例代码 P237

4)驱动程序与应用程序交互事件对象

在用户模式下创建一个同步事件,然后用DeviceIoControl把事件句柄传递给驱动程序。句柄是与进程相关的,一个进程中的句柄只能在这个进程中有效,句柄相当于事件对象在进程中的索引。DDK中提供了如下函数:

ObReferenceObjectByHandle

ObDereferenceObject

ObReferenceObjectByHandle 在得到指针的同时,会为对象的指针维护一个计数,每次调用ObReferenceObjectByHandle会使计数加1;为使计数平衡,应当在后面适当时候调用ObDereferenceObject 来使计数减一。

代码
1 int main()
2 {
3 HANDLE hDevice = CreateFile("\\\\.\\HelloDDK",
4 GENERIC_READ |GENERIC_WRITE,
5 0,
6 NULL,
7 OPEN_EXISTING,
8 FILE_ATTRIBUTE_NORMAL,
9 NULL);
10 if (hDevice == INVALID_HANDLE_VALUE)
11 {
12 printf("Failed to obtain file handle to device:%s with win32 error code:%d\n","MYWDMDevice", GetLastError());
13 return 1;
14 }
15 bool bRet;
16 DWORD dwOutput;
17 HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
18 //建立辅助线程
19   HANDLE hThread1 = (HANDLE)_beginthreadex(NULL, 0, Thread1, &hEvent, 0, NULL);
20 //将用户模式下的事件句柄传递给驱动
21 //IOCTL_TRANSMIT_EVENT是自己定义的CODE
22   bRet = DeviceIoControl(hDevice,
23 IOCTL_TRANSMIT_EVENT,
24 &hEvent,
25 sizeof(hEvent),
26 NULL,
27 0,
28 &dwOutput,
29 NULL);
30 WaitForSingleObject(hThread1, INFINITE);
31 CloseHandle(hDevice);
32 CloseHandle(hThread1);
33 CloseHandle(hEvent);
34 return 0;
35 }
36
37 NTSTATUS HelloDDKDeviceIoControl(IN PDEVICE_OBJECT pDevObj,
38 IN PIRP pIrp)
39 {
40 NTSTATUS status = STATUS_SUCESS;
41 KdPrint(("Enter HelloDDKDeviceIoControl\n"));
42 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
43 //获得输入参数大小
44 ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;
45 //获得输出参数大小
46 ULONG cbout = stack->Parameters.DeviceIoControl.OutPutBufferLength;
47 //获得IOCTL码
48 ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
49 ULONG info = 0;
50 switch (code)
51 {
52 case IOCTL_TRANSMIT_EVENT:
53 {
54 //得到应用程序传进来的事件
55 HANDLE hUserEvent = *(HANDLE*)pIrp->AssociatedIrp.SystemBuffer;
56 PKEVENT pEvent;
57 status = ObReferenceObjectByHandle(hUserEvent, EVENT_MODIFY_STATE, *ExEventObjecttype,
58 kernalmode,
59 (PVOID*)&pEvent, NULL);
60 keSetEvent(pEvent, IO_NO_INCREMENT, FALSE);
61 ObDereferenceObject(pEvent);
62 break;
63 }
64 default:
65 status = STATUS_INVALID_PARAMETER;
66 }
67 pIrp->IoStatus.Status = status;
68 pIrp->IoStatus.Information = info;
69 IoCompleteRequest(pIrp, IO_NO_INCREMENT);
70 return status;
71 }

示例代码 P238

5)驱动程序与驱动程序间交互事件对象

最简单的方法是创建一个有名字的事件对象,这样在另一个驱动程序中就可以根据名字来寻找到事件对象的指针。

6)内核模式下的信号灯

用户模式下,信号灯对象用句柄表示,在内核模式下,用ksemaphore结构表示。

KeInitializeSemaphore

KeReadStateSemaphore

KeReleaseSemaphore 释放信号灯会增加计数

KeWaitXXX可获取信号灯,如果成功获得,计数减一,否则等待。

示例代码 P240,基本原理同上,略

7)内核模式下的互斥体

KeInitializeMutex

KeReleaseMutex

代码
1 VOID MyProcessThread1(IN PVOID pContext)
2 {
3 PKMUTEX pkMutex = (PKMUTEX)pContext;
4 keWaitforSingleobject(pkMutex, Executive, KernalMode, FALSE, NULL);
5 kdPrint(("Enter MyProcessThread1\n"));
6 //停止50ms
7 KeStallExecutionProcessor(50);
8 kdPrint(("Leave MyProcessThread1\n"));
9 KeReleaseMutex(pkMutex, FALSE);
10 PsTerminateSystemThread(STATUS_SUCCESS);
11 }
12
13 VOID MyProcessThread2(IN PVOID pContext)
14 {
15 PKMUTEX pkMutex = (PKMUTEX)pContext;
16 keWaitforSingleobject(pkMutex, Executive, KernalMode, FALSE, NULL);
17 kdPrint(("Enter MyProcessThread2\n"));
18 //停止50ms
19 KeStallExecutionProcessor(50);
20 kdPrint(("Leave MyProcessThread2\n"));
21 KeReleaseMutex(pkMutex, FALSE);
22 PsTerminateSystemThread(STATUS_SUCCESS);
23 }
24 #pragma PAGEDCODE
25 VOID Test()
26 {
27 HANDLE hMyThread1, hMyThread2;
28 KMUTEX hMutex;
29 KeInitializeMutex(&hMutex, 0);
30 NTSTATUS status = PsCreateSystemThread(&hMyThread1, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread1, &hMutex);
31 NTSTATUS status2 = PsCreateSystemThread(&hMyThread2, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread2, &hMutex);
32 PVOID Pointer_Array[2];
33 ObReferenceObjectByHandle(hMyThread1, 0, NULL,
34 kernalmode,
35 Pointer_Array[0],
36 NULL);
37 ObReferenceObjectByHandle(hMyThread2, 0, NULL,
38 kernalmode,
39 Pointer_Array[1],
40 NULL);
41 KeWaitForMultipleObjects(2, Pointer_Array, WaitAll, Executive, KernelMode,
42 FALSE, NULL, NULL);
43 ObDereferenceObject(Pointer_Array[0]);
44 ObDereferenceObject(Pointer_Array[1]);
45 kdPrint(("After KeWaitForMultipleObjects\n"));
46 }

示例代码 P242

8)快速互斥体

比普通互斥体快。不过不能递归获取互斥体对象。普通互斥体数据结构是MUTEX,快带互斥体是FAST_MUTEX

示例代码 P244 基本原理同上,只是数据结构变了,此略

4、其他同步方法

1)使用自旋锁

KeInitializeSpinLock

KeReleaseSpinLock

代码
1 #include "windows.h"
2 #include "process.h"
3 #include "stdio.h"
4 #include "winioctl.h"
5 #define "..\NT_Driver\Ioctls.h"
6
7 UINT WINAPI Thread1(LPVOID pContext)
8 {
9 BOOL bRet;
10 DWORD dwOutput;
11 bRet = DeviceIoControl(*(PHANDLE)pContext,
12 IOCTL_MYDEFINITION,
13 NULL,
14 0,
15 NULL,
16 0,
17 &dwOutput,
18 NULL);
19 return 0;
20 }
21
22 int main()
23 {
24 HANDLE hDevice = CreateFile("\\\\.\\HelloDDK",
25 GENERIC_READ |GENERIC_WRITE,
26 0,
27 NULL,
28 OPEN_EXISTING,
29 FILE_ATTRIBUTE_NORMAL,
30 NULL);
31 if (hDevice == INVALID_HANDLE_VALUE)
32 {
33 printf("Failed to obtain file handle to device:%s with win32 error code:%d\n","MYWDMDevice", GetLastError());
34 return 1;
35 }
36 HANDLE hThread1 = (HANDLE)_beginthreadex(NULL, 0, Thread1, &hDevice, 0, NULL);
37 HANDLE hThread2 = (HANDLE)_beginthreadex(NULL, 0, Thread1, &hDevice, 0, NULL);
38 WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
39 CloseHandle(hThread1);
40 CloseHandle(hThread2);
41 CloseHandle(hDevice);
42 return 0;
43 }
44
45 NTSTATUS HelloDDKDeviceIoControl(IN PDEVICE_OBJECT pDevObj,
46 IN PIRP pIrp)
47 {
48 ASSERT(KeGetCurrentIrpl() == PASSIVE_LEVLE));
49 PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
50 KIRQL oldirql;
51 KeAcquireSpinLock(&pdx->My_SpinLock, &oldirql);
52 //...开始
53 ASSERT(KeGetCurrentIrpl() == DISPATCH_LEVEL));
54 pIrp->IoStatus.Status = STATUS_SUCCESS;
55 pIrp->IoStatus.information = 0;
56 IoCompleteRequest(pIrp, IO_NO_INCREMENT);
57 NTSTATUS status = STATUS_SUCESS;
58 //...结束
59 KeReleaseSpinLock(&pdx->My_SpinLock, oldirql);
60 return status;
61 }

 

示例代码 P245

2)互锁操作进行同步

       在多线程操作中,对于全局变量的自增、自减等操作,可能不同时候操作,结果是不一样的。因为一个自增等操作翻译成汇编是多条语句,多个线程访问导致了不可重入性。解决方法一是通过自旋锁等同步操作,另外一个方法,便是下面介绍的互锁操作。

保证操作的原子性。DDK提供了如下函数:

InterlockedXX

ExInterlockedXX

1 int number = 0;
2 void Foo()
3 {
4 KeAcquireSpinLock(...)
5 number++;
6 KeReleaseSpinLock(...)
7 }
8
9 int number2 = 0;
10 void Foo2()
11 {
12 InterlockedIncrement(&number2);//原子方式的自增
13 }

 

       前者不通过自旋锁实现,内部不会提升IRQL,可以操作非分页数据和分页数据;后者是通过自旋锁实现的,需要程序员提供一个自旋锁,不能操作分页内存的数据。

ExInterlockedXX

内核函数

功能

ExInterlockedAddLargeInteger

64位整数加法互锁操作

ExInterlockedAddLargeStatistic

64位整数加法互锁操作

ExInterlockedAddUlong

32位整数加法互锁操作

ExInterlockedAllocateFromZone

分配互锁操作

ExInterlockedCompareExchange64

两个32位整数互换互锁操作

ExInterlockedDecrementLong

32位整数减法互锁操作

ExInterlockedExchangeAddLargeInteser

64为整数加法互锁操作

ExInterlockedExchangeUlong

两个整数互换互锁操作

ExInterlockedFlushSList

删除链表全部元素的互锁操作

ExInterlockedIncrementLong

32位整数自增互锁操作

ExInterlockedInsertHeadList

插入双向链表互锁操作

ExInterlockedInsertTailList

插入双向链表互锁操作

ExInterlockedPopEntryList

删除单向链表互锁操作

ExInterlockedPopEntrySList

删除单向链表互锁操作

ExInterlockedPushEntryList

插入单向链表互锁操作

ExInterlockedPushEntrySList

插入单向链表互锁操作

ExInterlockedRemoveHeadList

插入双向链表互锁操作

InterlockedXX

内核函数

功能

InterlockedCompareExchange

比较互锁操作

InterlockedCompareExchangePointer

比较互锁操作

InterlockedDecrement

整型自减互锁操作

InterlockedExchange

整型交换互锁操作

InterlockedExchangeAdd

两个整型相加互锁操作

InterlockedExchangePinter

为指针赋值互锁操作

InterlockedIncrement

整型自增互锁操作

阅读(4228) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~