Linux 进程控制 基础IO
Linux 进程控制学习笔记
本节重点
- 学习进程创建:
fork()
/vfork()
- 学习进程等待
- 学习进程程序替换:
exec
函数族,微型 shell 实现原理 - 学习进程终止:认识
$?
一、进程创建
1. fork()
函数初识
在 Linux 中,fork()
函数用于从一个已存在的进程中创建一个新进程。新进程称为子进程,原进程称为父进程。
函数原型:
C
#include <unistd.h>
pid_t fork(void);
返回值:
- 在子进程中返回
0
。 - 在父进程中返回子进程的 ID。
- 如果出错,返回
-1
。
内核操作流程:
- 为子进程分配新的内存块和内核数据结构。
- 将父进程的部分数据结构内容拷贝至子进程。
- 将子进程添加到系统进程列表中。
fork()
返回后,开始由调度器调度。
fork()
调用前后对比:
- 调用前: 单个进程执行。
- 调用后: 产生父子两个进程,它们拥有几乎相同的代码和数据空间(通过写时拷贝技术共享),但从
fork()
返回后,它们可以执行不同的代码路径。 调度器决定哪个进程先执行。
示例代码:
C
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h> // For exit()int main(void) {pid_t pid;printf("Before: pid is %d\n", getpid()); [cite: 1]if ((pid = fork()) == -1) {perror("fork()");exit(1);}printf("After: pid is %d, fork return %d\n", getpid(), pid); [cite: 2]sleep(1);return 0;
}
运行结果分析:
输出会有三行:一行 “Before” 和两行 “After”。父进程打印 “Before” 和它自己的 “After”。子进程只打印它自己的 “After”,因为它从 fork() 之后开始执行,不会执行 fork() 之前的 printf 语句。
2. 写时拷贝 (Copy-on-Write)
fork()
之后,父子进程的代码段是共享的。数据段在不发生写入操作时也是共享的。当父进程或子进程尝试写入共享数据时,内核会为写入方创建一个该数据页的副本,从而实现父子进程数据段的独立性。这种机制称为写时拷贝。
3. fork()
常规用法
- 一个父进程复制自己,使父子进程同时执行不同的代码段(例如,父进程等待客户端请求,子进程处理请求)。
- 一个进程要执行一个不同的程序。子进程从
fork()
返回后,通常会调用exec
系列函数来加载并执行新的程序。
4. fork()
调用失败的原因
- 系统中的进程数量过多。
- 单个用户的进程数量超过了系统限制。
二、进程终止
1. 进程退出场景
- 代码运行完毕,结果正确。
- 代码运行完毕,结果不正确。
- 代码异常终止。
2. 进程常见退出方法
正常终止 (可通过 echo $?
查看进程退出码):
- 从
main
函数返回。 - 调用
exit()
函数。 - 调用
_exit()
函数。
异常退出:
- 通过信号终止,例如
Ctrl+C
。
3. _exit()
函数
函数原型:
C
#include <unistd.h>
void _exit(int status);
参数: status
定义了进程的终止状态,父进程可以通过 wait()
或 waitpid()
来获取该值。虽然 status
是 int
类型,但只有低8位可以被父进程获取。例如,_exit(-1)
后,在终端执行 echo $?
会得到 255
。
_exit()
直接请求内核终止进程,不会执行用户定义的清理函数或刷新标准I/O缓冲区。
4. exit()
函数
函数原型:
C
#include <stdlib.h> // 注意头文件与 _exit 不同
void exit(int status);
exit()
函数在调用 _exit()
之前会执行以下操作:
- 执行用户通过
atexit()
或on_exit()
定义的清理函数。 - 关闭所有打开的流,并将所有缓存数据写入。
- 调用
_exit()
。
示例对比 exit()
和 _exit()
:
- 使用
exit(0)
:printf("hello");
后会输出 “hello”。 - 使用
_exit(0)
:printf("hello");
后可能不会输出 “hello”,因为标准输出缓冲区可能未被刷新。
5. return
退出
在 main
函数中执行 return n;
等同于执行 exit(n);
。
三、进程等待
1. 进程等待的必要性
- 防止僵尸进程: 子进程退出后,如果父进程没有对其进行处理(即调用
wait()
或waitpid()
回收子进程资源),子进程会变成僵尸进程,占用系统资源,可能导致内存泄漏。 僵尸进程无法被kill -9
杀死。 - 获取子进程退出信息: 父进程需要知道子进程的任务完成情况,例如是否正常退出、退出码是多少。
2. 进程等待的方法
a. wait()
方法
函数原型:
C
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
返回值:
- 成功:返回被等待的子进程 ID。
- 失败:返回
-1
。 参数: status
:输出型参数,用于获取子进程的退出状态。如果不关心,可以设置为NULL
。
b. waitpid()
方法
函数原型:
C
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
-
正常返回:返回收集到的子进程的进程 ID。
-
如果设置了
WNOHANG
选项,并且没有已退出的子进程可收集:返回0
。 -
调用出错:返回
-1
。 参数: -
pid
:
pid == -1
:等待任一个子进程(与wait()
等效)。pid > 0
:等待进程 ID 与pid
相等的子进程。
-
status
:与
wait()
中的
status
类似,用于获取子进程退出状态。
WIFEXITED(status)
:若为正常终止子进程返回的状态,则为真(用于判断进程是否正常退出)。WEXITSTATUS(status)
:若WIFEXITED(status)
非零,则提取子进程的退出码(低8位)。
-
options
:
WNOHANG
:若pid
指定的子进程没有结束,则waitpid()
函数立即返回0
,不予以等待。若正常结束,则返回该子进程的 ID。
调用 wait()
/ waitpid()
的行为:
- 如果子进程已经退出:
wait()
/waitpid()
会立即返回,释放子进程资源,并获取其退出信息。 - 如果子进程存在且正常运行:进程可能会阻塞等待子进程退出(除非使用
WNOHANG
)。 - 如果不存在指定的子进程:立即出错返回。
3. 获取子进程 status
status
参数是一个输出型参数,由操作系统填充,不能简单地当作整型看待,应将其视为位图。只关注 status
的低16比特位:
- 正常终止:
- 位 0-7:为 0。
- 位 8-15:为子进程的退出状态(即
exit()
或main
return 的值)。
- 被信号所杀:
- 位 0-6:为导致进程终止的信号编号。
- 位 7:如果为 1,表示产生了 core dump 文件。
宏进行判断和提取:
WIFEXITED(status)
:如果子进程正常终止,则为真。WEXITSTATUS(status)
:如果WIFEXITED(status)
为真,则此宏提取子进程的退出码。WIFSIGNALED(status)
:如果子进程是因为一个未被捕获的信号终止的,则为真。WTERMSIG(status)
:如果WIFSIGNALED(status)
为真,则此宏提取导致子进程终止的信号编号。
测试代码示例(手动解析 status):
C
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // For sleep, fork, exitint main(void) {pid_t pid;if ((pid = fork()) == -1) {perror("fork");exit(1);}if (pid == 0) { // Child processsleep(2); // Simulate some work// exit(10); // Example of normal exitabort(); // Example of abnormal exit by signal (SIGABRT)} else { // Parent processint st;int ret = wait(&st); [cite: 10]if (ret > 0) {if ((st & 0x7F) == 0) { // Normal exit, bits 0-6 are 0 [cite: 11]printf("Child %d exited normally with code: %d\n", ret, (st >> 8) & 0xFF); [cite: 11]} else { // Abnormal exit by signalprintf("Child %d terminated by signal: %d\n", ret, st & 0x7F); [cite: 12]if (st & 0x80) { // Check core dump flag (bit 7)printf("Core dumped\n");}}} else {perror("wait");}}return 0;
}
使用宏的推荐代码:
C
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main() {pid_t pid;pid = fork(); [cite: 14]if (pid < 0) {printf("%s fork error\n", __FUNCTION__); [cite: 14]return 1;} else if (pid == 0) { // childprintf("child is run, pid is: %d\n", getpid()); [cite: 14]sleep(5);exit(257); // Exit code will be 1 (257 & 0xFF) [cite: 15]} else { // parentint status = 0;// 阻塞式等待pid_t ret = waitpid(-1, &status, 0); [cite: 15]printf("this is test for wait\n"); [cite: 15]if (ret > 0) {if (WIFEXITED(status)) { [cite: 16]printf("wait child %d success, child return code is:%d.\n", ret, WEXITSTATUS(status)); [cite: 16]} else if (WIFSIGNALED(status)) {printf("child %d terminated by signal %d\n", ret, WTERMSIG(status));} else {printf("wait child failed, return.\n"); [cite: 16]}} else {printf("waitpid error\n");return 1; [cite: 17]}}return 0; [cite: 18]
}
运行结果分析:
子进程 PID (例如 45110) 会被打印,然后父进程会等待5秒,打印 “this is test for wait”,最后打印子进程的退出码 (这里是 1,因为 257 & 0xFF = 1)。
4. 非阻塞等待
通过 waitpid()
的 WNOHANG
选项实现非阻塞等待。父进程可以周期性地检查子进程是否退出,而不会一直阻塞。
示例代码 (非阻塞等待):
C
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main() {pid_t pid;pid = fork(); [cite: 19]if (pid < 0) {printf("%s fork error\n", __FUNCTION__); [cite: 19]return 1;} else if (pid == 0) { // childprintf("child is run, pid is: %d\n", getpid()); [cite: 20]sleep(5);exit(1); [cite: 20]} else { // parentint status = 0;pid_t ret = 0;do {ret = waitpid(pid, &status, WNOHANG); // 非阻塞式等待 [cite: 21]if (ret == 0) { [cite: 21]printf("child is running, parent is doing other things...\n");sleep(1);}} while (ret == 0); [cite: 21] // Loop until child exits or errorif (ret == pid) { // Child has exitedif (WIFEXITED(status)) { [cite: 22]printf("wait child %d success, child return code is:%d.\n", ret, WEXITSTATUS(status)); [cite: 22]} else if (WIFSIGNALED(status)) {printf("child %d terminated by signal %d\n", ret, WTERMSIG(status));}} else if (ret == -1) {perror("waitpid error");return 1;} else {printf("wait child failed, return.\n"); // Should not happen in this logic if ret > 0 [cite: 22]// return 1; [cite: 23] // Original code has return 1 here}}return 0; [cite: 23]
}
四、进程程序替换
1. 替换原理
当用 fork()
创建子进程后,子进程执行的是和父进程相同的程序(可能执行不同的代码分支)。如果子进程需要执行另一个程序,它会调用 exec
系列函数。当进程调用 exec
函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。调用 exec
并不创建新进程,因此调用前后进程的 ID 不会改变。
替换过程示意图:
原进程的虚拟内存(代码段、数据段、堆栈)被新程序的相应部分替换。页表会更新以映射到新程序的物理内存页(通常从磁盘加载可执行文件ELF)。进程控制块(PCB)中的一些信息(如进程ID)保持不变。
2. exec
函数族
有六个以 exec
开头的函数,统称为 exec
函数:
C
#include <unistd.h>int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /* (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
函数解释:
- 如果调用成功,则加载新的程序从其启动代码开始执行,不再返回到原程序的调用点。
- 如果调用出错,则返回
-1
。 - 因此,
exec
函数只有出错的返回值,没有成功的返回值。
命名规则理解:
l
(list):表示参数采用参数列表的形式(可变参数,以NULL
结尾)。v
(vector):表示参数采用字符串指针数组 (char *argv[]
) 的形式(以NULL
结尾)。p
(path):表示函数会自动在环境变量PATH
指定的目录中搜索要执行的程序文件。e
(environment):表示允许调用者传递一个新的环境变量数组给被执行的程序。如果不带e
,则新程序继承当前进程的环境变量。
各 exec
函数特性总结:
函数名 | 参数格式 | 是否带路径 (自动搜索PATH) | 是否使用当前环境变量 (或自定义) |
---|---|---|---|
execl | 列表 | 否 (需提供完整路径) | 是 (继承当前环境) |
execlp | 列表 | 是 (自动搜索PATH) | 是 (继承当前环境) |
execle | 列表 | 否 (需提供完整路径) | 否 (需自己组装环境变量) |
execv | 数组 | 否 (需提供完整路径) | 是 (继承当前环境) |
execvp | 数组 | 是 (自动搜索PATH) | 是 (继承当前环境) |
execve | 数组 | 否 (需提供完整路径) | 否 (需自己组装环境变量) |
exec
调用示例:
C
#include <unistd.h>
#include <stdlib.h> // For exit()
#include <stdio.h> // For perror()int main() {char *const argv_ps[] = {"ps", "-ef", NULL}; [cite: 26]char *const envp_custom[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; [cite: 26]// Example 1: execl - full path, list arguments// execl("/bin/ps", "ps", "-ef", NULL); [cite: 26]// perror("execl failed"); // This line is reached only if execl fails// Example 2: execlp - command name (searches PATH), list arguments// execlp("ps", "ps", "-ef", NULL); [cite: 26]// perror("execlp failed");// Example 3: execle - full path, list arguments, custom environment// execle("/bin/ps", "ps", "-ef", NULL, envp_custom); // Note: "ps" may not be /bin/ps [cite: 27]// To be precise, use the path found by 'which ps' or a known absolute path.// For demonstration, if "ps" is in /usr/bin:// execle("/usr/bin/ps", "ps", "-ef", NULL, envp_custom);// perror("execle failed");// Example 4: execv - full path, array arguments// execv("/bin/ps", argv_ps); [cite: 27]// perror("execv failed");// Example 5: execvp - command name (searches PATH), array arguments// execvp("ps", argv_ps); [cite: 27]// perror("execvp failed");// Example 6: execve - full path, array arguments, custom environment (SYSTEM CALL)execve("/bin/ps", argv_ps, envp_custom); [cite: 27]perror("execve failed"); // This line is reached only if execve failsexit(0); // Should not be reached if exec succeeds
}
注意: execve
是真正的系统调用(在 man 手册第2节),其他五个函数都是库函数,最终都会调用 execve
(在 man 手册第3节)。 它们之间的关系可以简化为:其他 exec
函数通过不同的方式准备好参数和环境,最终调用 execve
。
3. 实现简易 Shell
一个典型的 Shell 交互过程(如 bash
)如下:
- Shell 读取用户输入的命令(例如
"ls"
)。 - Shell 创建一个新的子进程 (
fork()
)。 - 子进程使用
exec
函数族中的一个(通常是execvp
,因为它会自动搜索PATH
并使用当前环境)来执行用户指定的命令。 - 父进程(Shell)等待子进程结束 (
wait()
或waitpid()
)。 - 子进程结束后,Shell 再次提示用户输入,循环此过程。
简易 Shell 实现步骤:
- 获取命令行: 显示提示符,读取用户输入。
- 解析命令行: 将输入的字符串分割成命令名和参数列表。
- 建立子进程: 调用
fork()
。 - 替换子进程: 在子进程中,调用
execvp()
执行解析后的命令。 - 父进程等待: 父进程调用
waitpid()
等待子进程结束。
示例代码 (MiniShell):
C
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h> // For waitpid and W... macros
#include <ctype.h> // For isspace#define MAX_CMD 1024
char command[MAX_CMD]; [cite: 29]// Function to get command from user
int do_face() { [cite: 29]memset(command, 0x00, MAX_CMD); [cite: 29]printf("minishell$ ");fflush(stdout); [cite: 29]if (fgets(command, MAX_CMD, stdin) == NULL) { // Use fgets for safety// Handle EOF (Ctrl+D)if (feof(stdin)) {printf("\nExiting minishell.\n");exit(0);}perror("fgets failed");return -1;}// Remove trailing newline if presentcommand[strcspn(command, "\n")] = 0;if (strlen(command) == 0) { // Handle empty inputreturn -1;}return 0;
}// Function to parse the command string into arguments
char **do_parse(char *buff) { [cite: 30]int argc = 0;static char *argv[32]; // Max 31 arguments + NULL terminator [cite: 30]char *ptr = buff; [cite: 31]while (*ptr != '\0') {// Skip leading whitespacewhile (isspace((unsigned char)*ptr)) {ptr++;}if (*ptr == '\0') break; // End of string after whitespaceargv[argc++] = ptr; // Start of an argument [cite: 31]// Find end of the argumentwhile (*ptr != '\0' && !isspace((unsigned char)*ptr)) {ptr++; [cite: 31]}if (*ptr != '\0') { // If not end of string, it's whitespace*ptr = '\0'; // Terminate the argument string [cite: 32]ptr++; // Move to the next character}}argv[argc] = NULL; // Null-terminate the argument list [cite: 32]return argv;
}// Function to execute the command
int do_exec(char *buff) { [cite: 32]char **argv = {NULL};pid_t pid;// Basic "exit" command handlingif (strcmp(buff, "exit") == 0) {printf("Exiting minishell via 'exit' command.\n");exit(0);}pid = fork(); [cite: 32]if (pid < 0) {perror("fork failed");return -1;}if (pid == 0) { // Child process [cite: 33]argv = do_parse(buff); [cite: 33]if (argv[0] == NULL) { // No command after parsing (e.g., only whitespace)exit(1); // Or handle more gracefully [cite: 33]}execvp(argv[0], argv); [cite: 33]// If execvp returns, an error occurredperror("execvp failed");exit(127); // Standard exit code for command not found or exec error} else { // Parent processint status;waitpid(pid, &status, 0); [cite: 33]// Optionally, check child's exit status here// if (WIFEXITED(status)) {// printf("Child exited with status %d\n", WEXITSTATUS(status));// }}return 0;
}int main(int argc, char *argv_main[]) { [cite: 34]while (1) {if (do_face() < 0) {// If input is empty or error, just prompt againcontinue; [cite: 34]}do_exec(command); [cite: 34]}return 0; // Should not be reached [cite: 34]
}
五、函数与进程的相似性
Linux 将结构化程序设计中函数间通过调用、参数传递、返回值的通信模式,扩展到了程序(进程)之间:
- 函数调用 (
call
/return
) vs. 进程创建与执行 (fork
/exec
和wait
/exit
)- 一个C程序由多个函数组成。函数可以调用其他函数,传递参数,被调用函数执行操作后返回值。每个函数有其局部变量。
- 类似地,一个进程可以通过
fork()
创建子进程,然后子进程通过exec()
执行新的程序(相当于调用另一个“程序函数”),父进程可以向子进程传递参数(通过exec
的argv
和envp
)。被调用的程序执行操作后通过exit(n)
返回一个值,调用它的父进程可以通过wait(&status)
来获取这个返回值。
Linux 基础 I/O
一、C语言文件I/O回顾 (库函数)
-
核心接口:
fopen()
,fclose()
,fread()
,fwrite()
. -
文件指针:
FILE*
(如stdin
,stdout
,stderr
). -
打开模式
:
"r"
: 读 (文件需存在)"w"
: 写 (清空或创建)"a"
: 追加 (文件末尾写或创建)"r+"
: 读写 (文件需存在)"w+"
: 读写 (清空或创建)"a+"
: 读追加 (读从头,写从尾)
-
示例 (写文件)
:
C
#include <stdio.h> #include <string.h> int main() {FILE *fp = fopen("myfile", "w");if (!fp) { /* 错误处理 */ }const char *msg = "hello bit!\n";fwrite(msg, strlen(msg), 1, fp);fclose(fp);return 0; }
-
示例 (读文件)
:
C
#include <stdio.h> #include <string.h> int main() {FILE *fp = fopen("myfile", "r");if (!fp) { /* 错误处理 */ }char buf[1024];ssize_t s = fread(buf, 1, sizeof(buf)-1, fp); // 读到bufif (s > 0) {buf[s] = 0;printf("%s", buf);}fclose(fp);return 0; }
二、系统调用文件I/O
-
核心接口:
open()
,close()
,read()
,write()
,lseek()
.- 这些是操作系统提供的直接接口。C库函数 (
fopen
等) 封装了这些系统调用。
- 这些是操作系统提供的直接接口。C库函数 (
-
open()
:C
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> // int open(const char *pathname, int flags); // int open(const char *pathname, int flags, mode_t mode);
-
pathname
: 文件路径。 -
flags
: 打开选项 (按位或
|
组合):
- 必选其一:
O_RDONLY
(只读),O_WRONLY
(只写),O_RDWR
(读写). - 可选:
O_CREAT
(不存在则创建, 需mode
),O_APPEND
(追加写),O_TRUNC
(清空文件内容, 常与O_WRONLY
结合).
- 必选其一:
-
mode
: 文件权限 (如0644
), 仅当O_CREAT
时有效。受umask
影响。 -
返回值: 文件描述符 (fd, 非负整数);失败返回
-1
.
-
-
write()
:C
#include <unistd.h> // ssize_t write(int fd, const void *buf, size_t count);
fd
: 文件描述符。buf
: 数据缓冲区。count
: 期望写入的字节数。- 返回值: 实际写入的字节数;失败返回
-1
.
-
read()
:C
#include <unistd.h> // ssize_t read(int fd, void *buf, size_t count);
fd
: 文件描述符。buf
: 存储读取数据的缓冲区。count
: 期望读取的字节数。- 返回值: 实际读取的字节数 (0 表示文件末尾);失败返回
-1
.
-
close()
:C
#include <unistd.h> // int close(int fd);
- 关闭文件描述符。
-
示例 (系统调用写文件):
C
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <stdio.h> // For perrorint main() {umask(0); // 确保权限设置不受掩码影响int fd = open("myfile_sys", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd < 0) { perror("open"); return 1; }const char *msg = "hello syscall!\n";ssize_t s = write(fd, msg, strlen(msg));if (s < 0) { perror("write"); /* 处理写入错误 */ }close(fd);return 0; }
三、文件描述符 (fd)
-
概念: 内核中用于标识已打开文件的小整数。每个进程维护一个文件描述符表。
-
默认fd
:
0
: 标准输入 (stdin
) - 通常是键盘。1
: 标准输出 (stdout
) - 通常是显示器。2
: 标准错误 (stderr
) - 通常是显示器。
-
分配规则: 分配当前未使用的最小非负整数作为新的 fd。
-
task_struct
->files_struct
->fd_array[]
(指向file
结构体): 进程通过 fd 在其fd_array
中找到对应的file
结构体,该结构体包含文件信息。
四、重定向
-
- 本质
- 修改文件描述符表中 fd 指向的
file
结构体。
- 例如,关闭 fd
1
(标准输出),然后打开一个文件,新文件会获得 fd1
。此时,所有写入 fd1
的数据将进入该文件而非显示器。
-
示例 (关闭1实现输出重定向)
:
C
#include <fcntl.h> #include <unistd.h> #include <stdio.h> // For printf, fflush, perror #include <stdlib.h> // For exitint main() {close(1); // 关闭标准输出int fd = open("myfile_redir", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd < 0) { perror("open"); return 1; }//此时 fd 通常会是 1printf("fd: %d\n", fd); // 这行会写入 myfile_redirprintf("This goes to the file!\n");fflush(stdout); // 确保 printf 的内容被刷新close(fd);return 0; }
-
- 核心重定向函数。
dup2(int oldfd, int newfd)
- 使
newfd
指向oldfd
所指向的file
结构体。 - 如果
newfd
已打开,会先关闭它。 - 如果
oldfd
==newfd
,则不执行任何操作。 - 示例:
dup2(log_fd, 1);
// 将标准输出重定向到log_fd
指向的文件。
五、FILE
结构体与 fd 的关系
-
C标准库的
FILE
结构体内部封装了文件描述符fd
(通常名为_fileno
或fileno
)。 -
用户级缓冲区
:
-
printf
,
fwrite
(库函数) 自带用户级缓冲区。
- 写入文件时:全缓冲。
- 写入显示器时:行缓冲。
-
write
(系统调用) 不带用户级缓冲区 (数据直接或通过内核缓冲区写出)。 -
重定向影响:
printf
到普通文件时,缓冲方式可能从行缓冲变为全缓冲。 -
fork()
与缓冲区:fork()
后,父子进程会各自拥有缓冲区的副本 (写时拷贝)。若缓冲区未刷新,可能导致数据重复输出。
-
六、文件系统与 Inode
-
文件元数据 (Inode): 存储文件的属性信息 (大小、权限、所有者、时间戳、数据块位置等),文件名不存储在 inode 中。每个文件唯一对应一个 inode。
-
目录: 本质是文件,存储文件名和对应 inode 号的列表。
-
ls -i
: 查看文件的 inode 号。 -
stat
命令: 查看文件的详细元数据,包括 inode 号。 -
文件系统结构 (简要)
:
- Block Group: 将磁盘分区划分为多个块组。
- Super Block: 存放文件系统全局信息。
- Inode Table: 存放所有 inode。
- Data Blocks: 存放文件实际内容。
-
创建文件流程
:
- 分配空闲 inode,记录文件属性。
- 分配空闲数据块,存储文件内容。
- 在 inode 中记录数据块的分配情况。
- 在目录文件中添加 “文件名 -> inode号” 的条目。
七、硬链接与软链接
-
硬链接 (Hard Link)
:
- 多个文件名指向同一个 inode。
ln source_file hard_link_name
- inode 中有硬链接计数。删除文件时,计数减1,当计数为0且无进程打开该文件时,才真正删除文件数据。
- 不能对目录创建硬链接,不能跨文件系统。
-
软链接 (Symbolic Link / Soft Link)
:
- 一个独立的文件,其内容是另一个文件的路径名。
ln -s target_file_or_dir soft_link_name
- 类似于快捷方式。删除软链接不影响目标文件。若目标文件被删除,软链接失效 (悬空链接)。
- 可以对目录创建,可以跨文件系统。
-
文件时间戳
:
- Access: 最后访问时间。
- Modify: 文件内容最后修改时间。
- Change: 文件属性或 inode 最后修改时间。
八、静态库与动态库
-
静态库 (
.a
):
-
编译链接时,库代码被完整复制到可执行文件中。
-
优点: 运行不依赖外部库。
-
缺点: 可执行文件大,库更新需重新编译。
-
制作
:
gcc -c myfunc1.c -o myfunc1.o
(编译源文件到目标文件)ar rc libmylib.a myfunc1.o myfunc2.o
(打包目标文件成静态库)
-
使用:
gcc main.c -L. -lmylib
(-L.
指定库路径,-lmylib
指定库名mylib
)
-
-
动态库 (
.so
, Shared Object):
-
编译链接时,只记录对库函数的引用。程序运行时,由操作系统加载库代码到内存并链接。
-
优点: 可执行文件小,多程序共享库代码 (节省内存、磁盘),库更新方便。
-
缺点: 运行依赖外部库。
-
制作
:
gcc -fPIC -c myfunc1.c -o myfunc1.o
(-fPIC
: 生成位置无关代码)gcc -shared -o libmylib.so myfunc1.o myfunc2.o
(创建动态库)
-
使用 (编译):
gcc main.c -L. -lmylib
-
- 使用 (运行)
- 需让系统能找到
.so
文件:
- 拷贝到系统共享库路径 (如
/usr/lib
,/usr/local/lib
)。 - 设置环境变量
LD_LIBRARY_PATH
(如export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/your/lib
)。 - 配置
/etc/ld.so.conf.d/
下的配置文件,然后运行ldconfig
更新缓存。
-
-
库命名约定:
lib<库名>.a
或lib<库名>.so
。链接时使用-l<库名>
。
这基本涵盖了文档中的核心内容和操作,希望能满足您的复习需求。