Linux高性能服务器编程-游双——第六章 高级I/O函数

  • 用于创建文件描述符函数
  • 用于读写数据函数
  • 用于控制IO行为和属性的函数

6.1 pipe函数

  • 用于创建管道,实现进程之间的通信。
  • pipe函数定义
#include <unistd.h>
//成功返回0,失败返回-1并设置errno
int pipe(int fd[2]);
  • fd[1]写入的数据可以从fd[0]读出。
  • fd[1]只能用于数据写入。
  • fd[0]只能用于数据读出。
  • read读取一个空的管道,则read会阻塞,直到管道有可读。
  • write写入一个满的管道,则write会阻塞,直到管道有空闲可写。
  • 如果写端fd[1]的引用计数减少至0,也就是都关闭了,那么读端的read操作会返回0,也就是读到末尾。
  • 同理如果读端的fd[0]的引用计数都减少到0,也就是都关闭了,那么写端的write操作会失败,并引发SIGPIPE信号。
  • 管道大小有容量限制,默认65536字节,可以用fcntl函数修改容量。
  • 有一个双向管道:domain只能使用UNIX本地协议族AF_UNIX,所以socketpair只能在本地使用,不过创造的fd一端都是可读可写的。
#include <sys/types.h>
#include <sys/socket.h>
//成功返回0,失败返回-1设置errno
int socketpair(int domain, int type, int protocol, int fd[2]);

6.2 dup函数和dup2函数

  • 可以将标准输入重定向到一个文件,或者标准输出重定向到一个网络连接,可以使用复制文件描述符的dup和dup2
#include <unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);
  • dup函数创建一个新的文件描述符,该文件描述符相当于原有文件描述符file_descriptor的一个副本,指向相同的文件、管道或者网络连接,但是并不继承原有文件描述符的属性。
  • dup2类似,会返回一个不小于file_descriptor_two的整数值,常常表现为把file_descriptor_two覆盖掉,使之指向file_descriptor_one的文件。
  • 所以可以用这个来改变原来文件描述符的指向。
  • 下边的例子是打开了一个1.txt文件,并改变标准输出的文件描述符的指向,使其输出到1.txt文件夹中。
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main(){
    int fd = open("1.txt", O_RDWR|O_TRUNC|O_CREAT);
    //因为标准输出的文件描述符最小,是1,所以dup肯定就取到1了。
    close(STDOUT_FILENO);
    dup(fd);
    printf("123456\n");
    return 0;
}
/*1.txt内容
123456

*/

使用dup2

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main(){
    int fd = open("1.txt", O_RDWR|O_TRUNC|O_CREAT);
    dup2(fd, STDOUT_FILENO);
    printf("123456\n");
    return 0;
}
/*1.txt内容
123456

*/

6.3 readv函数和writev函数

  • 这个函数和之前的TCP、UDP通用数据读写的recvmsg和sendmsg有点类似,都是分散读和集中写,不同的是后者只能用于socket的读写,而且经过简化的。
#include <sys/uio.h>
//成功返回读写的字节数,失败返回-1并设置errno
ssize_t readv(int fd, const struct iovec* vector, int count); 	//分散读
ssize_t writev(int fd, const struct iovec* vector, int count); 	//集中写

struct iovec{
	void *iov_base;		//内存块起始地址
	size_t iov_len;		//内存块长度
};
  • fd是目标文件描述符,vector是iovec结构数组,count数组长度。
  • 例子,创建一个端口,发送指定的文件,文件和HTTP头部分别用集中写执行。
//使用writev分散写,写头部文件和给定的目标文件
#include "../create_sockfd.h"
#include "../filecheck.h"
#include "stdio.h"
#include "stdlib.h"
#include "errno.h"
#include <stdio.h>


//定义缓冲区大小
#define BUFFER_SIZE 1024

//定义两种HTTP状态码和状态信息
static const char *status_line[2] = { "200 OK", "500 Internal server error"};

int main(int argc, char *argv[]){
   if(argc <= 3){
       printf("usage: %s ip_address port_number filename\n", basename(argv[0]));
       return 1;
   }

   const char* fileName = argv[3];
   createSockfd sockfd(argv[1], atoi(argv[2]));

   assert(sockfd.bindSockfd() != -1);

   assert(sockfd.listenfd(5) != -1);

   sockfd.acceptfd();

   if(sockfd.connfd < 0) {
       printf("Accept error: %d\n", errno);
   } else {
       //保存头部信息
       char header_buf[BUFFER_SIZE];
       memset(header_buf, '\0', BUFFER_SIZE);
       //记录header用了多少空间
       int len = 0;
       //用于存放目标文件的缓存
       char *fileBuf;
       //存放文件信息
       struct stat file_stat;
       if(fileCheck(fileName, &file_stat, &fileBuf)){
           
           int ret = snprintf(header_buf, BUFFER_SIZE - 1, "%s %s\r\n", "HTTP/1.1", status_line[0]);
           len += ret;
           ret = snprintf(header_buf + len, BUFFER_SIZE - 1, "Content-Length: %d\r\n", int(file_stat.st_size));
           len += ret;
           ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "%s", "\r\n");
           len += ret;
           struct iovec iv[2];
           iv[0].iov_base = header_buf;
           iv[0].iov_len = strlen(header_buf);
           iv[1].iov_base = fileBuf;
           iv[1].iov_len = file_stat.st_size;
           writev(sockfd.connfd, iv, 2);
           delete [] fileBuf;
       } else {
           int ret = snprintf(header_buf, BUFFER_SIZE - 1, "%s %s\r\n", "HTTP/1.1", status_line[1]);
           len += ret;
           ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "%s", "\r\n");
           send(sockfd.connfd, header_buf, strlen(header_buf), 0);
       }
       
   }
   return 0;
} 
  • 下边是filecheck.h的内容,负责判断文件是否存在,存在的话读取文件到fileBuf,不存在返回false;
//检查给定的名字是否是文件而不是文件夹,是否有全新进行读取
#include <sys/stat.h>
#include <fcntl.h>
#include "stdio.h"
bool fileCheck(const char *filename, struct stat *file_stat, char **fileBuf) {
    //小于0代表目标文件不存在。
    if(stat(filename, file_stat) < 0){
        printf("filename is not exist.\n");
        return false;
    } else {
        //判断目标文件是不是目录
        if(S_ISDIR(file_stat->st_mode)) {
            printf("filename is dir.\n");
            return false;
        } else if (!(file_stat->st_mode & S_IROTH)) { //有无读取权限
            printf("filename meiyouquanxian.\n");
            return false;
        } 
    }
    
    int fd = open(filename, O_RDONLY);
    *fileBuf = new char [file_stat->st_size + 1];
    memset(*fileBuf, '\0', file_stat->st_size + 1);
    //读取失败
    if(read(fd, *fileBuf, file_stat->st_size) < 0){
        delete [] *fileBuf;
        printf("filename read error.\n");
        return false;
    }
    return true;
}
  • 如果传指针到函数中,在函数中重新new申请空间,那么改变的只是指针的形参,并不会改变原来的指针的指向,如果想要改变,那就传入指针的指针。
  • 测试
$testwritev 127.0.0.1 12345 1.txt
$telnet 127.0.0.1 12345
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
HTTP/1.1 200 OK
Content-Length: 7

123456
Connection closed by foreign host.

  • readv的例子
    • 把分散读和标准输入连接在一起
    • 输出
$ ./testreadv 
12
Read bytes: 2 
First message: 1 
Second message: 2 
#include <stdio.h>
#include <sys/uio.h>
#include <string.h>
#define BUF_SIZE 2

int main(int argc, const char * argv[]) {
    struct iovec vec[2];
    char buf1[BUF_SIZE];
    char buf2[BUF_SIZE];
    memset(buf1, '\0', BUF_SIZE);
    memset(buf2, '\0', BUF_SIZE);
    int str_len;

    vec[0].iov_base = buf1;
    vec[0].iov_len = BUF_SIZE-1;
    vec[1].iov_base = buf2;
    vec[1].iov_len = BUF_SIZE-1;

    //把数据放到多个缓冲中储存
    str_len = readv(0, vec, 2);  //2是从标准输入接收数据
    printf("Read bytes: %d \n", str_len);
    printf("First message: %s \n", buf1);
    printf("Second message: %s \n", buf2);

    return 0;
}

6.4 sendfile函数

  • sendfile函数在两个文件描述符之间直接传递数据,内核中操作,避免内核缓冲区和用户缓冲区之间的数据拷贝,效率高。
#include <sys/sendfile.h>
//成功返回传输的字节数,失败返回-1并设置errno
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
  • in_fd参数是待读出内容的文件描述符
  • out_fd参数待写入内容的文件描述符
  • offset参数指定从读入文件流的哪个位置开始读,空,使用默认起始位置
  • cout参数指定in_fd和out_fd之间传输的字节数。
  • 注意in_fd必须是一个正式的文件,不能是socket和管道,out_fd必须是一个socket

  • 例如,使用sendfile传递文件,用telnet测试
#include "../create_sockfd.h"
#include "../filecheck.h"
#include "stdio.h"
#include "stdlib.h"
#include "assert.h"
#include "errno.h"
#include "sys/sendfile.h"

int main(int argc, char *argv[]){
    if(argc <= 3){
        printf("usage: %s ip_address port_number filename\n", basename(argv[0]));
        return 1;
    }
    createSockfd sockfd(argv[1], atoi(argv[2]));
    assert(sockfd.bindSockfd() != -1);
    assert(sockfd.listenfd(5) != -1);
    sockfd.acceptfd();
    if(sockfd.connfd < 0){
        printf("accept error: %d\n", errno);
    } else {
        struct stat file_stat;
        int fd = fileCheck(argv[3], &file_stat);
        //使用sendfile
        sendfile(sockfd.connfd, fd, NULL, file_stat.st_size);
        close(fd);
    }
    return 0;
}
///放在filecheck.h中的重载函数,不读,直接获取fd
int fileCheck(const char *filename, struct stat *file_stat) {
    int fd = open(filename, O_RDONLY);
    //打开失败
    if (fd < 0)
    {
        close(fd);
        return fd;
    }
    fstat(fd, file_stat);
    return fd;
}
  • telnet测试
zsz@ubuntu:~$ telnet 127.0.0.1 12355
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
123456
Connection closed by foreign host.

mmap函数和munmap函数

  • 这两个函数是一对,mmap用于申请一段内存空间,这段内存可以用于进程间通信的共享内存,也可以直接将文件映射过去,munmap用于释放这段空间。
#include <sys/mman.h>
//成功返回指向目标区域的指针,失败返回MAP_FAILED((void*) -1),并设置errno
void* mmap(void *start, size_t length, int port, int flags, int fd, off_t offset);
//成功返回0,失败返回-1并设置errno
int munmap(void *start, size_t length);
  • start 参数允许用户使用某一个特定的地址作为这段内存的其实地址,如果是设置为NULL,则系统自动分配。
  • length指定这段内存的长度
  • port指定内存段的访问权限,
    在这里插入图片描述
  • flags参数控制内存段内容被修改后程序的行为
    在这里插入图片描述
  • fd是被映射文件的文件描述符,一般通过open系统调用获取
  • offset设置文件从何处来时映射,对于不需要读取整个文件的情况非常好用

6.6 splice函数

  • 用于两个文件描述符之间移动数据,也是零拷贝操作。
#include <fcntl.h>
ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags);
  • fd_in参数是输入数据的文件描述符,如果fd_in是一个管道文件描述符则off_in必须是NULL,off_in表示输入数据流从何处开始读取数据。
  • fd_out和off_out含义类似,用于输出数据里。
  • len指定移动数据的长度。
  • flags参数控制数据,可以是下表参数的按位或
    在这里插入图片描述
  • 使用splice时,fd_in和fd_out至少有一个是管道文件描述符。

  • splice成功时候返回移动的字节数量,失败时返回-1并设置errno
  • splice的常见errno
    在这里插入图片描述
  • 使用splice实现的高效回射服务
...
int pipefd[2];
int ret = pipe(pipefd);
//从socket中读取
ret = splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
//回射
ret = splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
...

6.7 tee函数

  • tee函数在两个管道文件描述符之间复制数据,不消耗数据,而splice从管道中读取数据,也就是消耗数据。
#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
  • 参数含义与splice相同,但是fd_in和fd_out必须都是管道文件描述符
  • tee成功时返回两个文件描述符之间复制的数据数量,字节数,失败返回-1并设置errno。

6.8 fcntl函数

  • 提供了对文件描述符的各种操作
#include <fcntl.h>
//失败返回-1并设置errno
int fcntl(int fd, int cmd, ...);
  • fd是被操作的文件描述符

  • cmd指定执行何种类型的操作

  • 由于操作类型的不同,可能需要第三个可选参数

  • fcntl支持的常用操作
    在这里插入图片描述
    在这里插入图片描述

  • 常用于把文件描述符设置为非阻塞

int setnonblocking(int fd) {
	//获取文件描述符旧标志
	int old_option = fcntl(fd, F_GETFL);
	//设置非阻塞标志
	int new_option = old_option | O_NONBLOCK;
	fcntl(fd, F_SETFL, new_option);
	return old_option;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值