文章目录
Linux文件编程
内核数据结构
在内核中有三种数据结构来表示一个已打开的文件
- 文件描述符表
- 文件描述符标志
- 文件表项指针
- 文件表项
- 文件状态标志位(
O_RDONLY、O_WRONLY、O_RDWR
等) - 当前文件偏移量(从文件开头距离当前文件的位置)
- i节点表项指针
- 引用计数器(若对一个文件修改的不止一个进程,那么就对引用的进程数进行加减操作)
- 文件状态标志位(
- i节点
- 文件类型和对文件操作指针(文件类型:七种文件类型中的一种)
- 当前文件长度(文件的大小)
- 文件的所有者(文件的拥有者、同组人、其他人等信息)
- 文件所在的设备、文件访问权限(当前文件存储的位置,以及文件以何种方式进行打开操作)
- 指向文件数据在磁盘块上所在位置的指针(指向磁盘块上的文件的存放位置)
重定向
在Linux中使用cat
指令去查看文件内容的时候,它会直接在屏幕上打印,如果想要将这些内容写入到一个文件中去,那么就需要用到重定向。
重定向分为输入重定向和输出重定向,默认输入cat
指令的话,它的输入输出都是标准输入STDIN_FILENO
和标准输出STDOUT_FILENO
,也就是从键盘中获取输入后从屏幕上输出出来。
如果想要改变它默认的输入和输出就涉及到了重定向的问题。在Linux中使用<
符号来表示输入重定向,使用>
表示输出重定向,使用>>
表示追加输出重定向。
输入重定向:使用<
符号改变输入,从指定的文件中获取输入输出到标准输出中
输出重定向:使用>
符号来改变输出的位置,将标准输入输出到指定的文件中去
追加输出重定向:默认输出重定向都是将之前的数据全部清空,然后再写入数据,如果要再之前的数据基础上再写入,可以使用>>
符号追加写入
同样也能将标准输入和标准输出同时改变,使用输入重定向从指定文件中获取输入,将内容输入到指定的文件中去。
dup2函数
dup2
函数用于复制文件描述符。这个复制不是字面意思的复制,而是将新的文件描述符的指针指向旧的文件描述符所指向的文件表项。这样的话就能通过新的文件描述符去访问之前旧的文件描述符所指向的文件。常常用在文件的重定向中,通过dup2
函数将标准输入、标准输出、标准错误这三个标准的文件流重定向到文件。然后就可以从文件中获得它的内容,将标准输出、标准错误重定向到文件中去。
#include <unistd.h>
int dup2(int fildes, int fildes2);
//参数1:旧的文件描述符
//参数2:新的文件描述符
//返回值:如果成功返回新的文件描述符,如果失败返回-1,并设置errno以指示错误原因
代码示例:将+
定义为输入重定向符号,将-
定义为输出重定向符号
#include "io.h"
int main(int argc, char **argv)
{
int fdin,fdout;
int i;
int flag = 0;
for(i=1;i<argc;i++)
{
//i=1,检测外部传参2的符号,如果输入的是+号,
//那么就将标准输入重定向到文件,去捕获文件的输入然后输出到标准输出去
if(!(strcmp("+",argv[i])))
{
//将+定义为输入重定向
fdin = open(argv[++i],O_RDONLY);
if(fdin < 0)
{
perror("open file error");
exit(EXIT_FAILURE);
}
//通过dup2函数复制文件描述符后,标准输入这个文件描述符指向了文件表项
if(dup2(fdin,STDIN_FILENO) != STDIN_FILENO)
{
perror("dup2 error");
exit(EXIT_FAILURE);
}
close(fdin); //将旧的文件描述符关闭
}
//如果检测到外部传参2是-号,那么就将标准输出重定向到文件
//将-重定向为输出重定向
else if(!(strcmp("-",argv[i])))
{
fdout = open(argv[++i],O_WRONLY | O_CREAT | O_TRUNC, 0777); //由于文件可能事先不存在,所以要创建文件
if(fdout < 0)
{
perror("open file error");
exit(EXIT_FAILURE);
}
//将标准输出重定向到文件,经过这个操作以后标准输出指向刚刚打开的文件
//dup2函数如果成功执行,那么返回新的文件描述符即参数2
if(dup2(fdout,STDOUT_FILENO) != STDOUT_FILENO)
{
perror("dup2 error");
exit(EXIT_FAILURE);
}
close(fdout);
}
//如果参数2不是+/-那么就说明是从某一个文件中读取然后输出到屏幕中去,所以要将标准输入重定向到文件
//以此来获取文件的内容,然后使用copy函数,将通过重定向后的标准输入和标准输出传到这个函数里去执行
else
{
flag = 1;
fdin = open(argv[i],O_RDONLY);
if(fdin < 0)
{
perror("open file error");
exit(EXIT_FAILURE);
}
if(dup2(fdin,STDIN_FILENO) != STDIN_FILENO)
{
perror("dup2 error");
exit(EXIT_FAILURE);
}
copy_function(STDIN_FILENO, STDOUT_FILENO);
close(fd);
}
}
if(!flag)
{
copy_function(STDIN_FILENO, STDOUT_FILENO);
}
return 0;
}
代码示例:将标准输入重定向到文件,将标准输出重定向到文件,最后的结果是从文件中获取数据然后输出到文件中去
#include "io.h"
//cat < test.txt > zz.txt
int main(int argc, char **argv)
{
int fdin,fdout;
int i;
//由于命令行参数argv[0]是命令,所以从argv[1]开始判断
for(i=1;i<argc;i++)
{
//判断argv[1]是否是"+",如果是就将标准输入重定向到文件
if(!(strcmp("+",argv[i])))
{
fdin = open(argv[++i], O_RDONLY);
if(fdin < 0)
{
perror("open file error");
exit(EXIT_FAILURE);
}
if(dup2(fdin,STDIN_FILENO) != STDIN_FILENO)
{
perror("dup22 error");
exit(EXIT_FAILURE);
}
close(fdin);
}
//判断argv[3]是否是"-",如果是就将标准输出重定向到文件
if(!(strcmp("-",argv[++i])))
{
fdout = open(argv[++i], O_WRONLY | O_CREAT | O_TRUNC, 0777);
if(fdout < 0)
{
perror("open file error");
exit(EXIT_FAILURE);
}
if(dup2(fdout,STDOUT_FILENO) != STDOUT_FILENO)
{
perror("dup22 error");
exit(EXIT_FAILURE);
}
close(fdout);
}
//重定向后标准输入和标准输出都指向了文件,调用copy函数,从标准文件中读取输出到标准文件中去
copy_function(STDIN_FILENO, STDOUT_FILENO);
}
return 0;
}
fcntl函数
fcntl
函数允许对已打开的文件描述符进行多种操作。例如复制文件描述符(作用同dup2
函数)、获取或设置文件描述符标志、获取或设置文件状态标志、获取或设置异步I/O所有权以及获取或设置状态锁。常用功能是后两种,前边的两种功能可以通过别的函数实现。
#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:设置fcntl函数功能的标志位
//参数arg:由用户设置文件描述符标志的参数
//参数lock:指向状态锁的一个指针
//返回值:若成功则依赖于cmd,若出错返回-1
cmd
常见的标志位
F_DUPED
:复制一个现存的描述符,新文件描述符作为函数值返回;
F_GETFD
:获取文件描述符标志;
F_SETFD
:设置文件描述符标志;
F_GETFL
:获取文件状态标志;
F_SETFL
:设置文件状态标志(可以更改的几个标志是:O_APPEND
、O_NONBLOCK
、SYNC
、O_ASYNC
)(其中O_RDONLY
、O_WRONLY
、O_RDWR
这几个标志位只能通过open
函数改变,不能通过fcntl
函数修改它的属性);
F_GETLK
:获取文件锁;
F_SETLK
:设置文件锁;
代码示例:使用O_APPEND标志位保证原子操作
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include "io.h"
int main(int argc, char **argv)
{
if(argc != 3)
{
fprintf(stderr,"usage: %s [string] [filename]\n",argv[0]);
exit(EXIT_FAILURE);
}
int fd;
char length = strlen(argv[1]);
fd = open(argv[2], O_WRONLY | O_CREAT | O_CREAT | O_APPEND , 0777);
if(fd < 0)
{
perror("open file error");
exit(EXIT_FAILURE);
}
sleep(10);
if(write(fd,argv[1],length) != length)
{
perror("write message error");
exit(EXIT_FAILURE);
}
close(fd);
return 0;
}
两个进程同时打开一个文件进行读写,由于指定了O_APPEND
参数,所以会以追加的方式进行写入,避免了数据的覆盖。如果想要创建文件的时候不指定O_APPEND
参数,那么数据就会被覆盖掉,所以这里还可以通过fcntl
函数将O_APPEND
这个参数加入到文件状态标志中去,使之能够正确运行。代码如下:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include "io.h"
int main(int argc, char **argv)
{
if(argc != 3)
{
fprintf(stderr,"usage: %s [string] [filename]\n",argv[0]);
exit(EXIT_FAILURE);
}
int fd;
char length = strlen(argv[1]);
fd = open(argv[2], O_WRONLY | O_CREAT | O_CREAT , 0777);
if(fd < 0)
{
perror("open file error");
exit(EXIT_FAILURE);
}
//设置追加标志位
set_fl(fd,O_APPEND);
//清除追加标志位
//clr_flag(fd,O_APPEND);
sleep(10);
if(write(fd,argv[1],length) != length)
{
perror("write message error");
exit(EXIT_FAILURE);
}
close(fd);
return 0;
}
I/O处理方式
I/O处理的五种模型
-
阻塞I/O模型
若所调用的I/O函数没有完成相关的功能就会使进程挂起,直到相关数据到达才会返回。例如:终端、网络设备的访问。(常见的函数例如:
scanf
、fgets
等从键盘获取输入的函数) -
非阻塞模型
当请求的I/O操作不能完成时,则不让进程休眠,而且返回一个错误。(常见的函数如
open
、read
、write
) -
I/O多路转接模型
如果请求的I/O操作阻塞,且它不是真正阻塞I/O,而且让其中一个函数等待,在这期间,I/O还能进行其他操作。如:
select
函数 -
信号驱动I/O模型
在这种模型下,通过安装一个信号处理程序,系统可以自动捕获特定信号的到来,从而启动I/O
-
异步I/O模型
在这种模型下,当一个描述符已经准备好,可以启动I/O时,进程会通知内核。由内核进行后续处理,这种用法现在较少。
现在着重介绍一下阻塞和非阻塞这两种模型
代码示例:阻塞I/O模型
#include <stdio.h>
#include <stdlib.h>
int main()
{
int data;
scanf("%d",&data);
printf("%d\n",data);
return 0;
}
就比如现在这个程序中的scanf
函数就一直在阻塞着程序进入到了挂起状态,直到程序等待条件满足也就是输入数据后,这个进程才会再次进入就绪状态并有机会被调度到CPU上运行。
代码示例:非阻塞I/O模型
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
char buffer[1024];
int nbytes;
memset(buffer,'\0',sizeof(buffer));
//获取文件状态标志
int val = fcntl(STDIN_FILENO,F_GETFL);
//将要设置的文件状态写入
val |= O_NONBLOCK;
//设置新的文件状态标志为非阻塞方式
if(fcntl(STDIN_FILENO,F_SETFL,val) < 0)
{
perror("fcntl");
}
sleep(3);
nbytes = read(STDIN_FILENO,buffer,sizeof(buffer));
if(nbytes < 0)
{
perror("read error");
exit(EXIT_FAILURE);
}
else if(nbytes == 0)
{
printf("read finished\n");
}
else
{
if(write(STDOUT_FILENO,buffer,nbytes) != nbytes)
{
perror("write error");
exit(EXIT_FAILURE);
}
}
return 0;
}
根据编译结果可知,如果将文件描述符设置为非阻塞模式,而且它又有从标准输入获得输入的函数,在sleep
期间,如果不做任何事,那么由于被设置为非阻塞模式,所以它就会立即报错返回。但是如果在sleep
睡眠期间写入,它首先被写入到缓存区中,在睡眠结束后,就会被输入到标准输入当中去。所以此时它有结果输出。而如果在睡眠期间按下Ctrl+D
就相当于它读到了外部输入的字符串的末尾,所以它会打印读取结束这句话。
文件类型
在Linux系统中可以使用ls -l
指令来获取一个文件的信息,比如说是一个什么类型的文件、它的拥有者、创建时间、文件权限等等。
那么如果在程序里想要获取这些信息应该怎么做?在Linux中有一个结构体存储了有关文件信息的结构体,首先跳转到头文件的存储路径cd /usr/include
,然后使用grep
指令查找这个这个结构体在哪里定义。使用grep "struct stat {" * -nir
查找,然后可以找到它对应的头文件,结构体内容如下:
struct stat {
unsigned long st_dev; /* Device. */
unsigned long st_ino; /* File serial number. */
unsigned int st_mode; /* File mode. */
unsigned int st_nlink; /* Link count. */
unsigned int st_uid; /* User ID of the file's owner. */
unsigned int st_gid; /* Group ID of the file's group. */
unsigned long st_rdev; /* Device number, if device. */
unsigned long __pad1;
long st_size; /* Size of file, in bytes. */
int st_blksize; /* Optimal block size for I/O. */
int __pad2;
long st_blocks; /* Number 512-byte blocks allocated. */
long st_atime; /* Time of last access. */
unsigned long st_atime_nsec;
long st_mtime; /* Time of last modification. */
unsigned long st_mtime_nsec;
long st_ctime; /* Time of last status change. */
unsigned long st_ctime_nsec;
unsigned int __unused4;
unsigned int __unused5;
};
文件属性操作函数
#include <sys/stat.h>
int stat(const char *restrict path, struct stat *restrict buf);
//参数1:要查看文件的相对路径或者绝对路径
//参数2:指向存放文件信息的一个结构体指针
//返回值:执行成功时返回0,失败时返回-1,错误码存储在errno中
#include <sys/stat.h>
int fstat(int fildes, struct stat *buf);
//参数1:一个已经打开的文件描述符
//参数2:指向存放文件信息的一个结构体指针
//返回值:执行成功时返回0,失败时返回-1,错误码存储在errno中
#include <sys/stat.h>
int lstat(const char *restrict path, struct stat *restrict buf);
//参数1:要查看的文件的路径
//参数2:指向存放文件信息的一个结构体指针
//返回值:执行成功时返回0,失败时返回-1,错误码存储在errno中
//这个函数和stat函数类似,但是当命名的文件是一个符号链接时,lstat返回该符号链接的有关信息,而不是由该符号链接引用的文件的信息
Linux中的七种文件和七种宏
- 普通文件(regular file)
S_ISREG()
- 目录文件(directory file)
S_ISDIR()
- 块特殊文件(block special file)
S_ISBLK()
- 字符特殊文件(character special file)
S_ISCHR()
- FIFO(named pipe)
S_ISFIFO()
- 套接字(socket)
S_ISSOCK()
- 符号链接(symbolic link)
S_ISLNK()
Linux底下一切皆文件
下边用实际操作来诠释这句话
目录文件:首先创建一个file
的目录,然后在它底下创建四个src
、include
、bin
、obj
子目录分别来存放源文件、头文件、编译生成的可执行文件、目标文件。然后使用ls -l
指令来查看文件的类型。
此时在最开头的d
代表此文件是一个目录文件;
普通文件:然后使用touch
指令创建一个文件,然后使用ls -l
来查看文件类型
这里的test.c
文件最开始的-
符号代表它是一个普通的文件;
管道文件:使用mkfifo
指令创建一个管道文件,然后使用ls -l
查看文件类型
可以看到管道的文件最开始是以p
开头的,代表它是一个管道文件
符号链接文件:符号链接本质是就类似于windows底下的快捷方式,通过符号链接到源文件,然后调用符号链接和调用源文件是一样的效果。在Linux中使用ln -s
创建符号软连接
设备文件:在Linux系统中,所有的设备文件都存放在/dev
目录下,通过ls -l
指令可以看到所有的设备文件。
字符特殊文件
这里的c
就代表它是一个字符特殊文件,字符特殊文件一般指的是通过单个字符进行传输的文件
块特殊文件
这里的b
就是块特殊文件,一般指的是像硬盘、磁盘类的文件
然后关于套接字文件的话,在/run/systemd/
目录,首先使用cd
指令跳转到此目录,然后使用ls -l
指令查看文件类型
这里以s
开头的文件就是有关于套接字的文件。
然后后边想要在文件中判断它们是哪些类型的文件,可以使用它们的宏定义进行判断。
代码示例:使用代码判断文件类型
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
int main(int argc, char **argv)
{
if(argc < 2)
{
fprintf(stderr,"usage: %s [filepath]\n",argv[0]);
exit(EXIT_FAILURE);
}
int i;
struct stat file_type;
for(i=1;i<argc;i++)
{
//根据外部传参来判断文件类型,lstat函数不仅能够判断文件,还能够判断符号链接文件
if(lstat(argv[i], &file_type) < 0)
{
perror("lstat error");
continue;
}
printf("%-20s",argv[i]); //先输出字符串,然后再输出20个空格
if(S_ISREG(file_type.st_mode))
{
printf("regularly file");
}
else if(S_ISDIR(file_type.st_mode))
{
printf("dictionary");
}
else if(S_ISLNK(file_type.st_mode))
{
printf("link file");
}
else if(S_ISBLK(file_type.st_mode))
{
printf("block device file");
}
else if(S_ISCHR(file_type.st_mode))
{
printf("character device file");
}
else if(S_ISSOCK(file_type.st_mode))
{
printf("socket device file");
}
else if(S_ISFIFO(file_type.st_mode))
{
printf("named pipe");
}
else
{
printf("unknown file type");
}
printf("\n");
}
return 0;
}
文件权限
在Linux系统中对一个文件使用ls -l
指令可以查看文件的所有信息包括文件的访问权限,例如:
上边已经讲了第一位代表文件是何种类型的文件,所以这里从第二位开始到第十位是文件权限,上边的2-4指的是创建这个文件的用户,也就是user
,5-7指的是和这个用户同组的用户,也就是group
,8-10指的是其他组的用户,也就是other
。
它的文件权限有三种:可读®、可写(w)、可执行(x),在写脚本的时候通常会使用指令sudo chmod +x
就是给这个脚本赋予一个可执行的权限。之前在使用open
函数创建一个新文件的时候指定0777
的权限,0
这一位实际上是一个粘着位,一般不设置,后边的三位777
就代表创建文件的权限为:创建它的用户、和它同组的用户、其它用户拥有可读可写可执行权限。在Linux中可以使用宏定义来指定文件的权限也可以使用三位8进制数来指定它的文件权限。
使用八进制指定文件权限
可读可写可执行:7 = r
(4) + w
(2) + x
(1)
使用宏定义来指定文件的访问权限
-
创建文件的用户的权限(可读可写可执行):
S_IRUSR
、S_IWUSR
、S_IXUSR
-
和它同组的用户的权限(可读可写可执行):
S_IRGRP
、S_IWGRP
、S_IXGRP
-
其他组的用户权限(可读可写可执行):
S_IROTH
、S_IWOTH
、S_IXOTH
通过这些宏定义使用按位或的操作可以指定创建文件的权限
代码示例:使用宏定义指定创建文件的权限
#include "header.h"
#include "io.h"
int main(int argc, char **argv)
{
if(argc < 2)
{
fprintf(stderr,"usage: %s [filename]\n",argv[0]);
exit(EXIT_FAILURE);
}
int fdin,fdout;
fdin = open(argv[1],O_RDONLY);
if(fdin < 0)
{
perror("open file error");
exit(EXIT_FAILURE);
}
fdout = open(argv[2],O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IROTH); //使用宏来指定文件访问权限
if(fdout < 0)
{
perror("open file error");
exit(EXIT_FAILURE);
}
copy(fdin, fdout);
close(fdin);
close(fdouut);
return 0;
}
由于在代码中指定了文件的访问权限,使用ls -l
指令可以看到它的访问权限是文件的拥有者拥有可读可写可执行的权限,同组人拥有读的权限,其他人拥有读的权限
access函数
在代码中可以使用access
函数来判断文件的访问权限
#include <unistd.h>
int access(const char *path, int amode);
//参数1:文件的路径和名字
//参数2:要判断的文件权限
//返回值:如果成功执行返回0,否则返回-1
amode
的一下宏定义:
R_OK
:判断文件是否有读的权限;
W_OK
:判断文件是否有写的权限;
X_OK
:判断文件是否有执行的权限;
F_OK
:判断文件是否存在
代码示例:使用access函数判断文件的权限
#include "header.h"
int main(int argc, char **argv)
{
if(argc < 2)
{
fprintf(stderr,"usage: %s [filename]\n",argv[0]);
exit(EXIT_FAILURE);
}
int i;
for(i=1;i<argc;i++)
{
if(!access(argv[i], F_OK))
{
printf("file: %s exits\n",argv[i]);
}
else
{
printf("there is no such file: %s\n",argv[i]);
continue;
}
if(!access(argv[i],R_OK))
{
printf("file: %s can read\n",argv[i]);
}
else
{
printf("file: %s can't read\n",argv[i]);
}
if(!access(argv[i],W_OK))
{
printf("file: %s can write\n",argv[i]);
}
else
{
printf("file: %s can't write\n",argv[i]);
}
if(!access(argv[i],X_OK))
{
printf("file: %s can excute\n",argv[i]);
}
else
{
printf("file: %s can't excute\n",argv[i]);
}
}
return 0;
}