加载中...

进程间文件同步写

这段期间没学到任何完整的、值得记录的东西,因此好久没有更新博客了,凑巧中午一边喝大红袍一边搞出了一点东西,虽然不太完整,也不妨一记。

最近在捣鼓Windows下应用程序调用API的情况统计,思路是向指定的进程中注入DLL,钩取系统API,这样每次进程调用API的时候先执行我们的语句,向统计文件中写入一条调用信息。

由此,引出了一个问题:如果我们注入了多个进程,这些进程同时调用一个API的时候,都要往统计文件中写一条信息,如何保持它们的同步呢?

答案就是文件锁

互斥锁是操作系统用来保持进程间同步的一个关键工具,多个进程同时对一个对象执行操作的时候,要分清楚先后顺序,否则可能产生混乱。比如,一个进程1要向一个文件里面写入1 ~ 100的数,而进程2要向这个文件里面写入101 ~ 200的数,我们希望进程1写完之后再让进程2写,但是系统在执行进程调度的时候,是可能在进程1写到一半的时候将其挂起,转而去执行其他进程的。想一想有没有可能出现这种情况:进程1写到了50,系统将其挂起,去执行其他进程,其他进程执行完之后,系统不执行进程1,而是执行进程2,于是我们的文件中的数字就变成了1,2…50,101,102…

显然,这样的情况是可能出现但是绝对不符合需求的,我们要想办法避免它。

于是操作系统为我们提供了互斥锁,即一个进程对某个对象执行操作的时候,将这个对象锁定,这时其他的进程就无法对这个对象执行操作了。

本篇中的文件锁其实就是作用在文件上的互斥锁。还是刚才的例子,如果进程1在一开始就为文件上了锁,当它执行到一半被挂起的时候,即使系统转为执行进程2,此时进程2也会因为无法获得文件锁而被阻塞;仅当进程1完成了写入,释放了文件锁,进程2才会被唤醒执行。

这样的工作模式可不止用来保持文件的读写同步,还可以解决一系列的同步问题。锁的思想在操作系统领域是非常重要的,这里的介绍不够全面,主要也是由于笔者目前的水平不够,有兴趣的朋友可以自行深入了解。

  1. 创建文件

初步了解了文件锁的含义之后,就要进入编码实践了。本篇后续编码是以C++为主体,但是核心部分完全兼容C语言

首先要明确一点,C++的文件流操作无法实现文件锁。这个是笔者目前的水平下得出的结论,欢迎见多识广的读者在评论区批评指正。

既然无法使用fstream实现文件锁,就必须老老实实使用C语言的文件操作了。

在这里,由于WindowsAPI提供的文件锁函数需要一个HANDLE类型作为参数,我们只能使用CreateFile函数去创建文件了。

该API详见此文档

我们使用以下两句话创建了一个文件,这里文件路径可以自由定义。

const char* logPath = "C:\\Users\\Administrator\\Desktop\\recLog.txt";
HANDLE hFile = ::CreateFileA(
    logPath,
    GENERIC_WRITE,
    FILE_SHARE_WRITE,
    0,
    OPEN_ALWAYS,
    0,
    0
);

值得注意的是CreateFileA的参数OPEN_ALWAYS,该参数指定了文件的打开方式:当文件不存在时,创建它;当文件存在时,打开它。

  1. 文件上锁

文件创建完成之后,正常的下一步操作应该是写入了。但是谨记,为了不发生开头提到的进程同步问题,我们要在写入文件之前先拿到文件的锁。这里使用Windows提供的一个关键函数LockFileEx()

该API详见此文档

我们使用以下几句话为文件上了个锁,这里的overlapped变量是API要求我们传入的,没有很大的用处,将其置零即可。

OVERLAPPED overlapped;
memset(&overlapped, 0, sizeof(overlapped));
const int lockSize = 10000;	// 上锁的字节数,没有很大的意义,非零即可。
if (!LockFileEx(hFile, LOCKFILE_EXCLUSIVE_LOCK, 0, lockSize, 0, &overlapped))
{
	DWORD err = GetLastError();
	printf("Error %i\n", err);
}

当文件上锁失败,if判断会成立,进入错误处理环节。记住开头提到的锁的机制,当一个进程无法获取当前的文件锁的时候,它应该是会被阻塞而非直接报错。在我的试验中,进入这个分支的情况是第一步CreateFile的时候得到了一个无效的句柄,而非无法获取当前文件的锁。

LickFileEx()函数的第二个参数比较关键,当它被指定为LOCKFILE_FAIL_IMMEDIATELY 的时候会直接返回失败,而不是阻塞当前进程。在这里,我们当然不选这个参数。

文件上锁成功之后,就可以进行文件的写入了。

else
{
	std::string str("");
	str += "[+] Process 1 locked the file. [+]\n";
    // 将文件指针移动到文件末尾,实现以追加方式写入
	SetFilePointer(hFile, 0, NULL, FILE_END);
	::WriteFile(hFile, str.c_str(), str.length(), 0, 0);
	getchar();	// 在此处停止,先不释放文件锁
	UnlockFileEx(hFile, 0, lockSize, 0, &overlapped);
	CloseHandle(hFile);
	std::cout << "Unlock file.\n";
}

将以上的三个部分结合起来,就是第一个进程的代码:

int main()
{
	const char* logPath = "C:\\Users\\Administrator\\Desktop\\recLog.txt";
	HANDLE hFile = ::CreateFileA(logPath, GENERIC_WRITE, FILE_SHARE_WRITE, 0,
		OPEN_ALWAYS, 0, 0);
	OVERLAPPED overlapped;
	memset(&overlapped, 0, sizeof(overlapped));
	const int lockSize = 10000;
	if (!LockFileEx(hFile, LOCKFILE_EXCLUSIVE_LOCK, 0, lockSize, 0, &overlapped))
	{
		DWORD err = GetLastError();
		printf("Error %i\n", err);
	}
	else
	{
		std::string str("");
		str += "[+] Process 1 locked the file. [+]\n";
        // 将文件指针移动到文件末尾,实现以追加方式写入
		SetFilePointer(hFile, 0, NULL, FILE_END);
		::WriteFile(hFile, str.c_str(), str.length(), 0, 0);
		getchar();	// 在此处停止,先不释放文件锁
		UnlockFileEx(hFile, 0, lockSize, 0, &overlapped);
		CloseHandle(hFile);
		std::cout << "Unlock file.\n";
	}
	system("pause");

	return 0;
}
  1. 第二个进程

第二个进程被我们用来验证这个锁的可行性,即当第一个进程为文件上了锁,且还没释放的时候,第二个进程究竟能否对文件进行写入。

int main()
{
	const char* logPath = "C:\\Users\\Administrator\\Desktop\\recLog.txt";
	HANDLE hFile = ::CreateFileA(logPath, GENERIC_WRITE, FILE_SHARE_WRITE, 0,
		OPEN_ALWAYS, 0, 0);
	OVERLAPPED overlapped;
	memset(&overlapped, 0, sizeof(overlapped));
	const int lockSize = 10000;
	if (!LockFileEx(hFile, LOCKFILE_EXCLUSIVE_LOCK, 0, lockSize, 0, &overlapped))
	{
		DWORD err = GetLastError();
		printf("Error %i\n", err);
	}
	else
	{
		std::cout << "Get lock.\n";
		std::string str("");
		str += "[+] Process 2 locked the file. [+]\n";
		SetFilePointer(hFile, 0, NULL, FILE_END);		// 将文件指针移动到文件末尾,实现以追加方式写入
		::WriteFile(hFile, str.c_str(), str.length(), 0, 0);
		// getchar();	// 在此处停止,先不释放文件锁
		UnlockFileEx(hFile, 0, lockSize, 0, &overlapped);
		CloseHandle(hFile);
		std::cout << "Unlock file.\n";
	}
	system("pause");
	return 0;
}
  1. 验证
  • 首先运行第一个进程,它将在getchar()函数部分停止,此时还没有释放文件锁;
  • 转为运行第二个进程,此时它无法对文件进行上锁操作;
  • 在第一个进程窗口按下任意键,它将释放文件锁,并关闭文件句柄;
  • 观察第二个进程输出,发现它已经得到文件锁,并在文件中写入了对应信息。

10 comments
Anonymous
Markdown is supported
@mpv945
mpv945commentedalmost 3 years ago

添加图片,如果使用外部图床的http链接 。图片无法点击放大,你那边怎么解决的?

@SGS4ever
SGS4evercommentedalmost 3 years ago

@mpv945
添加图片,如果使用外部图床的http链接 。图片无法点击放大,你那边怎么解决的?

我的博客没有使用图床,所以没办法帮到你~

@Celetherin
Celetherincommentedalmost 3 years ago

您好,我也是使用的stack主题,我在照着您的方法添加返回顶部按钮时,遇到了按钮虽然出现、也能够点击,但无法实现实际上的返回顶部功能的问题,我没有任何的代码知识,不知道您有没有解决方法?
另外,也是想提醒一下其他需要这篇教程的朋友,最新版的stack主题,添加返回按钮的组件应该在layouts/partials/sidebar/right.html, 在layouts/_default/single.html中添加代码会导致出现两个右边栏。

@jsjcjsjc
jsjcjsjccommentedover 2 years ago

请教一下博主,如何优雅的给stack主题添加广告哈?
我只想在左或者右侧边栏底部,或者每篇文章底部添加一个小小的广告,但是默认似乎的满屏广告哈~~
感谢

@SGS4ever
SGS4evercommentedover 2 years ago
@ClimbingMouse
ClimbingMousecommentedabout 2 years ago

你好,按照你的方法设置页面载入动画,这个动画不会停止咋办啊

@46fafa
46fafacommentedabout 2 years ago

博主你好,请问一下主页布局修改哪里的代码如何作用于整个网页,我发现修改后的布局只存在主页和前两篇文章,其他部分还是没修改的样子

@4kohakunushi
4kohakunushicommentedover 1 year ago

你好,关于左侧栏图标高亮我这里存在一些问题想请教你。我取消了原本主页直接抓取post的内容在中间显示的版块,这个部分改成了其他东西,与此同时新增了一个抓取post信息的与links、search等目录并列的一个目录,现在的问题是这些部分虽然都能正常显示,但是对应的抓取post的那个目录无法选中以后高亮,应该修改增加什么才能让它也可以选中后高亮呢?

@SGS4ever
SGS4evercommentedover 1 year ago

首先我只能基于本文使用的Stack版本来尝试解答,因为没看过当前的Stack主题的代码~
我重新翻了下此前写的关于高亮的内容,理论上只要你的post页面的标题在menu配置中即可高亮。如果post页面是你站点的根路径,那应该可以参考我的文章里写的方法,修改下active的触发逻辑~

@4kohakunushi
你好,关于左侧栏图标高亮我这里存在一些问题想请教你。我取消了原本主页直接抓取post的内容在中间显示的版块,这个部分改成了其他东西,与此同时新增了一个抓取post信息的与links、search等目录并列的一个目录,现在的问题是这些部分虽然都能正常显示,但是对应的抓取post的那个目录无法选中以后高亮,应该修改增加什么才能让它也可以选中后高亮呢?

@sansan-cc
sansan-cccommented5 months ago

感谢博主的建站帖子,有很大的帮助。

有朋自远方来,不亦说乎?