首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
80 阅读
2
nvim番外之将配置的插件管理器更新为lazy
58 阅读
3
2018总结与2019规划
54 阅读
4
PDF标准详解(五)——图形状态
33 阅读
5
为 MariaDB 配置远程访问权限
30 阅读
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
Java
emacs
linux
文本编辑器
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
308
篇文章
累计收到
27
条评论
首页
栏目
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
页面
归档
友情链接
关于
搜索到
2
篇与
的结果
2019-07-21
Java 文件操作
java文件操作主要封装在Java.io.File中,而文件读写一般采用的是流的方式,Java流封装在 java.io 包中。Java中流可以理解为一个有序的字符序列,从一端导向到另一端。建立了一个流就好似在两个容器中建立了一个通道,数据就可以从一个容器流到另一个容器文件操作Java文件操作使用 java.io.File 类进行。该类中常见方法和属性有:static String pathSeparator: 多个路径间的分隔符,这个分隔符常用于系统的path环境变量中。Linux中采用 : Windows中采用 ;static String separator: 系统路径中各级目录的分隔符,比如Windows路劲 c:\windows\ 采用的分隔符为 \, 而Linux中 /root 路径下的 分隔符为 /为了达到跨平台的效果,在写路径时一般不会写死,而是使用上述几个静态变量来进行字符串的拼接构造方法有:File(String pathname); 传入一个路径的字符串File(String parent, String child); 传入父目录和子目录的路径,系统会自动进行路径拼接为一个完整的路径File(File parent, String child); 传入父目录的File对象和子目录的路径,生成一个新的File对象常见方法:以can开头的几个方法,用于判断文件的相关权限,比如可读、可写、可执行String getAbsolutePath() 获取文件绝对路径的字符串String getPath() 获取文件的路径,这个方法会根据构造时传入的路径来决定返回绝对路径或者相对路径String getName() 获取文件或者路径的名称long length() 返回文件的大小,以字节为单位,目录会返回0;boolean exists(); 判断文件或者目录是否存在boolean isDirectory(); 判断对应的File对象是否为目录boolean isFile(); 判断对应的File对象是否为文件boolean delete(); 删除对应的文件或者目录boolean mkdir(); 创建目录boolean mkdirs(); 递归创建目录String[] list(); 遍历目录,将目录中所有文件路径字符串放入到数组中File[] listFiles(); 遍历目录,将目录中所有文件和目录对应的File对象保存到数组中返回下面是一个遍历目录中文件的例子public static void ResverFile(String path){ File f = new File(path); ResverFile_Core(f); } public static void ResverFile_Core(File f){ //System.out.println("开始遍历目录:" + f.getAbsolutePath()); File[] subFile = f.listFiles(); for(File sub : subFile){ if(sub.isDirectory()){ if(".".equals(sub.getName()) || "..".equals(sub.getName())){ continue; } ResverFile_Core(sub); }else{ System.out.println(sub.getAbsolutePath()); } } }上述代码根据传入的路径,递归遍历路径下所有文件。从 JDK文档中可以看到 list 和listFiles方法都可以传入一个FileFilter 或者FilenameFilter 的过滤器, 查看一下这两个过滤器:public interface FilenameFilter{ boolean accept(File dir, String name); } public interface FileFilter{ boolean accept(File pathname); }上述接口都是用来进行过滤的,FilenameFilter 会传入一个目录的File对象和对应文件的名称,我们在实现时可以根据这两个值来判断文件是否是需要遍历的,如果返回true则结果会包含在返回的数组中,false则会舍去结果将上述的代码做一些改变,该成遍历所有.java 的文件public static void ResverFile(String path){ File f = new File(path); ResverFile_Core(f); } public static void ResverFile_Core(File f){ //System.out.println("开始遍历目录:" + f.getAbsolutePath()); File[] subFile = f.listFiles(pathname->pathname.isDirectory() || pathname.getName().toLowerCase().endsWith(".java")); for(File sub : subFile){ if(sub.isDirectory()){ if(".".equals(sub.getName()) || "..".equals(sub.getName())){ continue; } ResverFile_Core(sub); }else{ System.out.println(sub.getAbsolutePath()); } } }IO 流Java将所有IO操作都封装在了 java.io 包中,java中流分为字符流(Reader、Writer)和字节流(InputStream、OutputStream), 它们的结构如下:字节流读写文件在读写任意文件时都可以使用字节流进行,文件字节流是 FileInputStream和FileOutputStream//可以使用路径作为构造方式 //FileInputStream fi = new FileInputStream("c:/test.dat"); //可以使用File对象进行构造 FileInputStream fi = new FileInputStream(new File("c:/test.dat")); int i = fi.read(); byte[] buffer = new byte[1024]; while(fi.read(buffer) > 0 ){ //do something } fi.close();下面是一个copy文件的例子public static void CopyFile() throws IOException{ FileInputStream fis = new FileInputStream("e:\\党的先进性学习.avi"); FileOutputStream fos = new FileOutputStream("党的先进性副本学习.avi"); int len = 0; byte[] buff = new byte[1024]; long start = System.currentTimeMillis(); while((len = fis.read(buff)) > 0){ fos.write(buff, 0, len); } long end = System.currentTimeMillis(); System.out.println("耗时:" + (end - start)); fos.close(); fis.close(); }字符流读写文件一般在读写文本文件时,为了读取到字符串,使用的是文件的字符流进行读写。文件字节流是FileReader和FileWriterFileReader fr = new FileReader(new File("c:/test.dat")); char[] buffer = new char[] while(fr.read(buffer) > 0 ){ //do something } fr.close();下面是一个拷贝文本文件的例子public static void CopyFile() throws IOException{ FileReader fr = new FileInputStream("e:\\党的先进性学习.txt"); FileWriter fw = new FileOutputStream("党的先进性副本学习.txt"); int len = 0; char[] buff = new char[1024]; long start = System.currentTimeMillis(); while((len = fr.read(buff)) > 0){ fw.write(buff, 0, len); } long end = System.currentTimeMillis(); System.out.println("耗时:" + (end - start)); fr.close(); fw.close(); }读写IO流的其他操作IO流不仅能够读写磁盘文件,在Linux的哲学中,一切皆文件。根据这点IO流是可以读写任意设备的。比如控制台;之前在读取控制台输入的时候使用的是Scanner,这里也可以使用InputStream或者InputStreamReader。Java中定义了用于控制台输入输出的InputStream 和 OutputStream 对象: System.in 和 System.out//多次读取单个字符 char c; InputStreamReader isr = new InputStreamReader(System.in); System.out.println("输入字符, 按下 'q' 键退出。"); // 读取字符 do { c = (char) isr.read(); System.out.println(c); } while (c != 'q'); isr.close(); //读取字符串 // 使用 System.in 创建 BufferedReader BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String str; System.out.println("Enter lines of text."); System.out.println("Enter 'end' to quit."); do { str = br.readLine(); System.out.println(str); } while (!str.equals("end")); br.close();控制台的写入与读取类似OutputStreamWriter ow = new OutputStreamWriter(System.out); char[] buffer = new char{'a', 'b', 'c'}; ow.write(buffer); ow.flush(); ow.close();由于write函数的功能有限,所以在打印时经常使用的是 System.out.println 函数。缓冲流在操作系统中提到内存的速度是超过磁盘的,在使用流进行读写操作时,CPU向磁盘下达了读写命令后会长时间等待,影响程序效率。而缓冲流在调用write和read方法时并没有真正的进行IO操作。而是将数据缓存在一个缓冲中,当缓冲满后或者显式调用flush 后一次性进行读写操作,从而减少了IO操作的次数,提高了效率。常用的缓冲流有下面几个BufferedInputStreamBufferedOutputStreamBufferReaderBufferWriter分别对应字节流和字符流的缓冲流。它们需要传入对应的Stream 或者Reader对象。下面是一个使用缓冲流进行文件拷贝的例子,与上面不使用缓冲流的拷贝进行对比,当文件越大,效率提升越明显BufferedInputStream bis = new BufferedInputStream(new FileInputStream("E:\\test.avi")); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("test.avi")); int len = 0; byte[] buff = new byte[1024]; long start = System.currentTimeMillis(); while((len = bis.read(buff)) > 0){ bos.write(buff, 0, len); } long end = System.currentTimeMillis(); System.out.println("耗时:" + (end - start)); bos.close(); bis.close();文件编码转换在读取文件时经常出现乱码的情况,乱码出现的原因是文件编码与读取时的解码方式不一样,特别是出现中文的情况。上面说过Java 中主要有字符流和字节流。从底层上来说,在读取文件时都是二进制的数据。然后将二进制数据转化为字符串。也就是先有InputStream/OutputStream 读出二进制数据,然后根据默认的编码规则将二进制数据转化为字符也就是 Reader/Writer。如果读取时的编码方式与文件的编码方式不同,则会出现乱码。我们在程序中使用 InputStreamReader和 OutputStreamWriter 来设置输入输出流的编码方式。//以UTF-8方式写文件 FileOutputStream fos = new FileOutputStream("test.txt"); OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8"); osw.write(FileContent); osw.flush(); //以UTF-8方式读文件 FileInputStream fis = new FileInputStream("test.txt"); InputStreamReader isr = new InputStreamReader(fis, "UTF-8"); BufferedReader br = new BufferedReader(isr); String line = null; while ((line = br.readLine()) != null) { FileContent += line; }序列化与反序列化在程序中经常需要保存类的数据,如果直接使用OutputStream 也是可以保存类数据的,但是需要考虑类中有引用的情况,如果里面有引用,需要保存引用所对应的那块内存。每个类都需要额外提供一个方法来处理存在引用成员的情况。针对这种需求,Java提供了序列化与反序列化的功能Java序列化与反序列化可以使用ObjectOutputStream 和 ObjectInputStream。public class Student{ public String name; public int age; public Date birthday; }比如我们要序列化 上述的 Student 类,可以使用下面的代码ObjectOutputStream oos = ObjectOutputStream(new FileOutputStream("student.dat")); Student stu = new Student(); stu.name = "Tom"; stu.age = 22; stu.brithday = new Date(); oos.writeObject(stu);当然如果要进行序列化和反序列化操作,必须要在类中实现Serializable接口, 这个接口没有任何方法它仅仅作为一个标志,拥有这个标志的方式才能进行序列化。也就是得将上述的Student 类做一个修改public class Student implements Serializable{ public String name; public int age; public Date birthday; }类的静态变量在类的对象创建之前就加载到了内存中。它与具体的类对象无关,所以在序列化时不会序列化静态成员。如果有的成员不想被序列化,可以将它变为静态成员;但是从设计上来说,也不是所有的类成员都可以变为静态成员。为了保证非静态成员可以不被序列化,可以使用 transient 关键字实现了serialiable 接口的类在保存为.class文件 时会增加 一个SerializableID, 序列化时会在对应文件中保存序列号,如果类发生了修改而没有进行序列化操作时,二者不同会抛出一个异常。例如说上述的Student类中先进行了一次序列化,在文件中保存了一个ID,后来根据需求又增加了一个 id 字段,在编译后又生成了一个ID,如果这个时候用之前的文件来反序列化,此时就会报错。为了解决上述问题,可以采用以下几种方法:改类代码文件后重新序列化。增加一个 static final long serialVerssionID = xxxx; 这个ID是之前序列化文件保存的ID。这个操作是为了让新修改的类ID与文件中的ID相同。调用 writeObject 方法时一个文件只能保存一个对象的内容。为了使一个文件保存多个对象,可以使用集合保存多个对象,在序列化时序列化 这个集合
2019年07月21日
5 阅读
0 评论
0 点赞
2017-06-11
windows 下文件的高级操作
本文主要说明在Windows下操作文件的高级方法,比如直接读写磁盘,文件的异步操作,而文件普通的读写方式在网上可以找到一大堆资料,在这也就不再进行专门的说明。判断文件是否存在在Windows中并没有专门提供判断文件是否存在的API,替代的解决方案是使用函数GetFileAttributes,传入一个路径,如果文件不存在,函数会返回INVALID_FILE_ATTRIBUTES,这个时候一般就可以认为文件不存在。更严格一点的,可以在返回INVALID_FILE_ATTRIBUTES之后调用GetLastError函数,判断返回值是否为ERROR_FILE_NOT_FOUND或者ERROR_PATH_NOT_FOUND(这个值适用于判断目录)下面是它的实例代码BOOL IsFileExist(LPCTSTR pFilePath) { DWORD dwRet = GetFileAttributes(pFilePath); if(INVALID_FILE_ATTRIBUTES == dwRet) { dwRet = GetLastError(); if (ERROR_FILE_NOT_FOUND == dwRet || ERROR_PATH_NOT_FOUND == dwRet) { return FALSE; } } return TRUE; }文件查找和目录遍历这个操作主要使用到了下面几个API函数:FindFirstFile:建立一个指定搜索条件的搜索句柄,函数原型如下:HANDLE FindFirstFile( LPCTSTR lpFileName, LPWIN32_FIND_DATA lpFindFileData ); 第一个参数是一个搜索起始位置路劲的字符串,但是这个字符串的格式为“路径+特定文件的通配符”这样它会以这个路径作为起始路径,依次查找到目录中文件名符合通配符的文件,比如"c:\."会返回c盘下的所有文件,而"c:\"直接返回错误,"c:\a*.txt"会返回c盘中以a开头的txt文件FindNextFile:搜索符合条件的下一项,在循环中调用它的话,它会依次返回符合FindFirstFile要求的文件信息和所有子目录新消息FindClose:关闭搜索句柄FindFirstFile和FindNextFile返回的文件信息结构为WIN32_FIND_DATA,它的定义如下:typedef struct _WIN32_FIND_DATA { DWORD dwFileAttributes; //文件属性 FILETIME ftCreationTime; //创建时间 FILETIME ftLastAccessTime; //最后访问时间 FILETIME ftLastWriteTime; //最后修改时间 DWORD nFileSizeHigh; DWORD nFileSizeLow; //这两个值是一个64位的文件大小的高32位和低32位 DWORD dwOID; TCHAR cFileName[MAX_PATH]; //文件名称 } WIN32_FIND_DATA; 一般在遍历的时候首先判断文件属性,如果为FILE_ATTRIBUTE_DIRECTORY(是个目录),并且文件名称不为".",".."则递归调用遍历函数遍历它的子目录,但是一定要记得进行文件路径的拼接,如果不为目录,这个时候一般就是普通文件,这个时候可以选择进行打印(遍历文件目录)或者比较文件名称与需要查找的名称是否相同(查找文件)。下面是一个全盘搜索特定文件名的实例代码:void FindFileByPath(LPCTSTR pszSearchEntry, LPCTSTR pszFileName) { WIN32_FIND_DATA fd = {0}; TCHAR szFilePath[MAX_PATH] = _T(""); StringCchCat(szFilePath, MAX_PATH, pszSearchEntry); StringCchCat(szFilePath, MAX_PATH, _T("*.*")); HANDLE hSearch = FindFirstFile(szFilePath, &fd); if (INVALID_HANDLE_VALUE == hSearch) { return; } do { if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && _tcscmp(fd.cFileName, _T(".")) != 0 && _tcscmp(fd.cFileName, _T("..")) != 0) { TCHAR szSubDir[MAX_PATH] = _T(""); StringCchCat(szSubDir, MAX_PATH, pszSearchEntry); StringCchCat(szSubDir, MAX_PATH, fd.cFileName); StringCchCat(szSubDir, MAX_PATH, _T("\\")); FindFileByPath(szSubDir, pszFileName); }else { if (_tcscmp(fd.cFileName, pszFileName) == 0) { TCHAR szFullPath[MAX_PATH] = _T(""); StringCchCat(szFullPath, MAX_PATH, pszSearchEntry); StringCchCat(szFullPath, MAX_PATH, _T("\\")); StringCchCat(szFullPath, MAX_PATH, fd.cFileName); printf("full path:%ws\n", szFullPath); return; } } ZeroMemory(&fd, sizeof(fd)); } while (FindNextFile(hSearch, &fd)); } void FindFile(LPCTSTR pFileName) { TCHAR szVolumn[MAX_PATH] = _T(""); GetLogicalDriveStrings(MAX_PATH, szVolumn); LPCTSTR pVolumnName = szVolumn; while (_tcscmp(pVolumnName, _T("")) != 0) { FindFileByPath(pVolumnName, pFileName); //偏移到下一个盘符的字符串位置 size_t nLen = 0; StringCchLength(pVolumnName, MAX_PATH, &nLen); nLen++; pVolumnName += nLen; } }由于这段代码会遍历整个磁盘,查找所有具有相同文件名称的文件,所以当某个逻辑分区的文件结构比较复杂的时候,可能执行效果比较慢。这段代码出现了两个函数,第一个函数是真正遍历文件的函数,由于FindFirst函数需要传入一个入口点,所以在需要进行全盘遍历的时候提供了另外一个函数来获取所有磁盘的逻辑分区名。获取所有逻辑分区名调用函数GetLogicalDriveStrings,这个函数会返回一个含有所有分区名称的字符串,每个分区名称之间以"\0"分割,所以在获取所有名称的时候需要自己进行字符串指针的偏移操作在遍历的时候为了要遍历所有文件及目录搜索的统配符应该匹配所有文件名称。另外FindFirst也会返回一个文件信息的结构,这个结构是当前目录中符合条件的第一个文件信息,在遍历的时候不要忘记也取一下它返回的文件信息。最后当文件为目录的时候需要判断它是否为当前目录或者当前目录的父目录,也就是是否为"."和"..",这段代码有一点不足就是不支持通配符,必须输入文件名的全称目录变更监视一般像notepad++等文本编辑器都会提供一个功能,就是在它们打开了一个文本之后,如果文本被其他程序更改,那么它们会提示用户是否需要重新载入,这个功能的实现需要对文件进行监控,windows中提供了一套API用于监控目录变更使用函数FindFirstChangeNotification创建一个监控句柄,该函数原型如下:HANDLE FindFirstChangeNotification( LPCTSTR lpPathName, BOOL bWatchSubtree, DWORD dwNotifyFilter);第一个参数是一个目录的字符串,表示将要监控哪个目录,注意这里必须穿入一个目录,不能穿文件路径第二个参数是一个bool类型,表示是否监控目录中的整个目录树第三个参数是监控的时间类型,如果要监控目录中的文件的改动,可以使用FILE_NOTIFY_CHANGE_LAST_WRITE 标记,该标记会监控文件的最后一次写入,其他类型请查阅MSDN创建监控句柄后使用Wait函数循环等待监控句柄,如果目录中发生对应的事件,wait函数返回,这个时候可以对比上次目录结构得出哪个文件被修改,做相应的处理后调用FindNextChangeNotification函数传入监控句柄,继续监控下一次变更。最后当我们不需要进行监控的时候调用FindCloseChangeNotification关闭监控句柄void WatchDirectoryChange(LPCTSTR lpDir) { HANDLE hChangNotify = FindFirstChangeNotification(lpDir, FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE ); if (hChangNotify == INVALID_HANDLE_VALUE) { printf("FindFirstChangeNotification function faild!\n"); return ExitProcess(GetLastError()); } while (TRUE) { printf("wait for change notify.......\n"); if(WAIT_OBJECT_0 == WaitForSingleObject(hChangNotify, INFINITE)) { printf("some file be changed in this directory\n"); } FindNextChangeNotification(hChangNotify); } FindCloseChangeNotification(hChangNotify); }如果嫌这个方法比较麻烦的话,为了实现这个功能,Windows专门提供了一个函数ReadDirectoryChangesW,就跟他的名字一样他只能用于UNICODE平台,这个函数不存在ANSI版本,所以在ANSI版本时需要进行字符串的转化操作。函数原型如下:BOOL WINAPI ReadDirectoryChangesW( __in HANDLE hDirectory, //需要监控的目录的句柄,这个句柄可以用CreateFile打开 __out LPVOID lpBuffer, //函数返回信息的缓冲 __in DWORD nBufferLength, //缓冲区的长度 __in BOOL bWatchSubtree, //是否监控它的子目录 __in DWORD dwNotifyFilter, //监控的事件 __out_opt LPDWORD lpBytesReturned, //实际返回数据长度 __inout_opt LPOVERLAPPED lpOverlapped, //异步调用时的OVERLAPPED结构 __in_opt LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine //异步调用时的APC函数);这个函数它的原理就类似于上面的三个函数,如果是同步操作,当需要监控的目录发生指定的事件时函数返回,并将监控得到的信息填充到结构体中,它会将数据以FILE_NOTIFY_INFORMATION结构的形式返回。该结构的定义如下:typedef struct _FILE_NOTIFY_INFORMATION { DWORD NextEntryOffset; DWORD Action; DWORD FileNameLength; WCHAR FileName[1]; } FILE_NOTIFY_INFORMATION, *PFILE_NOTIFY_INFORMATION;这个结构体中存储文件名称的成员为FileName,这个成员只是起到一个变量名称标识的作用,在存储文件名称时用到了越界访问的方式,所以定义缓冲的大小一定要大于这个结构,让其有足够的空间容纳FileName这个字符串。结构体中的Action表示当前发生了何种操作,具体的类型可以参考MSDN,它的意思根据字面的单词很容易理解下面是使用它的具体代码:void WatchFileChange(LPCTSTR lpFilePath) { DWORD cbBytes; char notify[1024]; HANDLE dirHandle = CreateFile(lpFilePath,GENERIC_READ | GENERIC_WRITE | FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); if(dirHandle == INVALID_HANDLE_VALUE) //若网络重定向或目标文件系统不支持该操作,函数失败,同时调用GetLastError()返回ERROR_INVALID_FUNCTION { cout<<"error"+GetLastError()<<endl; } memset(notify,0,strlen(notify)); FILE_NOTIFY_INFORMATION *pnotify = (FILE_NOTIFY_INFORMATION*)notify; cout<<"start...."<<endl; while(true) { if(ReadDirectoryChangesW(dirHandle,¬ify,1024,true, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_SIZE, &cbBytes,NULL,NULL)) { //设置类型过滤器,监听文件创建、更改、删除、重命名等 switch(pnotify->Action) { case FILE_ACTION_ADDED: _tprintf(_T("add file: %s\n"), pnotify->FileName); break; case FILE_ACTION_MODIFIED: _tprintf(_T("modify file:%s\n"), pnotify->FileName); break; case FILE_ACTION_REMOVED: _tprintf(_T("file removed %s\n"), pnotify->FileName); break; case FILE_ACTION_RENAMED_OLD_NAME: _tprintf(_T("file renamed:%s\n"), pnotify->FileName); break; default: cout<<"unknow command!"<<endl; } } } CloseHandle(dirHandle); }这段代码很容易理解,但是需要注意几点:之前说过的分配的缓冲一定要大于FILE_NOTIFY_INFORMATION 结构这个函数也是用来监控目录的,所以这里要传入一个目录路径,不能传入文件路径在使用CreateFile来打开目录的时候这个函数要求传入的文件句柄必须要以FILE_LIST_DIRECTORY标识打开,否则在调用的时候会报“参数错误”这个错文件映射Windows中,文件映射是文件内容到进程的虚拟地址空间的映射,这个映射称之为File Mapping,文件内容的拷贝就是文件视图(File View),从内存管理的角度来看,文件映射只是将磁盘的真实地址通过页表映射到进程的虚拟地址空间中,读写这段虚拟地址空间其实就是在读写磁盘。而文件视图就是将文件中的内容整个读到内存中,并将这段虚拟地址空间与真实物理内存对应。最终在关闭整个文件映射的时候如果存在文件视图,操作系统会将视图中的内容写会到磁盘,其实也就是简单的进行了下物理内存到磁盘的页面交换,从内存管理的角度来看,文件映射其实就是操作系统将磁盘上的数据与物理内存之间的页面交换,操作系统在二者之间来回倒腾数据而已文件映射本身是一个内核对象,操作系统在内核中维护了一个相关的数据结构,这个结构中记录了被映射到虚拟地址空间中的起始地址和被映射的数据的大小。由于内核对象的数据结构是在内核中被维护,而内核被所有进程共享,所以从理论上将不同的进程是可以共享同一个内核对象的,虽然它们的对象句柄会在不同进程中呈现不同的值,但是在内核中,却是指向同一个结构,那么虽然不同进程的文件映射对象不同,但是通过寻址得到的物理内存肯定是同一个,所以这就提供了另一种进程间共享内存的方法——文件映射。创建文件映射主要使用函数CreateFileMapping,这个函数第一个参数是一个文件句柄,这个句柄可以是一个真实存在在磁盘上的文件,这样创建的文件映射最终就是将磁盘中的数据映射到进程的虚拟地址空间,也可以传入一个INVALID_HANDLE_VALUE,这个时候也会返回成功,传入INVALID_HANDLE_VALUE一般是用来在进程间共享内存的。注意:这个函数只是创建了一个内核对象并返回它的句柄,并没有进行内存映射的相关操作。同时由于它第一个句柄参数可以填INVALID_HANDLE_VALUE,在使用CreateFile函数后一定要注意校验,不然可能看到CreateFileMapping函数返回的是一个有效句柄,但是并没有成功创建这个文件的映射然后调用MappingViewOfFile函数,将对应文件与一段进程的虚拟地址空间关联并将文件映射到内存,也就是将磁盘文件中的数据交换到物理内存中当我们不使用这块真实内存的时候,调用UnMapViewOfFile将内存中的数据交换到磁盘,最终使用文件映射完毕后,调用CloseHandle关闭所有句柄使用文件映射一般有几个好处:针对文件来说,文件映射本质上是磁盘到物理内存之间的页面交换,由操作系统的内存管理机制统一调度,效率比一般的文件读写要高,而且在使用完毕后,操作系统会自动的将内存中的数据写到磁盘中,不用手动的更新文件针对不同进程来说,使用文件映射来共享内存本质上是在使用同样一块内存,相比于管道油槽等方式传输数据来说显得更为高效下面通过几个例子来说明在这两种情况下使用文件映射void GetFileNameByHandle(HANDLE hFile) { HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); if (INVALID_HANDLE_VALUE == hMapping) { _tprintf(_T("create file mapping error\n")); return; } LPVOID lpMappingMemeory = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 1); if (NULL == lpMappingMemeory) { _tprintf(_T("MapViewOfFile error\n")); return; } TCHAR szFileName[MAX_PATH] = _T(""); if(0 == GetMappedFileName(GetCurrentProcess(), lpMappingMemeory, szFileName, MAX_PATH)) { _tprintf(_T("GetMappedFileName error\n")); return; } TCHAR szTemp[MAX_PATH] = _T(""); GetLogicalDriveStrings(MAX_PATH, szTemp); TCHAR szDriver[4] = _T(" :"); LPCTSTR p = szTemp; while (*p != _T('\0')) { *szDriver = *p; TCHAR szName[MAX_PATH] = _T(""); QueryDosDevice(szDriver, szName, MAX_PATH); size_t nPathLen = 0; StringCchLength(szName, MAX_PATH, &nPathLen); if(CSTR_EQUAL == CompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE, szName, nPathLen, szFileName, nPathLen)) { TCHAR szFullPath[MAX_PATH] = _T(""); StringCchCopy(szFullPath, MAX_PATH, p); //在这使用文件带卷名的字符串首地址 + 卷名长度 + 1(+1是为了偏移到卷名后面的"\"的下一个字符,因为这个盘符中自己带了"/"字符) StringCchCat(szFullPath, MAX_PATH, szFileName + nPathLen + 1); _tprintf(_T("文件全路径:%s"), szFullPath); break; } size_t dwLen = 0; StringCchLength(p, MAX_PATH, &dwLen); p = p + dwLen + 1; } UnmapViewOfFile(lpMappingMemeory); CloseHandle(hMapping); return; }该函数利用文件映射的方式,通过一个文件的句柄获取它的绝对路径。该函数首先根据文件句柄创建一个文件映射并调用GetMappedFileName获取文件的全路径,但是获取到的是类似于“\Device\HarddiskVolume6\Program\FileDemo\FileMapping\FileMapping.cpp”这样的卷名加上文件的相对路径,而不是我们常见的类似于C D E这样的盘符名称,所以为了获取对应的盘符,使用的方式是利用GetLogicalDriverString函数来获取系统所有逻辑卷的盘符,然后调用QueryDosDevice函数将盘符转化为卷名,再与之前获取到的路径中的卷名进行比较,在这使用了一个技巧,就是首先获取卷名对应的长度,然后调用比较函数时传入卷名的长度让其只比较卷名对应的字符,如果相同,就找到了卷名对应的盘符名称,最后将卷名与在卷中的相对路径进行拼接就得到了它的文件全路径。下面来看一个使用文件映射在不同进程间共享内存的例子//Process A #define BUFF_SIZE 1024 int _tmain(int argc, _TCHAR* argv[]) { TCHAR szHandleName[] = _T("Global\\ShareMemMapping"); HANDLE hMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BUFF_SIZE, szHandleName); if (INVALID_HANDLE_VALUE == hMapping) { printf("create file mapping error\n"); return GetLastError(); } LPVOID pMem = MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, BUFF_SIZE); if (NULL == pMem) { printf("MapViewOfFile Error\n"); return GetLastError(); } ZeroMemory(pMem, BUFF_SIZE); TCHAR pszData[] = _T("this is written by process A"); CopyMemory(pMem, pszData, sizeof(pszData)); _tsystem(_T("PAUSE")); UnmapViewOfFile(pMem); CloseHandle(hMapping); return 0; }#define BUFF_SIZE 1024 int _tmain(int argc, _TCHAR* argv[]) { TCHAR szHandleName[] = _T("Global\\ShareMemMapping"); HANDLE hMapping = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, szHandleName); if (INVALID_HANDLE_VALUE == hMapping) { printf("OpenFileMapping"); return GetLastError(); } LPCTSTR pMem = (LPCTSTR)MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, BUFF_SIZE); if (NULL == pMem) { printf("MapViewOfFile Error\n"); return GetLastError(); } printf("read date: %ws\n", pMem); _tsystem(_T("PAUSE")); UnmapViewOfFile(pMem); CloseHandle(hMapping); return 0; } 在上面的例子中,进程A做了如下工作:创建一个命名的文件映射对象构建文件映射的视口,并写入一段内存等待关闭相关句柄在进程B中做了如下工作:打开之前A创建的文件映射对象构建文件映射的视口,读取内存关闭相关句柄在使用文件映射共享内存时需要注意:使用命名对象的时候,对象前面必须要加上“Global//”表示该对象是一个全局的对象不同进程在使用文件映射共享内存时调用函数MapViewOfFile填写内存的起始偏移,视口大小必须完全一样这个例子中只是简单的一个进程写,另一个进程读,如果想要两个进程同时读写共享内存,可以使用Event等方式进行同步。直接读写磁盘扇区CreateFile可以打开许多设备,一般来说,它可以打开所有的字符设备,向串口,管道,油槽等等,在编写某些硬件的驱动程序时如果将其以字符设备的方式来操作,那么理论上在应用层是可以用CreateFile打开这个硬件设备的句柄,并操作它的,这里介绍下如何使用CreateFile来直接读取物理磁盘。读写物理磁盘只需要改变一下CreateFile中代表文件名称的第一个参数,将这个参数改为\.\PhysicalDrive0,后面的数字代表的是第几块物理硬盘,如果有多块硬盘,后面还可以是1、2等等注意这是在直接读写物理磁盘,当你不了解文件系统的时候,不要随意往里面写数据,以免造成磁盘损坏下面是一个简单的例子 DWORD dwSectorsPerCluster = 0; DWORD dwBytesPerSector = 0; DWORD dwNumberOfFreeClusters = 0; DWORD dwTotalNumberOfClusters = 0; TCHAR pDiskName[] = _T("\\\\.\\PhysicalDrive0"); //get disk info if(GetDiskFreeSpace(_T("c:\\"), &dwSectorsPerCluster, &dwBytesPerSector, &dwNumberOfFreeClusters, &dwTotalNumberOfClusters)) { printf("磁盘信息:\n"); LARGE_INTEGER size_disk = {0}; size_disk.QuadPart = (LONGLONG)dwTotalNumberOfClusters * (LONGLONG)dwSectorsPerCluster * (LONGLONG)dwBytesPerSector; printf("\t总大小 %dG", size_disk.QuadPart / (1024 * 1024 * 1024)); printf("\t簇总数%d, 簇中扇区总数:%d, 扇区大小:%d\n", dwTotalNumberOfClusters, dwSectorsPerCluster, dwBytesPerSector); } else { dwBytesPerSector = 512; } HANDLE hDisk = CreateFile(pDiskName,GENERIC_READ,FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,NULL,OPEN_EXISTING,0,NULL); if(hDisk == INVALID_HANDLE_VALUE) { printf("create file error\n"); return GetLastError(); } char* pMem = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBytesPerSector * 8); DWORD dwRead = 0; if(!ReadFile(hDisk, pMem, dwBytesPerSector * 8, &dwRead, NULL)) { printf("read file error\n"); return GetLastError(); } for(int i = 0; i < dwBytesPerSector * 8; i++) { if(i % 16 == 0 && i != 0) { printf("\n"); } printf("0x%02x ", pMem[i]); } CloseHandle(hDisk);上面的例子调用了GetDiskFreeSpace函数获取了逻辑卷的相关信息,它需要传入一个盘符,表示要获取哪个盘的数据,它会通过输出参数返回多个逻辑卷的信息,它们分别是:每个簇有多少个扇区,每个扇区的大小,有多少个空闲的簇,卷中簇的个数。根据这些信息就可以计算出逻辑卷的大小哦,在计算的时候由于磁盘空间一定是大于4G的,所以在这要用64位整数保存。知道了扇区大小后,直接调用文件操作函数,读取8个扇区的数据,然后输出。文件的异步操作在常规文件读写方式中,是严格串行化的,只有当读写操作完全完成时才会返回,由于磁盘读写相对于CPU的运行效率来说实在是太慢的,这就造成了程序长时间处理等待状态,这种读写方式称之为阻塞方式,早期的磁盘在进行读写时是需要CPU来控制,这样CPU必须来配合慢速的硬盘,造成了效率低下,于是硬件工程师在在磁盘中加入了一个控制设备,专门用来控制磁盘的读写,这个设备被称之为DMA,由于DMA的存在,使得CPU从漫长的磁盘操作中解放出来,一般在进行磁盘读写时,CPU主要向DMA发出一个读写命令,然后就继续执行后面的工作,当读写完成后DMA向CPU发出完成的指令,这个时候CPU会停下手上的工作,来处理这个通知,程序此时会陷入中断,直到CPU完成对应的操作。由于DMA的出现使得CPU从慢速的磁盘操作中解放出来,但是在同步的读写方式中,CPU发出磁盘的读写指令后什么都不做,一直等待磁盘的读写玩成,使CPU长时间陷入等待状态,浪费了宝贵的CPU的资源。所以为了程序效率,在读写磁盘时一般使用异步的方式,在发出读写命令后立即返回,然后执行后面的操作,这样就在一定程度上利用了闲置的CPU资源。重叠IO在Windows中默认使用同步的方式进行读写操作,如果要使用异步的方式,在创建文件句柄的时候,需要在CreateFile函数的dwFlagsAndAttributes参数中加上FILE_FLAG_OVERLAPPED标识,然后可以设置一个完成函数,并在对应线程中调用waitex函数或者使用SleepEx函数使线程陷入可警告状态,当读写操作完成时会将完成函数插入线程的APC队列,当线程进入可警告状态的时候会调用APC函数,这样就可以知道读写操作已经完成。这是一种方式,还可以使用一个OVERLAPPED结构,并给这个结构中填上一个事件对象,在需要进行同步的地方等待这个事件对象,在磁盘操作完成的时候会将其设置为有信号,上面的两种方式都利用的Windows提供的重叠IO模型不管使用哪种方式,在进行文件的异步操作时都需要自己维护并偏移文件指针。在同步的方式时Windows是完成之后返回,它一次只会写入一条数据到磁盘,而且它也知道具体写入了多少数据,这时候系统帮助我们完成了文件指针的偏移,但是在进行异步操作的时候可能会同时有多条数据写入,并且系统不知道具体会成功写入多少数据,所以它不可能帮我们进行文件指针的偏移,这个时候就需要自己进行偏移操作完成函数使用完成函数主要需要如下步骤:调用CreateFile在dwFlagsAndAttributes参数中加上FILE_FLAG_OVERLAPPED标识表示我们需要使用异步的方式来进行磁盘操作准备一个完成函数,函数的原型为:VOID CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode,DWORD dwNumberOfBytesTransfered,LPOVERLAPPED lpOverlapped);函数的最后一个参数是一个OVERLAPPED结构,该结构的定义如下:typedef struct _OVERLAPPED { ULONG_PTR Internal; ULONG_PTR InternalHigh; union { struct { DWORD Offset; DWORD OffsetHigh; }; PVOID Pointer; }; HANDLE hEvent; } OVERLAPPED, *LPOVERLAPPED;这个结构中有一个共用体,其实这个共用体都可以用来操作文件指针,如果用其中的结构体,那么需要分别给其中的高32位和低32位赋值,如果使用指针,这个时候指针变量不指向任何内存,这个指针变量仅仅是作为一个变量名罢了,使用时也是将其作为正常变量来使用,虽然它是一个指针占4个字节,但是由于是一个共用体,它后面还有4个字节的剩余空间可以使用,所以使用它来存储文件指针的偏移没有任何问题。调用ReadFileEx或者WriteFileEx函数(ReadFile WriteFile不支持完成函数的方式)并将完成函数作为最后一个参数传入调用WaitEx族的等待函数或者SleepEx函数使线程陷入可警告状态,这个时候会执行完成函数下面是一个演示的例子LARGE_INTEGER g_FilePointer = {0}; //全局的文件指针 struct ST_EXT_OVERLAPPED { OVERLAPPED m_ol; //后面的代码在使用的时候后 HANDLE m_hFile; //操作的文件句柄 LPVOID m_pData; //操作的内存 DWORD m_dwLen; //操作的数据长度 }; VOID CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode,DWORD dwNumberOfBytesTransfered,LPOVERLAPPED lpOverlapped) { ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)lpOverlapped; printf("线程[%04x]完成写入操作\n", GetCurrentThreadId()); HeapFree(GetProcessHeap(), 0, pExOl->m_pData); HeapFree(GetProcessHeap(), 0, pExOl); pExOl = NULL; } DWORD WriteThreadProc(LPVOID lpParameter) { HANDLE hFile = *(HANDLE*)(lpParameter); ST_EXT_OVERLAPPED* pExOl = NULL; TCHAR szBuf[256] = _T(""); StringCchPrintf(szBuf, 256, _T("这是一条模拟日志写入信息,由线程[%04x]写入\r\n"), GetCurrentThreadId()); size_t dwLen = 0; StringCchLength(szBuf, 256, &dwLen); dwLen += 1; //保存字符串结尾的\0 for (int i = 0; i < 100; i++) { pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExOl->m_dwLen = dwLen * sizeof(TCHAR); pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLen * sizeof(TCHAR)); StringCchCopy((TCHAR*)pExOl->m_pData, 256, szBuf); pExOl->m_hFile = hFile; //使用锁无关的方式进行同步操作 *((LONGLONG*)&pExOl->m_ol.Pointer) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + pExOl->m_dwLen, g_FilePointer.QuadPart); WriteFileEx(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, (OVERLAPPED*)&pExOl->m_ol, FileIOCompletionRoutine); //do something if(WAIT_IO_COMPLETION == SleepEx(INFINITE, TRUE)) { } } return 0; } int _tmain(int argc, _TCHAR* argv[]) { HANDLE hFile = CreateFile(_T("log.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);//让其支持异步操作 if (hFile == INVALID_HANDLE_VALUE) { printf("CreateFile error\n"); return GetLastError(); } ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExOl->m_hFile = hFile; pExOl->m_dwLen = sizeof(WORD); pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD)); *((WORD*)pExOl->m_pData) = MAKEWORD(0xff,0xfe); //文件指针的偏移 pExOl->m_ol.Offset = g_FilePointer.LowPart; pExOl->m_ol.OffsetHigh = g_FilePointer.HighPart; g_FilePointer.QuadPart += pExOl->m_dwLen; WriteFileEx(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, (LPOVERLAPPED)&pExOl->m_ol, FileIOCompletionRoutine); HANDLE hThreads[20] = {NULL}; for (int i = 0; i < 20; i++) //创建20个写线程 { hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThreadProc, &hFile, 0, NULL); } while(WAIT_IO_COMPLETION == WaitForMultipleObjectsEx(20, hThreads, TRUE, INFINITE, TRUE)) //函数返回WAIT_IO_COMPLETION 表示执行了完成函数 { printf("有一个读写操作完成\n"); } for (int i = 0; i < 20; i++) { CloseHandle(hThreads[i]); } CloseHandle(hFile); _tsystem(_T("PAUSE")); return 0; }在上面的例子中,我们首先向文件中写入0xff, 0xfe这两个值,在Windows中存储Unicode字符串的文件都是以0xff 0xfe开头,所以在写入Unicode字符串之前需要写入这两个值然后创建了20个线程,每个线程负责往文件中写入100条数据。线程先创建了一个包含OVERLAPPED结构的数据类型,然后再使用InterlockedCompareExchange64同步文件指针,这句话的意思是,向将高速缓存中的数据与内存中的数据进行比较,如果二者的值相同,那么久更改全局的文件指针,否则就不进行变化。实际上在Intel架构的机器上存在大量的高速缓存,为了效率,有的时候会将一些数据放置到高速缓存中,这样造成高速缓存中一份,内存中也有一份,有的时候在进行值得更改时它只会改变内存中的值,而高速缓存中的值不会更新,在调用这个函数的时候第一个参数传入的是一个指针,取值操作会强制CPU到内存中进行访问,这样这句话实质上是比较高速缓存与内存中的值是否一致,如果不一致,那么说明它被其他的线程进行过修改,将新的文件指针进行了替换,那么这个时候不需要进行任何操作,在之前写入文件的末尾进行追加即可,如果没有发生修改,那么其他线程可能会在当前位置写入,本线程也在当前位置写的话会造成覆盖,所以往后偏移文件指针,使其他线程使用新偏移的位置,本线程使用当前的位置,这样就不会发生覆盖在完成历程中完成清理内存的任务。每个WriteFileEx都对应着内存的分配,完成后都会调用这个完成历程清理对应的内存,这样就不会造成内存泄露。最后在主线程中等待子线程的完成,然后关闭句柄并结束进程事件模型事件模型与之前的完成历程相似,只是它不需要设置完成函数,需要在OVERLAPPED结构中设置一个事件,当IO操作完成时会将这个事件设置为有信号,然后在需要进行同步的位置等待这个事件即可下面是它的具体的例子LARGE_INTEGER g_FilePointer = {0}; //全局的文件指针 struct ST_EXT_OVERLAPPED { OVERLAPPED m_ol; //后面的代码在使用的时候后 HANDLE m_hFile; //操作的文件句柄 LPVOID m_pData; //操作的内存 DWORD m_dwLen; //操作的数据长度 }; DWORD WriteThreadProc(LPVOID lpParameter) { HANDLE hFile = *(HANDLE*)(lpParameter); ST_EXT_OVERLAPPED* pExOl = NULL; TCHAR szBuf[256] = _T(""); StringCchPrintf(szBuf, 256, _T("这是一条模拟日志写入信息,由线程[%04x]写入\r\n"), GetCurrentThreadId()); size_t dwLen = 0; StringCchLength(szBuf, 256, &dwLen); dwLen += 1; //保存字符串结尾的\0 for (int i = 0; i < 100; i++) { pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExOl->m_dwLen = dwLen * sizeof(TCHAR); pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLen * sizeof(TCHAR)); StringCchCopy((TCHAR*)pExOl->m_pData, 256, szBuf); pExOl->m_hFile = hFile; pExOl->m_ol.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //使用锁无关的方式进行同步操作 *((LONGLONG*)&pExOl->m_ol.Pointer) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + pExOl->m_dwLen, g_FilePointer.QuadPart); DWORD dwWritten = 0; WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (OVERLAPPED*)&pExOl->m_ol); //do something if(WAIT_OBJECT_0 == WaitForSingleObject(pExOl->m_ol.hEvent, INFINITE)) { printf("线程[%04x],写入操作完成一次,继续等待写入.....\n", GetCurrentThreadId()); HeapFree(GetProcessHeap(), 0, pExOl->m_pData); HeapFree(GetProcessHeap(), 0, pExOl); } } return 0; } int _tmain(int argc, _TCHAR* argv[]) { HANDLE hFile = CreateFile(_T("log.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);//让其支持异步操作 if (hFile == INVALID_HANDLE_VALUE) { printf("CreateFile error\n"); return GetLastError(); } ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExOl->m_hFile = hFile; pExOl->m_dwLen = sizeof(WORD); pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD)); *((WORD*)pExOl->m_pData) = MAKEWORD(0xff,0xfe); pExOl->m_ol.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //文件指针的偏移 pExOl->m_ol.Offset = g_FilePointer.LowPart; pExOl->m_ol.OffsetHigh = g_FilePointer.HighPart; g_FilePointer.QuadPart += pExOl->m_dwLen; DWORD dwWritten = 0; WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (LPOVERLAPPED)&pExOl->m_ol); HANDLE hThreads[20] = {NULL}; //等待当前写入完成 if (WAIT_OBJECT_0 == WaitForSingleObject(pExOl->m_ol.hEvent, INFINITE)) { printf("写入头部操作完成\n"); HeapFree(GetProcessHeap(), 0, pExOl->m_pData); HeapFree(GetProcessHeap(), 0, pExOl); } for (int i = 0; i < 20; i++) //创建20个写线程 { hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThreadProc, &hFile, 0, NULL); } WaitForMultipleObjects(20, hThreads, TRUE, INFINITE); for (int i = 0; i < 20; i++) { CloseHandle(hThreads[i]); } CloseHandle(hFile); _tsystem(_T("PAUSE")); return 0; }上面的例子与之前的完成历程的例子基本上一样,只是在OVERLAPPED结构中加入EVENT对象,并且没有完成历程,内存的清理工作需要在本线程中进行清理完成端口上述重叠IO在一定程度上解决的线程陷入等待的问题,但是从上面的代码上来看,仍然需要在本线程中进行等待操作,也就是说,如果在IO函数返回后进行某项操作,但是这项操作完成后而IO操作并没有完成,那么仍然要陷入等待,现在有一个想法,就是同步操作不在本线程中完成,另外开辟一个线程,将所有的等待操作都放到新线程中,而本线程就不必进行等待,同步线程只需要在操作完成的时候启动执行,这样几乎就不存在CPU等待IO设备的问题。主要的问题是,怎么向新线程传递同步对象,就像上面的例子来说,等待IO操作完成就是为了清理内存而已,这个时候如果创建新线程进行等待的话,总共有2000个写入操作,为了清理每块内存,需要定义一个2000O包含VERLAPPED结构的数组,然后当所有线程启动后将数组指针传入,如果为每个如果动态添加新的写入线程,那就必须修改数组大小。这给编程造成了很大的麻烦,为了解决这个问题,VC中引入了完成端口模型本质上完成端口利用了线程池机制并结合了重叠IO的优势,在Windows下这种IO模型是最高效的一种。完成端口首先创建对应数量的线程的线程池,然后将相关的文件句柄与完成端口对象绑定,并传入一个OVERLAPPED结构的指针,然后进行等待,一旦有IO操作完成,就会启动完成端口中的线程,完成后续的操作。完成端口的使用一般经过下面几个步骤:调用CreateIoCompletionPort创建完成端口对象,并制定最大并发线程数(一般制定CPU核数或者核数的两倍)创建用于完成端口的线程,一般大于等于最大并发数调用函数CreateIoCompletionPort,将文件句柄与完成端口绑定在IO操作中传入一个OVERLAPPED结构在完成端口的线程中调用GetQueuedCompletionStatus进行等待,当有IO操作完成时函数会返回,对应的线程就可以启动执行函数CreateIoCompletionPort原型如下HANDLE WINAPI CreateIoCompletionPort( __in HANDLE FileHandle, __in_opt HANDLE ExistingCompletionPort, __in ULONG_PTR CompletionKey, __in DWORD NumberOfConcurrentThreads );第一个参数是文件句柄,第二参数是完成端口句柄,第三个参数是一个完成的标识。一般给NULL,第四个是最大线程数。一般在操作的时候如果是创建完成端口句柄,那么只需要指定最大并发线程数,如果是将文件句柄和完成端口对象进行绑定,只需要提供前连个参数。在下面的例子中可以很清楚的看到它的用法下面是一个使用完成端口的例子:LARGE_INTEGER g_FilePointer = {0}; //全局的文件指针 struct ST_EXT_OVERLAPPED { OVERLAPPED m_ol; //后面的代码在使用的时候后 HANDLE m_hFile; //操作的文件句柄 LPVOID m_pData; //操作的内存 DWORD m_dwLen; //操作的数据长度 BOOL bExit; }; DWORD WriteThreadProc(LPVOID lpParameter) { HANDLE hFile = *(HANDLE*)(lpParameter); ST_EXT_OVERLAPPED* pExOl = NULL; TCHAR szBuf[256] = _T(""); StringCchPrintf(szBuf, 256, _T("这是一条模拟日志写入信息,由线程[%04x]写入\r\n"), GetCurrentThreadId()); size_t dwLen = 0; StringCchLength(szBuf, 256, &dwLen); dwLen += 1; //保存字符串结尾的\0 for (int i = 0; i < 100; i++) { pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExOl->m_dwLen = dwLen * sizeof(TCHAR); pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLen * sizeof(TCHAR)); StringCchCopy((TCHAR*)pExOl->m_pData, 256, szBuf); pExOl->m_hFile = hFile; pExOl->bExit = FALSE; //使用锁无关的方式进行同步操作 *((LONGLONG*)&pExOl->m_ol.Pointer) = InterlockedCompareExchange64(&g_FilePointer.QuadPart, g_FilePointer.QuadPart + pExOl->m_dwLen, g_FilePointer.QuadPart); DWORD dwWritten = 0; WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (OVERLAPPED*)&pExOl->m_ol); } return 0; } DWORD IocpThreadProc(LPVOID lpParameter) { HANDLE hIocp = *(HANDLE*)lpParameter; DWORD dwBytesTransfered = 0; DWORD dwFlags = 0; LPOVERLAPPED pOl = NULL; while (TRUE) { ST_EXT_OVERLAPPED* pExOl = NULL; BOOL bRet = GetQueuedCompletionStatus(hIocp, 0, 0, &pOl, INFINITE);//MSDN上说如果完成端口队列为空,那么函数会返回FLASE,并且pOl为NUULL, 所以在这进行判断,如果为FLASE,就不往下执行,否则程序会崩溃 if (!bRet) { continue; } pExOl = (ST_EXT_OVERLAPPED*)pOl; if (pExOl->bExit) { printf("收到退出消息,IOCP线程[%04x]退出", GetCurrentThreadId()); HeapFree(GetProcessHeap(), 0, pExOl); return 0; } printf("有一个线程的写入操作完成\n"); HeapFree(GetProcessHeap(), 0, pExOl->m_pData); HeapFree(GetProcessHeap(), 0, pExOl); } } int _tmain(int argc, _TCHAR* argv[]) { HANDLE hFile = CreateFile(_T("log.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);//让其支持异步操作 if (hFile == INVALID_HANDLE_VALUE) { printf("CreateFile error\n"); return GetLastError(); } //创建IOCP内核对象并制定最大并发线程数 SYSTEM_INFO si = {0}; GetSystemInfo(&si); HANDLE hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 2 * si.dwNumberOfProcessors); //创建IOCP线程 HANDLE* hIocpThreads = (HANDLE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 2 * si.dwNumberOfProcessors * sizeof(HANDLE)); for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { hIocpThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)IocpThreadProc, &hIocp, 0, NULL); } //将文件句柄与IOCP句柄绑定 CreateIoCompletionPort(hFile, hIocp, NULL, 0); ST_EXT_OVERLAPPED* pExOl = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExOl->m_hFile = hFile; pExOl->m_dwLen = sizeof(WORD); pExOl->m_pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WORD)); *((WORD*)pExOl->m_pData) = MAKEWORD(0xff,0xfe); pExOl->bExit = FALSE; //文件指针的偏移 pExOl->m_ol.Offset = g_FilePointer.LowPart; pExOl->m_ol.OffsetHigh = g_FilePointer.HighPart; g_FilePointer.QuadPart += pExOl->m_dwLen; DWORD dwWritten = 0; WriteFile(pExOl->m_hFile, pExOl->m_pData, pExOl->m_dwLen, &dwWritten, (LPOVERLAPPED)&pExOl->m_ol); HANDLE hThreads[20] = {NULL}; for (int i = 0; i < 20; i++) //创建20个写线程 { hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteThreadProc, &hFile, 0, NULL); } //等待写入线程的完成 WaitForMultipleObjects(20, hThreads, TRUE, INFINITE); for (int i = 0; i < 20; i++) { CloseHandle(hThreads[i]); } //关闭IOCP线程 for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { ST_EXT_OVERLAPPED* pExitMsg = (ST_EXT_OVERLAPPED*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ST_EXT_OVERLAPPED)); pExitMsg->bExit = TRUE; PostQueuedCompletionStatus(hIocp, 0, 0, &pExitMsg->m_ol); } //关闭IOCP线程句柄 for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { CloseHandle(hIocpThreads[i]); } CloseHandle(hFile); _tsystem(_T("PAUSE")); return 0; }
2017年06月11日
4 阅读
0 评论
0 点赞