Linux系统编程01--文件I/O

一、文件I/O

进程控制块PCB

内核管控进程通过pcb模块进行管控,PCB叫做进程控制块,操作系统把每一个进程当中一个PCB模块去管理,pcb是系统抽象出来的东西,方便管理进程。

进程与程序:

  • 程序,代码存放在磁盘中,没有运行,称为程序。
  • 进程,程序运行起来,则系统为程序开辟内存空间(32位为4G虚拟内存),运行在内存中的程序为进程。

操作系统为了方便管理进程,抽象出来PCB模块,每一个进程都有一个PCB模块(在内核中),PCB内含有该进程的信息数据结构。

PCB即一个结构体—>struct task_struct 结构体,其在:

/usr/src/linux-headers-5.4.0-99/include/linuxsched.h

这个结构体内的成员来描述进程信息和状态。

如pid,文件描述符结构体()等等

系统给每个进程在内存中开辟的空间都为4G(32位机器)其中0 ~ 3G为用户空间,3 ~ 4G为内核空间,并且所有进程共用一个内核空间,可以看成内核空间是本来就存在的,创建进程只给每个进程3G内存,另外1G是内核空间,所有进程共用,每个进程的pcb模块都在内核空间,内核缓冲区也是在这个内核空间内(所有不同进程可以读取其他进程写入内核缓冲区的数据)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UBHbeqJ2-1645689964370)(Linux系统编程笔记.assets/用户空间与内核空间.png)]

PCB成员:struct files_struct *files文件描述符结构体

struct files_struct *files是记录进程的文件描述符结构体,其内部是一个int 数组,从 0开始的整数。

每一个进程都有一个记录文件描述符的结构体,都是从0开始的整数,进程之间的文件描述符是不相关的,进程A的文件描述符56和进程B的文件描述符56不相关,是分开的。

每一个进程开始运行时默认打开三个文件描述符0, 1 , 2 ,分别对于标准输入、标准输出、标准错误输出。并且进程创建新的文件描述符是文件描述符表中未使用的最小的那个数,所以下一个文件描述符为3。

一个进程默认打开3个文件描述符

              文件描述符
STDIN_FILENO   --->0    即标准输入,对应c程序默认打开的stdin流
STDOUT_FILENO  --->1    即标准输出,对应stdout流
STDERR_FILENO  --->2    即标准错误,对应stderr流,只是流的本质是FILE,缓冲区在用户空间
区别:
STDIN_FILENO是文件描述符,int类型,在系统API层,无缓冲区,指定的是键盘等输入设备的文件
stdin是FILE*类型,有缓冲区在用户空间,

新打开文件返回文件描述符表中未使用的最小文件描述符。

open函数可以打开或创建一个文件。

C标准库函数fopen()打开一个文件,返回FILE*指针,指向结构体FILE,其主要内容有:

  • fd,文件描述符
  • f_pos,buffer缓冲区(用户空间)读写位置记录指针
  • buffer,指向进程用户内存中开辟的缓冲区,每一个FILE都会开辟一个缓冲区,这里缓冲区和内核缓冲区不一样,其他进程是无法读取这个buffer的,称为I/O缓冲区,其默认大小为8192B(两个块block)。

进程通过fd来唯一连接磁盘中的文件file,fd相当于一个进程穿过内核达到磁盘文件file的通道,并且是唯一通道,其他进程可以建立自己的通道也指向file,同一进程也可以再开辟一个FILE(fd2)指向该file。fd是对进程而言的,作用域为当前进程,A进程的fd到B进程是没有意义的,割裂开不相关。

注意:文件描述符其实是一个键值对,key为文件描述符数字,value为文件在磁盘中的地址位置。

open()函数与close()函数

open函数可以打开或创建一个文件。

#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

在Man Page中open函数有两种形式,一种带两个参数,一种带三个参数,其实在C代码中open函数是这样声明的:

int open(const char *pathname, int flags, ...);
当第二个参数为创建文件时,必须要加上第三个参数。

最后的可变参数可以是0个或1个,由flags参数中的标志位决定,见下面的详细说明。pathname参数是要打开或创建的文件名,和fopen一样,pathname既可以是相对路径也可以是绝对路径。flags参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数的宏定义都以O_开头,表示or。

必选项:以下三个常数中必须指定一个,且仅允许指定一个。

* O_RDONLY 只读打开

* O_WRONLY 只写打开

* O_RDWR 可读可写打开

以下可选项可以同时指定0个或多个,和必选项按位或起来作为flags参数。可选项有很多,这里只介绍一部分,其它选项可参考open(2)的Man Page:

* O_APPEND 表示追加。如果文件已有内容,这次打开文件所写的数据附加到文件的末尾而不覆盖原来的内容。

* O_CREAT 若此文件不存在则创建它。使用此选项时需要提供第三个参数mode,表示该文件的访问权限。

* O_EXCL 如果同时指定了O_CREAT,并且文件已存在,则出错返回。

* O_TRUNC 如果文件已存在,并且以只写或可读可写方式打开,则将其长度截断(Trun-cate)为0字节。

* O_NONBLOCK 对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O(Nonblock I/O),非阻塞I/O在下一节详细讲解。

注意open函数与C标准I/O库的fopen函数有些细微的区别:

以可写的方式fopen一个文件时,如果文件不存在会自动创建,而open一个文件时必须明确指定O_CREAT才会创建文件,否则文件不存在就出错返回。以w或w+方式fopen一个文件时,如果文件已存在就截断为0字节,而open一个文件时必须明确指定O_TRUNC才会截断文件,否则直接在原来的数据上改写。第三个参数mode指定文件权限,可以用八进制数表示,比如0644表示-rw-r-r–,也可以用S_IRUSR、S_IWUSR等宏定义按位或起来表示,详见open(2)的Man Page。要注意的是,文件权限由open的mode参数和当前进程的umask掩码共同决定。

补充说明一下Shell的umask命令。Shell进程的umask掩码可以用umask命令查看:

umask
0002

如创建文件时指定权限为777,umask为0022,那么最终的权限位为:0777 & ~0022 = 0755

可以在shell里面修改umask码,也可以在程序中用umask()系统函数修改umask

int umask(int mode)
如需要更改该进程的umask值为0000,则在程序中写入umask(0)即可,当然,此更改只对本
进程有效,不会更改系统的umask,
close()函数
#include <unistd.h>
int close(int fd);
用文件描述符来关闭文件,切断进程与文件的连通道
返回值:成功返回0,出错返回-1并设置errno

当进程结束时会关闭所有的文件描述符,但最好还是自己手动关闭文件比较严谨。

open()是给进程分配文件描述符,close()是回收文件描述符。

一个进程最大打开文件个数

查看当前系统允许一个进程打开的最大文件个数:

查看系统可以打开最大文件(跟内存大小相关)
cat /proc/sys/fs/file-max
或者用命令查看系统限制一个进程可以打开的最大个数
(与系统限制有关,默认1024,包括前面默认打开的0,1,2)
ulimit -a
此命令是想看系统的一些限制
修改最大个数为:
ulimit -n 4096
若要永久修改,则需要修改配置文件
read()/write()读写函数

read():

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
ssize_t 为typedef int ssize_t
返回值:成功返回读取的字节数,出错返回-1并设置errno,
如果在调read之前已到达文件末尾,则这次read返回0

参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是1。注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表示到达文件末尾)也可以返回负值-1(表示出错)。read函数返回时,返回值说明了buf中前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个字节而请求读100个字节,则read返回30,下次read将返回0。从终端设备读,通常以行为单位,读到换行符就返回了。从网络读,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数,后面socket编程部分会详细讲解。write函数向打开的设备或文件中写数据。

cp命令实例:

cp001.c:

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

int main(int argc, char* argv[])
{
    char buf_read[4096] = {0};
//  char buf_write[4096] = {0};
    int fd_sou, fd_des,len;
    fd_sou = open(argv[1], O_RDONLY);
    fd_des = open(argv[2], O_RDWR | O_CREAT,0777);
    int i =0;
    while((len = read(fd_sou, buf_read ,sizeof(buf_read))) > 0)
    {
        i++;
        write(fd_des , buf_read, len); //这里一定要对应;
    }
    printf("i = %d\n", i);
    close(fd_des);
    close(fd_sou);
    return 0;
}

需要注意的是:

while(read(fd_sou, buf_read ,4096) > 0)
    {
        i++;
        write(fd_des , buf_read, strlen(buf_read));
    }
这个写法是错误的,因为每一次读写,都必须对应读到的字符和要写入的字符
iaojian@KyLin:~/Linux/APUE/IO$ ll
总用量 49240

-rwxrwxr-x 1 jiaojian jiaojian 12593140 2月  15 20:20 cp2_w*
-rwxrwxr-x 1 jiaojian jiaojian 12574720 2月  15 20:24 cp2_w1*
-rwxrwxr-x 1 jiaojian jiaojian 12593140 2月  15 20:11 w4*
-rwxrwxr-x 1 jiaojian jiaojian 12571502 2月  15 20:15 w5*
注意文件大小不一样,复制源文件的大小为12571502。
w5是正确写法得到的文件,w4和cp2_w是错误写法得到的文件,
cp2_w1是在循环内结尾处加入char buf_read[4096] = {0};得到的文件。

write():

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
返回值:成功返回写入的字节数,出错返回-1并设置errno

count是要从buf内复制并写入fd文件的字节数

特别注意:read的返回值为成功读取到的字节数,可以和后面指定读取的字节数不同,write的返回值为成功写入文件的字节数。

阻塞与非阻塞(block/nonblock)

如运行./zs,进程等待终端输入,即加入阻塞,进程处于s+(sleep状态的一种)状态,read等待STDIN_FILENO输入内容

jiaojian   38878  0.0  0.0   2364   524 pts/1    S+   11:35   0:00 ./zs
jiaojian   38929  3.7  1.5 882768 60336 ?        Sl   11:36   0:00 /usr/bin/ukui-settings-
jiaojian   38944  0.1  0.0   9140  2412 ?        Ss   11:36   0:00 /usr/bin/imwheel -k
jiaojian   38957  0.0  0.0  14592  3320 pts/0    R+   11:36   0:00 ps aux

zs:

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

int main()
{
    char read_buf[10]= {0};
    int len = 0;
    len = read(STDIN_FILENO ,read_buf,sizeof(read_buf));
    write(STDOUT_FILENO ,read_buf,len);
}

需要注意的是:

  • 在read访问终端文件(标准输入)时,标准输入(STDIN_FILENO)是默认阻塞的,使用使用/dev/tty设备文件,在Linux下设备即文件,tty文件为输入输出设备文件,写入tty则打印在屏幕上,读取tty则是读取键盘输入内容(这里还有需探究写入tty与读取tty同时进行出现错误的问题,可能与文件读取位置指针有关),与STDIN_FILENO不一样,tty可以像普通文件一样设置是否阻塞。
  • 阻塞与非阻塞是文件本身的属性,并非是操作函数的选择项,故阻塞是在open()函数里面设置的。
  • read()函数在读到文件tty时,发现期内没有内容,并且tty非阻塞,所以返回-1,报错,并且设置errno = EAGAIN,其含义为再尝试一遍,读取文件但发现文件没有内容,可以理解为终端没有回应,则将这种情况当做出错处理,跳过该文件,这样就是一个非阻塞。一般读取本地文件不会出现非阻塞情况。
  • 在下面轮询实例中:
        if(errno == EAGAIN)
        {
            write(STDOUT_FILENO, try ,strlen(try));
            sleep(1);
            goto tryagain;
        }

这一个条件语句的作用是,当前面出现非阻塞情况,errno = EAGAIN,打印一句 “please try again!”,本来是使用printf()函数,但是出现了问题,打印不是在等待用户输入期间打印,而是在用户输入后才打印:

jiaojian@KyLin:~/Linux/APUE/IO/block$ ./lx 
jiao
jiao
please try again!please try again!

这是因为,printf函数是比write函数更高一层的c标准库函数,没有系统API快(printf在等待签面的操作完成,排队),所以直接用write写入,着不需要排队。直接调用系统资源了:

jiaojian@KyLin:~/Linux/APUE/IO/block$ ./lx 
please try again!
please try again!
please try again!
jplease try again!
iaojaiaplease try again!
n
jiaojaian

轮询实例:

#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
    char * try = "please try again!\n";
    int writee_return, read_return;
    int fd_tty;
    char read_buf[100] ={0};
    fd_tty = open("/dev/tty",O_RDONLY | O_NONBLOCK);
    if(fd_tty < 0)
        perror("open failed:");
tryagain:
    read_return = read(fd_tty ,read_buf ,sizeof(read_buf));
    if(read_return < 0)
    {
        if(errno == EAGAIN)
        {
            write(STDOUT_FILENO, try ,strlen(try));
            sleep(1);
            goto tryagain;
        }
        perror("read failed:");
        exit(1);
    }
    writee_return = write(STDOUT_FILENO , read_buf, read_return);
    if(writee_return < 0)
    {
        perror("write faile:");
        exit(1);
    }
    close(fd_tty);
}   

可以设置一个等待超时,设置一个int i = 5,循环轮询5次,每次几秒钟,超过5次则返回等待超时。

fcntl()与lseek()函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1yFBECrt-1645689964371)(Linux系统编程笔记.assets/文件描述符指向磁盘文件.png)]

如图,为进程指向磁盘文件的过程:

进程指向文件描述符:

  • 内核创建PCB模块管控A进程,pcb内有结构体成员files_struct*files,记录着进程A的文件描述符列表,进程A一打开,系统默认开启0,1,2,并且新的文件描述符从未使用的最小数字开始。

文件描述符指向file结构体:

  • 文件描述符作为进程通过内核连接磁盘硬件的通道,其在通向硬件中间,会先指向一个file结构体,该结构体记录着该文件描述符访问磁盘文件的相关权限、信息、文件读写指针位置等等属性信息,其中有Flag成员字段,记录着该描述符的文件访问控制属性(O_RDONLY、O_WRONLY、O_NONBLOCK等等),成员F_pos记录着当前文件读写指针位置,第一次打开f_pos指向起始位置值为0。

file结构体指向文件:

  • 每一个文件描述符都有对应的file结构体,记录着访问产品硬件的权限属性等等,再与磁盘文件连接

以上为进程A访问文件Abc的过程。

lseek()函数

为设置文件的读写指针位置值f_pos的函数,其中f_pos为偏移量的意思,即当前读写指针相当于文件开始字节的偏移量。

每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字节。但是有一个例外,如果以O_APPEND方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。lseek和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。

#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
fd文件描述符,offset调整偏移的距离,
whence调整偏移前设置起始位置 SEEK_SET(文件开头位置)、SEEK_CUR(当前位置)、SEEK_END(文件结尾)

参数offset和whence的含义和fseek函数完全相同。只不过第一个参数换成了文件描述符。和fseek一样,偏移量允许超过文件末尾,这种情况下对该文件的下一次写操作将延长文件,中间空洞的部分读出来都是0。

若lseek成功执行,则返回新的偏移量,因此可用以下方法确定一个打开文件的当前偏移量(即f_pos,文件开头到当前读写指针位置偏移量):

off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);

这种方法也可用来确定文件或设备是否可以设置偏移量,常规文件都可以设置偏移量,而设备一般是不可以设置偏移量的。如果设备不支持lseek,则lseek返回-1,并将errno设置为ESPIPE。注意fseek和lseek在返回值上有细微的差别,fseek成功时返回0失败时返回-1,要返回当前偏移量需调用ftell,而lseek成功时返回当前偏移量失败时返回-1。

fcntl()函数

获取或者设置文件的访问控制属性的函数

可以用fcntl函数改变一个已打开的文件的访问属性,可以重新设置读、写、追加、非阻塞等标志(这些标志称为File Status Flag,为32位int类型),而不必重新open文件。Flag即上图中结构体files中的Flag字段,记录文件描述符指向文件的访问控制属性(读、写、追加、非阻塞等)。

#include <unistd.h>
#include <fcntl.h>
可变参函数
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
fd文件描述符;cmd指定函数获取还是设置(获取F_GETFL、设置F_SETFL);可变参lock为控制属性名(如O_RDONLY),其取决于cmd

这个函数和open一样,也是用可变参数实现的,可变参数的类型和个数取决于前面的cmd参数。下面的例子使用F_GETFL和F_SETFL这两种fcntl命令改变STDIN_FILENO的属性,加上O_NONBLOCK选项,实现和例 28.3 “非阻塞读终端”同样的功能。

#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define MSG_TRY "try again\n"

int main(void)
{
char buf[10];
int n;
int flags;
flags = fcntl(STDIN_FILENO, F_GETFL);  //取文件控制属性
flags |= O_NONBLOCK;
if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1) {
perror("fcntl");
exit(1);
}
tryagain:
n = read(STDIN_FILENO, buf, 10);
if (n < 0) {
if (errno == EAGAIN) {
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read stdin");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
ioctl()函数
#include <sys/ioctl.h>

int ioctl(int fd, int request, ...);
fd文件描述符
request

d是某个设备的文件描述符。request是ioctl的命令,可变参数取决于request,通常是一个指向变量或结构体的指针。若出错则返回-1,若成功则返回其他值,返回值也是取决于request。

  • ioctl用于向设备发控制和配置命令,有些命令也需要读写一些数据,但这些数据是不能用read/write读写的,称为Out-of-band数据。也就是说,read/write读写的数据是in-band数据,是I/O操作的主体,而ioctl命令传送的是控制信息,其中的数据是辅助的数据。例如,在串口线上收发数据通过read/write操作,而串口的波特率、校验位、停止位通过ioctl设置,A/D转换的结果通过read读取,而A/D转换的精度和工作频率通过ioctl设置。

  • ioctl()与fcntl()的区别:

    在Linux中,万物皆文件,如磁盘文件、设备文件等等,所有文件都有属性,其中属性包含:访问控制属性、物理属性(如屏幕硬件属性分辨率、串口传输波特率等等)。所有文件都有文件访问控制属性即open()函数打开文件可读、可写等为共性,但是文件的物理属性却是各式各样的,不是共性,而是物理个性,所有ioctl()函数比较杂,没有固定的属性参数(request)。

如下图为应用层–>系统层–>驱动层一层一层调用过程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RDWUSu10-1645689964371)(Linux系统编程笔记.assets/ioctl.png)]

printf()函数调用write()函数写入数据,write()函数通过文件描述符再调用系统层的sys_write()函数,sys_write()函数根据文件描述符判断需要调用的驱动层函数,驱动函数再驱动具体的硬件。

对于ioctl()调用文件的物理属性,应用层的ioctl()在调用时是不知道文件有哪些的物理属性的,只有文件对于的驱动函数才知道硬件文件的物理属性有哪些,如串口的驱动函数uart_ioctl()。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UY1qcEZA-1645689964372)(Linux系统编程笔记.assets/文件属性的共性与个性.png)]

文件属性如上,共性(访问控制属性,都有),个性(物理属性,各式各样)。

以下程序使用TIOCGWINSZ命令获得终端设备的窗口大小:

原始终端的窗口大小,并不是伪终端的
#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);
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;
}

其中ioctl()函数的request参数为TIOCGWINSZ,每个设备文件的request都不同,其由驱动函数定义,ioctl可以获取也可以设置,上面的TIOCGWINSZ中的G表示get,获取窗口大小。

总结:

C标准库函数与Linux系统API函数区别:

  1. C标准函数在用户空间有i/o缓冲区,fread()中的文件当前位置指针是指i/o缓冲区的读写位置,而LinuxAPI函数没有缓冲区,有内核缓冲区,与C函数i/o缓冲区有本质区别,内核缓冲区是暂存区域,是等待守护进程缓输出,是系统运行的一种机制,并不是API函数定义的;而c函数的i/o缓冲区是函数定义在内存中的,c标准函数是直接操作缓冲区再将多次操作结果一次性反馈(缓冲区刷新)给API函数。所以API函数没有缓冲区这个概念,read(int fd, char* buf , int count)中的buf不是缓冲区的意思,而是一次性打包的意思更像是一次操作的容器。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V5AGNFAp-1645689964372)(Linux系统编程笔记.assets/C标准库与LinuxAPI.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qcfyFz2S-1645689964372)(Linux系统编程笔记.assets/C标准库与LinuxAPI2.jpg)]

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值