7. 进程控制
7.1 fork
前面已经讲过了。这里再讲一遍。
#include <unistd.h>
pid_t fork(void);
//返回值:自进程中返回0,父进程返回子进程id,出错返回-1
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
7.2 进程终止
进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
7.2.1 正常终止
可以通过echo $?
查看进程退出码:
- 从main返回
- 调用exit
- _exit
$?
是一个特殊的Shell变量,用于获取上一个命令的退出状态或返回值。当你执行一个命令后,Shell会将命令的退出状态保存在 $?
变量中。
异常退出:
- ctrl + c,信号终止
_exit函数和exit函数
_exit()
函数:void _exit(int status);
_exit()
函数是系统调用,用于立即终止进程的执行,并将退出状态传递给父进程。- 它不会执行任何清理工作,包括不会刷新缓冲区、不会关闭文件描述符等。
- 退出状态
status
是一个整数值,通常用于表示程序的执行结果,0 表示成功,非零值表示出错。 - 由于
_exit()
是一个系统调用,因此它不会执行任何已注册的退出处理程序,也不会执行任何终止信号的处理。
exit()
函数:void exit(int status);
exit()
函数是C库函数,它在终止进程之前会执行一系列清理工作,例如关闭所有标准IO流、调用atexit()
注册的清理函数等。- 它最终会调用
_exit()
来终止进程,因此会执行所有的atexit()
注册函数以及执行终止信号的处理。 exit()
函数的参数和作用与_exit()
函数相同,即退出状态status
,表示程序的执行结果。
return退出
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
strerror 和 errno
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
FILE *file = fopen("non_existent_file.txt", "r");
if (file == NULL) {
fprintf(stderr, "Failed to open file: %s\n", strerror(errno));
}
return 0;
}
错误码和退出码
- 错误码(Error Code):
- 错误码是指在程序运行时发生错误时,系统或库函数提供的一个标识该错误的整数值。
- 错误码通常是由系统或库函数设置的全局变量(例如,在C语言中通常是
errno
变量),用于指示最近一次调用发生错误的原因。 - 错误码是一种机制,使程序能够了解发生了什么样的错误,从而进行适当的错误处理。通常,错误码对应于一组预定义的错误常量,每个常量表示一种特定的错误类型。
- 退出码(Exit Code):
- 退出码是指一个正在退出的进程返回给其父进程的值,用于表示其执行的结果。
- 当一个进程正常结束时,通常会返回退出码 0,表示成功完成任务。
- 非零的退出码通常表示出现了某种问题或错误,具体的值通常由程序员自行定义,以便在调用进程中识别程序执行过程中的特定情况。
7.2.2 代码异常终止
- 退出数字:
- 退出数字表示进程正常或非正常终止时的退出状态码。这个退出状态码通常是一个整数值,用于表示进程终止的原因或状态。
- 如果进程正常终止,通常会返回退出状态码0,表示成功执行。
- 非零的退出状态码通常表示出现了错误或异常情况,具体的值可以由程序员自行定义。不同的非零值可以用于区分不同的错误或异常情况。
- 信号数字:
- 信号数字表示由操作系统或其他进程发送给目标进程的信号的标识符。
- 信号是一种异步事件,用于通知进程发生了某种特定的事件或情况,如错误、异常、终止等。
- 信号在进程间通信和进程控制中起着重要作用。操作系统或其他进程可以向目标进程发送信号,目标进程可以选择忽略、处理或响应这些信号。
7.3 进程等待
7.3.1 进程等待的必要性
- 防止僵尸进程: 当子进程终止时,其退出状态会保留在系统中,但是其进程描述符和其他资源仍然占用系统资源。如果父进程不回收子进程的资源,子进程将成为僵尸进程。僵尸进程占用系统资源,如果父进程创建了大量子进程但不等待它们终止,可能会导致系统资源耗尽。
- 获取子进程退出信息: 通过等待子进程终止,父进程可以获取子进程的退出状态信息。这对于了解子进程是否正常终止,以及子进程的执行结果是否符合预期非常重要。父进程可以根据子进程的退出状态做出相应的处理,比如重新启动子进程或记录错误日志。
- 避免僵尸进程的出现: 父进程通过调用
wait()
、waitpid()
或waitid()
等等函数等待子进程终止,可以及时回收子进程的资源,避免僵尸进程的出现。这样可以确保系统资源的正常释放,提高系统的稳定性和性能。
7.3.2 wait 和 waitpid
-
wait()
函数:pid_t wait(int *status);
wait()
函数会阻塞调用它的父进程,直到任一子进程终止为止。如果父进程没有子进程或者所有子进程都在运行中,那么父进程将会一直等待。- 如果有一个子进程终止了,
wait()
函数会返回终止子进程的进程ID,并将子进程的退出状态存储在status
(输出型参数)指向的整数变量中。 wait()
函数只能等待任一子进程的终止,而无法指定具体的子进程。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t id = fork(); int cnt = 50; if(id == 0) { // child while(cnt) { printf("I am child, I have %d seconds left...\n",cnt--); sleep(1); } exit(0); } // father pid_t ret_id = wait(NULL); printf("%d %d\n",id,ret_id); sleep(10); }
-
waitpid()
函数:pid_t waitpid(pid_t pid, int *status, int options);
waitpid()
函数提供了更多的灵活性,它可以用于等待指定进程ID的子进程终止,或者等待任意子进程终止,还可以选择是否阻塞等待。- 如果
pid
参数为负数,waitpid()
函数会等待任意一个子进程终止,类似于wait()
函数的行为。 - 如果
pid
参数为0,waitpid()
函数会等待与调用进程在同一个进程组的任一子进程终止。 - 如果
pid
参数大于0,则waitpid()
函数会等待指定进程ID的子进程终止。 options
参数用于控制等待行为的附加选项,如非阻塞模式等。waitpid()
函数的返回值与wait()
函数类似,返回终止子进程的进程ID,或者出现错误时返回 -1。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
void Worker(int number)
{
int *p = NULL;
int cnt = 10;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %d\n", getpid(), getppid(), cnt--, number);
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
Worker();
exit(1);
}
else{
//sleep(10);
// father
printf("wait before\n");
// 在子进程运行期间,父进程有没有调用wait呢?在干什么呢?
//pid_t rid = wait(NULL);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
printf("wait after\n");
if(rid == id)
{
// 我们不能对status整体使用
///printf("wait success, pid: %d, rpid: %d, exit sig: %d, exit code: %d\n", getpid(), rid, status&0x7F, (status>>8)&0xFF);
if(WIFEXITED(status))
{
printf("child process normal quit, exit code : %d\n", WEXITSTATUS(status));
}
else{
printf("child process quit except!\n");
}
}
}
return 0;
}
等待多个子进程
int main()
{
for(int i = 0;i < n; i++)
{
pid_t id = fork();
if(id == 0)
{
Worker(i);
//status = i;
exit(0);
}
}
//等待多个子进程?
for(int i = 0; i < n; i++)
{
int status = 0;
pid_t rid = waitpid(-1, &status, 0); // pid>0, -1:任意一个退出的子进程
if(rid > 0){
printf("wait child %d success, exit code: %d\n", rid, WEXITSTATUS(status));
}
}
return 0;
}
waitpid()
函数的 options
参数
用于控制等待行为的附加选项,通过按位或运算组合使用可以实现不同的功能。下面是一些常用的 options
选项:
- WNOHANG:
- 如果没有子进程终止,不会阻塞父进程,即使没有终止的子进程,
waitpid()
也会立即返回0。 - 如果指定了该选项,并且没有子进程终止,
waitpid()
返回0。
- 如果没有子进程终止,不会阻塞父进程,即使没有终止的子进程,
- WUNTRACED:
- 如果子进程进入暂停(stopped)状态,返回其状态信息,即使它还没有终止。
- 这个选项通常用于检查子进程是否处于暂停状态,例如在调试器中。
- WCONTINUED:
- 如果子进程被停止后又继续运行,返回其状态信息。
- 这个选项通常用于检查子进程是否被继续运行,例如在调试器中。
这些选项可以按位或运算组合使用,例如 WNOHANG | WUNTRACED
,以实现多个功能同时生效。需要根据具体的需求和场景来选择合适的选项组合。
进程的阻塞等待方式:
int main()
{
pid_t pid;
pid = fork();
if(pid < 0){
printf("%s fork error\n",__FUNCTION__);
return 1;
} else if( pid == 0 ){ //child
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(257);
} else{
int status = 0;
pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
printf("this is test for wait\n");
if( WIFEXITED(status) && ret == pid ){
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}else{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
运行结果:
[root@localhost linux]# ./a.out
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is :1.
进程的非阻塞等待方式:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if(pid < 0){
printf("%s fork error\n",__FUNCTION__);
return 1;
}else if( pid == 0 ){ //child
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(1);
} else{
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
if( ret == 0 ){
printf("child is running\n");
}
sleep(1);
}while(ret == 0);
if( WIFEXITED(status) && ret == pid ){
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}else{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
8. 进程程序替换
8.1 替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
8.2 替换函数
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值。
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
系统调用:
int execve(const char *path, char *const argv[], char *const envp[])
exec调用举例:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
extern char** environ;
pid_t id = fork();
if(id == 0)
{
// 子进程
execl("/bin/ls","ls","-a","-n","-l",NULL);
char* const myargv[] = {"ls","-a","-l",NULL};
// 带l的,需要跟上路径
execl("/bin/ls","ls","-a","-n","-l",NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ls","ls","-a","-l",NULL);
// 带V的,可以使用自己的参数列表数组
execvp("ls",myargv);
// 带e的,需要自己组装环境变量
execvpe("ls",myargv,environ);
printf("程序替换失败\n");
}
// 父进程
printf("等待子进程成功,child_id:%d\n",wait(NULL));
return 0;
}
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在 man手册第3节。
8.3 简易shell
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <assert.h>
#define MAX 1024
#define ARGC 64
#define SEP " "
int split(char* commandstr,char* argv[])
{
assert(commandstr);
assert(argv);
argv[0] = strtok(commandstr,SEP);
if(argv[0] == NULL) return -1;
int i = 1;
while((argv[i++] = strtok(NULL,SEP)));
return 0;
}
void showEnv()
{
extern char** environ;
for(int i = 0; environ[i]; i++) printf("%d:%s\n",i,environ[i]);
}
int main()
{
extern int putenv(char* string);
char myenv[32][256];
int env_index = 0;
int exitCode = 0;
while(1)
{
char commandstr[MAX] = {0};
char* argv[ARGC] = {NULL};
printf("[hxy@mychaimachine]$ ");
fflush(stdout);
char* s = fgets(commandstr,sizeof(commandstr),stdin);
assert(s);
(void)s;
commandstr[strlen(commandstr) - 1] = '\0'; // 去掉键盘输入的\n
int n = split(commandstr,argv); // 切割字符串
if(n != 0) continue;
if(strcmp(argv[0],"cd") == 0)
{
if(argv[1] != NULL) chdir(argv[1]);
continue;
}
else if(strcmp(argv[0],"export") == 0)
{
if(argv[1] != NULL)
{
strcpy(myenv[env_index],argv[1]); // 用户自己定义的环境变量,需要bash自己来维护
putenv(myenv[env_index++]);
}
continue;
}
else if(strcmp(argv[0],"env") == 0)
{
showEnv(); // env查看环境变量时,其实看的是父进程bash的变量
continue;
}
else if(strcmp(argv[0],"echo") == 0)
{
const char* target_env = NULL;
if(argv[1][0] == '$')
{
if(argv[1][1] == '?')
{
printf("%d\n",exitCode);
continue;
}
else target_env = getenv(argv[1] + 1);
if(target_env != NULL) printf("%s = %s\n",argv[1] + 1,target_env);
}
continue;
}
// ls设置颜色选项
if(strcmp(argv[0],"ls") == 0)
{
int pos = 0;
while(argv[pos] != NULL)
{
pos++;
}
argv[pos++] = (char*)"--color=auto";
argv[pos] = NULL;
}
pid_t id = fork();
if(id == 0)
{
// 子进程
execvp(argv[0],argv);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
exitCode = WEXITSTATUS(status); // 获取最近一次进程的退出码
}
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#define NUM 1024
#define SIZE 64
#define SEP " "
//#define Debug 1
//redir
#define NoneRedir 0
#define OutputRedir 1
#define AppendRedir 2
#define InputRedir 3
int redir = NoneRedir;
char *filename = NULL;
char cwd[1024];
char enval[1024]; // for test
int lastcode = 0;
char *homepath()
{
char *home = getenv("HOME");
if(home) return home;
else return (char*)".";
}
const char *getUsername()
{
const char *name = getenv("USER");
if(name) return name;
else return "none";
}
const char *getHostname()
{
const char *hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "none";
}
const char *getCwd()
{
const char *cwd = getenv("PWD");
if(cwd) return cwd;
else return "none";
}
int getUserCommand(char *command, int num)
{
printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
char *r = fgets(command, num, stdin); // 最终你还是会输入\n
if(r == NULL) return -1;
// "abcd\n" "\n"
command[strlen(command) - 1] = '\0'; // 有没有可能越界?不会
return strlen(command);
}
void commandSplit(char *in, char *out[])
{
int argc = 0;
out[argc++] = strtok(in, SEP);
while( out[argc++] = strtok(NULL, SEP));
#ifdef Debug
for(int i = 0; out[i]; i++)
{
printf("%d:%s\n", i, out[i]);
}
#endif
}
int execute(char *argv[])
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0) //child
{
// 程序替换会不会影响曾经的重定向呢??不会!! 为什么?如何理解??
int fd = 0;
if(redir == InputRedir)
{
fd = open(filename, O_RDONLY); // 差错处理我们不做了
dup2(fd, 0);
}
else if(redir == OutputRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
}
else if(redir == AppendRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
}
else
{
//do nothing
}
// exec command
execvp(argv[0], argv); // cd ..
exit(1);
}
else // father
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0){
lastcode = WEXITSTATUS(status);
}
}
return 0;
}
void cd(const char *path)
{
chdir(path);
char tmp[1024];
getcwd(tmp, sizeof(tmp));
sprintf(cwd, "PWD=%s", tmp); // bug
putenv(cwd);
}
// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char *argv[])
{
if(strcmp(argv[0], "cd") == 0)
{
char *path = NULL;
if(argv[1] == NULL) path=homepath();
else path = argv[1];
cd(path);
return 1;
}
else if(strcmp(argv[0], "export") == 0)
{
if(argv[1] == NULL) return 1;
strcpy(enval, argv[1]);
putenv(enval); // ???
return 1;
}
else if(strcmp(argv[0], "echo") == 0)
{
if(argv[1] == NULL){
printf("\n");
return 1;
}
if(*(argv[1]) == '$' && strlen(argv[1]) > 1){
char *val = argv[1]+1; // $PATH $?
if(strcmp(val, "?") == 0)
{
printf("%d\n", lastcode);
lastcode = 0;
}
else{
const char *enval = getenv(val);
if(enval) printf("%s\n", enval);
else printf("\n");
}
return 1;
}
else {
printf("%s\n", argv[1]);
return 1;
}
}
else if(0){}
return 0;
}
#define SkipSpace(pos) do{ while(isspace(*pos)) pos++; }while(0)
void checkRedir(char usercommand[], int len)
{
// ls -a -l > log.txt
// ls -a -l >> log.txt
char *end = usercommand + len - 1;
char *start = usercommand;
while(end>start)
{
if(*end == '>')
{
if(*(end-1) == '>')
{
*(end-1) = '\0';
filename = end+1;
SkipSpace(filename);
redir = AppendRedir;
break;
}
else
{
*end = '\0';
filename = end+1;
SkipSpace(filename);
redir = OutputRedir;
break;
}
}
else if(*end == '<')
{
*end = '\0';
filename = end+1;
SkipSpace(filename); // 如果有空格,就跳过
redir = InputRedir;
break;
}
else
{
end--;
}
}
}
int main()
{
while(1){
redir = NoneRedir;
filename = NULL;
char usercommand[NUM];
char *argv[SIZE];
// 1. 打印提示符&&获取用户命令字符串获取成功
int n = getUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) continue;
// "ls -a -l > log.txt" -> 判断 -> "ls -a -l" redir_type "log.txt"
// 1.1 检测是否发生了重定向
checkRedir(usercommand, strlen(usercommand));
// 2. 分割字符串
// "ls -a -l" -> "ls" "-a" "-l"
commandSplit(usercommand, argv);
// 3. check build-in command
n = doBuildin(argv);
if(n) continue;
// 4. 执行对应的命令
execute(argv);
}
}
9. 基础IO
9.1 C语言中的文件操作
fopen
fopen()
是一个 C 语言标准库函数,用于打开文件。其声明如下:
FILE *fopen(const char *path, const char *mode);
path
是一个指向以 null 结尾的字符串,表示要打开的文件的路径名。mode
是一个以 null 结尾的字符串,表示打开文件的模式。模式字符串可以是以下之一:"r"
:只读模式。打开文件以供读取,文件必须存在。"w"
:写入模式。创建一个空文件用于写入,如果文件已经存在,则截断该文件的长度为 0。"a"
:追加模式。写入文件的操作将在文件末尾进行,如果文件不存在,则创建该文件。"r+"
:读写模式。打开文件用于读取和写入,文件必须存在。"w+"
:读写模式。创建一个空文件用于读取和写入,如果文件已经存在,则截断该文件的长度为 0。"a+"
:读写模式。打开文件用于读取和追加,如果文件不存在,则创建该文件。
- 返回值:成功时,返回一个指向
FILE
结构的指针,该结构表示与文件关联的流。如果发生错误,则返回NULL
。
写入文件
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
:fwrite
函数用于将数据块写入文件。ptr
是指向要写入的数据块的指针。size
是每个数据项的大小(以字节为单位)。nmemb
是要写入的数据项的数量。stream
是一个指向FILE
类型对象的指针,它指定了要写入的文件流。- 函数返回成功写入的数据项数量。
int fprintf(FILE *stream, const char *format, …);
:fprintf
函数用于将格式化的数据写入文件。stream
是一个指向FILE
类型对象的指针,它指定了要写入的文件流。format
是一个格式字符串,类似于printf
函数中的格式字符串。- 后续参数是根据格式字符串指定的数据,可以是各种类型的数据,用于填充格式字符串中的占位符。
- 函数返回成功写入的字符数。
int sprintf(char *str, const char *format, …);
:sprintf
函数用于将格式化的数据写入字符串。str
是一个指向目标字符串的指针,用于存储格式化的数据。format
是一个格式字符串,类似于printf
函数中的格式字符串。- 后续参数是根据格式字符串指定的数据,用于填充格式字符串中的占位符。
- 函数返回成功写入的字符数。
int snprintf(char *str, size_t size, const char *format, …);
:snprintf
函数与sprintf
类似,但是多了一个参数size
,用于指定最多写入的字符数,防止溢出。str
是一个指向目标字符串的指针,用于存储格式化的数据。size
是str
指向的缓冲区的大小,即最多可以写入的字符数(包括结尾的空字符)。format
是一个格式字符串,类似于printf
函数中的格式字符串。- 后续参数是根据格式字符串指定的数据,用于填充格式字符串中的占位符。
- 函数返回成功写入的字符数(不包括结尾的空字符),如果缓冲区不够大,它会根据
size
截断输出并返回实际需要的字符数。
#include <stdio.h>
#define FILENAME "log.txt"
int main()
{
// 以 ' w' (只写)的方式打开文件
FILE* fp = fopen(FILENAME,"w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
// 对文件进行操作
const char* msg = "Hello World!";
int cnt = 5;
while(cnt)
{
fprintf(fp,"%s: %d\n",msg,cnt);
cnt--;
}
// 关闭文件
fclose(fp);
return 0;
}
读取文件
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
:fread
函数用于从文件中读取数据到指定的内存位置。ptr
是一个指向用于存储读取数据的缓冲区的指针。size
是每个数据项的大小(以字节为单位)。nmemb
是要读取的数据项的数量。stream
是一个指向FILE
类型对象的指针,它指定了要读取的文件流。- 函数返回成功读取的数据项数量,如果出现错误或到达文件末尾,则返回的数量可能小于请求的数量。
int fscanf(FILE *stream, const char *format, …);
:fscanf
函数用于从文件中按照指定格式读取数据。stream
是一个指向FILE
类型对象的指针,它指定了要读取的文件流。format
是一个格式字符串,指定了要读取的数据的格式。- 后续参数是用于存储读取数据的变量,根据格式字符串指定的格式进行读取。
- 函数返回成功匹配并读取的项目数量,如果出现错误或到达文件末尾,则返回值可能小于指定的参数数量。
int sscanf(const char *str, const char *format, …);
:sscanf
函数用于从字符串中按照指定格式读取数据。str
是一个指向包含要读取数据的字符串的指针。format
是一个格式字符串,指定了要读取的数据的格式。- 后续参数是用于存储读取数据的变量,根据格式字符串指定的格式进行读取。
- 函数返回成功匹配并读取的项目数量。
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "r");
if(!fp){
printf("fopen error!\n");
}
char buf[1024];
const char *msg = "hello fread!\n";
while(1){
//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
ssize_t s = fread(buf, 1, strlen(msg), fp);
if(s > 0){
buf[s] = 0;
printf("%s", buf);
}
if(feof(fp)){
break;
}
}
fclose(fp);
return 0;
}
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
9.2 系统接口I/O
9.2.1 open
在UNIX和类UNIX操作系统中,open
是一个系统调用,用于打开或创建文件。它是操作系统提供给应用程序访问文件系统的一种接口。
以下是open
系统调用的一般形式和功能:
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
pathname
参数是要打开或创建的文件的路径名,可以是相对路径或绝对路径。flags
参数是一组标志,指定了文件的打开方式和操作行为。一些常见的标志包括:O_RDONLY
:以只读方式打开文件。O_WRONLY
:以只写方式打开文件。O_RDWR
:以读写方式打开文件。O_CREAT
:如果文件不存在,则创建它。O_EXCL
:与O_CREAT
一起使用,确保创建文件时不覆盖已存在的同名文件。O_TRUNC
:如果文件存在且成功打开为写入,将其长度截断为0。- 等等,还有其他标志可用,具体取决于操作系统和文件系统。
mode
参数指定新创建文件的权限位,只有在指定了O_CREAT
标志时才会生效。它是一个八进制数,控制着文件的权限和属性。典型的权限包括读、写和执行权限,以及文件所有者、所属组和其他用户的权限。open
系统调用返回一个文件描述符,它是一个非负整数,用于标识打开的文件。如果出现错误,它将返回-1,并设置适当的错误码,例如errno
变量。
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); //按w方式打开文件
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); //按a方式打开文件
9.2.2 read
read:
-
read
系统调用用于从文件描述符中读取数据。它的原型如下:ssize_t read(int fd, void *buf, size_t count);
fd
是文件描述符,指定了要读取的文件或其他I/O资源。buf
是一个指向要读取数据存储位置的缓冲区。count
是要读取的字节数。- 返回值是实际读取的字节数,如果出现错误,则返回-1。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
const char *msg = "hello read!\n";
char buf[1024];
while(1){
ssize_t s = read(fd, buf, strlen(msg));
if(s > 0){
printf("%s", buf);
}else{
break;
}
}
close(fd);
return 0;
}
9.2.3 write
write:
-
write
系统调用用于向文件描述符中写入数据。它的原型如下:ssize_t write(int fd, const void *buf, size_t count);
fd
是文件描述符,指定了要写入的文件或其他I/O资源。buf
是一个指向包含要写入数据的缓冲区。count
是要写入的字节数。- 返回值是实际写入的字节数,如果出现错误,则返回-1。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
if(fd < 0){
perror("open");
return 1;
}
int count = 5;
const char *msg = "hello write!\n";
int len = strlen(msg);
while(count--){
write(fd, msg, len);//
}
close(fd);
return 0;
}
9.3 stdin & stdout & stderr
stdin
, stdout
, 和 stderr
是三个标准的I/O流,它们是在C语言中使用的标准文件描述符。它们在UNIX和类UNIX系统中广泛使用。
- stdin:
stdin
代表标准输入流,通常与键盘输入相关联。当程序需要从用户输入读取数据时,通常会使用stdin
。例如,使用scanf
函数读取用户输入就是从stdin
中读取的。
- stdout:
stdout
代表标准输出流,通常与控制台输出相关联。当程序需要向用户显示信息或结果时,通常会使用stdout
。例如,使用printf
函数将数据输出到控制台就是输出到stdout
。
- stderr:
stderr
代表标准错误流,也通常与控制台相关联。与stdout
类似,但stderr
主要用于输出错误消息和诊断信息。当程序发生错误时,通常会将错误消息输出到stderr
,以便及时发现和调试问题。
这三个流在程序中都以文件描述符的形式存在,通常有如下的对应关系:
- 文件描述符 0(标准输入)通常对应于
stdin
。 - 文件描述符 1(标准输出)通常对应于
stdout
。 - 文件描述符 2(标准错误)通常对应于
stderr
。
在UNIX系统中,这些标准I/O流可以被重定向,例如可以将stdout
重定向到文件中,以便将程序的输出保存到文件中而不是显示在控制台上。这种灵活性使得UNIX系统具有强大的I/O重定向和管道功能。
9.4 文件描述符fd
文件描述符(File Descriptor)是一个整数,用于标识一个正在被进程访问的文件或者其它的I/O资源,比如管道、套接字等。在UNIX和类UNIX系统中,所有的输入和输出都被视为文件,因此文件描述符也用于表示这些I/O资源。
文件描述符的典型用法包括:
- 打开文件时,
open()
系统调用返回一个文件描述符。 - 标准输入、标准输出和标准错误流,它们分别对应的文件描述符是0、1和2。
socket()
、pipe()
等系统调用创建的套接字和管道也有相应的文件描述符。
文件描述符的值从0开始,依次递增,但在不同的进程中可以有不同的文件描述符分配情况。因此,文件描述符本质上是进程级别的,而不是系统级别的。
在UNIX系统中,通常有三个标准的文件描述符:
- 文件描述符0(STDIN_FILENO):标准输入,通常关联着键盘输入。
- 文件描述符1(STDOUT_FILENO):标准输出,通常关联着控制台输出。
- 文件描述符2(STDERR_FILENO):标准错误,通常关联着控制台输出错误信息。
9.5 重定向
重定向是指改变一个进程的标准输入、标准输出或标准错误的目标,使得输入来自其他来源或输出被发送到其他地方。它的本质是通过操作文件描述符来改变进程的输入和输出流的方向。
在UNIX和类UNIX系统中,一切皆文件,包括标准输入、标准输出和标准错误流。因此,重定向本质上是将一个文件描述符指向另一个文件描述符或文件。常见的重定向操作有:
- 输入重定向:
- 通过
<
操作符将一个文件描述符(通常是标准输入流)重定向到一个文件,使得进程从文件而不是键盘获取输入。
- 通过
- 输出重定向:
- 通过
>
操作符将一个文件描述符(通常是标准输出流)重定向到一个文件,使得进程的输出被写入到文件而不是屏幕上。 - 通过
>>
操作符将一个文件描述符(通常是标准输出流)追加到一个文件末尾,而不是覆盖文件内容。
- 通过
- 错误重定向:
- 通过
2>
操作符将标准错误流重定向到一个文件,使得错误消息被写入到文件而不是屏幕上。
- 通过
9.6 dup2系统调用
dup2
是一个UNIX和类UNIX系统提供的系统调用,用于复制文件描述符。它的原型如下:
int dup2(int oldfd, int newfd);
dup2
的作用是将文件描述符 oldfd
复制到文件描述符 newfd
上。如果 newfd
已经打开,则会首先关闭它。如果 oldfd
等于 newfd
,则 dup2
不执行任何操作,直接返回 newfd
。
主要参数解释:
oldfd
是要复制的文件描述符。newfd
是新的文件描述符,它将被设置为和oldfd
指向同一个文件表项。如果newfd
已经被打开,则先关闭它。
dup2
主要用于重定向文件描述符。例如,可以用它来实现输入、输出和错误的重定向,也可以用来实现管道等功能。
示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "log.txt"
int main()
{
int fd = open(FILE_NAME,O_RDONLY);
if (fd < 0)
{
prror("open");
return 1;
}
dup2(fd,0);
char buffer[1024];
fread(buffer,1,1024,stdin);
printf("%s",buffer);
close(fd);
}
9.7 在myshell中添加重定向功能
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#define NUM 1024
#define SIZE 64
#define SEP " "
//#define Debug 1
//redir
#define NoneRedir 0
#define OutputRedir 1
#define AppendRedir 2
#define InputRedir 3
int redir = NoneRedir;
char *filename = NULL;
char cwd[1024];
char enval[1024]; // for test
int lastcode = 0;
char *homepath()
{
char *home = getenv("HOME");
if(home) return home;
else return (char*)".";
}
const char *getUsername()
{
const char *name = getenv("USER");
if(name) return name;
else return "none";
}
const char *getHostname()
{
const char *hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "none";
}
const char *getCwd()
{
const char *cwd = getenv("PWD");
if(cwd) return cwd;
else return "none";
}
int getUserCommand(char *command, int num)
{
printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
char *r = fgets(command, num, stdin); // 最终你还是会输入\n
if(r == NULL) return -1;
// "abcd\n" "\n"
command[strlen(command) - 1] = '\0'; // 有没有可能越界?不会
return strlen(command);
}
void commandSplit(char *in, char *out[])
{
int argc = 0;
out[argc++] = strtok(in, SEP);
while( out[argc++] = strtok(NULL, SEP));
#ifdef Debug
for(int i = 0; out[i]; i++)
{
printf("%d:%s\n", i, out[i]);
}
#endif
}
int execute(char *argv[])
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0) //child
{
// 程序替换会不会影响曾经的重定向呢??不会!! 为什么?如何理解??
int fd = 0;
if(redir == InputRedir)
{
fd = open(filename, O_RDONLY); // 差错处理我们不做了
dup2(fd, 0);
}
else if(redir == OutputRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
}
else if(redir == AppendRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
}
else
{
//do nothing
}
// exec command
execvp(argv[0], argv); // cd ..
exit(1);
}
else // father
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0){
lastcode = WEXITSTATUS(status);
}
}
return 0;
}
void cd(const char *path)
{
chdir(path);
char tmp[1024];
getcwd(tmp, sizeof(tmp));
sprintf(cwd, "PWD=%s", tmp); // bug
putenv(cwd);
}
// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char *argv[])
{
if(strcmp(argv[0], "cd") == 0)
{
char *path = NULL;
if(argv[1] == NULL) path=homepath();
else path = argv[1];
cd(path);
return 1;
}
else if(strcmp(argv[0], "export") == 0)
{
if(argv[1] == NULL) return 1;
strcpy(enval, argv[1]);
putenv(enval); // ???
return 1;
}
else if(strcmp(argv[0], "echo") == 0)
{
if(argv[1] == NULL){
printf("\n");
return 1;
}
if(*(argv[1]) == '$' && strlen(argv[1]) > 1){
char *val = argv[1]+1; // $PATH $?
if(strcmp(val, "?") == 0)
{
printf("%d\n", lastcode);
lastcode = 0;
}
else{
const char *enval = getenv(val);
if(enval) printf("%s\n", enval);
else printf("\n");
}
return 1;
}
else {
printf("%s\n", argv[1]);
return 1;
}
}
else if(0){}
return 0;
}
#define SkipSpace(pos) do{ while(isspace(*pos)) pos++; }while(0)
void checkRedir(char usercommand[], int len)
{
// ls -a -l > log.txt
// ls -a -l >> log.txt
char *end = usercommand + len - 1;
char *start = usercommand;
while(end>start)
{
if(*end == '>')
{
if(*(end-1) == '>')
{
*(end-1) = '\0';
filename = end+1;
SkipSpace(filename);
redir = AppendRedir;
break;
}
else
{
*end = '\0';
filename = end+1;
SkipSpace(filename);
redir = OutputRedir;
break;
}
}
else if(*end == '<')
{
*end = '\0';
filename = end+1;
SkipSpace(filename); // 如果有空格,就跳过
redir = InputRedir;
break;
}
else
{
end--;
}
}
}
int main()
{
while(1){
redir = NoneRedir;
filename = NULL;
char usercommand[NUM];
char *argv[SIZE];
// 1. 打印提示符&&获取用户命令字符串获取成功
int n = getUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) continue;
// "ls -a -l > log.txt" -> 判断 -> "ls -a -l" redir_type "log.txt"
// 1.1 检测是否发生了重定向
checkRedir(usercommand, strlen(usercommand));
// 2. 分割字符串
// "ls -a -l" -> "ls" "-a" "-l"
commandSplit(usercommand, argv);
// 3. check build-in command
n = doBuildin(argv);
if(n) continue;
// 4. 执行对应的命令
execute(argv);
}
}
9.8 C语言的缓冲区
在C语言中,缓冲区(Buffer)是指一块用来临时存储数据的内存区域。在程序中,使用缓冲区可以帮助管理数据的传输和处理,提高程序的效率和性能。
在进行输入输出操作时,C语言会使用缓冲区来暂存数据,以提高效率。例如,在使用printf()
函数输出数据时,数据首先被写入到输出缓冲区中,然后根据一定的条件(比如缓冲区满了或者遇到换行符)才会将数据真正输出到终端或文件中。同样地,使用scanf()
函数进行输入时,输入数据也会先被读入到输入缓冲区中。
先看一个问题
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
运行输出结果:
hello printf
hello fwrite
hello write
但如果对进程实现输出重定向呢? ./hello > file
, 我们发现结果变成了:
hello write
hello printf
hello fwrite
hello printf
hello fwrite
首先,write 是系统调用,并不是C库提供的函数,所以它并不会考虑缓冲区的问题。在 fork 之前就已经将数据写入到文件中了。
但是 fprintf 则面对的是两种不同的情况,当我们输出在屏幕上时,此时采取的刷新策略是行缓冲,所以在 fork 之前,它也已经输出到屏幕上了,并不会有奇怪的现象。
但当我们将输出的数据重定向到 test.txt 文件中时,此时刷新策略由行缓冲变为了全缓冲,由于缓冲区此时并没有被写满,所以暂时不会输出数据。在 fork 之后,父子进程在结束时会刷新各自的缓冲区,此时会发生写时拷贝,所以 “hello fprintf” 被打印了两次。
模拟实现缓冲区
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
所以C库当中的FILE结构体内部,必定封装了fd。
//mystdio.h
#pragma once
#define SIZE 4096
#define FLUSH_NONE 1
#define FLUSH_LINE (1<<1)
#define FLUSH_ALL (1<<2)
typedef struct _myFILE
{
int fileno;
int flag;
char buffer[SIZE];
int end;
}myFILE;
extern myFILE *my_fopen(const char *path, const char *mode);
extern int my_fwrite(const char *s, int num, myFILE *stream);
extern int my_fflush(myFILE *stream);
extern int my_fclose(myFILE*stream);
//mystdio.c
#include "mystdio.h"
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#define DFL_MODE 0666
myFILE *my_fopen(const char *path, const char *mode)
{
int fd = 0;
int flag = 0;
if(strcmp(mode, "r") == 0)
{
flag |= O_RDONLY;
}
else if(strcmp(mode, "w") == 0)
{
flag |= (O_CREAT | O_TRUNC | O_WRONLY);
}
else if(strcmp(mode, "a") == 0)
{
flag |= (O_CREAT | O_WRONLY | O_APPEND);
}
else{
// Do Nothing
}
if(flag & O_CREAT)
{
fd = open(path, flag, DFL_MODE);
}
else
{
fd = open(path, flag);
}
if(fd < 0)
{
errno = 2;
return NULL;
}
myFILE *fp = (myFILE*)malloc(sizeof(myFILE));
if(!fp)
{
errno = 3;
return NULL;
}
fp->flag = FLUSH_LINE;
fp->end = 0;
fp->fileno = fd;
return fp;
}
int my_fwrite(const char *s, int num, myFILE *stream)
{
// 写入
memcpy(stream->buffer+stream->end, s, num);
stream->end += num;
// 判断是否需要刷新, "abcd\nefgh"
if((stream->flag & FLUSH_LINE) && stream->end > 0 && stream->buffer[stream->end-1] == '\n')
{
my_fflush(stream);
}
return num;
}
int my_fflush(myFILE *stream)
{
if(stream->end > 0)
{
write(stream->fileno, stream->buffer, stream->end);
//fsync(stream->fileno);
stream->end = 0;
}
return 0;
}
int my_fclose(myFILE*stream)
{
my_fflush(stream);
return close(stream->fileno);
}
//main.c
#include "mystdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
myFILE *fp = my_fopen("./log.txt", "w");
if(fp == NULL)
{
perror("my_fopen");
return 1;
}
int cnt = 20;
const char *msg = "haha, this is my stdio lib";
while(cnt--){
my_fwrite(msg, strlen(msg), fp);
sleep(1);
}
my_fclose(fp);
return 0;
}
10. 文件系统
10.1 磁盘
-
硬盘驱动器(HDD):
- 硬盘驱动器使用旋转的磁性盘片来存储数据。数据通过读/写头(称为磁头)在盘片上创建和读取磁场来实现存储和检索。
- 传统的HDD通常有机械运动部分,包括盘片和读/写头的旋转运动。这种机械运动可能会导致一定的延迟,并且使得HDD对于物理冲击更为敏感。
- HDD的优势在于存储成本相对较低,可以提供较大的存储容量,适合存储大量数据。
-
固态硬盘(SSD):
- 固态硬盘使用闪存存储器而不是机械运动部件来存储数据。这意味着它们没有移动部分,因此速度更快,对物理冲击更具抵抗力。
- 由于没有机械部分,SSD通常比HDD更耐用,更适合需要频繁读写操作的应用场景。
- SSD的主要缺点是成本相对较高,以及存储容量较小。
-
其他类型的磁盘:
- 除了传统的HDD和SSD之外,还有一些其他类型的磁盘,如混合硬盘驱动器(Hybrid Hard Disk Drive,H-HDD),它结合了HDD和SSD的优点,在性能和成本之间寻求一种平衡。
-
df -h
是一个用于显示文件系统磁盘空间使用情况的常用命令。-
命令名称:
df
代表“disk free”,用于显示文件系统的磁盘空间情况。 -
选项:
-h
:以人类可读的格式显示磁盘空间大小。该选项会将磁盘容量显示为易于理解的单位,例如K、M、G等,而不是以字节为单位。
-
作用:
df -h
命令用于汇总和显示系统中所有挂载的文件系统的磁盘空间使用情况。 -
输出内容: 输出包括以下列:
- 文件系统: 每个挂载的文件系统的名称或设备。
- 容量: 文件系统的总容量。
- 已用: 文件系统已经使用的空间量。
- 可用: 文件系统中尚未使用的空间量。
- 已用%: 文件系统已用空间的百分比。
- 挂载点: 文件系统被挂载的路径。
-
示例:
下面是一个
df -h
命令的示例输出:Filesystem Size Used Avail Use% Mounted on /dev/sda1 20G 10G 8G 56% / /dev/sdb1 100G 60G 40G 60% /mnt/data
这个示例显示了两个文件系统的信息。第一行是根文件系统
/
,它有20GB容量,已使用10GB,可用8GB,已使用56%。第二行是挂载在/mnt/data
的另一个文件系统,有100GB容量,已使用60GB,可用40GB,已使用60%。
-
10.1.1 CHS定位法
CHS定位法是一种磁盘寻址方式,用于定位磁盘上的特定扇区。CHS代表Cylinder(磁道)、Head(磁头)和Sector(扇区),它是一种老式的寻址方式,在早期的计算机系统中被广泛使用。
- 确定磁头(Head):每个磁盘都有多个盘面(或称为磁头),每个盘面都由一个磁头负责读取和写入数据。通过磁头的编号可以确定要操作的磁盘表面。
- 确定磁道(Cylinder):磁盘表面被分成多个同心圆状的磁道,每个磁道由一个特定的磁头读取或写入数据。通过磁头的定位以及磁道的编号可以确定要操作的磁道。
- 确定扇区(Sector):每个磁道被划分为多个扇区,每个扇区可以存储一定量的数据。每个扇区都有自己的编号,通常是从0开始递增的。通过扇区的编号可以确定要操作的具体扇区。
通过以上步骤,可以确定磁盘上特定扇区的位置。这种寻址方式的缺点是它受限于硬件的物理结构,而且不够灵活,因此在现代计算机系统中,通常使用更高级别的逻辑寻址方式,如LBA(逻辑块地址)寻址方式。 LBA将磁盘的所有扇区看作一个连续编号的地址空间,从而简化了数据存储和访问的管理。
10.1.2 LBA寻址法
逻辑块地址(LBA)是一种磁盘寻址方式,它将磁盘上的每个扇区抽象为一个连续编号的逻辑地址。通过LBA,操作系统可以以逻辑方式访问磁盘而不需要了解其物理结构,从而实现了OS与硬件之间的解耦。
LBA的工作原理如下:
- 连续编号的地址空间:磁盘上的每个扇区都被分配了一个唯一的逻辑块地址,这些地址被连续编号,从0开始递增。这样,整个磁盘可以被视为一个连续的地址空间。
- 简化的访问方式:OS可以通过简单地指定一个LBA来读取或写入数据,而不需要考虑具体的物理结构,如磁头、磁道和扇区等。操作系统将所需数据的逻辑地址转换为对应的LBA,然后传递给磁盘控制器。
- 适应性:由于LBA提供了一种抽象的方式来表示磁盘上的数据块,因此无论磁盘的物理结构如何变化,操作系统都无需修改其代码。这使得系统更加灵活,并且减少了维护和更新的工作量。
10.2 Inode
10.2.1 文件系统
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的,
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子
- 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个 文件系统结构就被破坏了
- GDT,Group Descriptor Table:块组描述符,描述块组属性信息,有兴趣的同学可以在了解一下
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没 有被占用
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
- i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
- 数据区:存放文件内容
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?
10.2.2 Inode
ls -i
是一个UNIX和类UNIX操作系统中的命令,用于显示指定目录下文件和目录的inode号码。inode号码是用来唯一标识文件或目录的数字标识符。
Inode(索引节点)是UNIX和类UNIX操作系统中文件系统的重要概念之一。每个文件和目录在文件系统中都有一个对应的inode,用于存储文件的元数据信息,包括文件的权限、所有者、大小、访问时间、修改时间、链接数等等。inode还包含指向文件数据所在磁盘块的指针。
以下是inode的一些关键属性和作用:
- 唯一标识符:每个inode都有一个唯一的数字标识符,用于在文件系统中唯一标识一个文件或目录。
- 元数据存储:inode存储文件的元数据,包括但不限于文件类型、文件大小、权限、所有者、最后访问时间、最后修改时间等。
- 指针指向数据块:inode中包含指针,用于指向文件数据所在的数据块(或称为扇区)。对于小文件,这些指针直接指向数据块;对于大文件,这些指针可能指向间接块、双重间接块或三重间接块,以指向更多的数据块。
- 链接计数:inode中有一个链接计数字段,记录了指向该inode的目录项(硬链接)的数量。当链接计数为0时,表示该文件已经被删除,系统可以回收inode和相关数据块。
- 性能优化:通过inode,文件系统可以高效地管理文件和目录,提高文件系统的性能。例如,通过索引inode而不是文件名进行文件操作,可以加速文件系统的查找和访问速度。
总的来说,inode是文件系统中非常重要的数据结构,它记录了文件的关键元数据信息,并提供了文件数据的物理位置,为文件系统的正常运行和高效管理提供了基础。
10.2.3 创建文件
创建一个新文件主要有一下4个操作:
- 存储属性 内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
- 存储数据 该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
- 记录分配情况 文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
- 添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?
内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
10.4 软硬链接
软链接和硬链接是UNIX和类UNIX系统中用于创建文件链接的两种不同方法。
软链接(Symbolic Link)
软链接也称为符号链接,它是一个特殊类型的文件,包含了指向另一个文件或目录的路径。软链接与原始文件或目录之间是逻辑上的链接关系,类似于Windows系统中的快捷方式。软链接的创建是通过ln命令完成的。
主要特点:
- 软链接包含了指向目标文件或目录的路径,而不是实际的数据。
- 软链接可以跨越文件系统,并且可以链接到目录。
- 软链接可以指向不存在的目标文件或目录,此时称为“断链”,删除软链接不会影响目标文件或目录。
- 软链接的权限和拥有者信息基本上是无效的,它们取决于所指向文件或目录的权限和拥有者。
创建软链接的命令:
ln -s 源文件 目标文件
硬链接(Hard Link)
硬链接是指针指向文件数据块的另一个文件入口,它们指向同一个inode,因此与原始文件没有区别。硬链接的创建是通过ln命令完成的。
主要特点:
- 硬链接只能链接到文件,不能链接到目录,因为目录的硬链接会导致循环链接问题。
- 硬链接不能跨越文件系统。
- 删除硬链接不会影响原始文件的访问,只有当所有硬链接和原始文件都被删除后,文件系统才会释放文件的数据块。
- 硬链接与原始文件的inode号相同,它们是同一文件的不同名字。
创建硬链接的命令:
ln 源文件 目标文件
.
(当前目录) 和. .
(上级目录)就是硬链接
10.5 动静态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。 静态库的文件扩展名通常是
.a
(在Unix/Linux系统中)或.lib
(在Windows系统中)。 - 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。 动态库的文件扩展名通常是
.so
(在Unix/Linux系统中)或.dll
(在Windows系统中)。 - 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个 过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
10.5.1 ldd
ldd
命令用于打印可执行文件或共享对象的动态链接库依赖关系。在Linux系统中,可执行文件通常依赖于一些共享库(也称为动态链接库或共享对象),这些库在程序运行时被加载到内存中。ldd
命令可以列出一个可执行文件所依赖的共享库的名称及路径。
语法:
ldd [options] executable_file
参数:
executable_file
:指定要检查的可执行文件的路径。
选项:
-v
:显示详细信息,包括库的版本号和符号版本。-r
:递归检查可执行文件所依赖的所有库文件。-u
:显示不使用的依赖项。-d
:显示执行时动态链接器的调试信息。
示例:
ldd /bin/ls
输出示例:
linux-vdso.so.1 (0x00007fff20588000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007fb843a0b000)
libacl.so.1 => /lib/x86_64-linux-gnu/libacl.so.1 (0x00007fb843800000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb843418000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007fb84318e000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb843e8f000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fb842f80000)
libattr.so.1 => /lib/x86_64-linux-gnu/libattr.so.1 (0x00007fb842d7b000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fb842b5c000)
输出显示了/bin/ls
可执行文件所依赖的共享库及其路径。
10.5.2 生成静态库
[root@localhost linux]# ls
add.c add.h main.c sub.c sub.h
[root@localhost linux]# gcc -c add.c -o add.o
[root@localhost linux]# gcc -c sub.c -o sub.o
生成静态库
[root@localhost linux]# ar -rc libmymath.a add.o sub.o
ar是gnu归档工具,rc表示(replace and create)
查看静态库中的目录列表
[root@localhost linux]# ar -tv libmymath.a
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o
t:列出静态库中的文件
v:verbose 详细信息
[root@localhost linux]# gcc main.c -L. -lmymath
-L 指定库路径-l 指定库名
测试目标文件生成后,静态库删掉,程序照样可以运行。
注意:
这里我们需要注意库的命名规则。库的命名是以lib为开头,以.a或.so为结尾
。例如 libcalculate.a
的真实名称为 calculate
。
- 因为我们的库是第三方的,编译器并不知道这个库的存在,所以我们需要指明库所在的路径;
- 同样,我们需要告诉编译器该链接哪一个库;
- 同理,我们还需指明头文件所在的路径。但是目前头文件就在当前路径下,所以可省略;
库搜索路径
- 从左到右搜索-L指定的目录。
- 由环境变量指定的目录 (LIBRARY_PATH)
- 由系统指定的目录 /usr/lib /usr/local/lib
- /usr/lib
- /usr/local/lib
10.5.3 生成动态库
- shared: 表示生成共享库格式
- fPIC:产生位置无关码(position independent code)
- 库名规则:libxxx.so
示例: [root@localhost linux]# gcc -fPIC -c sub.c add.c [root@localhost linux]# gcc -shared -o libmymath.so *.o [root@localhost linux]# ls add.c add.h add.o libmymath.so main.c sub.c sub.h sub.o
使用动态库
编译选项
- l:链接动态库,只要库名即可(去掉lib以及版本号)
- L:链接库所在的路径.
示例: gcc main.o -o main –L. -lhello
运行动态库
-
拷贝.so文件到系统共享库路径下, 一般指/usr/lib
-
更改LD_LIBRARY_PATH
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/..../lib/ # 你的.so文件存放路径
[root@localhost linux]# export LD_LIBRARY_PATH=. [root@localhost linux]# gcc main.c -lmymath [root@localhost linux]# ./a.out add(10, 20)=30 sub(100, 20)=80
-
软链接
在程序运行时,mymath.so并没有在系统的默认路径下,所以OS找不到我们的库,那么这个默认路径在哪里呢?
# 一般在这两个路径下 $ /lib $ /lib64/
所以我们直接将库文件移动到这两个路径下也可以,但是还有比较优雅一点的方案,那就是为我们的库文件建立软链接。
sudo ln -s lib/libcalculate.so /lib64/libmymath.so
-
ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
[root@localhost linux]# cat /etc/ld.so.conf.d/mymath.conf /root/tools/linux [root@localhost linux]# ldconfig
10.5.4 静态库和动态库的区别
静态库(Static Library)和动态库(Dynamic Library)是两种不同类型的库文件,它们在软件开发中有着不同的作用和特点。
- 静态库:
- 静态库是在编译链接阶段将库的代码和程序的代码一起链接成可执行文件的一部分。
- 静态库的代码会被完整地复制到可执行文件中,因此可执行文件的大小会增加。
- 在运行时,所有需要的函数和代码都已经包含在可执行文件中,程序在运行时不需要外部的库文件支持。
- 静态库的使用简单,移植性好,但如果多个程序都使用同一份静态库,会导致可执行文件体积增大。
- 动态库:
- 动态库是在程序运行时才被加载到内存中的库文件。
- 动态库的代码只有一个副本存在于内存中,多个程序可以共享它,因此节省了内存空间。
- 当多个程序使用同一个动态库时,它们可以共享同一个库的实例,减少了系统资源的浪费。
- 动态库的更新和维护更加方便,因为只需替换库文件即可,不需要重新编译链接程序。
- 动态库的使用可以降低可执行文件的大小,但可能需要在运行时检查库文件的可用性,有一定的性能开销。
10.5.5 使用外部库
系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。比如用来处理屏幕显示情况的函数(ncurses库)
#include <math.h>
#include <stdio.h>
int main(void)
{
double x = pow(2.0, 3.0);
printf("The cubed is %f\n", x);
return 0;
}
gcc -Wall calc.c -o calc -lm
-lm表示要链接libm.so或者libm.a库文件
库文件名称和引入库的名称
如:libc.so -> c库,去掉前缀lib,去掉后缀.so,.a
10.5.6 动态库加载原理
gcc -fPIC -c XXX.c
动态库加载原理涉及操作系统的动态链接器和运行时链接器。当程序在运行时需要调用动态库中的函数或符号时,操作系统的动态链接器会根据程序中的动态链接信息找到相应的动态库,并将其加载到内存中。下面是动态库加载的一般原理:
- 查找动态库: 当程序需要调用动态库中的函数或符号时,操作系统会按照一定的搜索路径(通常包括系统默认路径、环境变量指定的路径等)查找相应的动态库文件。
- 加载动态库: 找到动态库文件后,操作系统会将其加载到内存中,并为动态库中的每个函数或符号建立相应的链接关系。
- 解析符号: 在加载动态库时,操作系统会解析动态库中的符号,将其与程序中的符号进行匹配,以便在运行时正确调用动态库中的函数。
- 地址重定位: 如果动态库中的某些函数或符号在加载时无法确定其准确地址(比如跳转地址),操作系统会进行地址重定位,将动态库中的符号地址修正为正确的值。
- 共享内存: 动态库通常会被多个程序共享使用,因此操作系统会在内存中为这些程序共享动态库的一份副本,以节省内存资源。
选项 -fPIC
是 GCC 编译器的一个选项,用于生成位置无关代码(Position Independent Code,PIC)。
位置无关代码(Position Independent Code,PIC)是一种在编译时不依赖于代码加载地址的代码格式。在程序执行时,位置无关代码可以被加载到任意内存地址并正确执行,而不需要进行额外的地址重定位操作。这种代码格式通常用于生成动态链接库(Dynamic Link Library,DLL)或共享对象(Shared Object,SO),以便在不同的内存地址上正确地加载和执行。
位置无关代码的特点包括:
- 相对寻址:位置无关代码使用相对寻址而不是绝对寻址。相对寻址可以保证指令在加载时能够正确地引用其他代码或数据,而不需要知道其具体的内存地址。
- 全局偏移表(GOT)和过程链接表(PLT):位置无关代码通常使用全局偏移表和过程链接表来间接引用全局变量和函数。这些表在加载时会被动态链接器填充正确的地址信息。
- 使用相对偏移量:位置无关代码中的指令使用相对偏移量而不是绝对地址。这样,当代码被加载到内存中时,指令可以正确地定位到目标地址,而无需额外的地址重定位。(平坦模式)
- 不依赖于加载地址:位置无关代码不依赖于代码加载的地址,因此可以在不同的内存地址上加载和执行,而不需要进行额外的地址修正或重定位操作。
通过使用位置无关代码,可以使得动态链接库或共享对象更具移植性和通用性,因为它们可以在不同的系统和环境中被正确地加载和执行,而不受代码加载地址的限制。
库被加载之后,要被映射到指定使用了该库的进程地址空间的共享区部分。我们想做到让库在共享区的任意位置都可以正确运行。
一旦库加载之后,位置就是确定的。调用库的函数就变成了库地址:方法偏移量。调用库函数就是在自己的进程地址空间内跳转的。