Linux系统编程之系统调用IO

一、文件描述符

文件描述符,简称fd(file description),如同标准IO中的FILE,在系统IO中贯穿始终.

         在Linux中文件描述符是一个进程级的由内核维护的文件描述符表(fd Table)的索引,通过该表进一步索引到一个系统级的文件表(File Table),该文件表是所有进程共享的,处于内核空间,其存储了对应文件描述符的模式,访问权限,以及偏移量等等,并且再进一步索引到inode结点表(inode Table),该表中的每个inode结点都对应了一个打开的真正意义上的文件。

        当进程使用open函数打开一个文件时,操作系统首先会在inode Table中创建(如果没有)一个与该文件关联的条目,然后在File Table创建一个条目关联该inode结点,最后在进程的fd Table中创建一个文件描述符与File Table中的条目关联

        也就是说,每次使用open函数打开同一个文件时,操作系统内核中都会创建一个File Table的条目,但是这些不同的File Table条目最终都会指向同一个inode 结点,因为一个文件只能对应唯一的一个inode。

        多个文件描述符也有可能指向同一个File Table条目,例如从父进程处继承(也就是说父子进程共享文件偏移量),或用dup(),dup2()来复制文件描述符。

        值得注意的是,这些资源内部都会有一个计数器记录了有几个索引指向自己,只有所有的索引都关闭了才会释放资源。

        其中默认文件描述符0,1,2分别对应stdin,stdout,stderr.是从父进程继承来的。

二、open(),close()

1.open()

open()用于打开或创建一个文件,并返回一个文件描述符。

man手册中open有两种形式,但C语言里面是没有重载的,所以它其实是变参函数.

open 函数的第一个参数是要打开或创建的文件的路径名。第二个参数是打开文件的标志,可以使用以下标志之一或多个:

  • O_RDONLY:只读方式打开文件。
  • O_WRONLY:只写方式打开文件。
  • O_RDWR:读写方式打开文件。
  • O_CREAT:如果文件不存在,则创建文件。
  • O_TRUNC:如果文件存在且以写方式打开,则将文件截断为零长度。
  • O_EXCL:和O_CREAT一起使用,确保会创建文件,即如果文件存在则打开失败。
  • O_APPEND:以追加方式打开文件,即每次写入都追加到文件末尾。

open 函数的第三个参数是可选的,用于指定文件的权限(仅在创建文件时有效)。它是一个八进制数,表示文件的访问权限。常用的权限值有:

  • S_IRUSR:用户可读权限。
  • S_IWUSR:用户可写权限。
  • S_IXUSR:用户可执行权限。
  • S_IRGRP:组可读权限。
  • S_IWGRP:组可写权限。
  • S_IXGRP:组可执行权限。
  • S_IROTH:其他用户可读权限。
  • S_IWOTH:其他用户可写权限。
  • S_IXOTH:其他用户可执行权限。

open 函数返回一个非负整数的文件描述符,表示打开或创建的文件。如果出现错误,返回值为 -1,并设置 errno 变量来指示错误的原因。

注意:open()优先返回可用文件描述符中最小的。可利用此特点close掉1号文件描述符,即stdout,然后open一个文件来达到输出重定向的功能。

        这是fopen()中各模式对应的open()中的模式。

 2.close()

close()用于关闭一个文件描述符,成功返回0,失败返回-1.

 三、read(),write(),lseek()

1.read()

read()用于从fd中尝试读取count个字节到buf中。

   read 函数的第一个参数是要读取数据的文件描述符。第二个参数是一个指向缓冲区的指针,用于存储读取的数据。第三个参数是要读取的最大字节数。

   read 函数返回实际读取的字节数,如果出现错误,返回值为 -1,并设置 errno 变量来指示错误的原因。

        需要注意的是,read 函数是一个阻塞函数,如果没有数据可读,它会一直等待直到有数据可读或者出现错误。如果需要进行非阻塞的读取操作,可以使用 fcntl 函数设置文件描述符为非阻塞模式。

2.write()

write()用于向fd中写入buf位置开始的count个字节。

   write 函数的第一个参数是要写入数据的文件描述符。第二个参数是一个指向要写入数据的缓冲区的指针。第三个参数是要写入的字节数。

   write 函数返回实际写入的字节数,如果出现错误,返回值为 -1,并设置 errno 变量来指示错误的原因。

        和read一样,write 是一个阻塞函数,如果写入的数据量大于文件描述符的缓冲区大小,它会一直等待直到数据被完全写入或者出现错误。如果需要进行非阻塞的写入操作,可以使用 fcntl 函数设置文件描述符为非阻塞模式。

3.lseek()

lseek()用于设置文件描述符的偏移量,即改变文件的当前读写位置,并返回改变后的偏移量。(相当于fseek()和ftell()的结合)。

 lseek 函数的第一个参数是要设置偏移量的文件描述符。第二个参数是要设置的偏移量值,可以是正数、负数或零。第三个参数是指定偏移量的起始位置,可以使用以下值之一:

  • SEEK_SET:从文件开头开始计算偏移量。
  • SEEK_CUR:从当前位置开始计算偏移量。
  • SEEK_END:从文件末尾开始计算偏移量。

lseek 函数返回设置后的偏移量值,如果出现错误,返回值为 -1,并设置 errno 变量来指示错误的原因。

        如果将偏移量设置为超过文件末尾(EOF)的位置,lseek 函数不会引发错误。它只是将文件的当前读写位置设置为超过文件末尾的位置。

        如果在设置偏移量为超过 EOF 的位置后使用 read 函数读取文件,将会返回 0,表示已经到达文件末尾,没有更多的数据可读取。

        如果在设置偏移量为超过 EOF 的位置后使用 write 函数写入文件,将会在文件中创建一个空洞(hole),即在文件末尾和新写入数据之间的区域将被填充为零字节。这些零字节不会占用实际的磁盘空间,但在文件大小上会增加。

        需要注意的是,lseek 函数对于普通文件和某些设备文件(如磁盘文件)是有效的,但对于某些特殊设备文件(如终端)可能无效。

四、dup(),dup2()

dup()和dup2()都是用于文件描述符复制的函数。它们的作用是创建一个新的文件描述符,该文件描述符与已有的文件描述符指向同一个文件或资源。即两个文件描述符指向同一个File Table条目。

        这两个函数的主要区别在于对新文件描述符的处理方式。dup 函数会使用系统中最小的未使用的文件描述符作为新的文件描述符,而 dup2 函数可以指定新的文件描述符的值,如果新旧值相等,dup2会什么都不做。

        这些函数在文件重定向、管道和进程间通信等场景中经常使用,可以方便地复制文件描述符,实现文件描述符的重定向或共享。

以下是将标准输出重定向到一个文件的例子

但是这样写会存在一定问题,在多线程中,如果一个线程close了文件描述符1,另一个线程这时候调用了open,文件描述符1又会被占用,这时dup就会将fd复制到其他的文件描述符上去,归根结底,就是因为这两步操作不是原子操作导致的,可以改用dup2解决。

 这样就能保证fd复制到1号文件描述符上去。

五、标准IO和系统IO的对比

1.优缺点及适用场景

标准IO的优点:

  1. 高级别的接口:标准 I/O 提供了高级别的函数(如 printf 和 scanf),使得输入输出操作更加简单和方便。
  2. 缓冲机制:标准 I/O 使用缓冲区来提高性能。它可以在内存中缓冲一定量的数据,然后一次性进行读取或写入,减少了系统调用的次数,提高了效率。
  3. 可移植性:标准 I/O 是与操作系统无关的,可以在不同的平台上使用相同的代码进行输入输出操作。

标准IO的缺点:

  1. 性能相对较低:由于标准 I/O 使用了缓冲机制,所以在某些情况下,它的性能可能相对较低。特别是在需要实时读写的场景下,标准 I/O 可能不够高效。
  2. 需要手动刷新缓冲区:标准 I/O 使用缓冲区,需要手动刷新缓冲区才能确保数据被写入或读取。如果忘记刷新缓冲区,可能会导致数据丢失或不一致。

系统IO的优点:

  1. 直接操作系统调用:系统 I/O 允许直接调用操作系统提供的底层 I/O 函数,可以更加精确地控制输入输出操作。
  2. 适用于实时读写:由于系统 I/O 不使用缓冲机制,所以在需要实时读写的场景下,系统 I/O 可以提供更高的性能。

系统IO的缺点:

  1. 低级别的接口:系统 I/O 使用较低级别的函数(如 read 和 write),需要更多的代码来完成输入输出操作,相对不够方便。
  2. 可移植性差:系统 I/O 的底层函数通常是与操作系统相关的,因此在不同的平台上可能需要使用不同的代码进行输入输出操作。

总的来说,标准 I/O 吞吐量更大,适用于大多数常规的输入输出操作,它提供了简单、方便和可移植的接口。而系统 I/O 响应速度更快,适用于对输入输出性能要求较高、需要实时读写或需要更精确控制的场景,但它需要更多的代码来完成相同的操作,并且不够可移植。选择使用哪种方法取决于具体的需求和优化目标。

2.标准IO与系统IO的转换

Linux提供了系统IO与标准IO相互转换的函数

1)int fileno(FILE *stream)

该函数传入一个FILE指针,返回该FILE结构体所管理的文件描述符。

2)FILE *fdopen(int fd,const char *mode)

该函数将已打开的文件描述符交给FILE结构体管理。

        虽然可以互相转换,但是尽量不要混用标准IO和系统IO,因为标准IO有缓冲机制,而系统IO没有,如果混用将导致输出混乱。

 这段代码的输出为bbbaaa,其系统调用如下所示:

 aaa被缓存在了缓冲区里,b直接输出了,而aaa到程序结束前才输出。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值