一、UNIX体系结构
从严格意义上来说,可以将操作系统可定义为一种软件,它控制计算机硬件资源,提供程序运行环境,通常将这种软件称为内核(kernel),它相对较小,位于环境的中心。内核的接口被称为系统调用(system call),公共函数库构建在系统调用接口之上,应用程序可调用公共函数库,也可以直接使用系统调用。shell是一种特殊的应用程序,为运行其他应用程序提供一个接口。UNIX操作系统的体系结构如下图所示:
二、系统登录
用户在登录UNIX时,先键入登录名,然后键入口令。系统在/etc/passwd中查看登录名。如下:
口令文件中的登录项是由7个以冒号分隔的字段组成,依次是:登录名(root)、加密口令(x)、数字用户ID(0)、数字组ID(0)、字段注释(root)、起始目录(/root)以及shell程序(/bin/bash)。
上面的起始目录也叫做该用户的工作目录,可以通过函数chdir进行修改。
三、文件和目录
在UNIX中,目录是一种特殊的文件,每个目录都包含两个文件名:"."(称为点)以及".."(称为点点),点指向的是当前目录,点点指向的是父目录,在根节点("\")中,点与点点含义相同。
关于路径名称,只要是以“\”开头的路径,都是绝对路径名称,否则就是相对路径。
通过几行简单的代码,我们可以很轻松的列出一个目录中的所有文件名称。代码如下:
// 标准输入输出头文件
#include <stdio.h>
// 目录操作头文件
#include <dirent.h>
int main(int argc,char** argv)
{
DIR* dp;
struct dirent* dirp;
// 参数个数小于1则报错
if (argc != 2) {
printf("please input which dir display\n");
return -1;
}
// 目录打开失败则报错
if ((dp = opendir(argv[1])) == NULL) {
printf("dir %s not exist!",argv[1]);
return -1;
}
// 读取指定目录下的文件信息,每次读取,指针自动指向下一个,直到为NULL则表示读取完毕
while((dirp = readdir(dp)) != NULL) {
printf("%s\n",dirp->d_name);
}
closedir(dp);
return 0;
}
运行结果如下:
四、文件I/O
对于内核而言,每个打开的文件都通过文件描述符引用,文件描述符是一个非负整数,当打开一个文件时,内核将向进程返回一个文件描述符,用于操作该文件,按照惯例,UNIX系统的shell已经占用了前3个文件描述符:
文件描述符 | 宏常量 | 作用 |
0 | STDIN_FILENO | 标准输入 |
1 | STDOUT_FILENO | 标准输出 |
2 | STDERR_FILENO | 标准出错 |
1、函数open与openat
调用函数open()或者openat()可以打开或创建一个文件。
#include<fcntl.h>
int open(const char* path,int flag,.../*mode_t mode*/);
int openat(int fd,const char* path,int flag,.../*mode_t mode*/);
函数作用:在指定路径下打开或新建一个文件
返回值:成功则返回对应文件描述符(最小的未被使用的描述符数值),失败则返回-1
参数解析:
path:表示要打开后创建文件名称(包含路径)
fd:openat函数中fd参数将的含义将根据path的值有不同的含义:
- path为绝对路径名:fd将被忽略,openat函数就相当于open函数
- path为相对路径名:fd参数指出了相对路径名在文件系统中的开始地址,即fd为这个相对路径所在目录的文件描述符
- path为相对路径名,且fd为特殊值AT_FDCWD,此时路径名为当前目录
flag:用下列的一个或多个进行或运算构成:
O_RDONLY 以只读方式打开 O_WRONLY 以只写方式打开 O_RDWR 以可读可写方式打开 O_EXEC 以可执行方式打开 O_SEARCH 以只搜索方式打开(目录) 以上5个常量必须指定一个且只能指定一个,下面的常量可以任意选:
O_APPEND 每次写时,内容追加到末尾 O_CLOEXEC O_CREAT 若文件不存在则创建它,若指定该标志位,必须也要启用mode参数 O_DIRECTORY 若path引用的不是目录,则报错 O_EXCL 如果已经指定了O_CREAT,但文件不存在,则创建,否则将会出错,这是一个原子操作 O_NOCTTY 如果path引用的是终端设备,则不将该设备作为此进程的控制终端 O_NOFOLLOW 如果path引用的是一个符号链接,则出错 O_NONBLOCK 如果path引用的是一个FIFO或一个块文件或一个字符文件,则选项将会为文件的本次打开及其后续I/O操作设置为非阻塞方式 O_SYNC 使每次write等待物理I/O操作完成,包括由write操作引起的文件属性的变更 O_TRUNC 如果此文件存在,而且为O_WRONLY或O_RDWR方式打开,则将其长度截断为0 O_DSYNC 使每次write要等待物理I/O完成,但是如果该写操作并不影响读取刚写入的数据,则不需要等待文件属性被更新 D_RSYNC 使每一个以文件描述符作为参数进行read操作等待,直至所有对文件同一部分挂起的写操作都完成
2、close函数
close函数用来关闭一个打开的文件
#include <unistd.h>
int close(int fd);
//成功返回0,失败返回-1
关闭一个文件时,还会释放该进程加在该文件上的记录锁。进程结束后,内核也会自动关闭该进程打开的所有文件。
3、lseek函数
每一个打开的文件都有一个与之关联的“当前文件偏移量”,除非指定聊O_APPEND选项,否则,新打开的文件都文件偏移量都是0,通常,读写操作都是从当前文件偏移量开始,并且每次读写都会使当前文件偏移量增加读写到的字节数。
#include <unistd.h>
off_t lseek(int fd,off_t off,int whence);
// 成功返回新的文件偏移量,失败返回-1
函数对不同的whence值有不同的意义:
whence | 含义 |
SEEK_SET | 将该文件的偏移量设置为距文件开始处off个字节 |
SEEK_CUR | 将该文件的偏移量设置为距当前位置off个字节,off可以为正也可以为负 |
SEEK_END | 将该文件的偏移量设置为距文件结尾处off个字节,off可以为正也可以为负 |
对lseek函数有以下几点需要特别注意:
- 1、lseek仅仅是设置文件偏移量,并不会触发文件I/O操作。
- 2、lseek函数仅仅能用来设置普通文件的文件偏移量,如果fd为套接字、FIFO、管道,则函数返回-1,并且将errno置为ESPIPE。
- 3、lseek函数对于某些设备,返回值可以为负值,所以判断函数成功与否,不能仅仅根据返回值是否小于0进行判断,而是要根据是否为-1进行判断。
- 4、如果将文件的偏移量设置为其总大小之后,则会造成文件空洞,空洞内容读取为0,文件空洞并不占有磁盘空间,即使它ls显示的大小很大
4、read函数
read函数用于读取文件内容。
#include <unistd>
ssize_t read(int fd,void* buf,size_t nbytes);
//成功返回实际写入的字节数,失败返回-1
5、write函数
write函数用于向一个文件写数据。注意磁盘已写满或者文件长度超过限制,都可能导致文件写入失败!
#include <unistd>
ssize_t write(int fd,const void* buf,size_t nbytes);
//成功返回读取到的字节数,失败返回-1
6、文件原子函数--pread/pwrite函数
我们知道,若同一时间,两个进程对同一个文件进行写操作,由于lseek和read/write 调用之间,内核可能会临时挂起进程,发生意想不到的覆盖问题,所以为了规避这个问题,UNIX系统提供了原子读写函数。
#include<unistd.h>
ssize_t pread (int filedes, void *buf, size_t nbytes, off_t offset );
ssize_t pwrite (int filedes, const void *buf, size_t nbytes, off_t offset );
//成功:返回读到的字节数;出错:返回-1;到文件结尾:返回0
pread/pwrite函数相当于顺序调用了lseek 和 read/write,并将这两个操作相当于一个捆绑的原子操作。
7、dup和dup2函数
函数dup用来复制一个描述符,即传给函数一个既有的描述符,它就会返回一个新的描述符,这个新的描述符是传给它的描述符的拷贝。
dup2函数跟dup函数相似,但dup2函数允许调用者规定一个有效描述符和目标描述符的id。dup2函数成功返回时,目标描述符(dup2函数的第二个参数)将变成源描述符(dup2函数的第一个参数)的复制品,换句话说,两个文件描述符现在都指向同一个文件,并且是函数第一个参数指向的文件。如果filedes已经打开,则dup2会先将其关闭(原子操作),若filedes2等于filedes,则dup2返回filedes2,而不关闭它
两个函数作用相同,但是dup返回的新文件描述符一定是当前可用文件描述符中最小数值,而dup2则可以是用filedes2参数指定新的描述符。
#include <unistd.h>
int dup(int files);
//等同于 fcntl(int files,F_DUPFD,0);
int dup2(int files, int filedes2);
//等同于 close(filedes2)
// fcntl(int files,F_DUPFD,filedes2);
// 成功则返回新的文件秒速符,出错则返回-1。
8、sync、fsync、fdatasync函数
#include <unistd.h>
void sync(void);
int fsync(int fd);
int fdatasync(int fd);
函数sync()始终成功,但只是将修改过的块的缓存排入写队列,并不等待 实际I/O操作结束。系统守护进程会周期性的(一般30s)会调用一次sync()函数,从而保证系统定期刷新内核缓存。
函数fsync()函数则等待实际I/O结束才返回,从而确保修改过的块立即写到硬盘上,成功返回0,否则返回-1。
函数fdatasync()只是更新硬盘文件内容,如果没有必要,并不更新元数据,即文件的属性(长度、上次修改时间等),成功返回0,否则返回-1。
9、fcntl函数
#include <fcntl.h>
#include <unistd.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
fcntl()针对(文件)描述符提供控制,参数fd是被参数cmd操作的描述符,针对特定的cmd的值,fcntl能够接受第三个参数(arg):
fcntl函数有5种功能:
- 复制一个现有的描述符(cmd=F_DUPFD).
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
具体含义如下:
cmd值 | 含义 |
F_DUPFD | 复制文件描述符fd,新文件描述符作为函数值返回,它是一个最小的大于或等于第三个参数的一个可用的描述符 |
F_GETFD | 对应于fd的文件描述符标志作为函数值返回 |
F_SETFD | 按第三个参数设置fd的文件描述符标志 |
F_GETFL | 对应于fd的文件描述符标志作为函数值返回 |
F_SETFL | 按第三个参数设置fd的文件描述符标志,可以更改的标志为:O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC和O_ASYNC。 |
F_GETOWN | 获取当前接收SIGIO和SIGURG信号的进程ID或进程组ID |
F_SETOWN | 设置当前接收SIGIO和SIGURG信号的进程ID或进程组ID,arg为进程ID |
fcntl函数的返回值与命令有关,如果出错则返回-1,但是如果成功,则F_DUPFD返回新的描述符、F_GETFD及F_GETFL返回对应的值、F_GETOWN返回进程或进程组ID。
注意如果我们需要修改文件属性,正常的流程应该是先获取文件属性,然后在已有的属性上进行属性修改,之后将修改完的属性设置会文件中,不能直接暴力修改,否则将会把文件的一些关键属性不小心丢失掉。
10、ioctl函数
ioctl 是设备驱动程序中设备控制接口函数,一个字符设备驱动通常会实现设备打开、关闭、读、写等功能,在一些需要细分的情境下,如果需要扩展新的功能,通常以增设 ioctl() 命令的方式实现。
#include <sys/ioctl.h>
int ioctl(int fd, int cmd, ...) ;
参数 | 描述 |
---|---|
fd | 文件描述符 |
cmd | 交互协议,设备驱动将根据 cmd 执行对应操作 |
… | 可变参数 arg,依赖 cmd 指定长度以及类型 |
ioctl() 函数执行成功时返回 0,失败则返回 -1 并设置全局变量 errorno 值 。
11、stat、fstat以及lstat函数
// from /usr/include/sys/stat.h
int stat(const char* filename,struct stat* buf); //普通文件(传入绝对路径或相对路径)
int fstat(int fd,struct stat* buf); //普通文件(传入已打开的fd)
int lstat(const char* filename,struct stat* buf); //符号链接文件
作用都是获取指定文件的属性,并保存在结构体buf中。其中结构体struct stat定义如下:
struct stat
{
unsigned short st_dev; //设备号
unsigned short _pad1;
unsigned long st_ino; //文件inode值
unsigned short st_mode; //文件类型及权限
unsigned short st_nlink; //硬件连接数
unsigned short st_uid; //用户ID
unsigned short st_gid; //用户组ID
unsigned short st_rdev; //设备号
unsigned short _pad2;
unsigned long st_size; //文件大小
unsigned long st_biksize; //数据块大小
unsigned long st_blocks; //数据块数量
unsigned long st_atime; //最后一次访问时间
unsigned long _unused1;
unsigned long st_mtime; //最后一次修改时间
unsigned long _unused2;
unsigned long st_ctime; //最后一次改变属性时间
unsigned long _unused3;
unsigned long _unused4;
unsigned long _unused5;
}
12、
头文件:#include<sys/stat.h>
mode_t umask(mode_t cmask);
返回值:成功返回之前的屏蔽字
作用:为进程创建文件模式屏蔽字,即创建的文件默认不再有cmask权限,除非主动调用chmod()函数修改
头文件:#include<unistd.h>
#include<sys/types.h>
int truncate(const char *path,off_t length);
int ftruncate(int fd,off_t length);
返回值:成功返回0,失败返回-1
作用:将一个名称为path或者被文件描述符fd引用的常规文件被截断成一个大小精为length字节的文件。如果先前的文件大于这个大小,额外的数据丢失。如果先前的文件小于当前定义的大小,那么,这个文件将会被扩展,扩展的部分将补以null,也就是‘\0’。
头文件:#include <stdio.h>
int rename(const char *oldname, const char *newname);
返回值:成功返回0,失败返回-1
作用:将指定目录/文件进行重命名,注意newname如果已存在,需要分情况处理:
(1) 如果oldname为一个文件,那么为该文件更名。在这种情况下,如果newname作为一个目录已存在,则它不能重命名一个目录。如果newname已存在,而且不是一个目录,则先将其删除(rename会负责删除操作)然后将oldname更名为newname。对oldname所在目录以及newname所在的目录,调用进程必须具有写许可权,因为将更改这两个目录。
(2) 如若oldname为一个目录,那么为该目录更名。如果newname已存在,则它必须是一个目录,而且该目录应当是空目录(空目录指的是该目录中只有. 和.. 项)。如果newname存在(而且是一个空目录),则先将其删除,然后将oldname更名为newname。另外,当为一个目录更名时,newname不能包含oldname作为其路径前缀。例如,不能将/usr更名为/usr/foo/testdir,因为老名字( /usr/foo)是新名字的路径前缀,因而不能将其删除。
(3) 作为一个特例,如果oldname和newname引用同一文件,则函数不做任何更改而成功返回。
头文件:#include<stdio.h>
FILE* fopen(const char *path, const char *mode);
FILE* fdopen(int fd, const char *mode);
FILE* freopen(const char *path, const char *mode, FILE *stream);
作用:打开指定路径的文件操作流,fopen指定的绝对路径,freopen可以指定相对路径
int fclose(FILE *fp);
作用:关闭指定的文件操作,此时缓冲区里面的数据会被刷新
void setbuf(FILE *stream, char *buf);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
作用:为指定的文件流指定缓冲区。其中mode可以取以下值:
_IOFBF(满缓冲):当缓冲区为空时,从流读入数据。或者当缓冲区满时,向流写入数 据。
_IOLBF(行缓冲):每次从流中读入一行数据或向流中写入一行数据。
_IONBF(无缓冲):直接从流中读入数据或直接向流中写入数据,而没有缓冲区。
关于缓冲区相关知识,建议阅读:Unix环境变量--缓冲区
int fflush(FILE *stream);
作用:主动刷新文件流的缓冲区
int ferror(FILE *stream);
作用:获取去文件流错误,文件流无错误时,返回一个零值,发生错误时返回一个非零值。
int feof(FILE *stream);
作用:判断文件流是否到达文件尾
void clearerr(FILE *stream);
作用:清除文件流错误状态
获取文件指针:
int ftell(FILE *stream); //获取文件指针相对于开头的偏移
int fseek(FILE *stream, long offset, int whence);
void rewind(FILE *stream);//设置文件位置为给定流 stream 的文件的开头
int fgetpos(FILE *stream, fpos_t *pos);//获取流 stream 的当前文件位置,并写入到pos
int fsetpos(FILE *stream, fpos_t *pos);//设置文件位置为给定pos位置
off_t ftello(FILE *stream);//同ftell,只是参数类型不同
int fseeko(FILE *stream, off_t offset, int whence);//同fseek,只是参数类型不同
字符IO:
int getc(FILE *stream);
int fgetc(FILE *stream);
int getchar(void);
int ungetc(int c, FILE *stream);
int putc(int c, FILE *stream);
int fputc(int c, FILE *stream); int putchar(int c);
行IO:
char* gets(char *s);
char* fgets(char *s, int size, FILE *stream);
int puts(const char *s);
int fputs(const char *s, FILE *stream);
二进制IO:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
临时文件:
FILE *tmpfile(void);
//以二进制更新模式(wb+)在 /tmp 文件夹中创建一个临时文件。被创建的临时文件会在流关闭的时候或者在程序终止的时候自动删除
char *tmpnam(char *s);
//生成并返回一个有效的临时文件名,该文件名之前是不存在的。如果 str 为空,则只会返回临时文件名
char *tempnam(const char *dir, const char *pfx);
//按照指定的目录dir及前缀pfx生成一个临时文件,并将文件绝对路径名返回
char* mktemp(char *template);
//按照template传入的字符创建临时文件,注意template 必须以 XXXXXX 结尾,随后创建的临时文件名保证其前缀不变,用随机字符替换这6个X
int mkstemp(char *template);
//意义基本同mktemp,但是该函数不仅创建临时文件,还会以当前用户可读可写的方式打开文件,并返回其文件描述符
二、目录操作
(1)打开/关闭目录文件
#include <dirent.h>
DIR* opendir(const char* dirname);
int closedir(DIR* dirp);
opendir()函数用于打开一个目录文件,其中dirname为目录路径,执行成功,返回一个目录流指针,失败返回NULL
closedir()函数用于关闭指定的目录流,成功返回0,失败返回-1
(2)读取目录内容
#include <dirent.h>
struct dirent* readdir(DIR* dirp);
int readdir_r(DIR* dirp,struct dirent* entry,struct dirent ** result);
其中struct dirent定义如下:
// from dirent.h
struct dirent
{
long ino; //目录中某文件的innode值
_kerbel_off_t d_off; // 从目录开始到当前目录条的距离
unsigned short d_reclen; // 目录中某文件名的长度
char d_name[256]; // 目录中某文件名
}
readdir()函数执行成功则返回一个指向struct dirent的结构体,,如果到达目录结尾,或失败,则范湖NULL。
readdir_r()函数在多线程编程中相对安全,且此函数第一个参数为打开的目录指针,它将初始化第二个参数引用的struct dirent结构,以表示第一个参数所引用的目录流的当前位置,并将所在位置的信息存储在第三个参数中。函数执行成功,将返回目录流中的一个文件信息,到达目录尾在返回0,执行失败返回-1。
(3)定位目录位置
long int telldir(DIR* dirp); //返回目录流的当前位置
void seekdir(DIR* dirp,long int loc); // 设置下一个readdir()位置
void rewinddir(DIR* dirp); //将目录流的位置重置到目录的开头
(4)当前工作路径操作
char* getcwd(char* buf,size_t size); //获取当前工作路径的绝对路径到buf中,并返回buf
int chdir(const char* path);//修改当前进程的环境变量的当前工作路径,成功返回0,失败返回-1