独钓寒江雪

用C++的优雅,驯服Windows的狂野

为什么要内存对齐

平台原因

不是所有的CPU都能访问任意地址上的任意数据的,有些CPU只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

性能原因

尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的,它一般会以双字节、四字节、8字节、16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度

如果数据存储时是按照内存存取粒度对齐的,那么处理器就可以在一个内存访问周期内完成数据的读取;反之,如果数据没有对齐,可能需要多个内存访问周期才能读取完整的数据。

阅读全文 »

为什么需要内存映射

“内存映射文件”可以将硬盘上的文件映射到虚拟地址空间,这样就不需要将所有东西都放入到页交换文件中,比如系统有许多程序同时运行时,如果将这些程序文件都加载到页交换文件中,页交换文件将会变得非常大。事实上,Windows 也并没有将硬盘上的程序文件复制到页交换文件中,因为这样不仅会让页交换文件将会变得非常大,也会浪费很多时间,特别是可执行程序非常大的时候。

当用户要求执行一个应用程序时,系统会打开该应用程序的.exe文件,并计算出应用程序的代码和数据的大小,然后系统会在进程的虚拟地址空间预定一块地址空间,并注明与该区域相关联的物理存储器就是.exe文件本身。

当把一个位于硬盘上的文件(可以是.exe.dll也可以是普通文件)映像用作地址空间区域对应的物理存储器时,我们称这个文件映像为“内存映射文件”

阅读全文 »

页交换文件

虚拟地址空间只是操作系统为进程“虚拟”出来的一块地址区域,并不代表任何实际的空间。而“页交换文件”却对应了实际的空间,这个空间一般是磁盘上名为“pagefile.sys”的文件。

“页交换文件”的大小和位置可以在系统设置(系统属性 -> 高级 -> 性能 -> 设置 -> 高级 )中进行设置:

从微软的官方文档来看,“虚拟内存”等于“物理内存”+“分页文件”总和。可以把“虚拟内存”理解为 Windows 的一种内存管理机制。

阅读全文 »

实模式下内存分配机制

在 8086 或者 80186 以前,程序运行时,操作系统会把程序全都装入内存,程序都是直接运行在物理内存上的。也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时,必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小。

例如某台计算机总的内存大小是128M ,现在同时运行两个程序 A 和 B ,A 需占用内存10M , B 需占用内存110M 。计算机在给程序分配内存时会采取这样的方法:先将内存中的前10M分配给程序 A ,接着再从内存中剩余的118M中划分出 110M分配给程序 B 。这种分配方法虽然可以保证程序 A 和程序 B 都能运行,但是这种简单的内存分配策略会导致很多问题:

阅读全文 »

WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能。

它并不是单一的协议,包含了媒体、加密、传输层等在内的多个协议标准以及一套基于 JavaScript 的 API。通过简单易用的 JavaScript API ,在不安装任何插件的情况下,让浏览器拥有了 P2P音视频和数据分享的能力。同时WebRTC 并不是一个孤立的协议,它拥有灵活的信令,可以便捷的对接现有的SIP 和电话网络的系统。

不同技术领域的人都可以从WebRTC中获取到需要的东西。

阅读全文 »

会话描述协议(Session Description Protocol 或简写 SDP)描述的是流媒体的初始化参数。此协议由 IETF 发表为 RFC 2327。

SDP 完全是一种会话描述格式,它不属于传输协议。

SDP 用于描述多媒体通信会话,包括会话建立、会话请求和参数协商。SDP 不用于传输媒体数据,只能用于两个通信终端的参数协商,包括媒体类型、格式以及所有其他和会话相关的属性。SDP 以字符串的形式描述上述初始化参数。

阅读全文 »

一、TCP 特性

尽管 TCP 和 UDP 都是用 IP 协议作为网络层,但 TCP 却提供和 UDP 完全不同的网络服务。TCP 是面向连接的稳定可靠字节流服务。TCP 首部的很多字段都是为了实现这 2 大特性而设计的。

阅读全文 »

消息队列

首先我们要明确一个观点:窗口是和线程相关联的,消息队列也是和线程相关联的,这个线程无论是主线程还是子线程

当一个线程被创建时,系统假定该线程不会被用于任何与用户界面相关的任务,所以不会为它分配相应的资源(如消息队列等),因为这样可以减少线程对系统资源的占用。

但是,一旦这个线程调用一个与图形用户界面有关的函数(例如检查它的消息队列或建立一个窗口),系统就会为该线程分配一些额外的资源,以便它能够执行与用户界面有关的任务。特别是,系统会分配一个THREADINFO结构,并将这个数据结构与线程关联起来。

THREADINFO结构是微软内部的、没有被公开的数据结构,我们无法找到这个结构体的准确的定义。但从其他文档中可以得知,THREADINFO结构包含:

  • 一组成员变量,利用这组成员,线程可以认为它是在自己独占的环境中运行。
  • 登记消息队列(posted-message queue)
  • 发送消息队列( send-message queue)
  • 应答消息队列( reply -message queue)
  • 虚拟输入队列(virtualized-input queue)
  • 唤醒标志(wake flag)
  • 用来描述线程局部输入状态的若干变量。

上图描述了THREADINFO结构中的各个成员。线程拥有了THREADINFO结构也就有了各种消息队列。

窗口消息处理函数

下面是一个完整的、简单的创建 Windows 窗体的 C++代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <windows.h>

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

return DefWindowProc(hwnd, message, wParam, lParam);
}


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpfnWndProc = WndProc; //设置窗体消息处理函数
wndclass.lpszClassName = TEXT("SimpleWindow");
if (!RegisterClass(&wndclass)) { //注册窗体类
return 0;
};

HWND hwnd = CreateWindow(TEXT("SimpleWindow"), // window class name
TEXT("SimpleWindow"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT,// initial x position
CW_USEDEFAULT,// initial y position
CW_USEDEFAULT,// initial x size
CW_USEDEFAULT,// initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL);

ShowWindow(hwnd, SW_SHOW);

MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

return msg.wParam;
}

上面代码中的WndProc函数就是窗口消息处理函数。消息循环中的DispatchMessage函数派发消息时,系统就会调用这个函数对消息进行处理。

消息循环

消息循环是程序员自己编写的从线程消息队列中循环获取(Get 或 Peek)消息的循环体,代码大致如下:

1
2
3
4
5
while(GetMessage(&Msg, NULL, 0, 0))
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}

循环何时结束?

GetMessage函数的返回值如下:

1
2
3
收到WM_QUIT消息,返回0
收到非WM_QUIT消息,返回非0
错误,返回-1

利用GetMessage函数返回值的特性,在收到WM_QUIT消息之后,消息循环就会结束。

TranslateMessage

TranslateMessage函数的作用就是将虚拟键值信息转换为字符信息。这一步并不是必须的。

DispatchMessage

将消息派发到窗口的消息处理函数(如第 2 节中的WndProc函数)。

PostMessage

PostMessage 原理

1
2
3
4
5
BOOL PostMessage(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);

当一个线程调用这个函数时,系统要确定是哪一个线程建立了用hwnd参数标识的窗口。然后系统分配一块内存,将这个消息参数存储在这块内存中,并将这块内存增加到相应线程的登记消息队列中。并且,这个函数还设置QS_POSTMESSAGE唤醒位。PostMessage函数在登记了消息之后立即返回,调用该函数的线程不知道登记的消息是否被指定窗口的窗口过程所处理。实际上,有可能这个指定的窗口永远不会收到登记的消息。

PostThreadMessage

还可以通过调用PostThreadMessage将消息放置在线程的登记消息队列中。

1
2
3
4
5
BOOL PostThreadMessage(
DWORD dwThreadId,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);

PostQuitMessage

为了终止线程的消息循环,可以调用PostQuitMessage函数。PostQuitMessage函数类似于:
PostThreadMessage(GetCurrentThreadId(), WM_QUIT, nExitCode, 0);
但是,PostQuitMessage并不实际登记一个消息到任何一个THREADINFO结构的消息队列。只是在内部,PostQuitMessage会设定QS_QUIT唤醒标志,并对THREADINFO结构的nExitCode成员进行设置。因为这些操作永远不会失败,所以PostQuitMessage的原型被定义成VOID返回类型。

SendMessage

SendMessage的实现分为 2 种情况:向本线程的窗口发送消息、向其他线程的窗口发送消息。

向本线程的窗口发送消息

如果调用SendMessage的线程向本线程所建立的一个窗口发送一个消息,SendMessage的做法很简单:直接调用指定窗口的“窗口消息处理函数”,将其作为一个子例程。当“窗口消息处理函数”完成对消息的处理后,该函数会返回一个值给SendMessageSendMessage再将这个值返回给调用线程。

向其他线程的窗口发送消息

但是,当调用SendMessage的线程向其他线程所建立的窗口发送消息时,SendMessage的内部工作就复杂得多(即使两个线程在同一进程中也是如此)。

Windows 要求建立窗口的线程来处理该窗口的消息。所以当一个线程调用SendMessage向一个由其他进程所建立的窗口发送一个消息,也就是向其他的线程发送消息,发送线程不可能处理窗口消息,因为发送线程不是运行在接收进程的地址空间中,因此不能访问相应窗口过程的代码和数据。实际上,发送线程要挂起,而由另外的线程处理消息。所以为了向其他线程建立的窗口发送一个窗口消息,系统必须执行下面的动作:

首先,发送的消息要追加到接收线程的发送消息队列,同时还为这个线程设定QS_SENDMESSAGE标志(后面将讨论)。
其次,如果接收线程已经在执行代码并且没有等待消息(等待消息是指:如调用GetMessagePeekMessageWaitMessage等),发送的消息不会被处理,系统不能中断线程来立即处理消息。当接收进程在等待消息时,系统首先检查线程的QS_SENDMESSAGE唤醒标志是否被设定,如果是,系统扫描发送消息队列中消息的列表,并找到第一个发送的消息。有可能在这个队列中有几个发送的消息。例如,几个线程可以同时向一个窗口分别发送消息。当发生这样的事时,系统只是将这些消息追加到接收线程的发送消息队列中。

当接收线程等待消息时,系统从发送消息队列中取出第一个消息并调用适当的窗口过程来处理消息。如果在发送消息队列中再没有消息了,则QS_SENDMESSAGE唤醒标志被关闭。当接收线程处理消息的时候,调用SendMessage的线程被设置成空闲状态(idle),等待一个消息出现在它的应答消息队列中。在发送的消息处理之后,窗口过程的返回值被登记到发送线程的应答消息队列中。发送线程现在被唤醒,取出包含在应答消息队列中的返回值。这个返回值就是SendMessage的返回值。这时,发送线程继续正常执行。

当一个线程等待SendMessage返回时,它基本上是处于空闲状态。但它可以执行一个任务:如果系统中另外一个线程向一个窗口发送消息,这个窗口是由这个等待SendMessage返回的线程所建立的,则系统要立即处理发送的消息。在这种情况下,系统不必等待线程去调用GetMessagePeekMessageWaitMessage

由于 Windows 使用上述方法处理线程之间发送的消息,所以有可能造成线程挂起。例如,当处理发送消息的线程含有错误时,会导致进入死循环。那么对于调用SendMessage的线程会发生什么事呢?它会恢复执行吗?这是否意味着一个程序中的 bug 会导致另一个程序挂起?答案是确实有这种可能。

利用 4 个函数—— SendMessageTimeOutSendMessageCallbackSendNotifyMessageReplayMessage,可以编写保护性代码防止出现这种情况。

一、什么是 UDP 协议?

UDP 是 User Datagram Protocol 的简称,中文名是用户数据报协议,是 OSI 参考模型中的传输层协议,它是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。

阅读全文 »
0%