Linux中的设备管理
Linux采用文件系统管理硬件设备,对于计算机/操作系统来说,屏幕网卡键盘鼠标都是硬件设备,也就是说Linux下有一个文件就可以代表一个硬件,所有的设备都看成是特殊的文件,从而将硬件设备的特性及管理细节对用户隐藏起来,实现设备无关性。
如/dev可以管理硬件设备,/mnt可以U盘挂载…
Linux系统要想管理硬件,就是通过文件来管理的,因此,文件怎么操作,非常重要!因为Linux下的文件的操作,就相当于是对设备的操作了。
pcb进程控制块本质是一个结构体:struct task_struct 结构体,结构体的其中一个成员是一个指向文件描述符表(如上图右边)的一个指针(file_struct *file)文件描述符表里面存的是文件描述符。
文件描述符是一个整数,本质上是一个指针(目前的理解)该指针指向一个文件结构体(struct file )将文件描述符表当成是一个数组,可以将文件描述符当成是数组的下标,数组里存的是文件结构体的指针。可以直接用文件描述符访问文件。
一个进程最多可以打开1024个文件,文件描述符表大小是1024
文件管理结构体file结构体
file结构体在,源码在linux/inlcude/linux/fs.h文件中
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
每个数据中文解释
struct file {
union {
struct list_head fu_list; //文件对象链表指针linux/include/linux/list.h
struct rcu_head fu_rcuhead; //RCU(Read-Copy Update)是Linux 2.6内核中新的锁机制
} f_u;
struct path f_path; //包含dentry和mnt两个成员,用于确定文件路径
#define f_dentry f_path.dentry //f_path的成员之一,当前文件的dentry结构
#define f_vfsmnt f_path.mnt //表示当前文件所在文件系统的挂载根目录
const struct file_operations *f_op; //与该文件相关联的操作函数
atomic_t f_count; //文件的引用计数(有多少进程打开该文件)
unsigned int f_flags; //对应于open时指定的flag
mode_t f_mode; //读写模式:open的mod_t mode参数
loff_t f_pos;//当前文件指针位置
off_t f_pos; //该文件在当前进程中的文件偏移量
struct fown_struct f_owner; //该结构的作用是通过信号进行I/O时间通知的数据。
unsigned int f_uid, f_gid;// 文件所有者id,所有者组id
struct file_ra_state f_ra; //在linux/include/linux/fs.h中定义,文件预读相关
unsigned long f_version;//记录文件的版本号,每次使用之后递增
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;//使用这个成员来指向分配的数据
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
spinlock_t f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
};
每个进程在PCB(Process Control Block)即进程控制块中都保存着一份文件描述符表,文件描述符就是这个表的索引,文件描述表中每个表项都有一个指向已打开文件的指针,现在我们明确一下:已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。
struct file结构体定义在include/linux/fs.h中定义。文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的 struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核创建和驱动源码中,struct file的指针通常被命名为file或filp。
其有两个非常重要的字段:文件描述符和缓冲区。
文件描述符fd:
fd只是一个小整数,在open时产生。起到一个索引的作用,进程通过PCB中的文件描述符表找到该fd所指向的文件指针filp。
文件描述符的操作(如: open)返回的是一个文件描述符,内核会在每个进程空间中维护一个文件描述符表, 所有打开的文件都将通过此表中的文件描述符来引用;
而流(如: fopen)返回的是一个FILE结构指针, FILE结构是包含有文件描述符的,FILE结构函数可以看作是对fd直接操作的系统调用的封装, 它的优点是带有I/O缓存。
缓冲区:
a、缓冲区机制
根据应用程序对文件的访问方式,即是否存在缓冲区,对文件的访问可以分为带缓冲区的操作和非缓冲区的文件操作:
1、带缓冲区文件操作:高级标准文件I/O操作,将会在用户空间中自动为正在使用的文件开辟内存缓冲区。
2、非缓冲区文件操作:低级文件I/O操作,读写文件时,不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),当然用于可以在自己的程序中为每个文件设定缓冲区。
两种文件操作的解释和比较:
1、非缓冲的文件操作访问方式,每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。
2、ANSI标准C库函数 是建立在底层的系统调用之上,即C函数库文件访问函数的实现中使用了低级文件I/O系统调用,ANSI标准C库中的文件处理函数为了减少使用系统调用的次数,提高效率,采用缓冲机制,这样,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,即需要少量的CPU状态切换,提高了效率。
b、缓冲类型
标准I/O提供了3种类型的缓冲区。
1、全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。第一次执行I/O操作时,ANSI标准的文件管理函数通过调用malloc函数获得需要使用的缓冲区,默认大小为8192。
2、行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区的大小为1024。
3、无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。
注:
1、标准输入和标准输出设备:当且仅当不涉及交互作用设备时,标准输入流和标准输出流才是全缓冲的。
2、标准错误输出设备:标准出错绝不会是全缓冲方式的。
3、对于任何一个给定的流,可以调用setbuf()和setvbuf()函数更改其缓冲区类型。
下面我们通过如下程序来进一步了解缓冲区:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
char* msg="hello bit\n";
char* msg1="hello world\n";
int main()
{
printf("printf\n");
fwrite(msg,1,strlen(msg),stdout);
write(1,msg1,strlen(msg1));
pid_t id=fork();
if(id<0){
printf("fork,%s\n",strerror(errno));
exit(0);
}else if(id==0) {
printf("child:pid=%d,ppid=%d\n",getpid(),getppid());
}else{
printf("father:pid=%d,ppid=%d\n",getpid(),getppid());
sleep(3);
}
return 0;
}
执行 ./a.out 输出打印如下
printf
hello bit
hello world
father:pid=19502,ppid=13555
child:pid=19503,ppid=19502
再来测试执行 ./a.out > testfile 将结果写到文件里
testfile内容如下所示:
hello world
printf
hello bit
child:pid=22995,ppid=22994
printf
hello bit
father:pid=22994,ppid=13555
那么为什么输出到屏幕只有5条输出命令而输出到文件有7条输出命令呢?根据输出结果我们可以看出printf和fwrite重复写了两次,没有重复打印的是write.。
printf和fwrite都是库函数,而write则是系统直接调用的:结合已有知识,我们了解到当使用库函数命令时,打印消息并没有直接写到输出位置上,而是先把数据写到输出缓冲区,在刷新至输出位置。
1、当输出目标位置为输出到显示器时,则刷新方式是行刷新;
2、当输出目标位置为输出到文件中时,刷新方式由行缓冲变为全缓冲,全缓冲是指当把缓冲区写满后才能刷新。(或者强制刷新)代码中printf和fwrite第一次打印在fork操作之前,第二次打印则在fork操作之后,原因是在fork操作前,printf和fwrite的输出命令将数据先写到缓冲区中,此时只执行了这两条命令,由于是全缓冲的刷新方式,所以这两条命令并不足以将缓存写满,所以数据暂存在缓冲区中;然后进行fork创建子进程,由于fork创建出的父子进程代码共享,而数据不共享,各自私有一份,缓冲区中的数据都属于数据,因为父进程的残留数据还在缓冲区中,所以fork完毕后,父子进程将缓存中的数据各自存一份有父进程残留数据的数据,所以当父子进程各自刷新时,父子进程会各自执行一次printf和fwrite的输出命令,所以命令就从原来的两条变为四条。
struct file 的其他重要成员有:.
1.mode_t f_mode;
文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可, 但是不需要检查读写许可, 因为内核在调用你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个情况.
2.loff_t f_pos;
当前读写位置. loff_t 在所有平台都是 64 位( 在 gcc 术语里是 long long ). 驱动可以读这个值,如果它需要知道文件中的当前位置, 但是正常地不应该改变它; 读和写应当使用它们作为最后参数而收到的指针来更新一个位置, 代替直接作用于 filp->f_pos. 这个规则的一个例外是在 llseek 方法中, 它的目的就是改变文件位置.
3.unsigned int f_flags;
这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查O_NONBLOCK 标志来看是否是请求非阻塞操作; 其他标志很少使用. 特别地, 应当检查读/写许可, 使用 f_mode 而不是f_flags. 所有的标志在头文件<linux/fcntl.h> 中定义.
*4.struct file_operations f_op;
和文件关联的操作. 内核安排指针作为它的open 实现的一部分, 接着读取它当它需要分派任何的操作时. filp->f_op 中的值从不由内核保存为后面的引用; 这意味着你可改变你的文件关联的文件操作, 在你返回调用者之后新方法会起作用. 例如, 关联到主编号 1 (/dev/null, /dev/zero, 等等)的 open 代码根据打开的次编号来替代 filp->f_op 中的操作. 这个做法允许实现几种行为, 在同一个主编号下而不必在每个系统调用中引入开销. 替换文件操作的能力是面向对象编程的"方法重载"的内核对等体.
*5.void private_data;
open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须记住在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data 是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.
*6.struct dentry f_dentry;
关联到文件的目录入口( dentry )结构. 设备驱动编写者正常地不需要关心 dentry 结构, 除了作为 filp->f_dentry->d_inode 存取 inode 结构.
三:系统调用和函数库调用的区别
系统调用比函数库调用效率更高 ,因为少了函数库这一过程,库函数需要调用系统调用函数。
缺点:系统调用(无法跨平台、Linux开发就只能Linux下使用,不能window下使用)
ile_operations结构体成员函数,这个结构体可以对文件进行打开,读写,定位,控制等操作;如下图所示:
一、c库函数
四、文件系统调用api
open系统调用
read系统调用
write系统调用
create系统调用
close系统调用
mkdir系统调用
lseek
open系统调用有两种,如下
有几种方法可以获得允许访问文件的文件描述符
最常用的是使用open()(打开)系统调用
函数原型
int open(const char *path, int flags);
参数
path :文件的名称,可以包含(绝对和相对)路径
flags:文件打开模式
返回值
打开成功,返回文件描述符;
打开失败,返回-1
函数原型
int open(const char *path, int flags,mode_t mode);
参数
path :文件的名称,可以包含(绝对和相对)路径
flags:文件打开模式
mode: 用来规定对该文件的所有者,文件的用户组及系统中其他用户的访问权限,则文件权限为:mode&(~umask)
返回值
打开成功,返回文件描述符;
打开失败,返回-1
打开文件的方式
访问权限
flags:文件打开方式
O_RDONLY|O_WRONLY|O_RDWR 只读、只写、读写
O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK …
O_TRUNC:如果该文件已经存在并且是常规文件,并且访问模式允许写入(即,为O_RDWR或O_WRONLY),则它将被截断为长度0。如果文件是FIFO或终端设备文件,O_TRUNC标志将被忽略。
mode: 参数3使用的前提, 参2指定了 O_CREAT。 取值8进制数,用来描述文件的 访问权限。 rwx 0664
创建文件最终权限 = mode & ~umask (自己设定的mode 和 umask的非 相与)(与:全1为1,其他情况都为0。或:全0为0,其他情况都为1)一般mode设为0664
在Linux系统中umask值代表的是我们创建新文件/目录的默认权限设置,系统root账号默认的umask的值为0022 普通账号的umask的值为0002
write系统调用
用write()系统调用将数据写到一个文件中
函数原型:
int write(int fd,void *buf,size_t nbytes);
函数参数:
fd :要写入的文件的文件描述符
buf: 指向内存块的指针,从这个内存块中读取数据写入 到文件中
nbytes: 要写入文件的字节个数
返回值
如果出现错误,返回-1
如果写入成功,则返回写入到文件中的字节个数
read系统调用
一旦有了与一个打开文件描述相连的文件描述符,只要该文件是用O_RDONLY或O_RDWR标志打开的,就可以用read()系统调用从该文件中读取字节
函数原型:
int read(int fd, void *buf, size_t nbytes);
参数
fd :想要读的文件的文件描述符
buf: 指向内存块的指针,从文件中读取来的字节放到这个内存块中
nbytes: 从该文件复制到buf中的字节个数
返回值
如果出现错误,返回-1
返回从该文件复制到规定的缓冲区中的字节数,文件结束,返回0否则
注意点:文件的随机读写
到目前为止的所有文件访问都是顺序访问
这是因为所有的读和写都从当前文件的偏移位置开始,然后文件偏移值自动地增加到刚好超出读或写结束时的位置,使它为下一次访问作好准备
有个文件偏移这样的机制,在Linux系统中,随机访问就变得很简单,你所需做的只是将当前文件移值改变到有关的位置,它将迫使一次read()或write()发生在这一位置(除非文件被O_APPEND打开,在这种情况下,任何write调用仍将发生在文件结束处)
lseek系统调用
计算文件多大小int lenth = lseek(fd, 0, SEEK_END);
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
//报错函数
void my_err(const char *err_string,int line)
{
fprintf(stderr,"line:%d",line);
perror(err_string);
_exit(1);
}
//读数据函数
int my_read(int fd)
{
int len;
int ret;
int i;
char read_buf[64];
//指针移到最后
if(lseek(fd,0,SEEK_END)==-1)
{
my_err("lseek",__LINE__);
}
//最后位置到文件开始位置字节数
if((len=lseek(fd,0,SEEK_CUR))==-1)
{
my_err("lseek",__LINE__);
}
//指针回到文件开始处,下一步方便读取数据
if(lseek(fd,0,SEEK_SET)==-1)
{
my_err("lseek",__LINE__);
}
printf("len:%d\n",len);
//读取数据,返回读取字节数
if((ret=read(fd,read_buf,len))<0)
{
my_err("read",__LINE__);
}
//打印
for(i=0;i<len;i++)
{
printf("%c",read_buf[i]);
}
printf("\n");
return ret;
}
int main()
{
int fd;
char write_buf[32]="Hello World";
char write_buf2[32]="Q";
if((fd=open("test.txt",O_CREAT|O_RDWR|O_TRUNC,S_IRUSR|S_IWUSR))==-1)
{
my_err("open",__LINE__);
}
else
{
printf("creat the file success\n");
}
if(write(fd,write_buf,strlen(write_buf))!=strlen(write_buf))
{
my_err("write",__LINE__);
}
printf("---------------------------------\n");
if(lseek(fd,10,SEEK_END)==-1)
{
my_err("lseek",__LINE__);
}
if(write(fd,write_buf2,strlen(write_buf2))!=strlen(write_buf2))
{
my_err("write",__LINE__);
}
my_read(fd);
close(fd);
return 0;
}
六、_exit,_Exit 函数
函数原型
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);
函数作用
当程序在前面步骤失败,导致后面的操作不能继续进行时,应该在前面的错误监测中结束整个程序。结束程序有两种方法:在main用return(一般原则是程序正常终止return 0,如果程序异常终止则return -1);或者使用_exit、_Exi、exit(C库函数)三者之一。
注意,_exit、_Exit函数没有返回值。
参数说明
status表示当前进程的退出状态,这个值会被父进程收集,以作为某些判断之用。
补充说明
上面的_exit、_Exit是Linux系统的API函数,两者的功能是一样的。
另外还有一个C库函数exit,也能实现同样效果,函数原型如下所示。
#include <stdlib.h>
void exit(int status);
七、perror函数
函数原型
#include <stdio.h>
#include <errno.h> //之所以要包含这个文件,是因为errno的定义在这个文件中
void perror(const char *s);
参数说明
字符串s表示要打印显示的内容。
补充说明
Linux系统中对各种常见错误做了编号,当函数执行错误时,函数会返回一个特定的errno编号来告诉我们这个函数到底哪里错了。errno全称是error number,意思是“错误号码”,它是由OS来维护的一个全局变量,任何OS内部函数都可以通过设置errno来告诉上层调用者究竟刚才发生了一个什么错误。
errno本质是一个int类型的数字,每个数字编号对应一种错误。我们从errno这个变量里只能得知它的值是多少,不知道这个值所代表的错误信息。
因此Linux系统提供了一个perror函数(意思print error),perror函数内部会读取errno,然后将这个不好认的数字转换成对应的错误信息字符串,然后print打印出来。
它用命令“ man 2 perror ”显示不出来,要用” man 3 perror ”才行,这说明它是C库函数。另外从它的头文件stdio.h也可以看出它是C库函数(虽然不是API,但既然写在这里了,那就保留着吧)。
八、dup、dup2、dup3函数
函数原型
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>
int dup3(int oldfd, int newfd, int flags);
函数作用
接下来都是关于dup函数的描述,dup2、dup3函数略。
dup函数对旧的 fd 进行复制,返回一个新的文件描述符,它们都指向同一个动态文件。相当于两个不同的指针指向同一个文件。
参数说明
oldfd是要进行复制的文件描述符。
补充说明
(1)dup函数不能指定复制得到的fd的值,而是由操作系统内部自动分配。分配的原则遵守fd分配的原则,即分配没被占用的且数值最小的fd。
(2)dup函数返回的 fd 和原来的 oldfd,都表示原来oldfd所表示的那个动态文件,操作这两个fd实际操作的是 oldfd 表示的那个文件,这就构成了文件共享(这是文件共享的方式之一,其他另种方式包括:同一进程中多次使用open函数打开同一文件,不同进程中打开同一文件)。
(3)利用dup返回的fd和原来的oldfd同时向一个文件写入时,结果应该是分别写。
(4)dup函数的应用场景:标准输出的重定位
利用上面3点的补充说明,可以实现标准输出的重定位。
我们已知fd=0,1,2默认分别被标准输入文件、标准输出文件、错误通道文件占用,但我们可以使用close函数关闭这三个文件。比如我们可以用close(1)来关闭标准输出,关闭后我们使用printf函数时,屏幕上不再显示内容输出的内容。close(1)之后,fd=1这个就空出来了,没有被占用。
由于dup函数分配fd时,分配原则是分配没被占用且数值最小的fd,那么此时我们使用dup函数去复制文件 test.txt 的fd时,返回的fd就是之前被释放不再被占用的fd=1。这样一来,fd=1就指向了文件test.txt。由于fd=1所表示的文件被默认是标准输出,所以使用echo命令、ls命令、printf函数等,都会将显示的内容输出到文件test.txt里面。这就实现了标准输出的重定位。
也就说,可以利用“open文本文件得到文件描述符+close标准输出的文件描述符+dup文本文件的描述符”的形式,实现标准输出的重定位。
Linux 中的重定位符“>”,其实也是利用open+close+dup来实现的。open打开一个文件2.txt,然后close关闭stdout,然后使用 dup 函数将 fd=1 和 2.txt 文件关联起来即可。
九、fcntl函数
函数原型
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
函数作用
fcntl函数是一个多功能的、用于文件管理的工具箱。
参数说明
(1)fd表示此函数要操作的文件的文件描述符。
(2)cmd表示此函数要进行的操作。
(3)变参用来传递参数,配合cmd使用。