高级IO

第十二篇 高级IO
本篇索引:
1、引言
2、非阻塞IO
3、记录锁(文件锁)
4、io多路复用(I/O multiplexing )
5、异步IO
6、存储映射IO

注意:这里面的读取Linux下的鼠标信号的时候 有的linux系统是mouse0,而有的是Mouse1.如果读取不到可以试着换一下。

1、引言
我们第三篇学习了对IO的open、read、write等的操作,这一篇我们将会学习对IO的一些高级操作,实际上这一篇的内容是对第三篇内容的进一步升华,主要的内容如下:
•非阻塞IO:对文件实现非阻塞操作。
•记录锁:利用文件实现锁的机制。
•IO多路复用:实现单线同时操作多个阻塞IO,分select和poll两种的操作。
•存储映射IO:mmap
本篇不重理论,重点是列举各种例子代码,教会大家如何使用这些高级IO的设置和使用。
2、非阻塞IO
2.1、低速系统调用之阻塞
上一篇说过,所有系统调用被分为两类,一类是低速系统调用,另一类是其它系统调用。
1)、低速系统调用:可能会使进程永远阻塞的一类系统调用,系统调用导致进程阻塞的原因
有两种。
a)函数操作文件时,因文件类型而阻塞,阻塞与函数本身无关
•读某些文件:由于数据不存在会导致调用者永远阻塞
读管道文件:管道是进程间通信用的特殊文件,读管道时,如果管道中并无数据会导致
对管道的读操作会阻塞。
读终端设备:如读鼠标、键盘等字符设备类文件,以及网络设备文件时,如果没有数据
的话,读操作也会阻塞。
注意:值得强调的是,低速系统调用读磁盘I/O(普通文件)并非是阻塞的,如果有数
据会带着数据正常返回,如果没有数据则也会返回,所以不会阻塞。
•写某些文件:在写某些文件时,当文件不能立即接收写入的数据时,也可能会导致写
操作长期的阻塞下去。
•打开文件:在某些条件成立之前可能有些文件是无法打开的,这同样可能会导致打开
操作长期的阻塞下去。
情况1:如果想要成功打开某些终端设备,那么你就必须等到某些调制解调器应答后才
能打开,否者会一直阻塞下去。
情况2:如果某个管道的读端没打开时,而你又想以写方式打开该管道,那么这个以写
方式打开的操作会一直阻赛直到某个地方以读打开这个管道为止。
b)某些函数本身就是阻塞的
pause函数,wait函数,sleep函数,某些ioctl操作,以及某些进程间通信的函数(如当消息队列的消息接受函数设置了阻塞时),这些函数调用本身就是阻塞的。
2)、其它系统调用:略
2.2、如何设置和修改阻塞为非阻塞
前面说过,某些文件默认打开后默认对文件的操作就是阻塞的,但是利用对文件描述符设置,可将其操作设置为非阻塞的,主要的方法有如下两种。
1)、打开文件时指定非阻塞,例子如下:
以非阻塞的方式打开标准输入文件。

int main(void){
        int fd = -1; 
        fd = open("/dev/stdin", O_RDONLY|O_NONBLOCK);
        if(fd < 0){   
                perror("open stdin is fail");
                exit(-1);
        }   
        return 0;
}

/dev/stdin是标准输入文件,对应着键盘输入,一般情况下默认就是以阻塞方式打开的,但如果我们在打开时指定O_NONBLOCK参数的话,就指定为了非阻塞,当我们去read该文件时就不会再阻塞了。
2)、用fcntl函数进行设置
上例中我们重新打开了标准输入文件,新返回的描述符fd(3)和描述符0同时指向了标准输入文件,虽然fd被设置为了非阻塞,但是描述符0任然是阻塞的。
因为0和3这两个文件描述符是分别各自调用open函数打开/dev/stdin文件而返回得到的,这种情况下各个文件描述符指向的文件结构关系如下:

这里写图片描述

从上图我们很明显的看到,0,3这两个描述符各自有一个文件表,可以设置自己的文件状态标志,所以0是阻塞的而3却是非阻塞的就不难理解了。那么如何将已经打开了的文件描述符设置为非阻塞的呢?这就又要使用到fcntl函数了,比如我们可以将已经打开了的0设置为非阻塞,代码实现如下:

int main(void)
{
        int fd = -1, flag = -1;

        /* F_GETFL:获取描述符原有状态给flag的命令,目的是为了保护原有的状态
         * STDIN_FILENO:指向标准输入文件的文件描述符0 */
        flag = fcntl(STDIN_FILENO, F_GETFL);
        flag |= O_NONBLOCK;//将原有文件状态 | 非阻塞标志
//将修改后的包含了非阻塞标志的新状态重新设置回去
        fcntl(STDIN_FILENO, F_SETFL, flag);

        return 0;
}

2.3、非阻塞举例
1)、同时阻塞地读键盘和读鼠标
我们在一个单进程里面同时实现键盘的输入和鼠标的输入,但是对于这两个低速系统调用默认情况的读都是阻塞的,所以这两个的输入会相互阻塞,如下例:

int main(void)
{
        char buf[100] = {0};  
int fd = -1, ret = -1; 
/* 打开鼠标文件 */  
        fd = open("/dev/input/mouse1", O_RDONLY);
        if(fd < 0)
        {    
                perror("open /dev/input/mouse1 is fail");
                exit(-1);
        }   

        /* 由于读键盘和读鼠标默认都是阻塞的操作,所以它们会相互阻塞 */
        while(1)
        {   
                /* 先读键盘 */
                bzero(buf, sizeof(buf));
                ret = read(0, buf, sizeof(buf));
                if(ret > 0) write(1, buf, strlen(buf));

                /* 后读鼠标 */
                bzero(buf, sizeof(buf));
                ret = read(fd, buf, sizeof(buf));
                if(ret > 0) write(1, buf, strlen(buf));
        }   

        return 0;
}          

该程序必须在超级用户下运行,因为鼠标文件只能在超级用户下才能被打开,运行程序时由于先读的是键盘,它阻塞了鼠标,所以我们先输入鼠标是没有用的,当从键盘桥如数据后,键盘数据打印出来,这时进程又阻塞在了读鼠标处,所以此时从键盘敲入数据是没有用的,这时必须移动鼠标输入数据,才能回到读键盘处(标准输入处),由于鼠标数据是整形的坐标值,所打印出来是乱码。
2)、非阻塞地实现读键盘和鼠标
从前面我们知道,键盘和鼠标的读导致了相互的阻塞,我们输入时并不通畅,现在我们将它们都改为非阻塞的,那么它们就不会相互阻塞,输入就会变得通畅,对上例修改后的代码如下:

int main(void)
{
        char buf[100] = {0};
        int fd = -1, ret = -1; 

        /* 以非阻塞方式打开鼠标文件的操作,0指向的标准输入在
 * 进程创建时就已经打开 */  
        fd = open("/dev/input/mouse1", O_RDONLY|O_NONBLOCK);
        if(fd < 0)
        {    
                perror("open /dev/input/mouse1 is fail");
                exit(-1);
        }   
        /* 将标准输入0也改为非阻塞的 */
        flag = fcntl(STDIN_FILENO, F_GETFL);
        flag |= O_NONBLOCK;
        fcntl(STDIN_FILENO, F_SETFL, flag);

        /* 由于读键盘和读鼠标默认都被改为了非阻塞的操作,所
 * 以它们不再会相互阻塞 */
        while(1)
        {   
                /* 先读键盘 */
                bzero(buf, sizeof(buf));
                ret = read(0, buf, sizeof(buf));
                if(ret > 0) write(1, buf, strlen(buf));

                /* 后读鼠标 */
                bzero(buf, sizeof(buf));
                ret = read(fd, buf, sizeof(buf));
                if(ret > 0) write(1, buf, strlen(buf));
        }   

        return 0;
}

上例黑体部分代码就是添加或修改后的部分,精工这么设置后,鼠标和键盘不再相互阻塞,所以运行这个程序时不必再忌讳谁先输入的问题了,谁先输入都可以。
只是这种非阻塞会导致进程时刻都处在循环的,这种轮询的机制会非常的消耗cpu资源,为了解决同时输入键盘和鼠标的问题,这并不是一个好的解决方法,我们前面学过了多进程,所以我们可以利用两个进程来时实现同时读鼠标和键盘,假如A进程读鼠标,B进程读键盘,虽然它们都是阻塞的,但确是各自阻塞各自的,它们互不相干扰。
3)、利用两个进程实现同时读键盘和鼠标
我们开两个进程,一个进程读键盘,一个进程读鼠标,由于进程本身就是迸发同时向前运行的,所以这里再也不需要将键盘和鼠标设置为非阻塞。例子的例子如下:

int main(void)
{
        char buf[100] = {0};
        int fd = -1, ret = -1;

        /* 开来两个进程,父进程读鼠标,子进程读键盘 */
        ret = fork();
        if(ret == 0){
                while(1)
                {
                        /* 读键盘 */
                        bzero(buf, sizeof(buf));
                        ret = read(0, buf, sizeof(buf));
                        if(ret > 0) write(1, buf, strlen(buf));
                }
        }
        else if(ret > 0)
        {
                fd = open("/dev/input/mouse1", O_RDONLY|O_NONBLOCK);
                if(fd < 0)
                { 
                        perror("open /dev/input/mouse1 is fail");
                        exit(-1);
                }
                while(1)
                {
                        /* 读鼠标 */
                        bzero(buf, sizeof(buf));
                        ret = read(fd, buf, sizeof(buf));
                        if(ret > 0) write(1, buf, strlen(buf));
                }
       }
        return 0;
}



例子中并没有将键盘和鼠标都设置为非阻塞的,但同样能够实现同时键盘和鼠标,但当后面我们学到了select、poll多路io机制和异步通知后,还可以用这些方法来解决同时读鼠标和键盘的问题。
3、记录锁(文件锁)
3.1、为什么需要记录锁
当多个进程试图对同一个文件都进行读写操作时,如下图所示:

这里写图片描述

我们肯定希不同进程之间各自的读写操作望满足如下条件,以保护各个进程向文件所写之数据不被篡改。
1)、当某个进程正在写文件时,其它所有进程肯定不能写,否者会相互篡改。
2)、当某个进程正在写文件时,其它所有进程不能够读,因为别人在没有写完之前读出
的数据是不完整的。
3)、当某个进程正在读时,其它所有的进程都可以共享的读,因为读不会照成数据篡改。
4)、当某个进程正在读时,其它所有的进程都不可以写,否者会导致读出的数据不完整。
总结以上几点就是:
1)、写与写之间互斥
2)、读与写之间互斥
3)、读与读之间共享
为了实现按照上述保护方式对文件进行读写,我们引入了记录锁,记录的设置也需要用到fcntl函数,这个函数我们在第3篇时就已经学习过了,当时说过该函数有很多的功能,今天我们就学习如何利用fcntl函数设置记录锁。
记录锁:利用文件描述fd实现对其指向的文件内容进行加锁,记录锁又称区域锁。
1)、对整个文件内容加锁
2)、对文件某部分内容加锁
记录锁主要使用目的:
1)、保护整个文件或文件某区域的内容。
2)、并不需要保护文件内容,只是借用文件记录锁,以实现加锁(例如保证原子操作)。
记录锁的种类:
1)、建议性记录锁,本篇重点
2)、强制性记录锁,不讨论
准确来讲,对整个文件加锁的记录锁应该被称为文件锁,只对文件部分内容加锁的才应被称为记录锁或区域锁,但通常情况下我们并不对这两个名称加以区别,都统称为记录锁或文件锁。
3.2、fcntl函数设置建议性记录锁
1)、函数原型和所需头文件

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, .../*struct flock *flockptr */ );

2)、函数功能:该函数有多种功能,在这里我们主要讨论如何利用该函数进行设置记录锁。
3)、函数参数
•int fd:文件描述符,指向需要加锁的文件。
•int cmd:设置记录锁时,cmd有三种设置,F_GETLK,F_SETLK,F_SETLKW。
•第三个参数:当设置记录锁时,为struct flock *flockptr,一个指向struct flock 结构体的指针,该结构体中设置好了我们需要设置的记录所的各个参数。
4)、函数返回值
设置记录锁成功返回0,失败则返回-1, 并且errno被设置。
5)、注意
1)、fcntl函数是一个变参函数,一般情况下只需设置前两个参数,但是设置记录锁时,
fcntl函数需要使用到第三个参数。
2)、F_GETLK、F_SETLK和F_SETLKW含义如下:
•F_GETLK:决定由flockptr所描述的锁是否被另外一把锁所排斥(阻塞)。如果存在
一把锁,它阻止创建由flockptr所描述的锁,则这把现存的锁的信息写到flockptr指
向的结构中。如果不存在这种情况,则除了将ltype设置为FUNLCK之外flockptr所指向
结构中的其他信息保持不变。
•F_SETLK:设置由flockptr所描述的锁。如果试图建立一把按上述兼容性规则并不允
许的锁,则fcntl立即出错返回,此时errno设置为EACCES或EAGAIN。
•F_SETLKW:这是F_SETLK的阻塞版本,命令名中的W表示等待(wait)。如果由于存
在其它锁,那么按兼容性规则由flockptr所要求的锁不能被创建,那么调用进程睡眠。
如果捕捉到信号则睡眠中断(可以手动重启这个系统调用)。如此直到设置成功为止。
3)、struct flock结构体如下:
•结构体原型

struct flock
{
                        short l_type;   // Type of lock: F_RDLCK,F_WRLCK, F_UNLCK 
            short l_whence; //How to interpret l_start:SEEK_SET, SEEK_CUR, SEEK_END
            off_t l_start;   // Starting offset for lock 
            off_t l_len;    //Number of bytes to lock 
            pid_t l_pid;    //PID of process blocking our lock(F_GETLK only) 
}

•结构体成员说明
short l_type:记录锁类型
a)、F_RDLCK:读锁(或称共享锁)
b)、F_WRLCK:写锁
c)、F_UNLCK:解锁
short l_whence:加锁位置粗定位,设置同lseek的whence
a)、SEEK_SET:文件开始处
b)、SEEK_CUR:文件当前位置处
c)、SEEK_END:文件末尾位置处
off_t l_start:精定位,相对l_whence的偏移,设置同lseek的offset
off_t l_len:文件中需被加锁区域的字节数,当设置为0时,表示加锁到文件末尾。
pid_t l_pid:加锁进程的PID,仅对F_GETLK有用)
4)、加锁区域的起点可以是文件的尾端或超过文件尾端的位置,可以是文件的起始位置, 但是绝不能在文件起始位置前设置。
5)、如若l_len为0,则表示锁的区域从其起点(起点由lstart和lwhence决定)开始直
至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于锁的范围。每当
项文件写入新的内容后,加锁区域会自动延伸到文件新的尾端。
6)、为了锁整个文件,通常将lstart设置为0,l_whence设置为SEEK_SET,l_len设置为0。
7)、可实现对文件组合加锁,比如对文件0-100字节加读锁,对101-250加写锁。
8)、可实现对文件部分区域解锁,例如文件0-500字节都被加了写锁,但解锁时可只解
300-500解锁,而0-299区域的锁将任然存在。
6)、测试用例
•设置记录锁
记录锁的设置比较繁琐,为了避免麻烦,我们只写一个相关函数,这个函数在自定义的头文件record_lock.h中实现,那么如何调用该函数实现非阻塞地加读锁、写锁,或阻塞的加读锁、写锁以及解锁则由不同的宏来实现,实现该函数和宏的头文件如下:

/* fcntl函数需要用到的头文件 */
#include <unistd.h>
#include <fcntl.h>

#define read_lock(fd, l_whence, l_offset, l_len) \
lock_fun(fd, F_SETLK, F_RDLCK, l_whence, l_offset, l_len)
#define read_lockw(wfd, l_whence, l_offset, l_len)\
lock_fun(fd, F_SETLKW, F_RDLCK, l_whence, l_offset, l_len)
#define write_lock(fd, l_whence, l_offset, l_len)\
        lock_fun(fd, F_SETLK, F_WRLCK, l_whence, l_offset, l_len)
#define write_lockw(fd, l_whence, l_offset, l_len)\
        lock_fun(fd, F_SETLKW, F_WRLCK, l_whence, l_offset, l_len)
#define unlock(fd, l_whence, l_offset, l_len)\
        lock_fun(fd, F_SETLK, F_UNLCK, l_whence, l_offset, l_len)

int lock_fun(int fd, int cmd, int l_type, int l_whence, off_t l_offset, off_t l_len)
{
        struct flock f_lock;
        f_lock.l_type   = l_type;
        f_lock.l_whence = l_whence;    
        f_lock.l_start  = l_offset;
        f_lock.l_len    = l_len;

        return(fcntl(fd, cmd, &f_lock));
}

•测试记录锁
测试记录锁的实现同上,只有一个函数,但多种不同测试都由不同的宏实现。

/* fcntl函数需要用到的头文件 */
#include <unistd.h>
#include <fcntl.h>

#define testlock(fd, l_whence, l_offset, l_len)  test_lock(fd, l_whence, l_offset, l_len)

int test_lock(int fd, int l_whence, off_t l_offset, off_t l_len)
{
        int ret = 0;
        struct flock flock = {0};

        flock.l_whence  = l_whence;
        flock.l_start   = l_offset; 
        flock.l_len     = l_len;

        ret = fcntl(fd, F_GETLK, &flock);
        if(ret < 0)
        {   
                perror("in test_lock fcntl is fail");
                exit(-1);
        }   
        else if(F_RDLCK == flock.l_type) printf("%d seted read_lock\n", flock.l_pid);
        else if(F_WRLCK == flock.l_type) printf("%d seted write_lock\n", flock.l_pid);
        else if(F_UNLCK == flock.l_type) printf("unlock\n");

        return 0;
}

•使用记录所的例子
大家请看下面这个例子:

int main(void)
{
        int ret = -1, fd = -1;

        fd = open("./file", O_CREAT|O_RDWR, 0664);
        if(fd < 0){
                perror("open ./file is fail");
                exit(-1);
        }
        /* 父子进程并发的向file文件里写hello worrd\n */
        ret = fork();
        if(ret == 0){
                while(1)
                {
                        write(fd, "hello ", 6);
                        write(fd, "world\n", 6);
                }
        }
         else if(ret > 0)
        {
                while(1)
                {
                        write(fd, "hello ", 6);
                        write(fd, "world\n", 6);
                }
        }

        return 0;
}



    上例中父/子进程并发地向文件file写“hello world\n”,打开file,看到写入的结果如下:
1 hello world
2 hello world
。。。。。。
2618 hello world
2619 hello hello world
2620 hello world
2621 hello world
2622 hello world
。。。。。。
        我们发现结果中居然出现了hello hello world的情况,而导致这个情况的原因是因为

write(fd, “hello “, 6)和write(fd, “world\n”, 6)的这两个操作并不是一个原子操作,假当父进程
刚写入“hello ”之后,父进程就被立即切换到子进程,子进程紧接着就也写“hello ”,就
会导致这样的结果,分析入下图所示:
这里写图片描述

这对write(fd, “hello “, 6)和write(fd, “world\n”, 6)不是原子操作的问题,我们可以利用我们的记录所来改进,改进后的代码如下:

/* 其它头文件自己添加 */
#include "record_lock.h"
int main(void)
{
        int ret = -1, fd = -1;
        fd = open("./file", O_CREAT|O_RDWR, 0664);
        if(fd < 0){
                perror("open ./file is fail");
                exit(-1);
        }

        /* 父/子进程并发的向file文件里写hello world\n */
        ret = fork();
        if(ret == 0){//子进程
                while(1){
                        write_lockw(fd, SEEK_SET, 0, 0);//对整个文件加锁
                        write(fd, "hello ", 6);
                        write(fd, "world\n", 6);
                        unlock(fd, SEEK_SET, 0, 0);//解锁
                }
        }
        else if(ret > 0){父进程
                while(1){
                        write_lockw(fd, SEEK_SET, 0, 0);// 对整个文件加锁
write(fd, "hello ", 6);
                        write(fd, "world\n", 6);
                        unlock(fd, SEEK_SET, 0, 0);//解锁
                }
        }

        return 0;
}

运行修改后的代码(留意黑体部分),然后vi file,发现再也没有“hello hello world”的现象了,这是因为加了记录锁后,write(fd, “hello “, 6)和write(fd, “world\n”, 6)变成了原子操作,分析如下图:
这里写图片描述

从上图的中可以看出,write(fd, “hello “, 6)和write(fd, “world\n”, 6)被强制成为了一个原子操作,进程B在进程A执行完write(fd, “hello “, 6)和write(fd, “world\n”, 6)之前是不会加锁成功的,会一直阻塞下去直到进程A执行完成write(fd, “hello “, 6)和write(fd, “world\n”, 6)并成功的解锁之后,进程B才能加锁成功。
3.3、记录锁讨论
3.3.1、记录锁的实现

这里写图片描述

观察上图得到:
1)、当同一进程中多个文件描述符指向同一文件时,只要其中的任何一个文件描述符被关闭,那么该进程加在文件上的记录锁将会被关闭,因为同一个文件不管被打开或复制多少次,但是它们共享却只有一个锁链表。
2)、进程终止时(不管是正常或异常终止),该进程所加的记录锁全部被释放。
3)当某个进程相想对某个文件加锁时,会首先检查锁链表。
a)如果发现已经被加了一个读锁,该进程可加读锁,但是不可以加写锁。
b)如果发现已经被加了一个写锁,该进程不能加写锁,也不能加读锁。
4)、fork产生的子进程不会继承父进程所加的锁,因为锁的作用是阻止多个进程同时写同一个文件(或同一文件区域) 。如果子进程继承父进程的锁,则父、子进程就可以同时写同一个文件。如果子进程想加锁,必须自己重新调用fcntl函数重新加锁。
5)、在执行exec后,新程序可以继承原执行程序的锁。
6)、同意进程加多个
3.4、强制性记录/文件锁(仅做了解)
3.4.1、为什么需要强制性锁
我们前面讲的锁都是建议性锁,对于建议性锁存在一个问题,那就是当多个进程对文件进行读写操作,这些进程设置的记录锁,在相互之间是起作用的,但如果这时有一个除了这几个进程(该进程没有对该文件加锁)外另一个进程也去写这个文件,那么其他进程锁加的锁对这一个进程是没有任何作用的,那么该文件内容就会被这个进程的写操作所篡改,很多情况下我们是不希望出现这种情况的,针对这种情况我们就需要设置强制性的记录锁。
3.4.2、强制性记录性的加锁和启动
1)、加锁方式:同建议性记录锁
2)、如何启动强制性记录锁?
对会被加锁的文件打开其设置-组-ID位,关闭其组-执行位,如此就对该文件启动了强制性锁机制。
3.4.3、强制性记录锁对其它进程的影响
如果一个进程试图读、写一个强制性锁起作用的文件,而欲读、写的部分又由其他进程加上了读、写锁,此时会发生什么呢?对这一问题的回答取决于三方面的因素:
a)操作类型(read或write)
b)其它进程保有的锁的类型(读锁或写锁)
c)以及对该文件操作的有关描述符是阻塞还是非阻塞的。
这里写图片描述

如果一个进程试图open文件,而其它进程又对文件加了强制性锁,此时又会发生什么呢?
1)、如果open的flag标识中设置了O_TRUNC,立即出错返回,errno被设置EAGAIN。
对O_TRUNC情况出错返回是有意义的,因为其他进程对该文件持有读、写锁,所以不能将 其截短为0。
2)、如果open的flag标识中设置了O_CREAT,立即出错返回,errno被设置EAGAIN。
对OCREAT情况在返回时也设置errno则无意义,因为该标志的意义是如果该文件不存在则
创建,由于其它进程对该文件持有记录锁,因而该文件肯定是存在的。

注意:并不是所有的linux操作系统都支持强制性记录锁。
4、io多路复用(I/O multiplexing )
前面我们为了实现同时读键盘和鼠标,采用非阻塞或多进程来实现,但是这两种方法都有一定的缺陷,非阻塞会需要轮训,很消耗cpu资源,如果采用多进程时,进程之间切换也是很耗费资源的,并且当程之间需要相互共享资源的话,这就需要加入进程间通信机制,这就会使得我们的程序变得更加的复杂。
对此我们引入一种新的解决办法,多路io复用,其分为如下两种:
•select机制;
•poll机制;
不管是那种机制,多路io复用的是原理是一致的,其基本思想是构造一个文件描述符的表,在这个表里面存放了会阻塞文件描述符,然后调用多路复用函数,该函数每隔一段时间会去检查一次,看表中是否有某个或几个文件描述符有动作,没有就休眠一段时间,再隔一段再去检查一次,如果其中的一个或多个文件名描述符有了动作,函数返回不在休眠,将分别对其有动作的文件描述符做相应操作。
实际上多路复用本身也存在轮询检查过程,但是绝大部分时间却是在休眠,所以就避免了一般的轮询机制,以及多进程实现所带来的相当的资源损耗。多路复用原理如下图所示:
这里写图片描述

注意:一般情况下集合中都设置阻塞的文件描述符,设置非阻塞的描述符是没有意义的。
4.1、select,pselect函数
1)、函数原型和所需头文件

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, 
struct timeval *timeout);
int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const       struct timespec *timeout, const sigset_t *sigmask);

2)、函数功能
•这两个函数都是为了实现多路复用,但是这两个函数都能够被信号中断,但是pselect函数能够通过sigmask参数屏蔽掉那些我们不希望中断pselect系统函数的信号。
3)、函数参数
a)select函数
•int nfds:readfds, writefds, exceptfds这三个集合中最大描述符+1(因为描述符
是从0算起的),用以说明需要关心描述符的范围,这个范围必须包含所有集合中的文
件描述符。
•fd_set *readfds:读集合,设置读会阻塞的描述符。
•fd_set *writefds: 写集合,设置写会阻塞的描述符。
•fd_set *exceptfds: 设置异常描述符的集合。
•struct timeval *timeout:成员结构如下:

struct timeval 
{
long    tv_sec;  /* seconds(秒) */
long    tv_usec; /* microseconds (微秒)*/
};

(1)该参数填NULL表示不设置超时,这种情况下如果没有描述符响应,同时也没有 信号中断的话则永远阻塞。
(2)如果需要设置超时,则需填写设置了时间结构体的地址,如果没有描述符响应
但设置的时间却到了,立即返回而不再阻塞。
b)、pselect函数
•前四个参数:同select函数
•struct timespec *timeout:成员结构体如下:

struct timespec
 {
    long    tv_sec;  /* seconds(秒) */
    long    tv_nsec; /* nanoseconds (纳秒)*/
 };

(1)该参数填NULL表示不设置超时,这种情况下如果没有描述符响应,同时也没有 信号中断的话则永远阻塞。
(2)如果需要设置超时,则需填写设置了时间结构体的地址,如果没有描述符响应
但设置的时间却到了,立即返回而不再阻塞。
•sigset_t *sigmask:信号屏蔽集,用于设置我们希望屏蔽的信号的,防止这些信号
中断pselect的调用。
4)、函数返回值
•返回-1:说明函数调用失败,errno被设置。
•返回0:超时时间到并且没有一个描述符有响应,返回0说明没有一个描述符准备好。
•返回值>0:返回有响应的文件描述符的数量。
5)、注意
a)当内核检测到集合中有某个或几个文件描述符有响应时,这个集合将会被重新设置,
用于存放那些有动作的文件描述符,所以一旦函数调用完毕我们必须重新设置这些集合。
b)当pselect被执行时,信号屏蔽字会被sigmask替换,相应的被设置了屏蔽的信号会
被屏蔽,但是这个函数一旦执行完毕,信号屏蔽字会被还原。
c)select和pselect都是会被信号中断的低速系统调用,当然我们可以手动重启该调用。
d)如果我们不希望select或pselect函数被信号中断,那么设置的方法如下:
•忽略那些我们不希望的信号。
•利用sigfilleset、。。。sigdelset、sigpromask等函数,去修改信号屏蔽字以屏 蔽这些信号。
•专门使用pselect函数,因为该函数调用期间,函数中的sigmask参数(设置了我们
希望屏蔽的信号)会去替换信号屏蔽字,被我们屏蔽的信号一旦发生后,就会成为未
决信号而被设置到了未决信号集中,但是该函数一旦调用完毕,信号屏蔽字会被还原,
以前因被屏蔽而被设置在未决信号集中的未决信号就可能会被响应(响应方式:调用
捕获函数或终止进程)。
e)文件描述符集合中设置的都是会导致阻塞的描述符,设置非阻塞的文件描述符没有太
大意义,所以集合中不要设置普通文件的描述符,因为不会阻塞。比如读普通文件时, 不管有没有数据,read函数都将返回。
f)select可以用来模拟精度为us级别的定时器,而pselect则可以用来模拟精度为
ns级别的定时器。
6)、测试用例
a)、select函数
•多路复用读键盘和鼠标,打开鼠标需要超级用户权限

void err_fun(const char *file_name, int line, const char *fun_name, int err_no)
{
        fprintf(stderr, "in %s, %d fun %s is fail: %s\n", file_name, \
                line, fun_name, strerror(err_no));
        exit(-1);
}

int main(void)
{
        sigset_t set; fd_set rdfds;  
        int mouse_fd = -1, ret = 0, i = 0;
        char buf[300] = {0}; 
struct timeval tim = {0};

        #if 0
        /* 防止信号终端select函数 */
        /* 方法一:忽略信号 */
        signal(SIGINT, signal_fun);

        for(i=1; i<65; i++) signal(i, SIG_IGN);
        /* 方法二:屏蔽信号 */
        sigfillset(&set);
        sigprocmask(SIG_SETMASK, &set, NULL);
        #endif
        /* 打开鼠标文件,需要在root用户才能下打开 */
        mouse_fd = open("/dev/input/mouse1", O_RDONLY);
        if(mouse_fd < 0) err_fun(__FILE__, __LINE__, "open", errno);

        while(1)
{    
                /* 设置读集合,设置操作必须放在循环内 */
                FD_ZERO(&rdfds); //清空读集合   
                FD_SET(mouse_fd, &rdfds); //将mouse_fd设置到读集合中 
                FD_SET(STDIN_FILENO, &rdfds); //将标准输入也设置到读集合中      

                /* 设置超时时间,设置必须放在循环内 */
                tim.tv_sec      = 3; //秒
                tim.tv_usec     = 0; //微秒

                /* -如果集合中没有描述符响应 
                 * 1.如果第四个参数被设为NULL,select将一直阻塞知道集
* 合中描述符有响应为止
                 * 2.如果第四个参数设置了超时时间,时间到则函数超时返回 
                 * -如果集合中有一个或多个描述符响应,则集合被内核重新设
                 * 置,用于存放有响应的描述符,设置的超时时间也被清空 */
        lab0:   ret = select(mouse_fd+1, &rdfds, NULL, NULL, &tim);
                if(ret<0 && EINTR==errno)  //重启被信号中断了的select系统调用
                {
                        printf(" interrupt select\n");
                        goto lab0;
                }
                else if(ret < 0) err_fun(__FILE__, __LINE__, "select", errno);
                else if(ret > 0) //集合中有
{        //mosue_fd是不是有响应的描述符
                        if(FD_ISSET(mouse_fd, &rdfds)) 
{          
                                ret = read(mouse_fd, buf, sizeof(buf));//读鼠标
                                if(ret > 0) write(1, buf, strlen(buf));
                        }
if(FD_ISSET(0, &rdfds)) //0是不是有响应的描述符
{ 
                                ret = read(0, buf, sizeof(buf));//读键盘
                                if(ret > 0) write(1, buf, strlen(buf));
                        }
                }
                else if(0 == ret) printf("time out\n"); //超时
        }

        return 0;
}



    •用select模拟精度为微秒级的定时器
void select_timer(int secs, int usecs)
{
        sigset_t set = {0};
int ret = 0, i = 0;
        struct timeval tim = {0};

        #if 0 
        /* 防止信号干扰
         * 方法一:忽略信号 */  
        for(i=1; i<65; i++) signal(i, SIG_IGN);

        /* 方法二:屏蔽信号 */  
        sigfillset(&set);
        sigprocmask(SIG_SETMASK, &set, NULL);
        #endif

        tim.tv_sec = secs;    //秒
        tim.tv_usec = usecs;  //微秒

        /* 做定时器时,除了超时设置,其余全部为设置0 */
lab0:    ret = select(0, NULL, NULL, NULL, &tim);
        if(ret<0 && EINTR==errno)//重启被中断的系统调用
        {
                printf(" interrupt select\n");
                goto lab0;
        }
        else if(ret < 0) //出错处理     
        {
                perror("select is fail");
                exit(-1);
        }
        else if(0 == ret) printf("time out\n");//超时
}

int main(void)
{
        /* 第一个参数:秒
         * 第二个参数:微秒 */
        select_timer(3, 3000);//调用select模拟的定时器
        printf("hello\n");

        return 0;
}

b)、pselect函数
•同样打开鼠标需要超级用户权限,pselect代码与select的基本一致,只是多了最后一
个参数,用于设置屏蔽字。
•用pselect模拟精度为纳秒级的定时器,代码与select的基本一致,只是多了最后一
个参数,用于设置屏蔽字。

4.2、poll机制
实际上poll机制,与select差不多,只是具体调用的实现不一样,与select 不同,poll不是为每个条件构造一个描述符集,而是构造一个 pollfd结构数组,每个数组元素指定一个描述符编号以及对其所关心的条件。
1)、函数原型和所需头文件

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout, 
const sigset_t *sigmask);

2)、函数功能
•这两个函数都是为了实现多路复用,这两个函数都能够被信号中断,但是ppoll函数能够通过sigmask参数屏蔽掉那些我们不希望中断ppoll系统调用的信号。
3)、函数参数
a)、poll函数
•struct pollfd *fds:struct pollfd结构体数组,每个数组成员设置了我们需要多路IO操作
的每个描述符,该结构体成员结构如下:

struct pollfd {
    int   fd;         /* file descriptor:文件描述符 */
    short events;     /* requested events:设置我们希望发生的事件 */
     short revents;    /* returned events :实际发生的事件*/
 };

•nfds_t nfds:结构体数组struct pollfd *fds的成员数量。
•int timeout:超时时间,单位为毫秒,如填写为3000,表示3秒超时,如果不希望设置
超时,该参数填写负数(如-1)即可。
b)、ppoll函数
•前两个参数同poll函数。
•const struct timespec *timeout:同pselect的超时设置的结构体。
a)、如果填写NULL, 在没有描述符响应时和信号中断时则永远阻塞。
b)、如果填写了设置超时的结构体地址,在没有描述符响应时则在超时后立即返回。
•sigset_t *sigmask:设置我们希望屏蔽的信号的,防止这些信号中断pselect的调用。
4)、函数返回值
•返回-1:说明函数调失败,errno被设置。
•返回0:超时时间到并且没有文件描述符有响应。
•返回值>0:返回有响应的文件描述符的数量。
5)、注意
a)当描述符有响应时,revents会被内核填写响应的类型,如果events==revents,说明这
个响应是我们希望的响应,利用该文件描述符实现相应操作,否则就不是我们希望的响
应,不做任何操作。
b)当ppoll被执行时,信号屏蔽字会被sigmask替换,相应的信号会被屏蔽,但是这个
函数一旦执行完毕,信号屏蔽字又会被还原为原来的信号屏蔽字,这一点与我们前面学
过的pselect是相同的。
c)poll和ppoll都是会被信号要中断的低速系统调用,但我们手动重启。
d)如果我们不希望poll、ppoll函数被信号中断,方法同select和pselect。
e)同select、pselect一样,对普通文件进行多路复用是没有意义的。
f)poll可模拟精度为ms级别的定时器,而ppoll则也可用来模拟精度为ns级定时的器。
e)poll的events和revents标志的设置如下表:
这里写图片描述

6)、测试用例
a)poll函数
•多路复用读键盘和鼠标,同样打开鼠标需要超级用户权限

void err_fun(const char *file_name, int line, const char *fun_name, int err_no)
{
        fprintf(stderr, "in %s, %d fun %s is fail: %s\n", file_name, line, fun_name, strerror(err_no));
        exit(-1);
}
int main(void)
{
        char buf[200] = {0};
        struct pollfd fds[2] = {0};
        int ret = -1, mouse_fd = -1; 

        mouse_fd = open("/dev/input/mouse1", O_RDONLY); //打开鼠标
        if(mouse_fd < 0) err_fun(__FILE__, __LINE__, "mouse_fd", errno);

        /* 向数组中设置设置需要多路监听的描
         * 述副,设置只需要设置一次就行 */
        fds[0].fd = 0; //标准IO
        fds[0].events = POLLIN; //设置希望的事件,POLLIN:输入事件
        fds[1].fd = mouse_fd; //鼠标
        fds[1].events = POLLIN; //设置希望的事件,POLLIN:输入事件       

        #if 0
        /* 防止信号终端select函数 */
        /* 方法一:忽略信号 */
        signal(SIGINT, signal_fun);
        for(i=1; i<65; i++) signal(i, SIG_IGN);
        /* 方法二:屏蔽信号 */
        sigfillset(&set);
        sigprocmask(SIG_SETMASK, &set, NULL);
        #endif
        while(1)
{
                /* fds:struct pollfd结构提数组,2:需要多路监听的数量,3000:超时时间 */
        lab0:    ret = poll(fds, 2, 3000);
                if(ret<0 && EINTR==errno) //重启被信号中断了的poll调用
                {
                        printf("interrupt poll\n");
                        goto lab0;
                }
                else if(ret < 0) err_fun(__FILE__, __LINE__, "mouse_fd", errno);
                else if(0 == ret) printf("time out\n"); //超时时间到
                else if(ret > 0)
{
                        /* 如果响应事件revents等于希望的时间events,说明
                         * 该描述符由动作了,可以执行相应操作了 */
                        if(fds[0].events == fds[0].revents)  //判断键盘是否有输入要求
{
                                bzero(buf, sizeof(buf));
                                ret = read(fds[0].fd, buf, sizeof(buf));
                                if(ret > 0) write(1, buf, strlen(buf));
                        }
                        if(fds[1].events == fds[1].revents)//判断鼠标是否由输入要求
                        {
                                bzero(buf, sizeof(buf));
                                ret = read(fds[1].fd, buf, sizeof(buf));
                                if(ret > 0) write(1, buf, strlen(buf));
                        }
                }
        }

        return 0;
}




•利用poll模拟ms定时器:略,自己仿照select自己实现。
a)、ppoll函数
•多路复用读键盘和鼠标,打开鼠标需要超级用户权限:略,仿照pselect自己实现。
•利用ppoll模拟ns定时器:略,仿照pselect自己实现。
5、异步IO
所谓异步io就是,当某个事件准备好,进程会被发送一个SIGIO的异步信号,进程受到这个信号的通知后,会调用信号处理函数去处理事件,在事件没有准备好时,进程并不需要轮询事件或者阻塞等待事件,进程可以忙自己的事情直到等到别人发送异步信号SIGIO通知某事件发生。
所谓异步就是,进程接收异步信号的时机完全是随机的,这个时机完全取决于事件发生的时刻,接受信号的进程是没有办法预测的。
异步IO设置的步骤如下:
(1) 调用signal或sigaction为该信号建立一个信号处理程序。
(2) 以命令F_SETOWN调用fcntl来设置接收信号进程PID和进程组GID。
(3) 以命令F_SETFL调用fcntl设置O_ASYNC状态标志,使在该描述符上可以进行异步I/O。第3步
仅用于指向终端或网络的描述符
5.1异步IO使用例子
•异步IO实现同时读键盘和鼠标

void signal_fun(int signo)
{
    char buf[200] = {0};
    int ret = -1;

    memset(buf, 0, sizeof(buf));
    ret = read(mouse_fd, buf, sizeof(buf));//读鼠标
    if(ret > 0) write(1, buf, strlen(buf)); 
    else if(ret < 0)
{
        perror("read is fail");
        exit(-1);
    }
}

int main(void)
{   
    char buf[200] = {0};
    int flag = -1, ret = -1;

    mouse_fd = open("/dev/input/mouse1", O_RDONLY); //打开鼠标字符设备文件
    if(mouse_fd < 0)
{
        perror("open mouse1 is fail");
        exit(-1);
    }
    /* 对mouse_fd设置异步IO */
flag = fcntl(mouse_fd, F_GETFL);
    flag |= O_ASYNC;
    fcntl(mouse_fd, F_SETFL, flag);
fcntl(mouse_fd, F_SETOWN, getpid()); //设置当前进程获取SIGIO信号
signal(SIGIO, signal_fun);//捕获SIGIO信号

    while(1)
{
        memset(buf, 0, sizeof(buf));
ret = read(0, buf, sizeof(buf));//读键盘
        if(ret > 0) write(1, buf, strlen(buf));
        else if(ret < 0)
        {
            perror("read is fail");
            exit(-1);
        }
    }
    return 0;
}   



6、存储映射IO
6.1、存储映射的好处
我们以前为了实现对文件的数据输入/输出,我们用的都是read,write等函数,这些函数处理数据时,数据需要在和各级缓冲区之间进行数据的复制,当要面对大量数据读写时,这些函数调用很费时间,如果我们能够直接通过地址对文件进行数据输入输出的话,将避免这样的缺点,本节的存储映射I/O就能实现这样的功能。
存储映射I/O使一个磁盘文件与虚拟存储空间中的一段缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。这样,就可以在不使用read和write的情况下执行I/O。

这里写图片描述

6.2、mmap函数
1)、函数原型和所需头文件

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

2)、函数功能
•mmap函数:将文件所在的磁盘空间映射到内存空间。
•munmap函数:取消映射。
3)、函数参数
a)、void *addr:在内存中映射时的映射起始地址。
•如果设置为NULL,由内核决定起始地址,这时最常见的方式。
•如果设置不为NULL,内核将采用该地址作为映射起始地址,如果这个地址不是内 存页的整数倍,内核会自动调整到最近的虚拟页整数倍的地址处。
b)、size_t length:映射的长度。
c)、int prot:指定对映射区的操作权限,可指定如下命令宏:
•PROT_EXEC: 映射区的内容可执行。
•PROT_READ: 映射区的内容可读。
•PROT_WRITE: 映射区的内容可写。
•PROT_NONE: 映射区不允许访问。
前三个选项可相互|操作,如果已经设置了前三个中一个或几个参数的话,设置第四个 参数就没有意义了,如果你想设置PROT_NONE,就不要设置前三个PROT_XXX参数。
d)、int flags:该标志决定对映射区的操作是否同步地更新文件内容,该映射区对于其它
也映射了文件该区域的进程是否可见,这些行为都有下述标志决定:
•MAP_SHARED:共享映射区。指定该标志后本进程对于映射区的修改将会被更新到 文件中,所以对于其它也映射了该区的进程来说文件的修改是可见的。
•MAP_PRIVATE:创建私有的写时复制映射区。指定了此标志后,对于映射区的修改
是不会去更新文件的,所以对于其它也映射了该文件的其它进程来说是不可见的。
• MAP_FIXED:返回值必须等于addr。由于该设置不利于可移植性,所以不鼓励使用此
标志,如果未指定此标志,但addr却非0,那么内核只把addr视为将何处设置为映射起
始地址的一种建议。
这里请注意,MAP_SHARED和MAP_PRIVATE不能同时指定。当然除了上面三个标志外,还有另外一些MAP_xxx标志值,详见情况请参见mmap(2)手册页,
e)、int fd:指向需要被映射文件的描述符。
f)、off_t offset:指定从文件起始位置偏移多少字节处开始映射。
4)、函数返回值
•mmap函数:函数调用成功,返回分配的映射地址,失败则返回(void*)-1,errno被设置。
•munmap函数:调用成功返回 0, 失败则-1, errno被设置。
5)、注意
a)设置addr和offset的值时,其值通常应该是虚拟页的整数倍,虚存页长度可用带参数
SC_PAGESIZE的sysconf函数得到。因为offset和addr常常指定为0,所以这种要求一般并 不是问题。
b)映射时遵守如下规则:
•需要被映射文件的长度不能为0,如果文件长度为0,则可以向文件写入一点数据或
者调用truncate函数对文件设置一个初始长度。
•映射时,不管length指定的大小是多少(length不能==0),真实映射的空间大小如下:
if(文件长度 % 虚拟页 != 0) 真实映射空间 = 虚拟页*(⌊文件大小/虚拟页 ⌋+1)
else if(文件长度 % 虚拟页 == 0) 真实映射空间 = 文件大小
•根据mmap时指定的映射长度length和真实映射空间的大小关系,映射空间的情况分
为如下几种情况:
情况一:length > 真实映射空间,映射情况如下:
这里写图片描述

-写有效映射空间:如果我们mmap时指定了PROT_SHARED标志的话,写入内存中内容
 会同步更新到文件中。
-写虚空间:写操作是有效的,但是只是写到了内存中,文件并不会被更新,因为文        件长度不包含这部分。 

-写无效映射空间:会导致SIGBUS信号的产生,对这个信号默认处理方式会终止进 程。实际上导致无效映射空间产生的有两种。
a)mmap时指定的length > 真实映射空间。
b)truncate将文件截断,真实映射的自动空间缩短,导致指定的length > 缩短
后的真实映射空间。
-当文件长度为0时,我们映射的空间都是无效映射空间。
情况二:length < 真实映射空间(length<文件长度就更不用说了),映射情况如下:
这里写图片描述

-写有效映射空间:如果我们mmap时指定了PROT_SHARED标志的话,写入内存中的内 容会被更新到文件中。
-写虚空间:写操作是有效的,但是只是写到了内存中,文件不会被更新的,因为文
件长度不包含这部分。
c)与存储映射有关的有两个信号,SIGBUS和SIGSEGV。
第一个信号产生的原因我们已经清楚,但是产生SIGSEGV信号的原因有哪些呢?
(1)、访问的空间并不存在
(2)、访问文件的权限不满足open时或者mmap是指定的权限。
d)open是指定的权限与mmap时指定的权限之间的关系
•open文件时权限一定要包含读权限,否者mmap会因权限不足而调用失败,比如如
果open文件时只指定O_WRONLY权限,mmap因权限受限而错误返回。
•mmap时指定的权限必须是open文件时所允许的权限,如果open文件时没有指定
写权限,即使mmap时指定了写权限,mmap时会因权限受限而错误返回。
但是如果open时指定了读权限,但是mmap时却没有指定读权限,对于这种情况确是允许读映射空间的,换句话说相当于mmap时会默认指定读权限。
e)在fork之后,子进程继承存储映射区(因为子进程复制父进程地址空间,而存储映射
区是该地址空间中的一部分),但是由于同样的理由,exec后的新程序则不继承此存 储映射区。
f)进程终止时,或调用了munmap之后,存储映射区就被自动去除。仅仅关闭文件描述符
filedes并不解除映射。
g)调用munmap并不会使映射区的内容更新磁盘文件上。因为对于映射后的磁盘文件的更
新,在写到存储映射区时会按内核虚存算法自动进行。
h)我们学到后面的LCD帧缓冲字符设备驱动时,我们还会接触到mmap函数,但是那个时
候不是为了映射磁盘文件,而是为了将虚拟内存中应用空间中的缓存和内核空间中的显
存映射起来,映射方和被映射都是内存。
我们知道显存是用来预存要被显示的图片数据的,但是我们如果应用程序直接使用write函数把图片数据写入到显存的话是非常耗费时间的,因为图片数据往往非常大,但是我们使用了的映射机制后,我们可以直接通过内存拷贝将应用空间存放的图片数据直
接复制到显存中,显然这种数据搬移方式对于大数据,其效率是非常高的。
6)、mmap的测试用例
•将内存中存放的多个学生信息写到文件中

struct student 
{
        int num;
        char name[30];
};

struct student stu[] =  //定义一个学生结构提数组,初始化5个学生信息
{
        {1, "aaa"},    
        {2, "bbb"},    
        {3, "ccc"},    
        {4, "ddd"},    
        {5, "eee"},    
};

int main(void)
{    
        void *addr = NULL;
        int fd=-1, n=0, i=0;
        struct stat stat = {0}; 
        char buf[400] = {0}, temp_buf[300] = {0};

        n = sizeof(stu)/sizeof(struct student);//计算学生人数

        /* 以读写权限打开file文件,权限中必须包含读权限 */
        fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664);
        if(fd < 0)
        {
                perror("open file is fail");
                exit(-1);
        }

        ftruncate(fd, 4000);//利用truncate函数将文件大小截为4000
        fstat(fd, &stat); //获取文件属性,以便从中获取文件大小的属性
       /* mmap,映射文件到内存空间中 */
        addr = mmap(NULL, stat.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
        if((void *)-1 == addr)
        {
                perror("mmap is fail");
                exit(-1);
        }

        /* 将学生信息全部格式化转换字符串,以便写入文件后我们能够看懂这些学生信息 */
        for(i=0; i<n; i++) sprintf(buf+strlen(buf), "%d %s\n", stu[i].num, stu[i].name);
        memcpy(addr, buf, strlen(buf)); //利用内存拷贝函数将内存中的5个学生信息到文件中

        //同样利用内存拷贝函数将文件中学生信息读到内
        //存应用应用空间中的temp_buf临时缓冲区中
        memcpy(temp_buf, addr, n*sizeof(struct student));
        printf("%s\n", temp_buf); //打印出临时缓冲区temp_buf中存放的学生信息

        return 0;
}



•将A文件中的内容赋值到B文件中
mmap.c

int main(void)
{    
        int fd1= -1, fd2 = -1; 
        struct stat stat = {0}; 
        void *addr1 = NULL, *addr2 = NULL;

        /* 以读写权限打开mmap.c文件,权限中必须包含读权限 */
        fd1 = open("./mmap.c", O_RDWR|O_CREAT, 0664);
        if(fd1 < 0)
        {   
                perror("open file is fail");
                exit(-1);
        }   
        /* 以读写权限打开文件new_file.c,权限中必须包含读权限 */
        fd2 = open("./new_mmap.c", O_RDWR|O_CREAT|O_TRUNC, 0664);
        if(fd2 < 0)
        {   
                perror("open file is fail");
                exit(-1);
        }   

        //获取mmap.c的文件属性,以便从中获取文件大小的属性
        fstat(fd1, &stat); 
        //利用truncate函数将new_mmap.c文件大小截为mmap.c的文件>大小
        ftruncate(fd2, stat.st_size);
   /* mmap,映射mmap.c文件映射到内存空间中 */
        addr1 = mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, fd1, 0); 
        if((void *)-1 == addr1)
        {   
                perror("mmap mmap.c is fail");
                exit(-1);
        }
        /* mmap,映射new_mmap.c文件映射到内存空间中 */
        addr2 = mmap(NULL, stat.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd2, 0);
        if((void *)-1 == addr2)
        {
                perror("mmap new_mmap.c is fail");
                exit(-1);
        }

        //利用内存拷贝函数将mmap.c文件内存般移到new_mmap.c中
        memcpy(addr2, addr1, stat.st_size);

        return 0;
}

我们打开new_mmap.c会看到,mmap.c中的内容被拷贝到了new_mmap.c中。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值