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
相关推荐
知识分享小能手7 小时前
React学习教程,从入门到精通, React 属性(Props)语法知识点与案例详解(14)
前端·javascript·vue.js·学习·react.js·vue·react
汇能感知9 小时前
摄像头模块在运动相机中的特殊应用
经验分享·笔记·科技
阿巴Jun9 小时前
【数学】线性代数知识点总结
笔记·线性代数·矩阵
茯苓gao9 小时前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾10 小时前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
DKPT10 小时前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
aaaweiaaaaaa10 小时前
HTML和CSS学习
前端·css·学习·html
ST.J11 小时前
前端笔记2025
前端·javascript·css·vue.js·笔记
Suckerbin11 小时前
LAMPSecurity: CTF5靶场渗透
笔记·安全·web安全·网络安全
看海天一色听风起雨落11 小时前
Python学习之装饰器
开发语言·python·学习