Linux系统编程笔记-文件IO

本文主要介绍了如下内容:

  • C标准库函数与系统函数的关系
  • 进程控制块
  • 文件描述符
  • 系统调用:open、close、read、write、lseek、fcntl和ioctl

先导概念

C标准库函数与系统函数的关系

API层次如图所示:


API层次

API调用顺序

由上往下(用户态 -> 内核态)的顺序依次是:

  • C标准库函数:调用系统库函数(即 系统调用);
  • 系统调用:即操作系统的应用层API,调用内核层API;
  • 内核层API: 调用具体的驱动层API(在Linux中一般以sys_开头);
  • 驱动层函数:直接控制硬件设备。

以调用fwrite()函数将文件内容显示在终端为例,fwrite()函数将调用write()系统调用,而write()系统调用的实现则是调用内核态的sys_write()函数,由sys_write()来判断具体调用哪个驱动函数来访问硬件设备。

当然,对于Linux操作系统而言,还多了一层VFS(virtual File System,虚拟文件系统)层:


write()系统调用将来自用户空间的数据流,首先通过VFS的通用系统调用,然后通过文件系统的特殊写法,最后写入物理介质中。

各API在缓冲区上的不同之处

  • fopen():每打开一个文件,都会对应一个单独的缓冲区;
  • open():无缓冲区;
  • sys_open:有缓冲区,但是由所有打开的文件共用。

关于缓冲区的刷新方式:

  • 刷新C标准缓冲区
  1. 缓冲区满,自动刷新;
  2. 手动调用fflush()函数刷新;
  3. 使用fclose()函数关闭文件时刷新;
  4. 程序正常结束后缓冲区自动刷新。
  • 刷新内核缓冲区
    由一个守护进程定时刷新。

PCB和文件描述符fd

PCB(process control block,进程控制块)在Linux源码中的实现即task_struct结构体,位于/include/linux/sched.h文件中。该结构体在Linux中被称为进程描述符(process descriptor)。 其部分结构如下(linux kernel 版本为4.4.36):

1380 struct task_struct {
1381     volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
1382     void *stack;
1383     atomic_t usage;
1384     unsigned int flags; /* per process flags, defined below */
1385     unsigned int ptrace;
1386
1387 #ifdef CONFIG_SMP
1388     struct llist_node wake_entry;
1389     int on_cpu;
1390     unsigned int wakee_flips;
1391     unsigned long wakee_flip_decay_ts;
1392     struct task_struct *last_wakee;
1393
1394     int wake_cpu;
1395 #endif
1396     int on_rq;


1483    pid_t pid;
1484    pid_t tgid;

Linux内核把进程的列表存放在任务队列(task list)中,该队列是一个双向循环链表,链表中的每一项都是一个task_struct结构体。

在Linux内核中,每一个进程都有一个PCB来管理,每一个PCB中都有一个指向files_struct结构体的指针:

1564 /* open file information */
1565     struct files_struct *files;

可以看到,task_struct结构体中的files是个指针(充当目录项的角色),指向files_struct结构体。而files_struct结构体是一张文件描述符表(实际上就是一个整形数组,里面存放的是诸如012这样的文件描述符,文件描述符即一些非负整数),这些文件描述符指向真正的设备文件,包括磁盘文件、显示屏文件等所有文件。

文件描述符struct files_struct 源码:
(位于linux-4.4.36/include/linux/fdtable.h中)

 43 /*
 44  * Open file table structure
 45  */
 46 struct files_struct {
 47   /*
 48    * read mostly part
 49    */
 50     atomic_t count;  /* 该结构体的引用计数 */
 51     bool resize_in_progress;
 52     wait_queue_head_t resize_wait;
 53
 54     struct fdtable __rcu *fdt;
 55     struct fdtable fdtab;
 56   /*
 57    * written part on a separate cache line in SMP
 58    */
 59     spinlock_t file_lock ____cacheline_aligned_in_smp;
 60     int next_fd;
 61     unsigned long close_on_exec_init[1];
 62     unsigned long open_fds_init[1];   
 63     unsigned long full_fds_bits_init[1];
 64     struct file __rcu * fd_array[NR_OPEN_DEFAULT]; /* 缺省的文件对象数组 */
 65 };

关系图:

文件句柄关系

文件‘开’ ‘关’ ‘读’ ‘写’的系统接口

open()

功能:打开或者创建(如果文件不存在)一个文件。

每打开一个文件,操作系统内核(kernel)就会在内存中新建一个files_struct结构体。

在同一个进程中 多次打开同一个文件,内核也会在内存中分别新建不同的files_struct结构体(由不同的文件描述符映射)。因此,每次打开的文件在使用完之后一定要及时关闭,否则可能会引起内存泄漏。

声明:

NAME
       open - open and possibly create a file

SYNOPSIS
       #include <sys/types.h>
       #include <sys/stat.h>
       #include <fcntl.h>

       int open(const char *pathname, int flags);
       int open(const char *pathname, int flags, mode_t mode);

返回值:

  • 成功:返回新分配的文件描述符;
  • 出错:则返回-1,并设置errno;

close()

功能:关闭一个打开的文件,一般与open()成对使用。

每调用一次close(fd),实际上是将该文件描述符fd所指向的files_struct结构体中的引用计数count值减一。当引用计数值减为0时,操作系统内核(kernel)才真正关闭该文件。

通过调用dup/dup2系统调用可使files_struct结构体中的引用计数count值加一。具体是dup/dup2新生成一个文件描述符newfd,并使其指向旧文件描述符oldfd所指向的files_struct结构体,即这两个文件描述符共用一个files_struct结构体。

声明:

NAME
       close - close a file descriptor

SYNOPSIS
       #include <unistd.h>

       int close(int fd);

返回值:

  • 成功:返回0;
  • 出错:则返回-1,并设置errno;

read()

功能: 从打开的设备或文件中读取数据。
声明:

NAME
       read - read from a file descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t read(int fd, void *buf, size_t count);

返回值:

  • 成功:返回读取的字节数;
  • 出错:则返回-1,并设置errno;
  • 如果在调read之前已到达文件末尾,则这次read返回0。

write()

功能:从内存地址buf开始,向打开的文件写入count字节(byte)的数据。
声明:

NAME
       write - write to a file descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);

返回值:

  • 成功:返回写入的字节数;
  • 出错:返回-1,并设置errno。

注意:
在向常规文件进行写操作时,write函数的返回值通常等于请求写的字节数count,而向终端设备或网络设备进行写操作时则不一定。

Demo:mycp.c

程序功能描述:模仿cp命令,将一个文件中的内容复制到一个新的文件之中。

code:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#define SIZE    8192

int main(int argc, char *argv[])
{
    int fd_src, fd_des, len;
    char buf[SIZE];

    /* 参数输入太少,不符合要求,打印命令使用提示信息并退出 */
    if (argc < 3) {
        printf("Usage: ./mycp src_file des_file\n");
        exit(1);
    }

    /* 打开源文件 */
    fd_src = open(argv[1], O_RDONLY);
    if (fd_src == -1) {
        printf("Openning file %s failed...\n", argv[1]);
        exit(-1);
    } 

    /* 新建目标文件 */
    fd_des = open(argv[2], O_CREAT | O_WRONLY | O_TRUNC, 0664);
    if (fd_des == -1) {
        printf("Creating file %s failed...\n", argv[2]);
        exit(-1);
    }

    /* 读取源文件中的内容,然后写入目标文件之中 */
    while ( (len = read(fd_src, buf, sizeof(buf))) > 0 ) {
        write(fd_des, buf, len);
    }

    /* 关闭文件 */
    close(fd_src);
    close(fd_des);

    return 0;
}

test

slot@slot-ubt:~/test$ gcc mycp.c -o mycp
slot@slot-ubt:~/test$ cat aa
Hello, this is my cp cmd.
Welcome to use...
slot@slot-ubt:~/test$ cat bb
cat: bb: No such file or directory
slot@slot-ubt:~/test$ ./mycp aa bb
slot@slot-ubt:~/test$ cat bb
Hello, this is my cp cmd.
Welcome to use...
slot@slot-ubt:~/test$

lseek()

功能:移动打开的文件的读写指针的位置。

每个打开的文件都记录着当前读写指针的位置,打开文件时读写位置是0,表示文件开头。通常,读写多少个字节,就会将读写位置往后移动多少个字节。但有一个例外,如果以O_APPEND(追加)方式打开,则每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。

声明:

NAME
       lseek - reposition read/write file offset

SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>

       off_t lseek(int fd, off_t offset, int whence);

lseek的两个"副作用"示例

demo1. 扩展一个文件

注意
拓展一个文件,一定要有一个写操作。

code:extend_file.c
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    int fd;

    /* 新建一个名为abc的文件 */
    fd = open("abc", O_CREAT | O_RDWR);
    if (fd < 0) {
        perror("Opening file failed: ");
        exit(-1);
    }

    /* 将读写指针移到文件末尾 */
    lseek(fd, 0x1000, SEEK_SET);
    /* 追加写一个字节到文件中去
     * string "a" will be translated to an addr
     * od -tcx see file abc
     */
    write(fd, "a", 1); 
    close(fd);

    return 0;
}

errno是个用户态的全局变量,声明在头文件/usr/include/errno.h中 :

 45 #ifndef errno
 46 extern int errno;
 47 #endif

Linux下的错误码可以查阅文件:/usr/include/asm-generic/errno-base.h (部分展示如下):

  1 #ifndef _ASM_GENERIC_ERRNO_BASE_H
  2 #define _ASM_GENERIC_ERRNO_BASE_H
  3 
  4 #define EPERM        1  /* Operation not permitted */        
  5 #define ENOENT       2  /* No such file or directory */
  6 #define ESRCH        3  /* No such process */
  7 #define EINTR        4  /* Interrupted system call */
  8 #define EIO      5  /* I/O error */
  9 #define ENXIO        6  /* No such device or address */
 10 #define E2BIG        7  /* Argument list too long */
 11 #define ENOEXEC      8  /* Exec format error */
 12 #define EBADF        9  /* Bad file number */
 13 #define ECHILD      10  /* No child processes */
 14 #define EAGAIN      11  /* Try again */
 15 #define ENOMEM      12  /* Out of memory */
 16 #define EACCES      13  /* Permission denied */

perror()函数将打印用户自定义信息和errno后面对应的注释信息,其声明为:

NAME
       perror - print a system error message
>
SYNOPSIS
       #include <stdio.h>
>
       void perror(const char *s);
>
       #include <errno.h>
>
       const char * const sys_errlist[];
       int sys_nerr;
       int  errno;        
test:
slot@slot-ubt:~/test$ gcc extend_file.c -o exf
slot@slot-ubt:~/test$ ./exf
slot@slot-ubt:~/test$ od -txc abc
0000000          00000000        00000000        00000000        00000000
          \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
*
0010000          00000061
           a
0010001

demo2. 获取文件的大小

方法:将指针移到文件末尾,然后输出返回值,该值即文件大小。

code: see_file_size.c
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    int fd;

    fd = open("abc", O_RDWR);
    if (fd < 0) {
        perror("Opening file failed: ");
        exit(-1);
    }

    /* print file size */ 
    printf("abc size is: %lld\n", lseek(fd, 0, SEEK_END));
     
    close(fd);

    return 0;
}

test:
slot@slot-ubt:~/test$ gcc see_file_size.c -o fsize
slot@slot-ubt:~/test$ ./fsize
abc size is: 4097
slot@slot-ubt:~/test$ ls -l abc
-rwxrwxrwx  1 slot  staff  4097 12 14 19:40 abc

fcntl()

功能: 获取或者设置已打开文件的访问属性。
声明:

NAME
       fcntl - manipulate file descriptor

SYNOPSIS
       #include <unistd.h>
       #include <fcntl.h>

       int fcntl(int fd, int cmd, ... /* arg */ );

demo:

改变文件的状态标志位为非阻塞状态

code: test_fcntl.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

int main()
{
    char buf[10];
    int n;
    int flags;

    /* get file flag */
    flags = fcntl(STDIN_FILENO, F_GETFL);
    
    /* change file flags to nonblock */
    flags |= O_NONBLOCK;
    if (fcntl(STDIN_FILENO, F_SETTL, flags) == -1) {
        perror("change file flag failed: ");
        exit(1);
    }

try_again:
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0) {
        if (errno == EAGAIN) {
            sleep(1);
            write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
            goto try_again;
        }
        perror("read stdin failed: ");
        exit(1);
    }
    write(STDOUT_FILENO, buf, n);

    return 0;
}

test:


ioctl()

功能:向设备发送控制和配置命令。

有些命令也需要读写一些数据,但这些数据是不能用read/write读写的,称为Out-of-band数据。也就是说,read/write读写的数据是in-band数据,是I/O操作的主体,而ioctl命令传送的是控制信息,其中的数据是辅助的数据。

例如,在串口线上收发数据通过read/write操作,而串口的波特率、校验位、停止位则通过ioctl来设置;A/D转换(模数转换)的结果通过read读取,而A/D转换的精度和工作频率则通过ioctl设置。

声明:

NAME
       ioctl - control device

SYNOPSIS
       #include <sys/ioctl.h>

       int ioctl(int fd, unsigned long request, ...);

fd是某个设备的文件描述符,request是ioctl的命令,可变参数取决于request,通常是一个指向变量或结构体的指针。

若出错,则返回-1;若成功,则返回其他值。返回值也取决于request

demo: 获取终端窗口的大小

code: get_tty_size.c

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

int main(void)
{
    struct winsize size;
    
    /* 不是终端设备文件则退出 */
    if (isatty(STDOUT_FILENO) == 0)
        exit(1);

    /* 通过 ioctl() 获取终端窗口的大小 */
    if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) < 0) {
        perror("ioctl TIOCGWINSZ error");
        exit(1);
    } 
    /* 打印终端窗口的长和宽 */
    printf("%d rows, %d columns\n", size.ws_row, size.ws_col);

    return 0;
}


test:

(测试结果依赖于当前终端的窗口大小)

slot@slot-ubt:~/test$ gcc get_tty_size.c -o ttysize
slot@slot-ubt:~/test$ 
slot@slot-ubt:~/test$ ./ttysize 
24 rows, 65 columns
slot@slot-ubt:~/test$ 
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容