Linux程序设计第三章

本文详细介绍了在C语言中如何处理文件与设备,包括基本的文件操作如open、close、read、write等系统调用,以及标准I/O库函数如fopen、fclose等的使用。文中还探讨了文件描述符的概念、文件权限设置、文件复制程序的设计,并讲解了目录操作的相关函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


             终于来到C语言的章节了。依然对C语言无限热爱。也依然学不懂C语言。资质愚笨。无可奈何。
在本章中,我们将学习:
1.文件和设备
2.系统调用
3.库函数
4.底层文件访问

5.管理文件
6.标准I/O库
7.格式化输出和输入
8.文件和目录的维护
9.扫描目录
10.错误及其处理
11./proc文件系统
12.高级主题:fcntl和mmap





           在Linux中,几乎一切都是文件。这就意味着,程序完全可以像使用文件那样使用磁盘文件,串行口,打印机和其他设备。在大多数情况下,我们只需要:open,close,read,write和ioctl函数。


3.1.1.目录
文件通常不仅包含文件本身的内容,也包含着一些属性,这些属性被保存在文件的inode(节点)中。系统使用的仅仅是文件的节点编号,至于文件名,那是为了方便人们使用才有的。Linux的目录结构中,/ 是根目录,由此目录延伸,其中包括/bin (二进制可执行文件)目录。/dev(物理设备及其接口)目录,/etc(系统配置文件)。/lib(函数库)。 Linux的文件系统,是一个树形的结构。

3.1.2.文件和设备
Linux中,通常甚至硬件设备也被(映射)为文件。例如,作为root用户,可以使用如下命令将IDE CD—ROM驱动器挂载为一个文件
#mount -t iso9660 /dev/hdc /mnt/cdrom
#cd /mnt/cdrom

Linux中三个比较重要的设备文件:/dev/console,/dev/tty,/dev/null。

1./dev/console
这个设备代表系统控制台。错误信息和诊断信息通常会被发送到这个设备。
2./dev/tty
如果一个进程有控制终端的话,那么特殊文件/dev/tty就是这个控制终端(键盘和显示屏等)的别名(逻辑设备)。
(有点不太理解这句话的意思。望高人解答。)
3./dev/null
空设备的意思。所有写向
这个设备的输出都将被丢弃,而读这个设备会立刻返回一个文件结尾标志,所以在CP命令里可以把它用作复制空文件的源文件。

3.2系统调用和设备驱动程序
系统调用,通向操作系统本身的接口。
open:打开文件或设备
read:从打开的文件或设备里读数据
write:向文件或设备写数据
close:关闭文件或设备
ioctl:把控制信息传递给设备驱动程序

系统调用ioctl用于提供一些与特定硬件设备有关的必要控制,所以用法随设备的不同而不同。这些系统调用和其他系统调用通常放在手册的第二页,你可以通过man 2 open这样的命令来查看手册。

3.3库函数
针对输入输出直接使用系统调用效率将会非常低。
原因是因为:1.系统调用比普通函数调用的开销要大,因为系统调用要从用户态切到内核态,然后在返回用户态。2.硬件会限制对底层调用一次所能够读写的数据块大小。
Linux提供了一系列的标准函数库。可以大大提高效率。库函数一般放在手册第三页。


3.4底层文件访问

3.4.1write系统调用
函数原型
#include<unistd.h>
size_t write(int fildes,const void *buf,size_t nbytes);
把缓冲区buf的前nbytes字节写与文件描述符fildes关联的文件中,返回实际写入的字节数。如果文件设备描述符有错或者底层的设备驱动程序对数据块长度比较敏感,返回值可能小于nbytes。如果函数返回0,表示未写入任何数据,如果返回-1,表示出错,错误代码保存在全局变量errno里。
实例:
#include<unistd.h>
#include<stdlib.h>
int main()
{
    if((write(1,"hello world!",12) != 12))
    {
        write(2,"A write error has occured\n",46);
    }
    return 0;
}

3.4.2read系统调用
函数原型
#include<unistd.h>
#include<stdlib.h>

size_t read(int fildes,void *buf,size_t nbytes);
作用是:从与文件描述符相关的文件里读入nbytes个字节的数据,并把他们存储在buf中。返回实际的读入字节数。如果返回0,表示未读入任何数据。返回-1,表明出错。

示例:



#include<unistd.h>
#include<stdlib.h>
int main()
{
    char buf[128];
    int nread;
    nread = read(0,buf,128);
    if(nread == -1)
        write(2,"A read error occurred\n",30);
    if((write(1,buf,nread) != nread))
        write(2,"A write error has occured\n",46);
    return 0;
}

3.4.3
open系统调用,
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>

int open(const char *path,int oflags);
int open(const char *path,int oflags,mode_t mode);
严格来说,后面两个文件在POSIX标准中是不需要包含的,但在某些UNIX系统,却又是不可缺少的,为了可移植性,应该建议包含。

open建立了一条到文件或设备的访问路径。如果调用成功,它返回一个文件描述符,可以被read,write或其他系统调用使用的。这个描述符是唯一的,他不会与其他运行中的进程共享。如果两个程序同时打开一个文件,他们会分别得到不同的文件描述符,如果他们进行写操作,他们会各写各的,并相互覆盖。可以通过文件锁来防止出现冲突。
其中path 是准备打开的文件或设备的名字,oflags用于打开文件所采取的动作。
其中oflags必须指定一下方式之一。
            模式                 说明
        O_RDONLY        以只读方式打开
        O_WRONLY       以只写方式打开
        O_RDWR           以读写方式打开

open调用还可以在oflags参数中包括下列可选模式的组合(用”按位或“操作)
O_APPEND:把写入数据追加在文件末尾
O_TRUNC:把文件长度设置为0,丢弃已有的内容
O_CREAT:如果需要,就按参数mode给出的访问模式创建文件
O_EXCL:与
O_CREAT一起使用,确保调用者创建出文件。使用这个可选模式,可以防止两个程序同时创建同一个文件,open是一个原子操作,只执行一个函数调用,如果文件已经存在,open调用将失败。
同样放回-1,表示出错。成功返回一个描述符。(每次返回的都是,未被使用的描述符的最小值,如果一个程序关闭了它的标准输出,然后再次调用open,文件描述符1就会被再次调用。)

POSIX标准还定义了一个creat调用,但是并不常用。其实用open就可以实现了,相当与oflgas标O_CREAT|O_WRONLY|O_TRUNC来调用open 就行。

3.4.4访问权限的初始值
当你使用带有O_CREAT标志的open时,你必须使用3个参数格式的open调用。
第三个参数mode是几个标志位按位或得到的。这些标志在sys/stat.h中定义。
定义如下所示:
S_IRUSR:读权限,文件属主
S_IWUSR:写权限,文件属主
S_IXUSR:执行权限,文件属主
S_IRGRP:读权限,文件所属主
S_IWGRP:写权限,文件所属主
S_IXGRP:执行权限,文件所属主
S_IROTH:读权限,其他用户
S_IWOTH:写权限,其他用户
S_IXOTH:执行权限,其他用户

例如:
open("myfile",O_CREAT,S_IRUSR|S_IXOTH)
创建一个myfile文件,其中文件属主具有读权限,其他用户具有执行权限.当然.umask也会影响权限设置,将mode与umask进行与操作.

1.umask是一个系统变量,当文件被创建时,为文件的访问权限设置一个掩码.执行umask可以修改这个变量的值.umask是3个8进制数字组成的值,具体想了解的,可以看看<鸟哥的linux私房菜>里面有更详细的介绍.

2.close系统调用.
函数原型
#include<unistd.h>
int close(int fildeds);
成功返回0,错误返回-1.注意检查close调用的返回结果非常重要.

3.ioctl调用
它提供了一个用于控制设备及其描述符行为和配置底层服务的接口.终端,文件描述符,套接字等都可以有它定义的ioctl.
函数原型:
#include<unistd.h>
int ioctl(int fildes,int cmd,...)

ioctl对描述符引用的对象执行cmd参数中给出的操作.
例如,在Linux系统上对ioctl的如下调用将打开键盘上的LED灯.
ioctl(tty_fd,KDSETLED,LED_NUM|LED_CAP|LED_SCR);

实验:一个文件复制程序
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
int main()
{
    char c;
    int in,out;
    in = open("file.in",O_RDONLY);
    out = open("file.out",O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR);
    while(read(in,&c,1) == 1)
    {
        write(out,&c,1);
    }
    return 0;
}
要注意的是,#include<unistd.h>行必须首先出现,因为它定义的与POSIX规范有关的标志可能影响到其他的头文件.

TIMEFORMAT="" time ./a.out  用这个命令去测试。
Linux中用TIMEFORMAT变量来重置默认的POSIX时间输出格式。POSIX输出格式不高扩CPU使用率,假设我们的输入文件足够大,你会看到这个程序,相当的耗资源和时间。当然,可以通过每一次,读取一个比较大的块,来改善程序。据不可靠消息表示,好像设置成4k的话,速度是最快的。因为我忘记了我是潜意识,还是哪本书看过这句话的。也许是我自己潜意识骗自己,所以是不可靠消息。我就不去做实验验证了。当年曾经研究过这个问题,缓冲区到底有多大?后来好像得到的一个结果是,如今的缓冲区是动态分配的。貌似是这样。。。


3.4.5其他与文件管理有关的系统调用
1.lseek 系统调用
函数原型:
#include<unistd.h>
#include<syt/types.h>
off_t lseek(int fildes,off_t offset,int whence);

对文件描述符fildes的读写指针进行设置。也就是说,你可以用它来设置文件的下一个读写位置。读写指针既可被设置为文件中的某个绝对位置,也可以把它设置为相对于当前位置或者文件尾的某个相对位置。
offset参数用来指定位置,而whence参数定义该偏移值的用法。whence可以取下列值:
SEEK_SET:offset是一个绝对位置
SEEK_CUR:offset是相对于当前位置的一个相对位置
SEEK_END;offset是相对于文件尾的一个相对位置

lseek返回从文件头到文件尾指针被设置处的字节偏移值.失败时返回-1.参数offset的类型off_t是一个与具体实现相关的整数类型,它定义在sys/types.h中

2.fstat,stat和lstat系统调用.
原型:
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>

int fstat(int fildes,struct stat *buf);
int stat(int fildes,struct stat* buf);
int lstat(const char *path,struct stat* buf);

这些系统调用返回与打开的文件描述符相关的文件的状态信息,该信息将会写到一个buf结构中,buf的地址以参数形式传递给fstat.
stat结构中其中包括一大堆成员,我就不记了,详见Linux程序设计第四版 p90.

3.dup和dup2系统调用
原型:
#include<unistd.h>
int dup(int fildes);
int dup2(int fildes,int fildes2);

dup系统调用提供了一种复制文件描述符的方法,让我们能够通过两个或者更多个不同的描述符来访问同一个文件.这可以用在对文件的不同位置进行数据读写.dup系统调用复制文件描述符fildes,并返回一个新的描述符.dup2则明确通过指定目标描述符来完成.


3.5标准I/O库.

1.fopen,fclose
2.fread,fwrite
3.fflush
4.fseek
5.fgetc,getc,getchar
6.fgets,gets.
7.printf,fprintf,sprintf
8.sanf,fscanf,sscanf.
这个我感觉自己已经相当熟练了,就不多说了.其中printf和scanf具有一些特殊用法.好吧,我忘了.用的时候谷歌.

3.6.3其他流函数

fgetpos:获得文件流的当前(读写)位置
fsetpos:设置文件流的当前(读写)位置
ftell:返回文件流的当前(读写)位置的偏移值.
rewind:重置文件流里的读写位置.
freopen:重新使用一个文件流.(个人感觉这个相当有用)
附上用法和说明:

函数名: freopen
功 能: 替换一个流,或者说重新分配 文件指针 ,实现重定向。如果stream流已经打开,则先关闭该流。如果该流已经定向,则freopen将会清除该定向。此函数一般用于将一个指定的文件打开一个预定义的流:标准输入、标准输出或者标准出错。
用 法: FILE *freopen(const char *filename,const char *type, FILE *stream);
头文件: stdio.h
返回值:如果成功则返回该指向该stream的指针,否则为NULL。
比如,我要重定向输入流到其他文件,就这么写.freopen("input.txt","w",stdin);当你每次都需要输入大量测试数据的时候,将它保存在input中,然后重定向,这样你每次都不用手动输入数据了.从文件直接读取.


setvbuf:设置文件流的缓冲机制(详见UNIX环境高级编程)
remove:相当于unlink函数,但如果他的path参数是一个目录的话,其作用相当于rmdir函数.


3.6.4文件流错误

为了表明错误,许多stdio函数会返回一个超出范围的值,比如空指针或EOF常数.此时,错误有外部变量errno指出.
#include<errno.h>
extern int errno;

许多函数都可能改变errno的值,他的值只在函数调用失败时才有意义.你必须在函数表明失败之后立刻对其进行检查.

也可以用文件流的状态来确定是否发生了错误.或者是否达到了文件尾.
#include<stdlo.h>
int ferror(FILE *stream);    //测试一个文件流的错误标志.
int feof(FILE *stream);      //测试是否达到末尾
void clearerr(FILE *stream);    //清除由stream指向的文件流的文件尾标识和错误标识.

3.6.5文件流和文件描述符

#include<stdio.h>
int fileno(FILE *stream);   //用来确定文件流使用的是哪个底层文件描述符.
FILE *fdopen(int fildes,const char *mode);  //与fopen类似,只不过fopen的参数是文件名,而这个是文件描述符.mode参数与fopen完全一样.




3.7文件和目录的维护

系统调用和标准库都提供了全面支持。

系统调用

1.chmod
函数原型:
#include<sys/stat.h>
int chmod(const char *path,mode_t mode);
path参数指定的文件被修改为具有mode参数给出的权限.参数mode的定义于open系统调用中一样.也是对所要求的访问权限进行OR操作.

2.chown
函数原型:
#include<sys/types.h>
#include<unistd.h>
int chown(const char *path,uid_t owner,gid_t group);
这个调用使用的是用户的ID和组ID的数字值(通过getuid和getgid调用获得)和一个用于限定谁可以修改文件属主的系统值。

3.unlink,link,和symlink.

函数原型:
int unlink(const char *path);  //删除文件
int link(const char* path1,const char *path2); //创建一个指向已有文件path1的新链接,新目录项由path2给出。
int symlink(const char *path1,const char *path2);//以类似的方式创建符号链接。(软链接)


4.mkdir和rmdir

#include<sys/types.h>
#include<sys/stat.h>
int mkdir(const char *path,mode_t mode);

#include<unistd.h>
int rmdir(const char *path);


5.chdir系统调用 和 getcwd函数
#include<unistd.h>
int chdir(const char *path);
程序可以像用户在文件系统里那样浏览目录,就像你在shell里使用cd命令来切换目录一样.程序使用的是chdir系统调用.


#include<unistd.h>
char *getcwd(char *buf,size_t size);

程序可以调用getcwd函数来确定自己的当前工作目录.将目录存在buf中,如果路径长度超过了size,则错误.成功的话,返回指向buf的指针.此外,如果程序在运行中,目录被删除了或者权限发生了变化,都会发生错误.


3.8扫描目录
        与目录操作想噶und函数在dirent.h头文件中声明.它们使用一个名为DIR的结构作为目录操作的基础.被称为目录流的指向这个结构的指针(DIR *)来完成各种目录的操作.这与FILE *文件流类似.

1.opendir函数
#include<sys/types.h>
#include<dirent.h>
DIR *opendir(const char *name);
打开一个目录并建立一个目录流,如果成功,返回一个指向DIR结构的指针,该指针用于读取目录数据项.opendir如果打开的文件过多,可能会失败.

2.readdir函数
#include<sys/types.h>
#include<dirent.h>

struct dirent *readdir(DIR *dirp);
该结构中包含:
ino_t   d_ino:文件的inode节点号.
char d_name[]:文件的名字.

返回一个指针,该指针指向的结构里保存着目录流dirp中下一个目录项的有关资料.后续的readdir将返回后续的目录项.

      
3.telldir
返回值记录这一个目录流里的当前位置.可以用seekdir调用中利用这个值来重置目录扫描到当前位置.
#include<sys/types.h>
#include<dirent.h>
long int telldir(DIR *dirp);

4.seekdir函数
#include<sys/types.h>
#include<dirent.h>
void seekdir(DIR *dirp,long int loc);
设置目录流dirp的目录项指针,loc用来设置指针位置。应该通过telldir获得。


5.closedir
#include<sys/types.h>
#include<dirent.h>
int closedir(DIR *dirp);
关闭一个目录流并释放与之相关的资源。
这里我们给出一个示例:
#include<unistd.h>
#include<stdio.h>
#include<dirent.h>
#include<string.h>
#include<sys/stat.h>
#include<stdlib.h>

void printdir(char *dir,int depth)
{
    DIR *dp;
    struct dirent *entry;
    struct stat statbuf;
    if((dp = opendir(dir)) == NULL)
    {
        //
        return;
    }
    chdir(dir);
    while((entry = readdir(dp)) != NULL)
    {
        lstat(entry->d_name,&statbuf);
        if(S_ISDIR(statbuf.st_mode))
        {
            if(strcmp(".",entry->d_name) == 0
                    || strcmp("..",entry->d_name) == 0)
                continue;
            printf("%*s%s/\n",depth,"",entry->d_name);

            printdir(entry->d_name,depth+4);

        }
        else
        {
            printf("%*s%s\n",depth,"",entry->d_name);
        }
    }

    chdir("..");
    closedir(dp);

}

int main()
{
    printf("Dirctory scan of /home:\n");
    printdir("/home",0);

    printf("done.\n");
    return 0;
}
该程序通过递归遍历,打印出/home目录下的文件。当然,我们可以修改main里面的参数。让这个程序更加实用。


3.9错误处理
终于到这里了。我们来看看errno.h的错误代码取值和含义。
EPERM:操作不允许
ENOENT:文件或目录不存在
EINTR:系统调用被中断
EIO:I/O错误
EBUSY:设备或资源忙
EINVAL:无效参数
EMFILE:打开文件过多
ENODEV:设备不能存在
EISDIR:是一个目录
ENOTDIR:不是一个目录

其中有两个函数来报告出现的错误。
1.strerror函数
strerror函数把错误代码映射为一个字符串,对该字符串发生的错误类型进行说明。
#include<string.h>
char *strerror(int errnum);

2.perror函数。
也将errno变量映射到一个字符串,并把它输出到标准错误输出流。
#include<stdio.h>
void perror(const char *s);



3.10 /proc文件系统
虚拟文件系统。是不是这个啊。。。算了,就这样吧。。。

3.11高级主题:fcntl和mmap。

1.fcntl系统调用。
#include<fcntl.h>
fcntl系统调用对底层文件描述符提供了更多的操纵方法。利用它,你可以对打开的文件描述符执行各种操作。包括对他进行复制,获取和设置文件描述标志,获取和设置文件状态标志,以及管理建议性文件锁等。

2.mmap(内存映射)
此函数的作用是建立一段可以被两个或者更多个程序读写的内存。一个程序对他做的修改也可以被其他车给你需看见。
mmap函数创建一个指向一段内存区域的指针,该内存区域与可以通过一个打开的文件描述符访问的文件的内容相关连。
#include<sys/mman.h>

void *mmap(void *addr,size_t len,int prot,int flags,int fildes,off_t off);


大功告成,不容易啊,看的速度太慢了。继续加油吧~。
桂林的天气真是太善变了。今天变得好冷。






 

                                                                                                                                                                                                                                                                                                            

























































 




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值