[16]Windows内核情景分析 --- 服务管理
随时可以看到任务管理器中有一个services.exe进程,这个就是系统的服务控制管理进程,简称SCM
这个进程专门用来管理服务(启动、停止、删除、配置等操作)
系统中所有注册的服务都登记在\HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services键下,这个键专门叫做‘服务键’,服务键下面的每个子键代表一个服务,记录了各个服务的信息。
每个服务可以是独立的服务,也可以位于某个服务组内。用户不仅可以注册服务,还可以注册服务组,并指定服务与服务组之间的隶属关系。\HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\ServiceGroupOrder键下有一个值List,类型是多字符串,记录了系统中注册的所有服务组(蕴含了他们之间的相对加载顺序)。系统加载启动服务时,首先是以服务组为加载顺序进行加载的,各个服务组的加载先后顺序不同,然后同一服务组内的各个服务也是有加载顺序的。
下面是各种启动类型:
#define SERVICE_BOOT_START 0 //系统引导时启动
#define SERVICE_SYSTEM_START 1 //系统初始哈时启动
#define SERVICE_AUTO_START 2 //AutoStart启动类型,即系统初始化完毕后,由SCM开机时自动启动
#define SERVICE_DEMAND_START 3 //按需启动
#define SERVICE_DISABLED 4 //禁用
系统在初始化完毕后,启动services.exe时,会检查注册表中那些登记为AutoStart启动类型的服务,一一予以启动。
我们看services.exe的WinMain函数
int WINAPI
wWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPWSTR lpCmdLine,int nShowCmd)
{
HANDLE hScmStartEvent;
HANDLE hEvent;
DWORD dwError;
if (!ScmCreateStartEvent(&hScmStartEvent))
ExitThread(0);
//读取系统中注册的所有服务组和服务,记录到各自的全局链表中
dwError = ScmCreateServiceDatabase();
if (dwError != ERROR_SUCCESS)
ExitThread(0);
ScmGetBootAndSystemDriverState();//标记出全局链表中那些已经启动了的驱动类型服务
ScmStartRpcServer();
RegisterServicesProcess(GetCurrentProcessId());
SetEvent(hScmStartEvent);
SetConsoleCtrlHandler(ShutdownHandlerRoutine, TRUE);
ScmWaitForLsass();
AcquireLoadDriverPrivilege();
ScmAutoStartServices();//关键。启动所有那些注册为AutoStart启动类型的服务
hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (hEvent)
WaitForSingleObject(hEvent, INFINITE);//services.exe进程永远不会结束
CloseHandle(hScmStartEvent);
ExitThread(0);
return 0;
}
DWORD ScmCreateServiceDatabase(VOID)
{
WCHAR szSubKey[MAX_PATH];
HKEY hServicesKey;
HKEY hServiceKey;
DWORD dwSubKey;
DWORD dwSubKeyLength;
FILETIME ftLastChanged;
DWORD dwError;
dwError = ScmCreateGroupList();//读取注册表中所有注册的服务组,加入全局服务组链表
if (dwError != ERROR_SUCCESS)
return dwError;
InitializeListHead(&ServiceListHead);
RtlInitializeResource(&DatabaseLock);
dwError = RegOpenKeyExW(HKEY_LOCAL_MACHINE,L"System\\CurrentControlSet\\Services",
0,KEY_READ,&hServicesKey);
if (dwError != ERROR_SUCCESS)
return dwError;
dwSubKey = 0;
for (;;)
{
dwSubKeyLength = MAX_PATH;
//枚举系统中注册的所有服务(按注册顺序)
dwError = RegEnumKeyExW(hServicesKey,dwSubKey,szSubKey,&dwSubKeyLength,NULL,NULL,
NULL,&ftLastChanged);
if (dwError == ERROR_SUCCESS && szSubKey[0] != L'{') //跳过com服务
{
dwError = RegOpenKeyExW(hServicesKey,szSubKey,0,KEY_READ,&hServiceKey);
if (dwError == ERROR_SUCCESS)
{
dwError = CreateServiceListEntry(szSubKey,hServiceKey);//加入全局服务链表
RegCloseKey(hServiceKey);
}
}
if (dwError != ERROR_SUCCESS)
break;
dwSubKey++;
}
RegCloseKey(hServicesKey);
WaitForLSA();
ScmDeleteMarkedServices();//删除那些标记为‘已删除’的服务
return ERROR_SUCCESS;
}
DWORD ScmCreateGroupList(VOID)
{
RTL_QUERY_REGISTRY_TABLE QueryTable[2];
NTSTATUS Status;
InitializeListHead(&GroupListHead);
InitializeListHead(&UnknownGroupListHead);
RtlZeroMemory(&QueryTable,sizeof(QueryTable));
QueryTable[0].Name = L"List";
QueryTable[0].QueryRoutine = CreateGroupListRoutine;
//查询ServiceGroupOrder键中的List值,对list中的每个组执行CreateGroupListRoutine函数
Status = RtlQueryRegistryValues(RTL_REGISTRY_CONTROL,L"ServiceGroupOrder",//这个键
QueryTable,NULL,NULL);
return RtlNtStatusToDosError(Status);
}
NTSTATUS WINAPI
CreateGroupListRoutine(PWSTR ValueName,//值名,即list
ULONG ValueType,
PVOID ValueData,//即list中的每个组名
ULONG ValueLength,
PVOID Context,
PVOID EntryContext)
{
PSERVICE_GROUP Group;
RTL_QUERY_REGISTRY_TABLE QueryTable[2];
NTSTATUS Status;
if (ValueType == REG_SZ)
{
//分配一个服务组描述符
Group = (PSERVICE_GROUP)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,
sizeof(SERVICE_GROUP) + ((wcslen((const wchar_t*) ValueData) + 1) * sizeof(WCHAR)));
wcscpy(Group->szGroupName, (const wchar_t*) ValueData);
Group->lpGroupName = Group->szGroupName;
Group->dwRefCount = (DWORD)-1;
RtlZeroMemory(&QueryTable, sizeof(QueryTable));
QueryTable[0].Name = (PWSTR)ValueData;//组名
QueryTable[0].QueryRoutine = CreateGroupOrderListRoutine;
//查询GroupOrderList键中对应组名的信息,执行CreateGroupOrderListRoutine函数
Status = RtlQueryRegistryValues(RTL_REGISTRY_CONTROL,L"GroupOrderList",QueryTable,
(PVOID)Group,NULL);//返回组描述符
InsertTailList(&GroupListHead,&Group->GroupListEntry);//将组描述符挂入全局服务组链表
}
return STATUS_SUCCESS;
}
NTSTATUS
CreateGroupOrderListRoutine(PWSTR ValueName,//即组名
ULONG ValueType,
PVOID ValueData,//值的数据,即tag总数|tag|tag…|tag格式
ULONG ValueLength,
PVOID Context,//返回该组包含的服务tag个数和各个服务的tag
PVOID EntryContext)
{
PSERVICE_GROUP Group;
if (ValueType == REG_BINARY &&
ValueData != NULL &&
ValueLength >= sizeof(DWORD) &&
ValueLength >= (*(PULONG)ValueData + 1) * sizeof(DWORD))
{
Group = (PSERVICE_GROUP)Context;
Group->TagCount = ((PULONG)ValueData)[0];
if (Group->TagCount > 0)
{
if (ValueLength >= (Group->TagCount + 1) * sizeof(DWORD))//多此一举
{
//即该组中每个服务的tag
Group->TagArray = (PULONG)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,
Group->TagCount * sizeof(DWORD));
RtlCopyMemory(Group->TagArray, ValueData + 1,Group->TagCount * sizeof(DWORD));
}
else
{
Group->TagCount = 0;
return STATUS_UNSUCCESSFUL;
}
}
}
return STATUS_SUCCESS;
}
//获取指定服务的信息,构造一个服务描述符,将服务加入全局服务链表中
DWORD CreateServiceListEntry(LPCWSTR lpServiceName,HKEY hServiceKey)
{
PSERVICE lpService = NULL;
LPWSTR lpDisplayName = NULL;
LPWSTR lpGroup = NULL;
DWORD dwSize;
DWORD dwError;
DWORD dwServiceType;
DWORD dwStartType;
DWORD dwErrorControl;
DWORD dwTagId;
if (*lpServiceName == L'{')
return ERROR_SUCCESS;
dwSize = sizeof(DWORD);
dwError = RegQueryValueExW(hServiceKey,L"Type",NULL,NULL, (LPBYTE)&dwServiceType,
&dwSize);
if (dwError != ERROR_SUCCESS)
return ERROR_SUCCESS;
//没有进程的服务不必加入全局链表(但驱动型服务除外)
if (((dwServiceType & ~SERVICE_INTERACTIVE_PROCESS) != SERVICE_WIN32_OWN_PROCESS) &&
((dwServiceType & ~SERVICE_INTERACTIVE_PROCESS) != SERVICE_WIN32_SHARE_PROCESS) &&
(dwServiceType != SERVICE_KERNEL_DRIVER) &&
(dwServiceType != SERVICE_FILE_SYSTEM_DRIVER))
return ERROR_SUCCESS;
dwSize = sizeof(DWORD);
dwError = RegQueryValueExW(hServiceKey,L"Start",NULL,NULL, (LPBYTE)&dwStartType,&dwSize);
if (dwError != ERROR_SUCCESS)
return ERROR_SUCCESS;
dwSize = sizeof(DWORD);
dwError = RegQueryValueExW(hServiceKey,L"ErrorControl",NULL,NULL,
(LPBYTE)&dwErrorControl,&dwSize);
if (dwError != ERROR_SUCCESS)
return ERROR_SUCCESS;
//查询该服务的tag id
dwError = RegQueryValueExW(hServiceKey,L"Tag",NULL,NULL, (LPBYTE)&dwTagId,&dwSize);
if (dwError != ERROR_SUCCESS)
dwTagId = 0;
//查询该服务所属的组
dwError = ScmReadString(hServiceKey,L"Group",&lpGroup);
if (dwError != ERROR_SUCCESS)
lpGroup = NULL;
dwError = ScmReadString(hServiceKey,L"DisplayName",&lpDisplayName);
if (dwError != ERROR_SUCCESS)
lpDisplayName = NULL;
//创建一个服务描述符
dwError = ScmCreateNewServiceRecord(lpServiceName, &lpService);
if (dwError != ERROR_SUCCESS)
goto done;
lpService->Status.dwServiceType = dwServiceType;
lpService->dwStartType = dwStartType;
lpService->dwErrorControl = dwErrorControl;
lpService->dwTag = dwTagId;
if (lpGroup != NULL)
{
dwError = ScmSetServiceGroup(lpService, lpGroup);//记录它所属的服务组
if (dwError != ERROR_SUCCESS)
goto done;
}
if (lpDisplayName != NULL)
{
lpService->lpDisplayName = lpDisplayName;
lpDisplayName = NULL;
}
if (ScmIsDeleteFlagSet(hServiceKey))
lpService->bDeleted = TRUE;
done:;
if (lpGroup != NULL)
HeapFree(GetProcessHeap(), 0, lpGroup);
if (lpDisplayName != NULL)
HeapFree(GetProcessHeap(), 0, lpDisplayName);
return dwError;
}
当构造好了服务组链表和服务链表后,下面的函数用于标记出那些已经启动了的驱动服务
VOID ScmGetBootAndSystemDriverState(VOID)
{
PLIST_ENTRY ServiceEntry;
PSERVICE CurrentService;
ServiceEntry = ServiceListHead.Flink;
while (ServiceEntry != &ServiceListHead)
{
CurrentService = CONTAINING_RECORD(ServiceEntry, SERVICE, ServiceListEntry);
if (CurrentService->dwStartType == SERVICE_BOOT_START ||
CurrentService->dwStartType == SERVICE_SYSTEM_START)
{
ScmCheckDriver(CurrentService);//检查这两种早期启动型服务
}
ServiceEntry = ServiceEntry->Flink;
}
}
NTSTATUS ScmCheckDriver(PSERVICE Service) //标记成所有已经启动了的驱动型服务
{
OBJECT_ATTRIBUTES ObjectAttributes;
UNICODE_STRING DirName;
HANDLE DirHandle;
NTSTATUS Status;
POBJECT_DIRECTORY_INFORMATION DirInfo;
ULONG BufferLength;
ULONG DataLength;
ULONG Index;
if (Service->Status.dwServiceType == SERVICE_KERNEL_DRIVER)
RtlInitUnicodeString(&DirName,L"\\Driver");
else
RtlInitUnicodeString(&DirName,L"\\FileSystem");
InitializeObjectAttributes(&ObjectAttributes,&DirName,0,NULL,NULL);
Status = NtOpenDirectoryObject(&DirHandle, DIRECTORY_QUERY | DIRECTORY_TRAVERSE,
&ObjectAttributes);
if (!NT_SUCCESS(Status))
{
return Status;
}
BufferLength = sizeof(OBJECT_DIRECTORY_INFORMATION) + 2 * MAX_PATH * sizeof(WCHAR);
DirInfo = HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,BufferLength);
Index = 0;
while (TRUE)
{
Status = NtQueryDirectoryObject(DirHandle,DirInfo,BufferLength,TRUE,FALSE,&Index,
&DataLength);
if (Status == STATUS_NO_MORE_ENTRIES)
break;
if (!NT_SUCCESS(Status))
break;
//if 该服务的驱动对象已在对象目录中,也即if该服务已经启动
if (_wcsicmp(Service->lpServiceName, DirInfo->Name.Buffer) == 0)
{
Service->Status.dwCurrentState = SERVICE_RUNNING;//标记
if (Service->lpGroup != NULL)
Service->lpGroup->ServicesRunning = TRUE;//标记那个组有服务启动了
break;
}
}
HeapFree(GetProcessHeap(),0,DirInfo);
NtClose(DirHandle);
return STATUS_SUCCESS;
}
下面是最关键的了:我们看下所有AutoStart启动类型的服务是按什么顺序启动的
VOID ScmAutoStartServices(VOID) //启动所有AutoStart启动类型的服务
{
PLIST_ENTRY GroupEntry;
PLIST_ENTRY ServiceEntry;
PSERVICE_GROUP CurrentGroup;
PSERVICE CurrentService;
ULONG i;
ServiceEntry = ServiceListHead.Flink;
//先全部标记为‘未启动’
while (ServiceEntry != &ServiceListHead)
{
CurrentService = CONTAINING_RECORD(ServiceEntry, SERVICE, ServiceListEntry);
CurrentService->ServiceVisited = FALSE;
ServiceEntry = ServiceEntry->Flink;
}
// 1、先按服务组之间的相对顺序,启动所有服务组中的服务
GroupEntry = GroupListHead.Flink;
while (GroupEntry != &GroupListHead)
{
CurrentGroup = CONTAINING_RECORD(GroupEntry, SERVICE_GROUP, GroupListEntry);
for (i = 0; i < CurrentGroup->TagCount; i++)
{
ServiceEntry = ServiceListHead.Flink;
while (ServiceEntry != &ServiceListHead)
{
CurrentService = CONTAINING_RECORD(ServiceEntry, SERVICE, ServiceListEntry);
if ((CurrentService->lpGroup == CurrentGroup) &&
(CurrentService->dwStartType == SERVICE_AUTO_START) &&
(CurrentService->ServiceVisited == FALSE) &&
(CurrentService->dwTag == CurrentGroup->TagArray[i]))
{
CurrentService->ServiceVisited = TRUE;
ScmStartService(CurrentService, 0, NULL);
}
ServiceEntry = ServiceEntry->Flink;
}
}
// 2、启动处于服务组中,但没有tag或者tag无效的那些服务
ServiceEntry = ServiceListHead.Flink;
while (ServiceEntry != &ServiceListHead)
{
CurrentService = CONTAINING_RECORD(ServiceEntry, SERVICE, ServiceListEntry);
if ((CurrentService->lpGroup == CurrentGroup) &&
(CurrentService->dwStartType == SERVICE_AUTO_START) &&
(CurrentService->ServiceVisited == FALSE))
{
CurrentService->ServiceVisited = TRUE;
ScmStartService(CurrentService, 0, NULL);
}
ServiceEntry = ServiceEntry->Flink;
}
GroupEntry = GroupEntry->Flink;
}
//3、启动那些有服务组,但服务组本身不存在的服务
ServiceEntry = ServiceListHead.Flink;
while (ServiceEntry != &ServiceListHead)
{
CurrentService = CONTAINING_RECORD(ServiceEntry, SERVICE, ServiceListEntry);
if ((CurrentService->lpGroup != NULL) &&
(CurrentService->dwStartType == SERVICE_AUTO_START) &&
(CurrentService->ServiceVisited == FALSE))
{
CurrentService->ServiceVisited = TRUE;
ScmStartService(CurrentService, 0, NULL);
}
ServiceEntry = ServiceEntry->Flink;
}
// 4、启动所有独立的服务(不在任何服务组中)
ServiceEntry = ServiceListHead.Flink;
while (ServiceEntry != &ServiceListHead)
{
CurrentService = CONTAINING_RECORD(ServiceEntry, SERVICE, ServiceListEntry);
if ((CurrentService->lpGroup == NULL) &&
(CurrentService->dwStartType == SERVICE_AUTO_START) &&
(CurrentService->ServiceVisited == FALSE))
{
CurrentService->ServiceVisited = TRUE;
ScmStartService(CurrentService, 0, NULL);
}
ServiceEntry = ServiceEntry->Flink;
}
//完毕
ServiceEntry = ServiceListHead.Flink;
while (ServiceEntry != &ServiceListHead)
{
CurrentService = CONTAINING_RECORD(ServiceEntry, SERVICE, ServiceListEntry);
CurrentService->ServiceVisited = FALSE;
ServiceEntry = ServiceEntry->Flink;
}
}
可以看出,各个服务组之间相对的加载顺序、组内服务之间的相对加载顺序都记录在注册表。
有组的服务优先比没组的服务优先启动,没组的那些服务,则按注册表中的登记的顺序启动。
DWORD ScmStartService(PSERVICE Service, DWORD argc, LPWSTR *argv)
{
PSERVICE_GROUP Group = Service->lpGroup;
DWORD dwError = ERROR_SUCCESS;
Service->ControlPipeHandle = INVALID_HANDLE_VALUE;
if (Service->Status.dwServiceType & SERVICE_DRIVER)//驱动类型服务
{
dwError = ScmLoadDriver(Service);//用这个函数加载驱动,启动服务
if (dwError == ERROR_SUCCESS)
{
Service->Status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
Service->Status.dwCurrentState = SERVICE_RUNNING;
}
}
Else 。。。
return dwError;
}
DWORD ScmLoadDriver(PSERVICE lpService)
{
WCHAR szDriverPath[MAX_PATH];
UNICODE_STRING DriverPath;
NTSTATUS Status;
DWORD dwError = ERROR_SUCCESS;
wcscpy(szDriverPath,L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\");
wcscat(szDriverPath, lpService->lpServiceName);
RtlInitUnicodeString(&DriverPath, szDriverPath);
//看到没。通过SCM启动的服务,都使用这个函数加载驱动,而这个函数我们前面看过,只能加载NT式驱动,不能加载PNP驱动,或者即使是个PNP驱动,通过这个函数加载的,也会变成老式的NT式驱动,不支持即插即用。
Status = NtLoadDriver(&DriverPath);
if (!NT_SUCCESS(Status))
dwError = RtlNtStatusToDosError(Status);
return dwError;
}
[16]Windows内核情景分析 --- 服务管理的更多相关文章
- [3]windows内核情景分析--内存管理
32位系统中有4GB的虚拟地址空间 每个进程有一个地址空间,共4GB,(具体分为低2GB的用户地址空间+高2GB的内核地址空间) 各个进程的用户地址空间不同,属于各进程专有,内核地址空间部分则几乎完全 ...
- [15]Windows内核情景分析 --- 权限管理
Windows系统是支持多用户的.每个文件可以设置一个访问控制表(即ACL),在ACL中规定每个用户.每个组对该文件的访问权限.不过,只有Ntfs文件系统中的文件才支持ACL. (Ntfs文件系统中, ...
- 几个常用内核函数(《Windows内核情景分析》)
参考:<Windows内核情景分析> 0x01 ObReferenceObjectByHandle 这个函数从句柄得到对应的内核对象,并递增其引用计数. NTSTATUS ObRefer ...
- [1]windows 内核情景分析---说明
本文说明:这一系列文章(笔记)是在看雪里面下载word文档,现转帖出来,希望更多的人能看到并分享,感谢原作者的分享精神. 说明 本文结合<Windows内核情景分析>(毛德操著).< ...
- windows内核情景分析之—— KeRaiseIrql函数与KeLowerIrql()函数
windows内核情景分析之—— KeRaiseIrql函数与KeLowerIrql()函数 1.KeRaiseIrql函数 这个 KeRaiseIrql() 只是简单地调用 hal 模块的 KfRa ...
- [14]Windows内核情景分析 --- 文件系统
文件系统 一台机器上可以安装很多物理介质来存放资料(如磁盘.光盘.软盘.U盘等).各种物理介质千差万别,都配备有各自的驱动程序,为了统一地访问这些物理介质,windows设计了文件系统机制.应用程序要 ...
- [11]Windows内核情景分析---设备驱动
设备驱动 设备栈:从上层到下层的顺序依次是:过滤设备.类设备.过滤设备.小端口设备[过.类.过滤.小端口] 驱动栈:因设备堆栈原因而建立起来的一种堆栈 老式驱动:指不提供AddDevice的驱动,又叫 ...
- [4]Windows内核情景分析---内核对象
写过Windows应用程序的朋友都常常听说"内核对象"."句柄"等术语却无从得知他们的内核实现到底是怎样的, 本篇文章就揭开这些技术的神秘面纱. 常见的内核对象 ...
- [7] Windows内核情景分析---线程同步
基于同步对象的等待.唤醒机制: 一个线程可以等待一个对象或多个对象而进入等待状态(也叫睡眠状态),另一个线程可以触发那个等待对象,唤醒在那个对象上等待的所有线程. 一个线程可以等待一个对象或多个对象, ...
随机推荐
- bootstrape学习
bootstrape学习 已分享到有道上:http://note.youdao.com/share/?id=076fb6314c99c742a79f6fb66b2a58b0&type=note ...
- Page11:状态反馈、输出反馈的概念及性能比较,极点配置的基本概念、意义及其算法[Linear System Theory]
内容包含离散时间线性时不变系统的稳定判据 状态反馈.输出反馈的基本概念及其性能比较 极点配置的基本概念.意义及其算法
- 将获得datebox值的文本形式转为日期格式
在使用datebox时,已选择结束日期后,再次选择开始日期.此时判断开始日期不能大于结束日期. datebox的onSelect: function (date){}事件传入的参数是日期类型,而使用d ...
- day5_集合
集合也是一种数据类型,一个类似列表东西,它的特点是无序的,不重复的,也就是说集合中是没有重复的数据 集合的作用: 1.它可以把一个列表中重复的数据去掉,而不需要你再写判断---天生去重 2.可以做关系 ...
- filter的基本介绍和使用
简介 过滤器是处在客户端和服务器资源之间的一到过滤网,我们可以根据具体的需求来对请求头和数据就行预处理,也可以对响应头和和数据进行后处理.例如Jsp, Servlet, 静态图片文件或静态 html ...
- 深度剖析fork()的原理及用法
我们都知道通过fork()系统调用我们可以创建一个和当前进程印象一样的新进程.我们通常将新进程称为子进程,而当前进程称为父进程.而子进程继承了父进程的整个地址空间,其中包括了进程上下文,堆栈地址,内存 ...
- Python摸爬滚打之day04----基本数据类型(列表,元组)
1.列表 列表是可变的, 有序的数据类型,列表是按照添加顺序来保存的,可以存放各种数据类型. 1.1 列表的切片(同字符串) 1.2 列表的增删改查 注意: 列表是可以直接在列表上面进行操 ...
- 洛谷P4064 加法 [JXOI2017] 贪心
正解:贪心 解题报告: 传送门! 首先最小值最大显然考虑二分?然后就二分一个值mid,从左往右考虑,对于小于等于mid的点显然可以求出这个点至少要加几次,然后找到覆盖这个点的右端点max的区间区间加上 ...
- Linux命令小计
一.yum和apt-get的区别 Linux系统下安装包格式有:rpm包和deb包. pm包主要应用在RedHat系列包括 Fedora等发行版的Linux系统上 deb包主要应用于Debian系列包 ...
- 腾讯互动课堂(Tencent Interact Class,TIC)SDK 词汇表
词汇表 https://cloud.tencent.com/document/product/266/11732 封装格式 封装格式(Format)是将已经编码压缩好的视频流和音频流按照一定的格式规范 ...