码迷,mamicode.com
首页 > 系统相关 > 详细

《Linux系统编程》第二章笔记(一)

时间:2016-06-14 12:06:33      阅读:202      评论:0      收藏:0      [点我收藏+]

标签:

文件I/O

前言

文件概念对于Linux系统的重要性不言而喻,本章主要介绍了内核为文件的创建、读、写、定位等系统调用以及高效的I/O机制。Linux系统为文件操作提供了通用的系列系统调用,使开发人员能够对所有“文件”做相同的操作,同时还提供了ioctl()系统调用对非通用文件操作,fcntl()系统调用对文件描述符做操作。
此外Linux内核为了弥补CPU运算速度和磁盘I/O速度的巨大差异,引入了页缓存、页回写机制。简单来说就是读取文件时多读取一部分并保存在内核缓冲区中,在下次收到读取请求时直接将内存中的数据返回;写入文件时只是在内核缓冲区中保存要写入的数据,在合适的时机提交给磁盘(这样在写入之后的读取请求是从内核缓冲区中获取数据的,多个写入请求合并为一次磁盘操作能够提高效率)。当然,内核也提供了同步I/O机制来确保数据立即被写入磁盘;直接I/O机制来确保数据不经由内核缓存,直接提交给磁盘。

文件相关知识

文件描述符与偏移量

Linux系统调用一般通过文件描述符来指定对哪个文件进行操作,文件描述符与文件直接的关系在系统中由三个不同层次的表来维护。
* 进程级的文件描述符表(open file descriptor)
系统为每个进程维护了一个表,该表中每条记录保存了控制文件描述符操作的一组标志(目前仅有一个close-on-exec标志——使用exec()系列函数加载程序时自动关闭)和打开的文件句柄的引用
* 系统级的打开文件表(open file table/open file descriptor)
每个打开的文件都在该表中有一条对应数据,表中每条记录称为文件句柄(open file handle),一个文件句柄对应了一个打开的文件的全部信息,包括:当前文件偏移量;打开文件时使用的标志(open()的flags参数);文件访问模式(读写模式);与信号驱动I/O相关的设置;inode节点的引用
* 文件系统级的inode表
inode表被文件系统保存,任何文件系统中的文件都拥有inode信息,一个inode节点包括:文件类型和访问权限;指向文件持有的锁的指针;文件的各种类型例如大小、时间戳等。
由此可见,文件描述符与打开的文件之间有如下关系:
1 文件的偏移量和文件句柄相关,和文件描述符无关,同一个文件句柄可能被不同的文件描述符或进程共享。每次调用open()时内核都会分配一个文件句柄。
2 同一进程的不同文件描述符指向同一个文件句柄。这种情况可能是dup()dup()2fcntl()等系统调用或重定向>>>导致的,尽管文件描述符值不同,但其指向的句柄是同一个。此时不同文件描述符的文件偏移量是通用的,因为文件句柄是同一个。
3 不同进程的相同文件描述符指向同一个文件句柄。这种情况可能是打开文件描述符后调用了fork()函数,此时文件描述符的文件偏移量是通用的,因为文件句柄是同一个(见2.1的例子)。
4 不同进程的不同文件描述符指向同一个文件句柄,这种情况是2和1的顺序组合的结果,此时文件描述符的文件偏移量也是通用的。
5 同一个进程的文件描述符指向同一个文件(例如多次open()),这种情况下尽管一个进程多次打开同一个文件,资源描述符不相同,但是文件句柄也不相同,其文件偏移量必然不同:

#include <unistd.h>  
#include <stdio.h>   
#include <fcntl.h>
#include <iostream>
using namespace std;
int main ()   
{   
    //test.txt存在,内容为
    //1234567890
    int fd1 = open("test.txt", O_RDWR);
    int fd2 = open("test.txt", O_RDWR);
    cout << "fd1: " << fld1 << " fld2: " << fld2 << endl;//fld1:3 fld2:4
    char arr1[2] = {0};
    char arr2[2] = {0};
    read(fld1, arr1, 1);//简单起见,不再错误处理
    read(fld2, arr2, 1);
    cout << "arr1: " << arr1 << endl;//arr1:1,说明是从文件头开始读取的
    cout << "arr2: " << arr2 << endl;//arr2:1,说明fd2的偏移量不与fd1共享
    return 0;  
} 

/dev/fd目录
内核为每个进程提供了一个/dev/fd/n的虚拟路径,链接到对应该进程打开的文件描述符,例如对每个进程来说,/dev/fd/0代表标准输入。在进程中打开该目录,相当于复制了一个文件描述符,其指向同一个文件句柄。fd=open("/dev/fd/1")等于fd=dup(1)(dup()的作用见【重定向与复制文件描述符】)。在这种情况下open()传递的标准应该与对应的文件描述符一样。
此外系统还提供了/dev/stdin/dev/stdout/dev/stderr三个文件来方便引用。
一个例子来演示如何操作该目录下的文件:

#include <unistd.h>   
#include <stdio.h>    
#include <fcntl.h> 
#include <iostream> 
using namespace std; 
int main ()    
{    
    //从标准输入读取信息并输出到文件test.txt中
    //简单起见不做错误处理
    int fd1 = open("/dev/stdin", O_RDONLY); 
    int fd2 = open("test.txt", O_WRONLY|O_CREAT);
    char buf[1024];
    int iCount = read(fd1, buf, 1024);
    if(iCount != -1)
        write(fd2, buf, iCount);
    return 0;   
}  

临时文件
临时文件是进程关闭时自动删除的文件,glibc提供了类似

#include <stdlib.h>
int mkstemp(char* template);
FILE* tempfile();

的函数来帮助建立临时文件。
* mkstemp
template参数是一个字符数组,表示文件的模板,后六个字符必须是”X”,库函数会修改该值并在成功时返回文件描述符。例如char tempname[]="/temp/fileXXXXXX",在成功后会创建tempname的值会被修改并创建同名文件,进程所有者对其有读写权限。一般程序不再使用该临时文件时可以使用unlink()系统调用解除对该文件的引用,稍后在close()时该文件由于引用数为0,会被自动删除。当然,如果不显示close()的话,在进程结束时系统自动调用close()。

#include <stdlib.h>   
#include <unistd.h>
#include <stdio.h>
int main ()    
{    
    char tempFile[]="/root/cpproot/test/tempfileXXXXXX";
    int fd = mkstemp(tempFile);
    if(fd == -1)
    {
        perror("mkstemp");
        return -1;
    }
    unlink(tempFile);   
    char temp[32] = "hi?";
    write(fd, temp, sizeof(temp));
    lseek(fd, 0, SEEK_SET);
    char temp2[32];
    read(fd, temp2, sizeof(temp2));
    printf("read : %s\n", temp2);
    return 0;   
}  

unlink()系统调用作用是解除进程对文件描述符的引用(记得吗,引用计数记录在inode节点中),若在close(fd)时fd对应的文件引用数为0,系统会删除该文件。在对文件进行操作时,unlink()行为与remove()行为一样。

  • tempfile
    创建一个可读可写的临时文件并返回文件流,函数内部会调用unlink()保证流关闭时临时文件被删除。

2.1 打开文件

在对文件操作前需要打开文件,内核为每个进程维护了一个打开文件的列表。在打开文件后创建子进程,子进程会共享父进程打开的文件、当前文件位置等信息,子进程关闭文件不会对父进程有影响。

以下代码验证不同的进程中文件描述符是独立的:

#include <unistd.h>  
#include <stdio.h>   
#include <fcntl.h>
int main ()   
{   
    pid_t fpid; 
    fpid=fork();   
    if (fpid < 0)   
        printf("error in fork!");   
    //父进程返回子进程pid,子进程返回0
    else if (fpid == 0) {  
    int fd1 = open("test.txt", O_RDWR|O_CREAT );
    printf("child process fd %d\n",fd1 );    //3
    }  
    else {  
    int fd1 = open("test.txt", O_RDWR|O_CREAT );
    printf("parent process fd %d\n",fd1 );    //3
    }  
    return 0;  
} 

可以看出对于不同的进程而言,每个进程的文件描述符都是私有的,内核会为每个进程重新分配文件描述符。

以下代码验证子进程共享父进程文件描述符和文件当前位置:

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main ()
{
    pid_t fpid;
    int fd1 = open("test.txt", O_RDWR|O_CREAT );
    fpid=fork();
    if (fpid < 0)
        printf("error in fork!");
    //父进程返回子进程pid,子进程返回0
    else if (fpid == 0)
    {
        write(fd1, "123", 3);
    }
    else
    {
        write(fd1, "abc", 3);
    }
    return 0;  //文件最后是6个字符,如果父子进程分别向0位置写的话,会有数据被覆盖
}

可以看出在fork()之后,子进程会共享父进程的文件描述符和文件位置(即共享同一个文件句柄),相对的测试如下:

#include <unistd.h> 
#include <stdio.h> 
#include <fcntl.h> 
int main () 
{ 
    pid_t fpid;  
    fpid=fork(); 
    if (fpid < 0) 
        printf("error in fork!"); 
    //父进程返回子进程pid,子进程返回0 
    else if (fpid == 0) 
    { 
        int fd1 = open("test.txt", O_RDWR|O_CREAT );
        write(fd1, "123", 3); 
    } 
    else 
    { 
        int fd1 = open("test.txt", O_RDWR|O_CREAT );
        write(fd1, "abc", 3); 
    } 
    return 0;  //文件最后是3个字符,因为父子进程都是从文件头开始写的,有一个进程写的内容会被另一个覆盖
} 

2.1.1 open()系统调用

系统调用open()打开文件并返回系统分配的文件描述符,错误时返回-1并设置errno,错误码取值可以通过man 2 open查看。

#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode );

pathname-文件路径
flags-读写模式,有O_RDONLY、O_WRONLY、O_RDWR三种,同时可以和以下至少一个选项做或运算:

取值 说明
O_APPEND 每次写操作都写入文件的末尾,该标志保证了写文件的原子性,避免了多个进程写入同一个文件时内容被覆盖
O_ASYNC 文件可读或可写时产生一个信号(默认SIGIO),只能用于终端和套接字文件上,普通文件无法使用
O_CREAT 如果指定文件不存在,则创建这个文件
O_DIRECT 打开的文件用于直接I/O
O_DIRECTORY 如果打开的文件不是目录则返回错误,用于opendir()内部使用
O_LARGEFILE 打开文件时使用64位偏移量,这样大于2G的文件也能打开,64位架构默认就有该标志
O_EXCL 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值。该标识保证了创建文件的原子性,避免多个进程同时创建文件后都认为文件是自己创建的
O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端
O_NOFOLLOW 如果文件是一个符号链接,则open()会失败
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode)
O_DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新
O_RSYNC read 等待所有写入同一区域的写操作完成后再进行
O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的 I/O
O_CLOEXEC 内核2.6.23开始支持,指定在子进程执行exec()系列函数时自动关闭文件描述符,该参数出现之前使用fcntl()实现相同功能
O_NOATIME 内核2.6.8开始支持,使用该标志时,读取文件不会更新inode节点中的最新访问时间,减少读写的数据量

mode-新创建文件时用于指定权限,在创建文件时不指定权限会产生未定义的行为。权限在第一次创建文件时不会检查,因此第一次打开文件时权限是无效的。POSIX规定了以下参数可以用或运算同时指定:

取值 说明
S_IRWX(U/G/O) 所有者、所有组、其他用户有读写执行权限
S_IX(USR/GRP/OTH) 所有者、所有组、其他用户有执行权限
S_IR(USR/GRP/OTH) 所有者、所有组、其他用户有可读权限
S_IW(USR/GRP/OTH) 所有者、所有组、其他用户有可写权限

2.1.4 creat()系统调用

由于O_WRONLY|O_CREAT|O_TRUNC组合非常常见,有专门的函数来实现相同功能(某些架构上可能没有该系统调用,使用的是库函数):

#include <fcntl.h>
int creat(const char* name, mode_t mode);

返回值与open()一样。

2.2 用read()读取文件

read()是系统调用,用于从fd代表的文件中读取最多len个字节到buf中,正确时返回读取的字节数,失败时返回-1并设置errno,只读到一个文件结束符(EOF)时返回0。

#include <unistd.h>
ssize_t read (int fd, void *buf, size_t len);

每次read()成功后内核保存的文件位置指针会向前移动len个字节长度。这里的位置是内核记录的位置,与库调用时的位置不一样——库函数例如fgetc(),可能每次从内核读取1024个字符,但是每次只返回一个字符,此时内核的位置应该是1024而不是1。
以下代码验证对于一个文件描述符,不同进程共享同一个文件位置:

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main ()
{
    pid_t fpid;
    //假设test.txt存在且内容为"abcd",父子进程读取出来的是a和b而不是a和a
    int fd1 = open("test.txt", O_RDWR );
    fpid=fork();
    if (fpid < 0)
        printf("error in fork!");
    //父进程返回子进程pid,子进程返回0
    else if (fpid == 0)
    {
        char temp[1024] = {0};
        read(fd1, temp, 1);
        printf("child read:%s", temp);
    }
    else
    {
        char temp[1024] = {0};
        read(fd1, temp, 1);
        printf("parent read:%s", temp);
    }
    return 0;
}

当文件没有数据可读或未读完len个字节时(并且没读取到EOF,例如网络通信时)read()调用会阻塞,直到读满。

read()可能会有如下返回值对应的场景:
返回len,说明需要的数据长度全部读取到内存中
返回小于len,说明读取过程中有信号大端、读取时发生了错误、读取到EOF,再次读取时将剩余数据读到内存中
返回0,读到EOF
调用阻塞,等待可读的数据到来
调用返回-1,errno为EINTR,表示读取之前收到信号,可以再次调用
调用返回-1,errno为EAGAIN,表示没有可用数据,在非阻塞模式下发生
调用返回-1,errno为其他值,表示发生的其他严重错误

一个比较全面的代码是这样的:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) 
{
    if (ret == -1) {
        if (errno == EINTR)
            continue;
        perror (”read”);
        break;
    }
    len -= ret;
    buf += ret;
}

函数在读取到len个字节或遇到EOF之前会一直尝试从fd中读取,在出现中断时再次读取,在出现错误时打印错误信息并退出循环。

2.2.3 非阻塞读

read()系统调用在等待数据时会阻塞,在有些情况下我们不希望等待数据时阻塞,而是做一些其他操作——例如有多个文件要读,阻塞在一个上面不如看看其他文件有没有数据(当然有更好的办法,见I/O多路复用);或者有大量数据要在后面的代码中处理,而且数据不依赖于从文件中读取的内容,此时可以使用非阻塞I/O模式。在没有数据可读时read()也立即返回-1,并将errno设置为EAGAIN。进入非阻塞I/O模式需要在open()时指定O_NONBLOCK参数。

2.2.5 read()大小限制

POSIX规定了size_t和ssize_t类型表示占用内存的大小,ssize_t是有符号版的size_t,32位操作系统上是int,64位系统是long int。size_t最大值是SIZE_MAX,ssize_t则为SSIZE_MAX。若len大于SSZIE_MAX,read()调用结果是未定义的。

2.3 用write()来写

write()也是系统调用,表示从fd代表的当前位置读取最多count个字符到buf中。读取成功时返回真实的读取字符数,否则返回-1且设置errno。当count比SSIZE_MAX还大,调用的结果未定义。

#include <unistd.h>
ssize_t write (int fd, const void *buf, size_t count);

2.3.1 部分写

write()不太可能返回一个小于count的数,对于普通文件,发生部分写时很可能出现了错误。因此对于普通文件,出现部分写时不需要通过循环保证所有字符都写入文件。但对于socket等特殊文件来说,最好使用循环来保证全部写入文件。

ssize_t ret, nr;
while (len != 0 && (ret = write (fd, buf, len)) != 0)
{
  if (ret == -1)
  {
    if (errno == EINTR)
        continue;
    perror (”write”);
    break;
    }
  len -= ret;
  buf += ret;
}

2.3.2 追加模式

文件在open()时如果指定O_APPEND参数,则写操作每次都从文件末尾开始。例如有多个进程向同一个文件写入,追加模式保证了每个进程都不会覆盖其他进程写入的数据,因为系统能够保证每次写入都从尾部开始,对日志功能非常有用。

2.3.3 非阻塞写

文件在open()时如果指定了O_NONBLOCK参数,则write()会正常阻塞时,调用会立即返回-1并设置errno为EAGAIN,普通文件不会出现这种情况。

2.3.4 其他错误码

write()操作会产生的错误码还有:

错误码 描述
EBADF 给定的fd非法或不是以写方式打开的
EFAULT 给定的buf不在进程地址空间内
EFBIG 写操作使文件超出进程最大文件限制
EINVAL fd对应的对象无法进行写操作
EIO 发生了一个底层I/O错误
ENOSPC 文件系统没有足够空间
EPIPE fd对应的管道或socket的读端被关闭,同时进程会受到SIGPIPE信号

2.3.6 write()的行为

由于硬盘I/O速度与CPU处理速度相差较大,在程序执行时等待数据真正写入硬盘对程序性能有较大影响,因此内核提供了内核缓冲区用于存放要写入硬盘的数据。write()操作在数据从用户空间拷贝到内核空间时即返回,内核会确保在某个合适的时机将数据写入硬盘。
这种延迟写入的方式不会改变交替读写的结果:当需要read()刚刚写入硬盘的数据时,若此时数据在内核缓冲区中还未写入硬盘,读取的将是缓冲区的数据,减少了读取硬盘的次数。
为了保证及时将输入写入硬盘,在/proc/sys/vm/dirty_expire_centiseconds中配置了最大时效,内核会在超出最大时效之前将数据写入硬盘。内核缓冲区

大文件读写

系统变量类型是指一些系统实现细节相关的变量类型typedef成不暴露实现细节的变量类型,从而保证跨平台时源码级别的兼容性。例如对于进程id,其系统变量类型是pid_t,常见的还有size_tsocklen_t等,在sys/types.h中定义。
32位Linux上,文件偏移量类型off_t类型为int,32位,即能够寻址的文件最大为2G。若想实现32位系统上对超过2G的文件寻址,需要将_FILE_OEFFSET_BISTS宏设置为64,可以在包含其他头文件之前#define _FILE_OEFFSET_BISTS 64或在makefile中添加-D_FILE_OEFFSET_BISTS=64,编译时根据该条件会再次将off_t类型typedef成__off64_t,这个变量是64位长度的。通过这个宏,在不修改源码的前提下将只支持最大2G操作的源码扩展成支持最大2^63-1大小。
除此之外还能使用过度型API来指明对大文件读写,包括open64()lseek64()等。更推荐的是在32位Linux上使用_FILE_OEFFSET_BISTS宏。此外使用宏定义的方式对大文件做支持时要注意,所有与文件读写相关的模块在编译时都要使用该宏,避免类型不一致导致的问题。
64位Linux的off_t长度默认就是64位的,因此不需要定义上述的宏。

《Linux系统编程》第二章笔记(一)

标签:

原文地址:http://blog.csdn.net/wylblq/article/details/51668946

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!