admin管理员组文章数量:1572325
转载自:http://mzf2008.blog.163/blog/static/355997862010112041821953/
1.内核模式下的等待
NTSTATUS
KeWaitForSingleObject(
IN PVOID Object,
IN KWAIT_REASON WaitReason,
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Timeout OPTIONAL
);
Object:是一个同步对象的指针,注意这里不是句柄
WaitReason:等待的原因,一般为Executive
WaitMode:等待模式,一般为KernelMode
Alertable:指明等待是否为“警惕”的,一般为FALSE
Timeout:等待时间,如果为NULL,就表示无限期等待,直到同步对象变为激发态
NTSTATUS
KeWaitForMultipleObjects(
IN ULONG Count,
IN PVOID Object[],
IN WAIT_TYPE WaitType,
IN KWAIT_REASON WaitReason,
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Timeout OPTIONAL,
IN PKWAIT_BLOCK WaitBlockArray OPTIONAL
);
2.内核模式下创建线程
内核函数PsCreateSystemThread负责创建新线程。该函数可以创建两种线程,一种是用户线程,它属于当前进程中的线程。例如,如果IRP_MJ_READ的派遣函数中调用了PsCreateSystemThread,那么新创建的线程属于调用ReadFile的进程。另一种是系统线程,系统线程不属于当前用户进程,而是属于系统进程,一般PID为4,名字为“System”的进程。
NTSTATUS
PsCreateSystemThread(
OUT PHANDLE ThreadHandle, //新创建的线程句柄
IN ULONG DesiredAccess, //创建的权限
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,//线程的属性,一般设为NULL
IN HANDLE ProcessHandle OPTIONAL,//指定创建用户线程还是系统线程。如果为NULL,则为系统进程,如果该值是一个进程句柄,则新创建的线程属于这个指定的进程。DDK提供的NTCurrentProcess可以得到当前进程的句柄。
OUT PCLIENT_ID ClientId OPTIONAL,
IN PKSTART_ROUTINE StartRoutine,//新线程的运行地址
IN PVOID StartContext //新线程接收的参数
);
注意:在内核模式下创建的线程是无法自动退出的,必须使用PsTerminateSystemThread强制结束线程。
这里介绍一个方法,可以很方便的让线程知道自己属于哪个进程。首先,使用IoGetCurrentProcess函数得到当前进程。IoGetCurrentProcess函数会得到一个PEPEOCESS数据结构。PEPROCESS结构记录进程的信息,其中包括进程名。在Windows xp 和Windows Server 2003中,PEPROCESS结构的0x174偏移位置记录着进程名。因此,可以用下面的方法查看当前进程信息:
PEPROCESS pEProcess = IoGetCurrentProcess();
PTSTR pString = (PTSTR)((ULONG)pEProcess + 0x174);
KdPrint(("这个线程运行在 %s 进程中!\n", pString));
下面代码演示了如何在驱动程序中创建线程。代码如下:
VOID SystemThread(IN PVOID lpParam)
{
KdPrint(("进入系统线程函数!\n"));
PEPROCESS pEProcess = IoGetCurrentProcess();
PTSTR pString = (PTSTR)((ULONG)pEProcess + 0x174);
KdPrint(("这个线程运行在 %s 进程中!\n", pString));
KdPrint(("离开系统线程函数!\n"));
//需要自己结束线程
PsTerminateSystemThread(STATUS_SUCCESS);
}
VOID UserThread(IN PVOID lpParam)
{
KdPrint(("进入用户线程函数!\n"));
PEPROCESS pEProcess = IoGetCurrentProcess();
PTSTR pString = (PTSTR)((ULONG)pEProcess + 0x174);
KdPrint(("这个线程运行在 %s 进程中!\n", pString));
KdPrint(("离开用户线程函数!\n"));
PsTerminateSystemThread(STATUS_SUCCESS);
}
VOID CreateSystemThread()
{
HANDLE hSystemThread, hUserThread;
NTSTATUS status = PsCreateSystemThread(&hSystemThread, 0, NULL, NULL,
NULL, SystemThread, NULL);
status = PsCreateSystemThread(&hUserThread, 0, NULL, NtCurrentProcess(),
NULL, UserThread, NULL);
ZwClose(hSystemThread);
ZwClose(hUserThread);
}
3.内核模式下的事件对象
在内核中,用KEVENT数据结构表示一个事件对象。在使用前,需要进行初始化。
VOID
KeInitializeEvent(
IN PRKEVENT Event, //要初始化的KEVENT结构对象
IN EVENT_TYPE Type, //事件的类型。
IN BOOLEAN State //如果为真,初始为激发态
);
Type:事件的类型,有两类,一类是“通知事件”,对应参数为NotificationEvent,当事件变为激发态时,需要手动将其该为未激发状态。另一类为“同步事件”,对应参数为SynchronizationEvent,当事件为激发态时,如果遇到KeWaitFor*的函数,事件对象则自动变为未激发状态。
示例代码:
VOID SystemThread(IN PVOID lpParam)
{
KdPrint(("进入系统线程函数!\n"));
KEVENT kEvent = *(PKEVENT)lpParam;
KeSetEvent(&kEvent, IO_NO_INCREMENT, FALSE);
KdPrint(("离开系统线程函数!\n"));
PsTerminateSystemThread(STATUS_SUCCESS);
}
VOID CreateSystemThread()
{
KdPrint(("进入主线程!\n"));
HANDLE hSystemThread;
KEVENT kEvent;
//初始化Event
KeInitializeEvent(&kEvent, SynchronizationEvent, FALSE);
//创建线程
NTSTATUS status = PsCreateSystemThread(&hSystemThread, 0, NULL, NULL, NULL,
SystemThread, &kEvent);
KeWaitForSingleObject(&kEvent, Executive, KernelMode, FALSE, NULL);
ZwClose(hSystemThread);
KdPrint(("离开主线程!\n"));
}
4.驱动程序与应用程序交互事件对象
应用程序中创建的事件和在内核模式下创建的事件对象,本质上是同一个东西。在用户模式下,它用句柄代表,在内核模式下,它用KEVENT数据结构代表。
在应用程序中,所有内核对象都不会被用户看到,用户看到的只是代表内核对象的对象句柄。
我们要将用户模式下创建的事件传递给驱动程序,可以用DeviceIoControl API函数。DDK提供了内核函数将句柄转化为指针,该函数如下:
NTSTATUS
ObReferenceObjectByHandle(
IN HANDLE Handle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType OPTIONAL,
IN KPROCESSOR_MODE AccessMode,
OUT PVOID *Object,
OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL
);
ObReferenceObjectByHandle函数在得到指针的同时,会为对象的指针维护一个计数。每次调用ObReferenceObjectByHandle函数时会使计数加1.因此为了计数平衡,在使用完ObReferenceObjectByHandle函数后,需要调用如下函数:
VOID
ObDereferenceObject(
IN PVOID Object
);
ObDereferenceObject函数使计数减一。
示例代码:
用户模式下代码:
#include <windows.h>
#include <stdio.h>
#include <winioctl.h>
#include "../NT_Driver/ioctl.h"
int main(void)
{
HANDLE hFile = CreateFile("\\\\.\\HelloDDK", 0, 0,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("error:%d", GetLastError());
return -1;
}
printf("创建Event!\n");
//创建一个自动重置的,初始为未激发的事件对象
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
DWORD dwRet;
//调用DeviceIoControl把事件句柄传进内核
DeviceIoControl(hFile, CLT_TRANSMIT_EVENT,
&hEvent, sizeof(hEvent), NULL, 0, &dwRet, NULL);
//等待事件被激发
WaitForSingleObject(hEvent, INFINITE);
printf("程序退出!\n");
return 0;
}
内核代码:
NTSTATUS FuckDeviceControl(IN PDEVICE_OBJECT pDeviceObj,
IN PIRP pIrp)
{
KdPrint(("进入IRP_MJ_DEVICE_CONTROL处理函数!\n"));
NTSTATUS status = STATUS_SUCCESS;
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
ULONG inLength =
stack->Parameters.DeviceIoControl.InputBufferLength;
ULONG outLength =
stack->Parameters.DeviceIoControl.OutputBufferLength;
ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
switch(code)
{
case CLT_TRANSMIT_EVENT:
{
//获得传递进来的事件句柄
HANDLE hEvent =
*(PHANDLE)pIrp->AssociatedIrp.SystemBuffer;
PKEVENT pkEvent;
//把句柄转化为KEvent结构
status = ObReferenceObjectByHandle(hEvent,
EVENT_MODIFY_STATE, *ExEventObjectType, KernelMode,
(PVOID*)&pkEvent, NULL);
//使事件激发
KeSetEvent(pkEvent, IO_NO_INCREMENT, FALSE);
//递减计数
ObDereferenceObject(pkEvent);
break;
}
default:
status = STATUS_INVALID_VARIANT;
}
pIrp->IoStatus.Information = outLength;
pIrp->IoStatus.Status = status;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
KdPrint(("离开IR_MJ_DEVICE_CONTROL处理函数!\n"));
return STATUS_SUCCESS;
}
5.驱动程序与驱动程序交互对象
有时候,还需要驱动程序与驱动程序之间交互事件对象。例如,驱动程序A的某个派遣函数要与驱动程序B的派遣函数进行同步,就需要两个驱动程序间交互事件对象。
最简单的方法是驱动程序B创建一个有名字的事件对象,这样,驱动程序A就可以根据名字找到事件对象的指针。
创建有名字的事件可以通过IoCreateSynchronizationEvent和IoCreateNotificationEvent函数。为了进一步得到事件内核对象的指针,可以使用前面介绍的ObReferenceObjectByHandle函数。
6.内核模式下的信号灯
在内核模式下,信号灯对象用KSEMAPHORE数据结构表示。在使用信号灯对象钱,需要对其进行初始化,函数如下:
VOID
KeInitializeSemaphore(
IN PRKSEMAPHORE Semaphore,
IN LONG Count, //初始化计数
IN LONG Limit //最大计数
);
KeReadStateSemaphore函数可以读取信号灯当前计数。
LONG
KeReadStateSemaphore(
IN PRKSEMAPHORE Semaphore
);
释放信号灯会增加信号灯计数,它对应内核函数KeReleaseSemaphore函数
LONG
KeReleaseSemaphore(
IN PRKSEMAPHORE Semaphore,
IN KPRIORITY Increment,
IN LONG Adjustment,
IN BOOLEAN Wait
);
示例代码:
void UserThreadProc(IN PVOID lpParam)
{
KdPrint(("进入新建的线程!\n"));
//得到信号灯
KSEMAPHORE kSemaphore = *(PKSEMAPHORE)lpParam;
//增加信号灯计数
KeReleaseSemaphore(&kSemaphore, IO_NO_INCREMENT, 1, FALSE);
KdPrint(("离开新建的线程!\n"));
PsTerminateSystemThread(STATUS_SUCCESS);
}
VOID Test()
{
KdPrint(("进入主线程!\n"));
HANDLE hUserThread;
ULONG count;
KSEMAPHORE kSemaphore;
//初始化信号灯
KeInitializeSemaphore(&kSemaphore, 2, 2);
//两次等待,用信号灯处于未激发状态
KeWaitForSingleObject(&kSemaphore, Executive, KernelMode,
FALSE, NULL);
KeWaitForSingleObject(&kSemaphore, Executive, KernelMode,
FALSE, NULL);
//读取现有的信号灯计数,为0
count = KeReadStateSemaphore(&kSemaphore);
KdPrint(("信号灯计数为:%d\n", count));
//创建新线程
NTSTATUS status = PsCreateSystemThread(&hUserThread,
THREAD_ALL_ACCESS, NULL, NtCurrentProcess(), NULL,
UserThreadProc, &kSemaphore);
//等待信号灯被激发
KeWaitForSingleObject(&kSemaphore, Executive, KernelMode,
FALSE, NULL);
KdPrint(("离开主线程!\n"));
}
7.内核模式下的互斥体
互斥体在内核模式下的结构体为KMUTEX,使用前需要进行初始化:
VOID
KeInitializeMutex(
IN PRKMUTEX Mutex,
IN ULONG Level //保留值,一般为0
);
释放互斥体使用KeReleaseMutex内核函数:
LONG
KeReleaseMutex(
IN PRKMUTEX Mutex,
IN BOOLEAN Wait
);
示例代码:
void UserThreadProc1( IN PVOID lpParam)
{
//获得互斥体
PKMUTEX pMutex = (PKMUTEX)lpParam;
//等待互斥体
KeWaitForSingleObject(pMutex, Executive, KernelMode,
FALSE, NULL);
KdPrint(("进入新建的线程1!\n"));
//强迫停止50ms
KeStallExecutionProcessor(50);
KdPrint(("离开新建的线程1!\n"));
//释放互斥体
KeReleaseMutex(pMutex, FALSE);
PsTerminateSystemThread(STATUS_SUCCESS);
}
void UserThreadProc2( IN PVOID lpParam)
{
PKMUTEX pMutex = (PKMUTEX)lpParam;
KeWaitForSingleObject(pMutex, Executive, KernelMode,
FALSE, NULL);
KdPrint(("进入新建的线程2!\n"));
KeStallExecutionProcessor(50);
KdPrint(("离开新建的线程2!\n"));
KeReleaseMutex(pMutex, FALSE);
PsTerminateSystemThread(STATUS_SUCCESS);
}
VOID Test()
{
HANDLE hUserThread1, hUserThread2;
KMUTEX kMutex;
//初始化互斥体
KeInitializeMutex(&kMutex, 0);
//创建两个新线程
NTSTATUS status = PsCreateSystemThread(&hUserThread1, 0,
NULL, NtCurrentProcess(), NULL, UserThreadProc1, &kMutex);
status = PsCreateSystemThread(&hUserThread2, 0, NULL,
NtCurrentProcess(), NULL, UserThreadProc2, &kMutex);
PVOID ThreadArray[2];
//把线程句柄转换为可以等待的指针
ObReferenceObjectByHandle(hUserThread1, 0, NULL,
KernelMode, &ThreadArray[0], NULL);
ObReferenceObjectByHandle(hUserThread2, 0, NULL,
KernelMode, &ThreadArray[1], NULL);
//等待2个新建线程执行完毕
KeWaitForMultipleObjects(2, ThreadArray, WaitAll,
Executive, KernelMode, FALSE, NULL, NULL);
//减少引用计数
ObDereferenceObject(ThreadArray[0]);
ObDereferenceObject(ThreadArray[1]);
//关闭线程句柄
ZwClose(hUserThread1);
ZwClose(hUserThread2);
}
8.快速互斥体
快速互斥体是DDK提供的另外一种内核同步对象,他的特征类似于前面介绍的普通互斥体。他们两的作用完全一样,之所以被称为是快速互斥体,是因为它执行的速度比普通的互斥体速度快(这里指的是获取和释放的速度)。然而,快速互斥体对象不能被递归获取。
普通互斥体在内核中用KMUTEX数据结构表示,而快速互斥体在内核中用FAST_MUTEX数据结构描述。
除此之外,对快速互斥体的初始化,获取和释放对应的内核函数也和普通互斥体不同。
初始化:
VOID
ExInitializeFastMutex(
IN PFAST_MUTEX FastMutex
);
获取:
VOID
ExAcquireFastMutex(
IN PFAST_MUTEX FastMutex
);
释放:
VOID
ExReleaseFastMutex(
IN PFAST_MUTEX FastMutex
);
示例代码:
void UserThreadProc1( IN PVOID lpParam)
{
PFAST_MUTEX pFastMutex = (PFAST_MUTEX)lpParam;
ExAcquireFastMutex(pFastMutex);
KdPrint(("进入新建的线程1!\n"));
KeStallExecutionProcessor(50);
KdPrint(("离开新建的线程1!\n"));
ExReleaseFastMutex(pFastMutex);
PsTerminateSystemThread(STATUS_SUCCESS);
}
void UserThreadProc2( IN PVOID lpParam)
{
//获得快速互斥体
PFAST_MUTEX pFastMutex = (PFAST_MUTEX)lpParam;
ExAcquireFastMutex(pFastMutex);
KdPrint(("进入新建的线程2!\n"));
//强迫停止50ms
KeStallExecutionProcessor(50);
KdPrint(("离开新建的线程2!\n"));
//释放互斥体
ExReleaseFastMutex(pFastMutex);
PsTerminateSystemThread(STATUS_SUCCESS);
}
void Test()
{
HANDLE hUserThread1, hUserThread2;
//初始化快速互斥体
FAST_MUTEX fastMutex;
ExInitializeFastMutex(&fastMutex);
//创建两个新线程
NTSTATUS status = PsCreateSystemThread(&hUserThread1, 0,
NULL, NtCurrentProcess(), NULL, UserThreadProc1, &fastMutex);
status = PsCreateSystemThread(&hUserThread2, 0, NULL,
NtCurrentProcess(), NULL, UserThreadProc2, &fastMutex);
PVOID ThreadArray[2];
//把线程句柄转换为可以等待的指针
ObReferenceObjectByHandle(hUserThread1, 0, NULL,
KernelMode, &ThreadArray[0], NULL);
ObReferenceObjectByHandle(hUserThread2, 0, NULL,
KernelMode, &ThreadArray[1], NULL);
//等待2个新建线程执行完毕
KeWaitForMultipleObjects(2, ThreadArray, WaitAll,
Executive, KernelMode, FALSE, NULL, NULL);
//减少引用计数
ObDereferenceObject(ThreadArray[0]);
ObDereferenceObject(ThreadArray[1]);
//关闭线程句柄
ZwClose(hUserThread1);
ZwClose(hUserThread2);
}
9.使用自旋锁进行同步
如果自旋锁已经被锁住,这时有程序申请“获取”这个自旋锁时,程序则处于“自旋”状态。所谓自旋状态,就是不停的询问是否可以“获取”自旋锁。
自旋锁不同于其他的等待事件。在线程中如果等待某个事件(Event),操作系统会让这个线程进入睡眠状态,CPU会转而运行其他线程。而自旋锁则不同,它不会切换到别的线程,而是一直让这个线程“自旋”。因此,对自旋锁占用时间不宜过长,否则会导致申请自旋锁的其他线程处于自旋,这会浪费宝贵的CPU时间。
自旋锁的作用一般是使各派遣函数之间同步。尽量不要将自旋锁放在全局变量中,而应该将自旋锁放在设备扩展里面。自旋锁用KSPIN_LOCK数据结构表示。
如下定义:
typedef struct _DEVICE_EXTENSION{
…
KSPIN_LOCK My_SpinLock; //在设备扩展中定义自旋锁
}DEVICE_EXTENSION,*PDEVICE_EXTENSION;
在使用自旋锁前需要对它进行初始化,一般在DriverEntry或者AddDevice函数中初始化自旋锁:
VOID
KeInitializeSpinLock(
IN PKSPIN_LOCK SpinLock
);
申请自旋锁可以使用内核函数KeAcquireSpinLock,它有两个参数,一个是自旋锁的指针,另一个用来记录在获得自旋锁前的IRQL级别。
VOID
KeAcquireSpinLock(
IN PKSPIN_LOCK SpinLock,
OUT PKIRQL OldIrql
);
一般用法如下:
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION)pDevObj->DeviceExtension;
KIRQL oldirql;
KeAcquireSpinLock(&pdx->My_SpinLock, &oldirql);
释放自旋锁函数如下:
VOID
KeReleaseSpinLock(
IN PKSPIN_LOCK SpinLock,
IN KIRQL NewIrql
);
注意:如果在DISPATHC_LEVEL级别申请自旋锁,那么不会改变IRQL级别。这时,申请和释放自旋锁可以简单的使用KeAcquireSpinLockAtDpcLevel 和KeReleaseSpinLockFromDpcLevel内核函数。
示例代码:
定义设备扩展:
typedef struct _DEVICE_EXTENSION {
PDEVICE_OBJECT pDevice;
UNICODE_STRING ustrDeviceName; //设备名称
UNICODE_STRING ustrSymLinkName; //符号链接名
KSPIN_LOCK My_SpinLock; //自旋锁
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
在DriverEntry中初始化自旋锁:
PDEVICE_EXTENSION pDevExt =
(PDEVICE_EXTENSION)pDevObj->DeviceExtension;
……
KeInitializeSpinLock(&pDevExt->My_SpinLock);
IRP_MJ_DEVICE_CONTROL派遣函数:
NTSTATUS FuckTest(IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
{
//为了避免多个派遣函数并行运行,所以进行同步
//对DeviceIoControl调用来源自用户线程,因此处于PASSIVE_LEVEL。
ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);
KdPrint(("进入IRP_MJ_DEVICE_CONTROL处理函数!\n"));
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION)pDevObj->DeviceExtension;
KIRQL oldIrql;
//请求获得自旋锁
KeAcquireSpinLock(&pdx->My_SpinLock, &oldIrql);
KdPrint(("获得自旋锁!\n"));
//获得自旋锁后,IRQL提升至DISPATCH_LEVEL
ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);
KdPrint(("IRQL提升至DISPATCH_LEVEL!\n"));
pIrp->IoStatus.Information = 0;
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
//释放自旋锁
KeReleaseSpinLock(&pdx->My_SpinLock, oldIrql);
KdPrint(("释放互斥锁!\n"));
KdPrint(("离开IRP_MJ_DEVICE_CONTROL处理函数!\n"));
return STATUS_SUCCESS;
}
测试程序代码:
#include <windows.h>
#include <winioctl.h>
#include <stdio.h>
#include <process.h>
#include "../Driver/ioctl.h"
UINT WINAPI ThreadProc1(LPVOID lpParam)
{
HANDLE hFile = *(PHANDLE)lpParam;
BOOL bRet = DeviceIoControl(hFile, TEST_CTL, NULL,
0, NULL, 0, NULL, NULL);
return 0;
}
int main(void)
{
HANDLE hFile = CreateFile("\\\\.\\HelloDDK",
GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("打开设备失败!\n");
return -1;
}
HANDLE hThread1 = (HANDLE)_beginthreadex(NULL, 0,
ThreadProc1, &hFile, 0, 0);
HANDLE hThread2 = (HANDLE)_beginthreadex(NULL, 0,
ThreadProc1, &hFile, 0, 0);
HANDLE hThread[2] = {hThread1, hThread2};
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread1);
CloseHandle(hThread2);
CloseHandle(hFile);
return 0;
}
10.使用互锁操作进行同步
DDK提供了两类互锁操作来提供简单的同步处理。一类是InterlockedXX函数,另一类是ExInterlockedXX函数。
其中InterlockedXX函数不是通过自旋锁实现的,内部不会提升IRQL,因此既可以操作分页数据又可以操作非分页数据。而ExInterlockedXX函数是通过自旋锁实现的,在使用的时候需要我们提供一个自旋锁。内部依靠这个自旋锁实现同步,因此它不能操作分页内存。
DDK提供的函数和功能如下图:
版权声明:本文标题:线程同步(2) - 内核模式下的线程同步 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/xitong/1727724876a1127108.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论