Linux下文件输入/输出端口的试炼分析

96次阅读
没有评论

共计 7936 个字符,预计需要花费 20 分钟才能阅读完成。

这篇文章将为大家详细讲解有关 Linux 下文件输入 / 输出端口的试炼分析,丸趣 TV 小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。

文件描述符(File Descriptor)

a small, nonnegative integer for use in subsequent system calls (read(2), write(2), lseek(2), fcntl(2), etc.) ($man 2 open). 一个程序开始运行时一般会有 3 个已经打开的文件描述符:

0 :STDIN_FIFLENO, 标准输入 stdin

1 :STDOUT_FILENO, 标准输出 stdout

2 :STDERR_FILENO, 标准错误 stderror

fd 原理

fd 从 0 开始, 查找最小的未被使用的描述符, 把文件表指针与文件表描述符建立对应关系(VS pid 是一直向上涨,满了再回来找)

文件描述符就是一个 int, 用于代表一个打开的文件, 但是文件的管理信息不能够不是存放在文件描述符中, 当使用 open()函数打开一个文件时, OS 会将文件的相关信息加载到文件表等数据结构中, 但出于安全和效率等因素的考虑, 文件表等数据结构并不适合直接操作, 而是给该结构指定一个编号, 使用编号来进行操作, 该编号就是文件描述符

OS 会为每个进程内部维护一张文件描述符总表, 当有新的文件描述符需求时, 会去总表中查找最小的未被使用的描述符返回, 文件描述符虽然是 int 类型, 但其实是非负整数, 也就是 0~OPEN_MAX(当前系统中为 1024), 其中 0,1,2 已被系统占用, 分别表示 stdin, stdout,stderror

使用 close()关闭 fd 时, 就是将 fd 和文件表结构之间的对应关系从总表中移除, 但不一定会删除文件表结构, 只有当文件表没有与其他任何 fd 对应时 (也就是一个文件表可以同时对应多个 fd) 才会删除文件表, close()也不会改变文件描述符本身的整数值, 只会让该文件描述符无法代表一个文件而已

duplicate fdVS copy fd:dup 是把 old_fd 对应的文件表指针复制给 new_fd, 而不是 int new_fd=old_fd

UNIX 使用三种数据结构描述打开的文件:每个进程中用于描述当前进程打开文件的文件描述符表,表示当前文件状态的文件状态标识表,和用于找到文件 i 节点 (索引节点) 的 V 节点表,Linux 中并不使用这种 Vnode 结构,取而代之的是一种通用的 inode 结构,但本质没有区别,inode 是在读取文件时通过文件系统从磁盘中导入的文件位置

 

文件描述符标志(File Descriptor Flag)

当下的系统只有一个文件描述符标志 close-on-exec,仅仅是一个标志,当进程 fork 一个子进程的时候,在子进程中调用了 exec 函数时就用到了该标志。意义是执行 exec 前是否要关闭这个文件描述符。

一般我们会调用 exec 执行另一个程序,此时会用全新的程序替换子进程的正文,数据,堆和栈等。此时保存文件描述符的变量当然也不存在了,我们就无法关闭无用的文件描述符了。所以通常我们会 fork 子进程后在子进程中直接执行 close 关掉无用的文件描述符,然后再执行 exec。但是在复杂系统中,有时我们 fork 子进程时已经不知道打开了多少个文件描述符(包括 socket 句柄等),这此时进行逐一清理确实有很大难度。我们期望的是能在 fork 子进程前打开某个文件句柄时就指定好: 这个句柄我在 fork 子进程后执行 exec 时就关闭”。所以就有了 close-on-exec

每个文件描述符都有一个 close-on-exec 标志。在系统默认情况下,这个标志 *** 一位被设置为 0。即关闭了此标志。那么当子进程调用 exec 函数,子进程将不会关闭该文件描述符。此时,父子进程将共享该文件,它们具有同一个文件表项,也就有了同一个文件偏移量等。

fcntl()的 FD_CLOEXEC 和 open()的 O_CLOEXEC 用来设置文件的 close-on-exec,当将 close-on-exec 标志置为 1 时,即开启此标志, 此时子进程调用 exec 函数之前,系统就已经让子进程将此文件描述符关闭。

Note: 虽然新版本支持在 open 时设置 CLOEXEC,但是在编译的时候还是会提示错误 – error: lsquo;O_CLOEXEC rsquo; undeclared (first use in this function)。这个功能需要设置宏 (_GNU_SOURCE) 打开。

#define _GNU_SOURCE // 在源代码中加入
-D_GNU_SOURCE // 在编译参数中加入

文件状态标志(File Status Flag)

File status flags 用来表示打开文件的属性,file status flag 可以通过 duplicate 一个文件描述符来共享同一个打开的文件的状态,而 file descrptor flag 则不行

Access Modes: 指明文件的 access 方式:read-only,write-only,read-write。通过 open()设置,通过 fcntl()返回,但不能被改变

Open-time Flags: 指明在 open()执行的时候的操作,open()执行完毕这个 flag 不会被保存

Operating Modes: 影响 read,write 操作,通过 open()设置,但可以用 fcntl()读取或改变

open()

// 给定一个文件路径名,按照相应的选项打开文件,就是将一个 fd 和文件连接到一起,成功返回文件描述符,失败返回 - 1 设 errno #include fcntl.h  int open(const char *pathname, int flags) int open(const char *pathname, int flags, mode_t mode) // 不是函数重载,C 中没有重载,  是可变长参数列表  //pathname: 文件或设备路径  //flags :file status flags=Access mode+Open-time flags+Operating Modes、 /*Access Mode(必选一个): O_RDONLY:0 O_WRONLY:1 O_RDWR:2 */ /*Open-time Flags(Bitwise Or): O_CLOEXEC :为新打开的文件描述符使能 close-on-exec。可以避免程序再用 fcntl()的 F_SETFD 来设置 FD_CLOEXEC O_CREAT :如果文件不存在就创建文件,并返回它的文件描述符,如果文件存在就忽略这个选项,必须在保护模式下使用,eg:0664 O_DIRECTORY :如果 opendir()在一个 FIFO 或 tape 中调用的话,这个选项可以避免 denial-of-service 问题,  如果路径指向的不是一个目录,就会打开失败。 O_EXCL :确保 open()能够穿件一个文件,如果文件已经存在,则会导致打开失败,总是和 O_CREAT 一同使用。 O_NOCTTY :如果路径指向一个终端设备,那么这个设备不会成为这个进程的控制终端,即使这个进程没有一个控制终端  O_NOFOLLOW :如果路径是一个符号链接,就打开它链接的文件 //If pathname is a symbolic link, then the open fails. O_TMPFILE :创建一个无名的临时文件,文件系统中会创建一个无名的 inode,当 *** 一个文件描述符被关闭的时候,所有写入这个文件的内容都会丢失,除非在此之前给了它一个名字  O_TRUNC :清空文件  O_TTY_INIT *//*Operating Modes(Bitwise Or) O_APPEND :以追加的方式打开文件,  默认写入结尾,在当下的 Unix/Linux 系统中,这个选项已经被定义为一个原子操作  O_ASYNC :使能 signal-driven I/O O_DIRECT :试图最小化来自 I / O 和这个文件的 cache effect//Try to minimize cache effects of the I/O to and from this file. O_DSYNC :每次写操作都会等待 I / O 操作的完成,但如果文件属性的更新不影响读取刚刚写入的数据的话,就不会等待文件属性的更新  。 O_LARGEFILE :允许打开一个大小超过 off_t(但没超过 off64_t)表示范围的文件  O_NOATIME :不更改文件的 st_time(last access time) O_NONBLOCK /O_NDELAY :如果可能的话,用 nonblock 模式打开文件  O_SYNC :每次写操作都会等待 I / O 操作的完成,包括 write()引起的文件属性的更新。 O_PATH :获得一个能表示文件在文件系统中位置的文件描述符
#include fcntl.h  #include stdlib.h  int fd=open(b.txt ,O_RDWR|O_CREAT|O_EXCL,0664); if(-1==fd) perror(open),exit(-1);

FA: 猜想有以下模型: 用一串某一位是 1 其余全是 0 的字符串表示一个选项, 选项们作“按位与”就可得到 0 / 1 字符串, 表示整个 flags 的状态, Note: 低三位表示 Access Mode

creat()

等价于以 O_WRONLY |O_TRUNC|O_CREAT 的 flag 调用 open()

#include fcntl.h  int creat(const char *pathname, mode_t mode);

dup()、dup2()、dup3()

/ 复制一个文件描述符的指向,新的文件描述符的 flags 和原来的一样,成功返回 new_file_descriptor,  失败返回 - 1 并设 errno #include  unistd.h  int dup(int oldfd); // 使用未被占用的最小的文件描述符编号作为新的文件描述符  int dup2(int oldfd, int newfd);
#include  fcntl.h  #include  unistd.h  int dup3(int oldfd, int newfd, int flags);
#include unistd.h  #include stdlib.h  int res=dup2(fd,fd2); if(-1==res){ perror( dup2),exit(-1); }

read()

// 从 fd 对应的文件中读 count 个 byte 的数据到以 buf 开头的缓冲区中,成功返回成功读取到的 byte 的数目,失败返回 - 1 设 errno #include  unistd.h  ssize_t read(int fd, void *buf, size_t count);
#include  unistd.h  #include stdlib.h  int res=read(fd,buf,6); if(-1==fd) perror(read),exit(-1);

write()

// 从 buf 指向的缓冲区中读取 count 个 byte 的数据写入到 fd 对应的文件中,成功返回成功写入的 byte 数目,文件的位置指针会向前移动这个数目,失败返回 - 1 设 errno #include  unistd.h  ssize_t write(int fd, const void *buf, size_t count);// 不需要对 buf 操作,  所以有 const, VS read()没有 const
#include  unistd.h  #include stdlib.h  int res=write(fd, hello ,sizeof( hello)); if(-1==res) perror(write),exit(-1);

Note: 上例中即使只有一个字符 rsquo;A rsquo;, 也要写”A”, 因为”A”才是地址, rsquo;A rsquo; 只是个 int

lseek()

l 表示 long int, 历史原因

// 根据移动基准 whence 和移动距离 offset 对文件的位置指针进行重新定位,返回移动后的位置指针与文件开头的距离,失败返回 - 1 设 errno #include  unistd.h  #include  sys/types.h  off_t lseek(int fd, off_t offset, int whence); /*whence: SEEK_SET:以文件开头为基准进行偏移,0 一般不能向前偏  SEEK_CUR:以当前位置指针的位置为基准进行偏移,1 向前向后均可  SEEK_END:以文件的结尾为基准进行偏移,2 向前向后均可向后形成”文件空洞”
#include unistd.h  #include stdlib  int len=lseek(fd,-3,SEEK_SET); if(-1==len){ perror( lseek),exit(-1); }

fcntl()

// 对 fd 进行各种操作,成功返回 0, 失败返回 - 1 设 errno #include  unistd.h  #include  fcntl.h  int fcntl(int fd, int cmd, ... ); //... 表示可变长参数  /*cmd: Adversory record locking: F_SETLK(struct flock*) // 设建议锁  F_SETLKW(struct flock*) // 设建议锁,如果文件上有冲突的锁,且在等待的时候捕获了一个信号,则调用被打断并在信号捕获之后立即返回一个错误,如果等待期间没有信号,则一直等待  F_GETLK(struct flock*) // 尝试放锁,如果能放锁,则不会放锁,而是返回一个含有 F_UNLCK 而其他不变的 l_type 类型,如果不能放锁,那么 fcntl()会将新类型的锁加在文件上,并把当前 PID 留在锁上  Duplicating a file descriptor: F_DUPFD (int) // 找到 =arg 的最小的可以使用的文件描述符,并把这个文件描述符用作 fd 的一个副本  F_DUPFD_CLOEXEC(int)// 和 F_DUPFD 一样,除了会在新的文件描述符上设置 close-on-execF_GETFD (void) // 读取 fd 的 flag,忽略 arg 的值  F_SETFD (int) // 将 fd 的 flags 设置成 arg 的值. F_GETFL (void) // 读取 fd 的 Access Mode 和其他的 file status flags;  忽略 arg F_SETFL (long) // 设置 file status flags 为 arg F_GETOWN(void) // 返回 fd 上接受 SIGIO 和 SIGURG 的 PID 或进程组 ID F_SETOWN(int) // 设置 fd 上接受 SIGIO 和 SIGURG 的 PID 或进程组 ID 为 arg F_GETOWN_EX(struct f_owner_ex*) // 返回当前文件被之前的 F_SETOWN_EX 操作定义的文件描述符 R  F_SETOWN_EX(struct f_owner_ex*) // 和 F_SETOWN 类似,允许调用程序将 fd 的 I / O 信号处理权限直接交给一个线程,进程或进程组  F_GETSIG(void) // 当文件的输入输出可用时返回一个信号  F_SETSIG(int) // 当文件的输入输出可用时发送 arg 指定的信号  */ /* hellip;:  可选参素,是否需要得看 cmd,如果是加锁,这里应是 struct flock* struct flock { short l_type; //%d Type of lock: F_RDLCK(读锁), F_WRLCK(写锁), F_UNLCK(解锁) short l_whence; //%d How to interpret l_start,  加锁的位置参考标准:SEEK_SET, SEEK_CUR, SEEK_END off_t l_start; //%ld Starting offset for lock,  加锁的起始位置  off_t l_len; //%ld Number of bytes to lock ,  锁定的字节数  pid_t l_pid; // PID of process blocking our lock, (F_GETLK only)加锁的进程号,, 默认给 -1}; */

建议锁(Adversory Lock)

限制加锁, 但不限制读写, 所以只对加锁成功才读写的程序有效, 用来解决不同的进程 同时对同一个文件的同一个位置“写”导致的冲突问题

读锁是一把共享锁(S 锁): 共享锁 + 共享锁 + 共享锁 + 共享锁 + 共享锁 + 共享锁

写锁是一把排他锁(X 锁): 永远孤苦伶仃

释放锁的方法(逐级提高):

将锁的类型改为:F_UNLCK, 再使用 fcntl()函数重新设置

close()关闭 fd 时, 调用进程在该 fd 上加的所有锁都会自动释放

进程结束时会自动释放所有该进程加过的文件锁

Q: 为什么加了写锁还能 gedit 或 vim 写???

A: 可以写, 锁只可以控制能否加锁成功, 不能控制对文件的读写, 所以叫”建议”锁, 我加了锁就是不想让你写, 你非要写我也没办法. vim/gedit 不通过能否加锁成功来决定是否读写, 所以可以直接上

Q: So 如何实现文件锁控制文件的读写操作????

A: 可以在读操作前尝试加读锁, 写操作前尝试加写锁, 根据能否加锁成功决定能否进行读写操作

int fd=open(./a.txt ,O_RDWR); // 得到 fd if(-1==fd) perror(open),exit(-1);struct flock lock={F_RDLCK,SEEK_SET,2,5,-1}; // 设置锁  // 此处从第 3 个 byte 开始 (包含第三) 锁 5byte int res=fcntl(fd,F_SETLK, lock); // 给 fd 加锁  if(-1==res) perror(fcntl),exit(-1);

ioct1()

这个函数可以实现其他文件操作函数所没有的功能,大多数情况下都用在设备驱动程序里,每个设备驱动程序可以定义自己专用的一组 ioctl 命令,系统则为不同种类的设备提供通用的 ioctl 命令

// 操作特殊文件的设备参数,成功返回 0, 失败返回 - 1 设 errno #include  sys/ioctl.h  int ioctl(int d, int request, ...); //d:an open file descriptor.//request: a device-dependent request code

close()

// 关闭 fd,这样这个 fd 就可以重新用于连接其他文件,成功返回 0, 失败返回 - 1 设 errno #include  unistd.h  int close(int fd);
#include  unistd.h  #include stdlib.h  int res=close(fd); if(-1==res) perror(close),exit(-1);

关于“Linux 下文件输入 / 输出端口的试炼分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-25发表,共计7936字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)