1.区分系统调用和库函数
库函数是我们一开始进行学习就接触到的函数,是c语言中已经包装好的函数,所以这里不多叙述。但是我们要理解系统调用,系统调用是用户进入"内核态"的门(进入内核态 = 让操作系统"亲自出马",帮你完成只有它才能做的底层、敏感、特权操作)。只有使用系统调用,我们写的c语言程序才能真正的进入内核态,用系统提供的函数来完成我们想做的工作。
总结一句话:我们可以通过系统调用进入内核态,并且真正调用内核中提供的函数,这就是系统调用。
系统调用 是 用户程序进入内核 的"门";
库函数 是 在用户空间 帮你"包装"这些门、或额外提供功能的"工具箱"。
📌 具体区别(对照表)
维度 | 系统调用(System Call) | 库函数(Library Function) |
---|---|---|
所在空间 | 内核空间 | 用户空间 |
调用方式 | 触发软中断(syscall 指令) |
普通函数调用(call 指令) |
执行权限 | 需要切换到内核态(ring 0) | 用户态即可(ring 3) |
执行速度 | 慢(用户↔内核切换开销) | 快(无上下文切换) |
功能范围 | 直接与硬件、资源交互(如文件、进程、网络) | 可以是系统调用的封装,也可以纯计算(如字符串处理) |
例子 | open , read , write , fork , execve |
fopen , fread , printf , malloc , strlen |
来源 | Linux 内核提供 | libc(如 glibc)或其他库提供 |
可移植性 | 依赖操作系统(Linux 特有) | 标准库函数跨平台(如 ANSI C) |
✅ 总结一句话:
系统调用是"内核提供的最小功能单元",库函数是"程序员友好的封装或扩展"。
写系统程序时,优先用库函数 (更简单、更安全),需要精细控制或性能时再直接用系统调用。
2.open和close函数
所有函数都可以通过查看手册的方式进行学习,不过难度比较大,这里贴出打开手册的方法,供有志者学习。
bash
man 2 open
open 函数
int open(char *pathname, int flags);
pathname:要打开的文件路径
flags :文件打开方式
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读写
O_CREAT 不存在则创建
O_APPEND 追加写
O_TRUNC 清空再写
O_EXCL 必须创建且文件不存在
O_NONBLOCK 非阻塞
返回值
成功:文件描述符(非负整数)
失败:-1,并设置 errno
三参数原型
int open(char *pathname, int flags, mode_t mode);
参数
pathname:文件路径
flags :同上
mode :仅当 flags 含 O_CREAT 时生效(八进制)
例:0664 → rw-rw-r--
最终权限 = mode & ~umask
"最终权限 = mode & ~umask" 就是 "你给的权限先去掉 umask 禁止的位,才是真正落盘的权限。" 这里是二进制,我们用二进制的每一位的0,1来代表是否打开权限。
返回值
成功:文件描述符
失败:-1,并设置 errno
close函数
close 函数
int close(int fd);
demo
cpp
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd;
/* 1. 权限:读+写;2. 不存在则创建;3. 若已存在则清空 */
fd = open("./dict.cp",
O_RDWR | O_CREAT | O_TRUNC,
0644); /* rw-r--r-- */
/* 4. 必须检查返回值 */
if (fd == -1) {
//stderr标准输入流 默认是终端
fprintf(stderr, "open failed: %s\n", strerror(errno));
return 1;
}
printf("fd = %d\n", fd);
/* 5. 关闭同样检查错误(虽然极少失败,但养成习惯) */
if (close(fd) == -1) {
//错误处理函数
perror("close");
return 1;
}
return 0;
}
2.1文件描述符:
相当于拿到了进程中文件的唯一id,用这个id去控制文件

2.2预读入和缓输出

-
预读入 :提前 把用户还没要的 数据块从磁盘读进页缓存,等你真正
read()
时直接命中内存,磁盘几乎零等待。 -
缓输出 :先 把用户
write()
的数据写进页缓存 就立即返回,内核稍后 再异步刷盘(或按fsync/O_SYNC
强制落盘)。
2.3阻塞和非阻塞(设备文件或者网络文件)
ftml函数
int fcntl(int fd, int cmd, ... /* arg */ );
参数
-
fd
:文件描述符。 -
cmd
:命令,指定要执行的操作。 -
arg
:命令的参数,具体取决于cmd
的值。
返回值
-
成功时,返回执行命令后的结果,这可能因命令不同而有所不同。
-
失败时,返回 -1,并设置
errno
以指示错误。
常见的命令
-
F_DUPFD
:复制文件描述符。这个命令会创建一个新的文件描述符,它与原始文件描述符引用同一个文件。 -
F_GETFD
:获取文件描述符的标志。这些标志控制文件描述符的行为,例如是否应该在执行exec
函数时关闭文件描述符。 -
F_SETFD
:设置文件描述符的标志。 -
F_GETFL
:获取文件状态标志。这些标志控制文件的打开模式,例如是否为只读或只写。
阻塞(可以简单的理解为文件的一种属性 设备文件和网络文件)
阻塞:阻塞掉CPU等待输入
非阻塞:返回错误信息,不会阻塞。
设置文件的阻塞和非阻塞(一种属性)
cpp
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int set_nonblock(int fd, int on) {
int flags = fcntl(fd, F_GETFL);
if (flags == -1) return -1;
if (on)
flags |= O_NONBLOCK;
else
flags &= ~O_NONBLOCK;
//设置flag,成功返回1,否则返回0
return fcntl(fd, F_SETFL, flags);
}
int main() {
int fd = open("fifo", O_RDWR);
if (fd == -1) { perror("open"); return 1; }
set_nonblock(fd, 1); /* 设为非阻塞 */
/* ... 读写 ... */
set_nonblock(fd, 0); /* 恢复阻塞 */
close(fd);
return 0;
}
2.4使用fcntl函数设置文件描述符
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
int fd1 = open(argv[1], O_RDWR);
printf("fd1 = %d\n", fd1);
int newfd = fcntl(fd1, F_DUPFD, 0); // 0被占用,fcntl使用文件描述符表中可用的最小文件描述符返回
printf("newfd = %d\n", newfd);
int newfd2 = fcntl(fd1, F_DUPFD, 7); // 7,未被占用,返回 >= 7 的文件描述符.
printf("newfd2 = %d\n", newfd2);
return 0;
}
3.read和write函数
read 函数
原型:
ssize_t read(int fd, void *buf, size_t count);
参数:
fd 文件描述符
buf 存放读入数据的缓冲区
count 缓冲区大小(最多读多少字节)
返回值:
成功:实际读到的字节数(可能 < count)
失败:-1,并设置 errno
-1:并且errno=BAGIN或EWOULDBLOCK,说明不是read失败,而是被阻塞(设备文件或者网络文件)
write 函数
原型:
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd 文件描述符
buf 待写入数据的缓冲区
count 要写入的字节数
返回值:
成功:实际写入的字节数(可能 < count)
失败:-1,并设置 errno
demo(实现copy效果)
cpp
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, char *argv[])
int fd1 = open(argv[1], O_RDONLY);
if (fd1 == -1) { perror("open src"); return 1; }
//以读写打开,有就截断为空,没有就是create,0664是创建的权限
int fd2 = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (fd2 == -1) { perror("open dst"); close(fd1); return 1; }
char buf[1024]; /* 64K 缓冲更快 */
ssize_t n;
while ((n = read(fd1, buf, sizeof(buf))) > 0) {
if (write(fd2, buf, n) != n) { perror("write"); break; }
}
if (n == -1) perror("read");
close(fd1);
close(fd2);
return 0;
}
4.lseek函数
lseek 函数(控制光标的偏移)
off_t lseek(int fd, off_t offset, int whence);
参数:
fd 文件描述符
offset 偏移量(字节数)
whence 偏移基准
SEEK_SET 文件开头
SEEK_CUR 当前位置
SEEK_END 文件末尾
返回值:
成功:返回新的偏移量(相对于文件起始位置的字节数)
失败:-1,并设置 errno
demo(1. 文件的"读"和"写"使用同一偏移位置。)
cpp
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main(void)
{
int fd, n;
char msg[] = "It's a test for lseek\n";
char ch;
fd = open("lseek.txt", O_RDWR | O_CREAT, 0644);
if (fd < 0) {
perror("open lseek.txt error");
exit(1);
}
write(fd, msg, strlen(msg));
/* 使用 fd 对打开的文件进行写操作,文件读写位置位于文件结尾处 */
lseek(fd, 0, SEEK_SET);
/* 修改文件读写指针位置,位于文件开头。注释该行会怎样呢? */
while ((n = read(fd, &ch, 1))) {
if (n < 0) {
perror("read error");
exit(1);
}
/* 此处缺少输出或处理逻辑,可自行补充 */
}
close(fd);
return 0;
}
demo(2. 使用 lseek 获取文件大小。)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd = open(argv[1], O_RDWR);
if (fd == -1) {
perror("open error");
exit(1);
}
int length = lseek(fd, 0, SEEK_END);
printf("file size: %d\n", length);
close(fd);
return 0;
}
demo(3. 使用 lseek 拓展文件大小:要想使文件大小真正拓展,必须引起一次 IO 操作(如 write 一个字节)
cpp
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd = open(argv[1], O_RDWR);
if (fd == -1) {
perror("open error");
exit(1);
}
int length = lseek(fd, 110, SEEK_END);
printf("file size:%d\n", length);
//引起真正的io操作,否则只是简单的偏移。
write(fd, "a", 1);
close(fd);
return 0;
}
5.inode和detry
总结一句话是:inode本身是文件的一种数据结构,他记录了许多文件的属性。dentry是目录项一个目录中有许多dentry,dentry记录了文件名->inode的映射。
inode(索引节点)
-
inode 是文件系统中的一个数据结构,用于存储文件的元数据(metadata),但不包括文件名。
-
元数据 包括文件的权限、所有者、大小、创建时间、修改时间、访问时间、文件类型(如普通文件、目录、符号链接等)、以及指向文件数据块的指针等。
-
每个文件在文件系统中都有一个唯一的 inode 号,通过这个 inode 号可以找到文件的 inode,进而访问文件的元数据和数据块。
dentry(目录项)
-
dentry 是目录中的一个条目,它将文件名与 inode 号关联起来。
-
目录际上是一个特殊的文件,其中包含了多个 dentry,每个 dentry 包含一个文件名和一个指向 inode 的指针。
- · 当你访问一个文件时,系统首先在目录中查找对应的 dentry,通过 dentry 中的 inode 号找到文件的 inode,然后通过 inode 访问文件数据。
6.lstat和stat函数
其实就是用文件名获得inode,通过inode结构体获取文件的一些属性。
stat/lstat 函数:
int stat(const char *path, struct stat *buf);
参数:
path:文件路径
buf:(传出参数)存放文件属性。
返回值:
成功:0
失败:-1 errno
获取文件大小:buf.st_size
获取文件类型:buf.st_mode
获取文件权限:buf.st_mode
符号穿透:stat会。lstat不会。
demo
cpp
#include <sys/stat.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
struct stat sb;
int ret = stat(argv[1], &sb);
if (ret == -1) {
perror("stat error");
exit(1);
}
if (S_ISREG(sb.st_mode)) {
printf("It's a regular\n");
} else if (S_ISDIR(sb.st_mode)) {
printf("It's a dir\n");
} else if (S_ISFIFO(sb.st_mode)) {
printf("It's a pipe\n");
} else if (S_ISLNK(sb.st_mode)) {
printf("it's a sym link\n");
}
return 0;
}
文件权限位

7.link和unlink函数
link函数

硬链接和软连接的区别主要是在,硬链接是不同的dentry但还是相同的inode,相当于给文件取了一个别名(我的理解)而软连接是新创建了一个文件用于链接可以类比windows中的快捷方式。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
link(argv[1], argv[2]);
unlink(argv[1]);
return 0;
}
unlink函数(调用unlink后并没有直接删除,而是系统择机删除)

cpp
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main(void)
{
int fd;
char *p = "test of unlink\n";
char *p2 = "after write something.\n";
fd = open("temp.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0){
perror("open temp error");
exit(1);
}
int ret = write(fd, p, strlen(p));
if (ret == -1) {
perror("----write error");
}
printf("hi! I'm printf\n");
ret = write(fd, p2, strlen(p2));
if (ret == -1) {
perror("----write error");
}
printf("Enter anykey continue\n");
getchar();
close(fd);
int unlink_ret = unlink("temp.txt");
if(unlink_ret < 0){
perror("unlink error");
exit(1);
}
return 0;
}
7.opendir和closedir以及readdir()
7.1文件夹的权限概念

7.2opendir()
根据传入的目录名打开一个目录(库函数)
DIR *opendir(const char *name); 成功返回指向该目录结构体指针,失败返回 NULL
参数支持相对路径、绝对路径两种方式:DIR是目录结构体,目录里有许多目录项(dentry)
'例如:打开当前目录:① getcwd(), opendir() ② opendir(".");
7.3closedir()
关闭打开的目录。
int closedir(DIR *dirp); 成功:0;失败:-1 设置 errno 为相应值。
7.4readdir函数
struct dirent *readdir(DIR *dirp);
参数
dirp
:一个指向DIR
结构的指针,该结构由opendir
函数返回,表示一个已经打开的目录。
返回值
-
成功时,
readdir
返回一个指向struct dirent
结构的指针,该结构包含了目录中下一个条目的信息。 -
如果到达了目录流的末尾,返回
NULL
。 -
如果发生错误,也返回
NULL
并设置errno
以指示错误类型。
返回值struct dirent内容
cpp
#include <dirent.h>
struct dirent {
long d_ino; // 文件的 inode 编号
off_t d_off; // 文件在目录中的偏移量
unsigned short d_reclen; // 结构体的长度
unsigned char d_type; // 文件类型
char d_name[256]; // 文件或目录的名称
};
demo(实现递归遍历目录)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
void isFile(char *path);
void isDir(char *path){
DIR *dir = opendir(path);
char buf[256];
if(dir == NULL){
perror("opendir error");
exit(1);
}
struct dirent *entry;
while((entry = readdir(dir)) != NULL){ {
//递归调用需要拼接路径 默认目录里都带有.和..代表当前文件夹和上一级文件夹
if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0){
continue; // 跳过当前目录和上级目录
}
//path是目录,d_name是文件名
sprintf(buf, "%s/%s", path, entry->d_name);
isFile(buf);
}
}
}
void isFile(char *path){
struct stat buf;
int ret = stat(path,&buf);
if(ret==-1){
perror("stat error");
exit(1);
}
if(S_ISDIR(buf.st_mode)){
//目录处理逻辑
isDir(path);
}
//文件处理逻辑
printf("%s \t %ld\n",path,buf.st_size);
}
int main(int argc, char *argv[]) {
if(argc==1){
isFile(".");
}else{
//遍历多参数
for(int i=1;i<argc;i++){
isFile(argv[i]);
}
}
return 0;
}
8.dup和dup2重定向函数
这里的重定向其实就是将fd(文件描述符)重定向掉其他文件,比如fd=1 本来指向hello.c文件,我们可以通过重定向,将fd=1 指向world.c文件。 这样可以实现相同的fd来操作不同的文件。
8.dup和dup2重定向函数
dup 函数
dup
函数用于复制一个现有的文件描述符,并返回一个新的文件描述符,该描述符与原始文件描述符引用相同的文件或资源。
函数原型
int dup(int oldfd);
参数
oldfd
:要复制的原始文件描述符。
返回值
-
成功时,返回一个新的文件描述符。
-
失败时,返回 -1,并设置
errno
以指示错误。
dup2函数
dup2
函数不仅复制文件描述符,还可以指定新文件描述符的值。如果指定的新文件描述符已经打开,它会被关闭并替换为新的文件描述符。
int dup2(int oldfd, int newfd);
参数
-
oldfd
:要复制的原始文件描述符。 -
newfd
:指定的新文件描述符的值。
返回值
-
成功时,返回新的文件描述符(通常与
newfd
相同)。 -
失败时,返回 -1,并设置
errno
以指示错误。
demo
cpp
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
int fd1 = open(argv[1], O_RDWR); // 012 --- 3
int fd2 = open(argv[2], O_RDWR); // 012 --- 3
//如果无异常fd2和fdret相同
int fdret = dup2(fd1, fd2);
printf("fdret = %d\n", fdret);
//向fd2写入数据,但其实此时已经是像fd1写入数据了
int ret = write(fd2, "1234567", 7);
printf("ret = %d\n", ret);
//此时可以用新的操作旧的了。这里新的是标准输入流,如果对标准输入流进行读操作,实际上是对fd1进行读操作
dup2(fd1, STDOUT_FILENO);
printf("-----------------------------886\n");
return 0;
}