Linux程序设计(15)第三章:标准I/O库(7)错误处理 perror strerror /proc文件系统 和 fcntl mmap

1. strerror perror

#include <string.h>
char * strerror (int errnxim);
	strerror 函数把错误代码映射为一个字符串,该字符串对发生的错误类型进行说明
	
void perror(const char *s)
	perror函数也把errno变量中报告的当前错误映射到一个字符串,并把它输出到标准错误输出流。
	该字符串的前面先加上字符串s (如果不为空)中给出的信息,再加上一个冒号和一个空格。

例子:

perror("test failed")

//它可能在标准错误输出中给出如下的输出结果:
test failed: Too many open files

打印errno
printf("errno is: %d\n",errno); 

2. proc 文件系统

Linux将一切事物都看作为文件,硬件设备在文件系统中也有相应的条目。
这样我们通过/dev目求中的文件来访问硬件

Linux提供了一个特殊的文件系统procfs,它通常以/proc目录的形式呈现。
该目录中包含了许多 特殊文件用来对驱动程序和内核信息进行更高层的访问
只需直接读取这些文件就可以获得状态信息

例如:

cat /proc/cpuinfo # 查看CPU信息
cat /proc/cpuinfo | grep “processor” | wc -l查看逻辑CPU的个数:
cat /proc/meminfo 查看内存情况
cat /etc/redhat-release 查看linux 版本如:Linux release 7.5.1804
cat /proc/net/sockstat 获得网络套接字的使用统计
cat /proc/sys/fs/file-max 系统能打开的文件最大数

3. fcntl

参考之前的博客
通俗易懂说多路复用(4)fcntl
https://blog.csdn.net/lqy971966/article/details/105390106

4. mmap

4.1 定义:

mmap是一种内存映射文件的方法
即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系

4.2 作用:

1.实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。
2.内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

4.3 映射空间位置

内存映射服务的地址空间处在堆栈之间的空余部分。

4.4 vm_area_struct 结构

linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问。

vm_area_struct结构中包含区域起始和终止地址以及其他相关信息,同时也包含一个vm_ops指针,其内部可引出所有针对这个区域可以使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用要的信息,都可以从vm_area_struct中获得。

mmap函数就是要创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。

4.5 mmap内存映射原理

4.5.1 第一步:进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域

进程在用户空间调用库函数mmap
寻找一段空闲的满足要求的连续的虚拟地址,
将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中

4.5.2 第二步:内核调用系统调用函数mmap(区别于用户态mmap),实现文件物理地址和进程虚拟地址的一一映射关系

通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,
通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体
内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址
通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系

4.5.3 第三步:进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。

之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
(脏页面:也就是被写过,但还没有同步到磁盘上的数据)

4.6 mmap和常规文件操作的区别

4.6.1 常规文件系统操作(调用read/fread等类函数)中,函数的调用过程

1.进程发起读文件请求。
2.内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,
	从而找到此文件的inode。
3.inode 在 address_space 上查找要请求的文件页是否已经缓存在页缓存中。
	如果存在,则直接返回这片文件页的内容。
4.如果不存在,则通过inode定位到文件磁盘地址,将数据从磁盘复制到页缓存。
	之后再次发起读页面过程,进而将页缓存中的数据发给用户进程。

常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。
这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。
这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。

4.6.2 mmap 操作文件

mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。

只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

4.6.3 总结:

总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。
而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。
简而言之,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。

4.7 mmap优点总结

1.对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,
	用内存读写取代I/O读写,提高了文件读取效率。
2.实现了用户空间和内核空间的高效交互方式。
	两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
3.提供进程间共享内存及相互通信的方式。
	不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。
	从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。
4.可用于实现高效的大规模数据传输。
	内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。
	但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。
	但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效

4.8 mmap API

4.8.1 mmap

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
创建映射

返回值:

成功执行时,mmap()返回被映射区的指针。
失败时,mmap()返回MAP_FAILED[其值为(void *)-1]

参数

start:映射区的开始地址
length:映射区的长度
	mmap映射区域大小必须是物理页大小(page_size)的整倍数(32位系统中通常是4k字节)
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
	PROT_EXEC:页内容可以被执行
	PROT_READ:页内容可以被读取
	PROT_WRITE:页可以被写入
	PROT_NONE:页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。
	MAP_SHARED //与其它所有映射这个对象的进程共享映射空间
	MAP_PRIVATE //建立一个写入时拷贝的私有映射。
fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1
offset:被映射对象内容的起点

4.8.2 msync

int msync( void *addr, size_t len, int flags )
刷写数据至磁盘

进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用msync()后才执行该操作。

4.8.3 munmap

int munmap( void * addr, size_t len ) 
解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小

返回值:

成功执行时,munmap()返回0。失败时,munmap返回-1,

4.9 代码示例

4.9.1 mmap修改文件

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <error.h>

#define BUF_SIZE 100

int main(int argc, char **argv)
{
	int fd, nread, i;
	struct stat sb;
	char *mapped, buf[BUF_SIZE];

	for (i = 0; i < BUF_SIZE; i++) {
		buf[i] = '#';
	}

	/* 打开文件 */
	if ((fd = open(argv[1], O_RDWR)) < 0) {
		perror("open");
	}

	/* 获取文件的属性 */
	if ((fstat(fd, &sb)) == -1) {
		perror("fstat");
	}

	/* 将文件映射至进程的地址空间 */
	if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ | 
					PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) {
		perror("mmap");
	}

	/* 映射完后, 关闭文件也可以操纵内存 */
	close(fd);

	printf("%s", mapped);

	/* 修改一个字符,同步到磁盘文件 */
	mapped[20] = '9';
	if ((msync((void *)mapped, sb.st_size, MS_SYNC)) == -1) {
		perror("msync");
	}

	/* 释放存储映射区 */
	if ((munmap((void *)mapped, sb.st_size)) == -1) {
		perror("munmap");
	}

	return 0;
}

结果:

[root@localhost linux]# ./a.out file.in2
abcae
123
456
[root@localhost linux]# 
[root@localhost linux]# 
[root@localhost linux]# cat file.in2
abc9e
123
456
[root@localhost linux]#

4.9.2 mmap实现进程间通信

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <error.h>

#define BUF_SIZE 100

int main(int argc, char **argv)
{
	int fd, nread, i;
	struct stat sb;
	char *mapped, buf[BUF_SIZE];

	for (i = 0; i < BUF_SIZE; i++) {
		buf[i] = '#';
	}

	/* 打开文件 */
	if ((fd = open(argv[1], O_RDWR)) < 0) {
		perror("open");
	}

	/* 获取文件的属性 */
	if ((fstat(fd, &sb)) == -1) {
		perror("fstat");
	}

	/* 将文件映射至进程的地址空间 */
	if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ | 
					PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) {
		perror("mmap");
	}

	/* 文件已在内存, 关闭文件也可以操纵内存 */
	close(fd);
	
	/* 每隔两秒查看存储映射区是否被修改 */
	while (1) {
		printf("%s\n", mapped);
		sleep(2);
	}

	return 0;
}

结果:

[root@localhost linux]# ./a.out file.in2
abc9e
123
456

abc9e
123
456

abc9e
123
456

^C
[root@localhost linux]#

4.9.3 mmap匿名映射实现父子进程通信

#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUF_SIZE 100

int main(int argc, char** argv)
{
	char    *p_map;

	/* 匿名映射,创建一块内存供父子进程通信 */
	p_map = (char *)mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE,
			MAP_SHARED | MAP_ANONYMOUS, -1, 0);

	if(fork() == 0) {
		sleep(1);
		printf("child got a message: %s\n", p_map);
		sprintf(p_map, "%s", "hi, dad, this is son");
		munmap(p_map, BUF_SIZE); //实际上,进程终止时,会自动解除映射。
		exit(0);
	}

	sprintf(p_map, "%s", "hi, this is father");
	sleep(2);
	printf("parent got a message: %s\n", p_map);

	return 0;
}

结果:

[root@localhost linux]# gcc mmap3.c 
[root@localhost linux]# ./a.out
child got a message: hi, this is father
parent got a message: hi, dad, this is son
[root@localhost linux]#

参考:
https://blog.csdn.net/bbzhaohui/article/details/81665370
https://blog.csdn.net/qq_26222859/article/details/80902047

猜你喜欢

转载自blog.csdn.net/lqy971966/article/details/120705836