内核:当前操作系统的核心程序(主要是驱动程序--一个驱动程序唯一对应一个硬件设备,是组成操作系统内核的关键)
操作系统的本质:程序,内核就是操纵系统的核心程序
内核服务于上层应用,与硬件(硬盘、内存等)打交道
系统调用:由内核提供的函数,由操作系统实现并提供给外部应用程序的编程接口,是应用程序与系统之间数据交互的桥梁
接下来学习严格来说是系统函数--在manpage中,为什么说是系统函数?--系统函数是内核函数(真正调用的函数)做了一层浅封装(系统不想让人看到)
接下来举例说明32位操作系统下,c标准函数和系统函数调用的关系:一个helloword如何打印到屏幕
32位操作系统中打开一个程序(进程)有0-4G地址区域,自己所编写的函数和库函数是用户级函数,在0-3G位置(用户空间),文件描述符表在4G区域(内核空间)。
Printf(“hello”)是从库函数(libc)的printf中,向下调用了write函数,write函数的作用是完成用户空间到内核空间的一个进入工作,write函数传递给sys_write,sys_write再找相应驱动,通过驱动找相应硬件。
其中write函数是我们后面要学习的系统函数中的一个
都在manpage第二卷 man 2 函数
定义:内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。
同时还规定系统刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符会是3,再打开一个文件文件描述符就是4
0- STDIN_FILENO 标准输入
1- STDOUT_FILENO 标准输出
2- STDERR_FILENO 标准错误
一个进程可打开1024个文件
PCB进程控制块:本质 结构体(有很多成员,其中有一个成员是一个指针,其能够指向文件描述符表,表里头描述的是文件描述符,文件描述符是指向一个文件结构体的指针(指针指向的文件结构体),其其实再往下细究是键值对)。
PCB描述整个进程的消息的(a.out就是一个进程)
注:如果把文件描述符3关闭,那么4会变为3吗
不会,下一个打开的文件描述符会是3
文件描述符了解一下 - 知乎 (zhihu.com)
[Linux系统编程]文件IO(一)_io系统_Windalove的博客-CSDN博客
更细致的,将hello写入到文件1.txt流程
首先fopen打开文件 fwrite写入文本内容
文本内容来到C标准缓冲区
如果缓冲区满了或者满足条件就刷新C标准缓冲区,调用系统函数write进行写
write把要写入的内容写到内核缓冲区
如果内核缓冲区满了或者满足条件就刷新内核缓冲区,系统调用sys_write将缓冲区内容写入到磁盘(补充:有个进程会择机刷新内核缓冲区)
此时如果有进程读取1.txt文件内容,发现内核缓冲区就有这个文件内容,就直接从内核缓冲区读取
为什么要有缓冲区:
定义:缓冲区就是内存里的一块区域,把数据先存内存里,然后一次性写入硬盘中的文件,类似于数据库的批量操作。
好处:减少对硬盘的直接操作,硬盘的执行速度为毫秒级别,内存为纳秒级别。在硬盘直接操作读写效率太低。
内核缓冲区和C标准缓冲区的区别
C语言标准库函数fopen()每打开一个文件时都会对应一个单独一个缓冲区,而内核缓冲区是公用的。
注:不管是内核缓冲区还是c标准缓冲区,大小默认都是4096
而系统函数的缓冲区,可以自定义,但不是用户级缓冲也不是内核缓冲区
c标准缓冲区属于用户级缓冲区
阻塞
阻塞、非阻塞: 是设备文件、网络文件的属性。(读常规文件无阻塞概念。)
是程序在等待消息时的状态。
阻塞方式:就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。
非阻塞方式:就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回。
/dev/tty -- 终端文件。
open("/dev/tty",O_RDWR|O_NONBLOCK) --- 设置 /dev/tty 非阻塞状态。(默认为阻塞状态)
传入参数:
1. 指针作为函数参数。
2. 通常有const关键字修饰。
3. 指针指向有效区域, 在函数内部做读操作。
传出参数:
1. 指针作为函数参数。
2. 在函数调用之前,指针指向的空间可以无意义,但必须有效。
3. 在函数内部,做写操作。
4. 函数调用结束后,充当函数返回值。
传入传出参数:
1. 指针作为函数参数。
2. 在函数调用之前,指针指向的空间有实际意义。
3. 在函数内部,先做读操作,后做写操作。
4. 函数调用结束后,充当函数返回值。
一个文件主要由:dentry、inode和数据块
目录项:包括文件名和inode节点号。
Inode:又称文件索引节点,包含文件的基础信息以及数据块的指针。
数据块:包含文件的具体内容,保存在磁盘里
inode:
本质是结构体,存储文件的属性信息,如:权限、类型、大小、时间、用户(ls -l出的)、盘块位置…也叫做文件属性管理结构,大多数的inode都存储在磁盘上。
少量常用、近期使用的inode会被缓存到内存中。
所谓的删除文件,就是删除inode,但是数据其实还是在硬盘上,以后会覆盖掉。
所以数据恢复就是重新建立inode,指向硬盘
创建硬链接时,就是增加一个一个的目录项(dentry),目录项不同,Inode相同
dentry:
本质依然是结构体,重要成员变量有两个 {文件名,inode,…}
man 2 open
如果创建一个新文件就使用第二个open,打开一个文件就使用第一个open
成功打开或创建返回一个文件描述符(非负整数),出错返回-1
pathname 文件路径
flags 权限控制
O_RDONLY|O_WRONLY|O_RDWR | O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK ....
O_RDONLY:只读
O_WRONLY:只写
O_RDWR:读写
O_CREAT:创建一个新的文件
O_APPEND:追加,再往里头写东西
O_EXCL:判断文件是否存在
O_TRUNC:截断文件大小为0,相当于清空
fd = open("./dict.cp", O_RDONLY | O_CREAT | O_TRUNC, 0644);//rw-r--r--
//如果文件存在,把它以只读方式打开,并截断为零;要是文件不存在就进行创建,并指定文件权限是644
mode: 参数3使用的前提,参2指定了 O_CREAT。 取值8进制数,用来描述文件的 访问权限。
rwx 0664
创建文件最终权限 = mode & ~umask(文件默认权限775)
文件权限 = mode & ~umask
打开文件不存在
以写方式打开只读文件(权限问题)
以只写方式打开目录
当open出错时,程序会自动设置errno,可以通过strerror(errno)来查看报错数字的含义
以打开不存在文件为例:
include
include包含在include 中
O_RDONLY, O_WRONLY,O_RDWR包含在include
include是输出的
#include
#include
#include
#include
#include
int main (int argc, char *argv[]){
int fd;
fd = open("./wife.c",O_RDONLY);
printf("fd = %d, errno = %d:%s\n", fd, errno, strerror(errno));
close(fd);
return 0;
}
执行结果为:
int close(int fd);
关闭打开文件,一般有open就要有close
ssize_t read(int fd, void *buf, size_t count);
参数:
fd:文件描述符
buf:存数据的缓冲区
count:缓冲区大小
返回值:
0:读到文件末尾。
成功; > 0 读到的字节数。
失败: -1, 设置 errno
-1: 并且 errno = EAGIN 或 EWOULDBLOCK, 说明不是read失败,而是read在以非阻塞
方式读一个设备文件(网络文件),并且文件无数据。
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd:文件描述符
buf:待写出数据的缓冲区
count:数据大小
返回值:
成功; 写入的字节数。
失败: -1, 设置 errno
int fd2 = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0664)
//如果文件存在以读写方式打开,并将其截断为零(清空);如果文件不存在就创建文件,并赋予0664的
权限
int mian(int argc, char* argv[])
//char* argv[]声明一个数组argv,该数组保存多个指向char类型的指针,
//char** argv[]声明argv为指向char类型指针的指针
即:一个是数组类型的声明,一个是指针类型的声明
//argc是命令行总的参数个数
//argv[]是argc的参数,其中第0个参数是程序的全名,以后的参数是命令行后面跟的用户输入的参数
比如:
test.c
#include
int main(int argc, char *argv[])
{
printf("argc is %d\n",argc);
for(int i=0;i
编译gcc test.c -o test后,执行./test
结果:
argc is 1
argv[0] is: ./argtest
表明在执行这个程序时,输入的参数只有一个,而且这个参数就是执行程序的这个命令
执行./test 123 abc
结果:
argc is 3
argv[0] is: ./test
argv[1] is: 123
argv[2] is: abc
表明程序输入的参数有3个,命令的后面两个用空格分隔的字符串都传给了main函数。
通过argc和argv[ ]我们就可以通过命令向程序传递参数了
#include
#include
#include
#include
#include
int main (int argc, char *argv[]){
int fd1;
int fd2;
char buf[1024];//缓冲区
int n = 0;
fd1 = open(argv[1],O_RDONLY);
if(fd1 == -1) {
perror("open argv[1] error");//错误处理函数
//printf("xxx error: %d\n", errno);也可以
exit(1);//退出
}
fd2 = open(argv[2],O_RDWR|O_CREAT|O_TRUNC, 0664);
while((n = read(fd1, buf, 1024)) != 0){
write(fd2, buf, n);
}
close(fd1);
close(fd2);
return 0;
}
make test编译后
./test hello.c hello1.c
hello1.c中与hello.c相同
用来改变一个【已经打开】的文件的 访问控制属性
int fcntl (int fd, int cmd, ... /*arg*/ );
int flgs = fcntl(fd, F_GETFL);//获取文件状态
flgs|= O_NONBLOCK //非阻塞
fcntl(fd, F_SETFL, flgs);//设置文件状态
获取文件状态: F_GETFL
设置文件状态: F_SETFL
off_t lseek(int fd, off_t offset, int whence);
参数:
fd:文件描述符
offset: 偏移量,就是将读写指针从whence指定位置向后偏移offset个单位(字节)
whence:起始偏移位置: SEEK_SET/SEEK_CUR/SEEK_END
SEEK_SET:文件起始位置;
SEEK_CUR:当前位置;
SEEK_END:文件末尾位置;
返回值:
成功:较起始位置偏移量
失败:-1 errno
应用场景:
1.文件的“读”、“写”使用同一偏移位置。
2.使用lseek获取文件大小
3. 使用lseek拓展文件大小:要想使文件大小真正拓展,必须引起IO操作。
使用 truncate 函数,直接拓展文件。 int ret = truncate("dict.cp", 250);
写一个句子到空白文件,之后调整光标位置,读取刚才写那个文件。如果不调整光标位置,是读取不到内容的,因为读写指针在内容的末尾
代码如下:
1. #include
2. #include
3. #include
4. #include
5. #include
6.
7. int main(void)
8. {
9. int fd, n;
10. char msg[] = "It's a test for lseek\n";
11. char ch;
12.
13. fd = open("lseek.txt", O_RDWR|O_CREAT, 0644);
14. if(fd < 0){
15. perror("open lseek.txt error");
16. exit(1);
17. }
18.
19. write(fd, msg, strlen(msg)); //使用fd对打开的文件进行写操作,读写位置位于文件结尾处。
20.
21. lseek(fd, 0, SEEK_SET); //修改文件读写指针位置,位于文件开头。
22.
23. while((n = read(fd, &ch, 1))){
24. if(n < 0){
25. perror("read error");
26. exit(1);
27. }
28. write(STDOUT_FILENO, &ch, n); //将文件内容按字节读出,写出到屏幕
29. }
30.
31. close(fd);
32.
33. return 0;
34. }
编译后:
用lseek的偏移来读取文件大小
结果如下:
使用lseek将fcntl.c原本的698字节填充为800字节,差值102字节:
其实比起读取大小的代码,就时修改了个偏移量,然后要引起IO操作,写入一个“$”
编译运行:
stat函数
获取文件属性,(从inode结构体中获取)
stat会拿到符号链接指向那个文件或目录的属性。
不想穿透符号就用lstat
stat/lstat 函数:
int stat(const char *path, struct stat *buf);
参数:
path: 文件路径
buf:(传出参数) 存放文件属性,inode结构体指针。
返回值:
成功: 0
失败: -1 errno
获取文件大小: buf.st_size
获取文件类型: buf.st_mode
获取文件权限: buf.st_mode
符号穿透:stat会。lstat不会。
可以使用stat获取文件大小
link和Unlink隐式回收
硬链接数就是dentry数目
link就是用来创建硬链接的
link可以用来实现mv命令
函数原型:
int link(const char *oldpath, const char *newpath)
用这个来实现mv,用oldpath来创建newpath,完事儿删除oldpath就行。
删除一个链接 int unlink(const char *pathname)
unlink是删除一个文件的目录项dentry,使硬链接数-1
unlink函数的特征:清除文件时,如果文件的硬链接数到0了,没有dentry对应,但该文件仍不会马上被释放,要等到所有打开文件的进程关闭该文件,系统才会挑时间将该文件释放掉。
目录操作函数:头文件
DIR * opendir(char *name);
DIR*是目录结构体指针,类似于文件结构体指针(FILE*)
DIR*其对应于目录结构体,实现方式是无法看到的
int closedir(DIR *dp);
struct dirent *readdir(DIR * dp);
struct dirent {
inode
char dname[256];
}
没有写目录操作,因为目录写操作就是创建文件。可以用touch
char dname[256];表明文件名最大是255
用来做重定向,本质就是复制文件描述符:
dup 和 dup2:
int dup(int oldfd); 文件描述符复制。
oldfd: 已有文件描述符
返回:新文件描述符,这个描述符和oldfd指向相同内容。
int dup2(int oldfd, int newfd); 文件描述符复制,oldfd拷贝给newfd。返回newfd
下一个部分就是进程知识了!!!!!!!!!!!