Linux系统编程学习笔记--第三章

3 系统调用

该节对应APUE的第三章------文件IO

3.1 简介

UNIX系统的大多数文件IO只需用到5个函数:

open
close
read
write
lseek

3.2 文件描述符

3.2.1 FILE结构体

查看stdio.h头文件中,有FILE结构体的定义:

cs 复制代码
//stdio.h
typedef struct _iobuf {
    char*  _ptr;        //文件输入的下一个位置
    int    _cnt;        //当前缓冲区的相对位置
    char*  _base;       //文件初始位置
    int    _flag;       //文件标志
    int    _file;       //文件有效性
    int    _charbuf;    //缓冲区是否可读取
    int    _bufsiz;     //缓冲区字节数
    char*  _tmpfname;   //临时文件名
} FILE;

其中_file就是文件描述符。

3.2.2 文件描述符

文件描述符(fd,file descriptor)是文件IO(也系统IO)中贯穿始终的类型。如下图所示:

1 当某一个进程执行系统调用open函数,会创建一个结构体,该结构体类似于FILE结构体,其中最基本的成员有一个指针pos,用于指向inode文件的某一个位置;
2 同时,该进程会维护一个数组(文件描述符表),该数组存储上述结构体的地址,而数组下标就是文件描述符fd,即文件描述符的本质就是一个整型数;

该数组默认大小为1024,即可以打开的最大文件数量为1024,但可以设置ulimit来更改数组大小;注意该数组和对应产生的结构体只存在于这个进程空间内,而不是进程间共享;

当调用open函数时,系统会自动打开三个流stdin,stdout和stderr,这三个流分别占据该数组的0,1,2号位置;

结构体FILE中的成员_file就是整型数组下标fd,即文件描述符

每打开一个新文件,则占用一个数组空间,而且是空闲的最小的数组下标。即文件描述符优先使用当前可用范围内最小的。同一个文件可以被多次打开,但是每打开一次都需要一个新的文件描述符和新的结构体,例如图中的结构体1和结构体2,指向了同一个inode;
3 执行系统调用close时,就将对应fd下标的数组空间清除掉,并清除该地址指向的结构体;

4 结构体中有一个成员用于记录引用计数,例如图中,将5号位置的0x006地址复制一份存储在6号位置,此时有两个指针指向了同一个结构体3,此时结构体3的引用计数为2,当5号指针free时,结构体3的引用计数减1为1,不为0,则不会释放掉,否则6号位置的指针将成为野指针;

3.3 open和close

3.3.1 文件权限

① rwx

Linux下一切皆文件,不同的用户对文件拥有不同的权限。

文件具有三种权限:

cs 复制代码
rwx    可读可写可执行,转换为数字就是421

|---|------------|-------------------------------------------------------------|
| | 针对文件 | 针对目录 |
| r | 是否可以查看文件内容 | 是否能够列出ls目录内容 |
| w | 是否可以编辑文件内容 | 是否能够创建、删除、复制、移动文档 |
| x | 是否能够执行该文件 | 是否可以进入目录和获得目录下文件的列表,要对目录下存在的文件进行读取和修改,必须要能进入目录,所以必须要目录有执行权限 |

② 文件属性

查看当前目录下的所有文件的属性:

命令:ll

或者可以查看单个文件权限:

cs 复制代码
ll mycpy.c

基本的文件属性格式如下:

cs 复制代码
类型 权限 链接数 属主 属组 大小 修改日期 文件名

类型和权限:

第1列表示文件的类型:dcb-lsp

d:目录

-:普通文件

l:软链接(类似Windows的快捷方式)

b:块设备文件(例如硬盘、光驱等)

p:管道文件

c:字符设备文件(例如屏幕等串口设备)

s:套接口文件

第2至10列为文件或目录的权限,分为3组:

拥有者权限owner:文件和文件目录的所有者

所属组group:文件和文件目录的所有者所在的组的其他用户

其它用户other:不属于上面的两个的用户

链接数:有多少文件名链接到此节点(i-node);每个文件都会将它的权限与属性记录到文件系统的i-node中,不过我们使用的目录树却是使用文件名来记录,因此每个文件名就会连接到一个i-node,这个属性记录的就是有多少不同的文件名链接到相同的一个i-node号码。

3.3.2 修改文件权限

参考下面文章:linux修改文件权限

3.3.3 open

open用于打开或创建一个文件或者设备。

所在头文件:

cs 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

函数原型1:

cs 复制代码
int open(const char *pathname, int flags);

将准备打开的文件或是设备的名字作为参数path传给函数,flags用来指定文件访问模式。

open系统调用成功返回一个新的文件描述符,失败返回-1。

其中,flags是由必需文件访问模式和可选模式一起构成的(通过按位或|):

|------------------|------------------------------------------------------------|
| 必需部分 | 可选部分(只列出常用的) |
| O_RDONLY:以只读方式打开 | O_CREAT:按照参数mode给出的访问模式创建文件 |
| O_WRONLY:以只写方式打开 | O_EXCL:与O_CREAT一起使用,确保创建出文件,避免两个程序同时创建同一个文件,如文件存在则open调用失败 |
| O_RDWR:以读写方式打开 | O_APPEND:把写入数据追加在文件的末尾 |
| | O_TRUNC:把文件长度设置为0,丢弃原有内容 |
| | O_NONBLOCK:以非阻塞模式打开文件 |

其中,对于可选部分,又分为文件创建选项和文件状态选项:

文件创建选项:O_CREAT,O_EXCL,O_NOCTTY,O_TRUNC

文件状态选项:除文件创建选项之外的选项

fopen和open的文件访问模式的联系

cs 复制代码
r -> O_RDONLY // 只读存在的文件
r+ -> O_RDWR // 读写存在的文件
w -> O_WRONLY|O_CREAT|O_TRUNC // 只写,并且有则清空,无则创建
w+ -> O_RDWR|O_CREAT|O_TRUNC // 读写,并且有则清空,无则创建
// ...

函数原型2:

cs 复制代码
int open(const *path, int flags, mode_t mode);

在第一种调用方式上,加上了第三个参数mode,主要是搭配O_CREAT使用,这个参数规定了属主、同组和其他人对文件的文件操作权限。只列出部分:

|---------|-----------|
| 字段 | 含义 |
| S_IRUSR | 读权限 |
| S_IWUSR | 写权限--文件属主 |
| S_IXUSR | 执行权限 |

注意mode还要和umask计算才能得出最终的权限;

例如:

cs 复制代码
int fd = open("./file.txt",O_WRONLY | O_CREAT, 0600);

创建一个普通文件,权限为0600,拥有者有读写权限,组用户和其他用户无权限。

补充:变参函数

变参数函数的原型声明为:

cs 复制代码
type VAFunction(type arg1, type arg2, ...);

变参函数可以接受不同类型的参数,也可以接受不同个数的参数。

参数可以分为两部分:个数确定的固定参数和个数可变的可选参数。函数至少需要一个固定参数,固定参数的声明和普通函数一样;可选参数由于个数不确定,声明时用 ... 表示。固定参数和可选参数共同构成一个函数的参数列表。

以printf为例,它就是一个变参函数:

cs 复制代码
int printf(const char *fmt, ...){    
    int i;    
    int len;    
    va_list args; /* va_list 即 char * */
    
    va_start(args, fmt);    
    /* 内部使用了 va_arg() */
    len = vsprintf(g_PCOutBuf,fmt,args);
    
    va_end(args);    
    for (i = 0; i < strlen(g_PCOutBuf); i++)
    {
        putc(g_PCOutBuf[i]);
    }    
    return len;
}

3.3.4 close

close:关闭一个文件描述符

cs 复制代码
#include <unistd.h>

int close(int fd);

返回 0 表示成功,或者 -1 表示有错误发生,并设值errno;

3.4 read,write和sleek

read所在头文件和函数原型:

cs 复制代码
#include <unistd.h>

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

从与文件描述符fd相关联的文件中读取前count字节的内容,并且写入到数据区buf中

read系统调用返回的是实际读入的字节数,发生错误返回-1

write所在头文件和函数原型:

cs 复制代码
#include <unistd.h>

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

把缓存区buf中的前count字节写入到与文件描述符fd有关的文件中

write系统调用返回的是实际写入到文件中的字节数,发生错误返回-1,注意返回0不是发生错误,而是写入的字节数为0

lseek所在头文件和函数原型:

cs 复制代码
#include <sys/types.h>
#include <unistd.h>

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

lseek设置文件位置为给定的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数。

whence取值:

lseek设置文件位置为给定的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数。

whence取值:

|----------|--------|
| 字段 | 含义 |
| SEEK_SET | 文件开头 |
| SEEK_CUR | 文件当前位置 |
| SEEK_END | 文件末尾 |

代码示例

用系统调用io实现mycpy的功能。

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define BUFSIZE 1024 // 缓冲区大小

int main(int argc, char **argv) {
    // 源文件和目标文件的文件描述符
    int sfd, dfd;
    // 读写缓冲
    char buf[BUFSIZE];
    // len:读文件的返回字节数
    // ret:写文件的返回字节数
    // pos:写文件的当前位置
    int len, ret, pos;

    if(argc < 3) {
        fprintf(stderr, "Usage...\n");
        exit(1);
    }
	
    // 以只读方式打开文件,打开文件失败
    if((sfd = open(argv[1], O_RDONLY)) < 0) {
        perror("open()");
        exit(1);
    }
	
    // 以只读方式打开文件,有则清空,无则创建
    // 打开文件失败
    dfd = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0600
    if(dfd < 0) {
        close(sfd);
        perror("open()");
        exit(1);
    }

    while(1) {
        if((len = read(sfd, buf, BUFSIZE)) < 0) {
            perror("read()");
            break;
        }
		
        // 读完文件
        if(len == 0)
            break;

        pos = 0;
        // 防止读到的字节没有完全写入文件
        // 保证读多少,就写多少
        while(len > 0) {
            if((ret = write(dfd, buf + pos, len)) < 0) {
                perror("write()");
                exit(1);
            }
            pos += ret;
            len -= ret;
        }
    }
	// 关闭文件描述符
    close(dfd);
    close(sfd);

    exit(0);
}

3.5 IO效率

**文件I/O:**即系统调用IO,又称为无缓冲IO,低级磁盘I/O,遵循POSIX相关标准。任何兼容POSIX标准的操作系统上都支持文件I/O。

**标准I/O:**标准I/O是ANSI C建立的一个标准I/O模型,又称为高级磁盘I/O,是一个标准函数包和stdio.h头文件中的定义,具有一定的可移植性。标准I/O库处理很多细节。例如缓存分配,以优化长度执行I/O等。标准的I/O提供了三种类型的缓存(行缓存、全缓存和无缓存)。

Linux 中使用的是GLIBC,它是标准C库的超集。不仅包含ANSI C中定义的函数,还包括POSIX标准中定义的函数。因此,Linux 下既可以使用标准I/O,也可以使用文件I/O。

缓存 是内存上的某一块区域。缓存的一个作用是合并系统调用即将多次的标准IO操作合并为一个系统调用操作。

文件IO不使用缓存 ,每次调用读写函数时,从用户太切换到内核态,对磁盘上的实际文件进行读写操作 ,因此响应速度快,坏处是频繁的系统调用会增加系统开销(用户态和内核态来回切换),例如调用write写入一个字符时,磁盘上的文件就会多一个字符。

标准IO使用缓存,未刷新缓冲前的多次读写时,实际上操作的是内存上的缓冲区与磁盘上的实际文件无关 ,直到刷新缓冲时,才调用一次文件IO,从用户态切换到内核态,对磁盘上的实际文件进行操作。因此标准IO的吞吐量大 ,相应的响应时间比文件IO长 。但是差别不大,优先建议使用标准IO来操作文件。

两种IO可以相互转化:

fileno:返回结构体FILE的成员_file,即文件描述符。标准IO->文件IO

cs 复制代码
int fileno(FILE *stream);

fdopen:通过文件描述符fd,返回FILE结构体。文件IO->标准IO

cs 复制代码
FILE *fdopen(int fd, const char *mode);

注意:即使对同一个文件,也不要混用两种IO,否则容易发生错误。

原因:FILE结构体的pos和进程中的结构体的pos基本上不一样。

cs 复制代码
FILE *fp;
// 连续写入两个字符
fputc(fp) -> pos++
fputc(fp) -> pos++    

但是,进程维护的结构体中的pos并未加2;只有刷新缓冲区时,该pos才会加2;

代码示例

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    putchar('a');
    write(1, "b", 1);

    putchar('a');
    write(1, "b", 1);

    putchar('a');
    write(1, "b", 1);

    exit(0);
}

打印结果:

bbbaaa

解析:putchar为标准IO函数,write为文件IO函数,遇到文件IO则立即输出,遇到标准IO,则需要等待缓冲区刷新的时机,这里是进程结束后,进行了强制刷新,将3个a字符输出到终端上。

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    putchar('a');
    fflush(stdout);
    write(1, "b", 1);

    putchar('a');
    fflush(stdout);
    write(1, "b", 1);

    putchar('a');
    fflush(stdout);
    write(1, "b", 1);

    exit(0);
}

打印结果:

ababab


strace命令能够显示所有由用户空间程序发出的系统调用。

以上面第一个程序为例:

cs 复制代码
strace ./ab

打印结果:

BUFSIZE对IO效率的影响

图中用户CPU时间是程序在用户态下的执行时间;系统CPU时间是程序在内核态下的执行时间;时钟时间是两个时间的总和;

BUFSIZE受栈大小的影响;此测试所用的文件系统是Linux ext4文件系统,其磁盘块长度为4096字节。这也证明了图中系统 CPU 时间的几个最小值差不多出现在BUFFSIZE 为4096 及以后的位置,继续增加缓冲区长度对此时间几乎没有影响。

3.6 文件共享

如果两个独立进程各自打开了同一文件:

在完成每个write后,在文件表项(即类似于FILE的结构体)中的当前文件偏移量即增加所写入的字节数。如果这导致当前文件偏移量超出了当前文件长度,则将i节点表项中的当前文件长度设置为当前文件偏移量(也就是该文件加长了)。

如果用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度。这就使得每次写入的数据都追加到文件的当前尾端处。

若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度。lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。

可能有多个文件描述符指向同一个文件表项(例如使用dup),对于多个进程读取同一文件都能正确工作。每个进程都有它自己的文件表项,其中也有它自己的当前文件偏移量。但是,当多个进程写同一文件时,则可能产生预想不到的结果。为了说明如何避免这种情况,需要理解原子操作的概念。

原子操作:不可分割的操作;

原子操作的作用:解决竞争和冲突;

3.7dup和dup2

dup函数用于复制文件描述符,重定向输入输出。

cs 复制代码
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);

返回值:

成功:

dup函数返回当前系统可用的最小整数值,并且该描述符对应的文件与参数描述符oldfd所对应的文件一致,即指向同一个结构体;

dup2函数返回第一个不小于newfd的整数值,分两种情况:

如果newfd已经打开,则先将其关闭,再指向文件描述符oldfd的结构体;

如果newfd等于oldfd,则什么也不做;

失败:dup和dup2函数均返回-1,并设置errno。

代码示例

需求:将puts重定向到一个文件中

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#define FNAME "/tmp/out"

int main(void) {
    int fd;
    close(1); // 关闭stdout,使描述符1空闲
    if((fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0) {
        perror("open()");
        exit(1);
    }

    puts("Hello World");

    exit(0);
}

结果:

cs 复制代码
[root@HongyiZeng sysio]# ./dup
[root@HongyiZeng sysio]# cat /tmp/out
Hello World

方法2:使用dup

cs 复制代码
#define FNAME "/tmp/out"

int main(void) {
    int fd;

    if((fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0) {
        perror("open()");
        exit(1);
    }
    // 关闭stdout
    close(1);
    // 复制fd,让其占据1的描述符
    dup(fd);
    // 关闭fd
    close(fd);

    puts("Hello World");

    exit(0);
}

图示:

注意结构体中有引用计数,当fd=3被关闭时,还有fd=1指向这个结构体,因此结构体不会被销毁掉。存在并发问题。

方法3:使用dup2

cs 复制代码
#define FNAME "/tmp/out"

int main(void) {
    int fd;

    if((fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0) {
        perror("open()");
        exit(1);
    }
    // 如果fd = 1,则什么也不做,返回fd
    // 如果fd != 1,则关闭1指向的结构体,再打开1,指向fd的结构体,返回1
    dup2(fd, 1);
    if(fd != 1) {
        close(fd);
    }
    puts("Hello World");

    exit(0);
}

dup2是一个原子操作,相当于:

cs 复制代码
dup2(fd, 1);
// 相当于:
close(1);
dup(fd);

3.8 fcntl和ioctl

fcntl针对文件描述符提供控制。

cs 复制代码
#include <unistd.h>
#include <fcntl.h>

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

返回值:若成功,则依赖于cmd,若失败,则返回-1

函数功能:

复制一个已有的描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC)

获取/设置文件描述符标志(cmd=F_GETFD或F_SETFD)

获取/设置文件状态标志(cmd=F_GETFL或F_SETFL)

获取/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)

获取/设置记录锁(cmd=F_GETLK、F_SETLK或F_SETLKW)

ioctl:用于控制设备

cs 复制代码
#include <sys/ioctl.h>

int ioctl(int d, int request, ...);

ioctl函数一直是IO操作的杂物箱。不能用本章中其他函数表示的I/O操作通常都能用ioctl表示。终端I/O是使用ioctl最多的地方。

3.9 /dev/fd目录

对于每个进程,内核都提供有一个特殊的虚拟目录/dev/fd。

该目录中包含/dev/fd/n形式的文件名,其中n是与进程中打开文件描述符相对应的编号。也就是说,/dev/fd/0就对应于进程的标准输入stdin。

打开/dev/fd目录中的一个文件等同于复制对应的文件描述符,所以下面两行代码是等价的:

cs 复制代码
fd = open("/dev/fd/1", O_WRONLY);
// 等价于:
fd = dup(1);

3.10 补充:几个文件的区别

3.10.1 用户变量

~/.bashrc和~/.bash_file这两个看到~,应该明白,这是用户目录下的,即里面的环境变量也叫shell变量,是局部的,只对特定的shell有效,用vim在用户目录下的.bash_profile文件中增加变量,变量仅会对当前用户有效,并且是"永久的"。

要让刚才的修改马上生效,需要执行以下代码:

cs 复制代码
source ~/.bash_profile

两个的区别:.bash_profile只在会话开始时被读取一次,而.bashrc则每次打开新的终端时,都会被读取。

当shell是交互式登录shell时,读取.bash_profile文件,如在系统启动、远程登录或使用su -切换用户时;

当shell是交互式登录和非登录shell时都会读取.bashrc文件,如:在图形界面中打开新终端或使用su切换用户时,均属于非登录shell的情况。

3.10.2 全局变量

/etc/profile 和/etc/profile.d,前面的是文件,后面一看也就明白.d表示目录, /etc/profile里面的变量是全局的,对所有用户的shell有效。

用vim在文件/etc/profile文件中增加变量,该变量将会对Linux下所有用户有效,并且是"永久的"。

要让刚才的修改马上生效,需要执行以下代码

cs 复制代码
source /etc/profile
相关推荐
今天我又学废了1 分钟前
Scala学习记录,List
学习
幸运超级加倍~5 分钟前
软件设计师-上午题-16 算法(4-5分)
笔记·算法
王俊山IT25 分钟前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
Mephisto.java1 小时前
【大数据学习 | kafka高级部分】kafka中的选举机制
大数据·学习·kafka
Yawesh_best1 小时前
思源笔记轻松连接本地Ollama大语言模型,开启AI写作新体验!
笔记·语言模型·ai写作
南宫生2 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法
武子康3 小时前
大数据-212 数据挖掘 机器学习理论 - 无监督学习算法 KMeans 基本原理 簇内误差平方和
大数据·人工智能·学习·算法·机器学习·数据挖掘
CXDNW3 小时前
【网络面试篇】HTTP(2)(笔记)——http、https、http1.1、http2.0
网络·笔记·http·面试·https·http2.0
使者大牙3 小时前
【大语言模型学习笔记】第一篇:LLM大规模语言模型介绍
笔记·学习·语言模型
ssf-yasuo3 小时前
SPIRE: Semantic Prompt-Driven Image Restoration 论文阅读笔记
论文阅读·笔记·prompt