加载中...

进程间文件同步写

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

最近在捣鼓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()函数部分停止,此时还没有释放文件锁;
  • 转为运行第二个进程,此时它无法对文件进行上锁操作;
  • 在第一个进程窗口按下任意键,它将释放文件锁,并关闭文件句柄;
  • 观察第二个进程输出,发现它已经得到文件锁,并在文件中写入了对应信息。

有朋自远方来,不亦说乎?
Built with Hugo
Theme Stack designed by Jimmy