Linux基础IO

目录

1.C文件相关操作(IO)

写文件

读文件

stdin & stdout & stderr

文件的打开方式

2.系统文件I/O

读文件操作示例:

写文件操作示例:

3.open、write、read、close、lseek ,类比C文件相关接口

4.open函数返回值

4.1文件描述符 'fd' (返回值)

5.重定向和文件描述符的分配规

5.1使用dup2系统调用

6.如何理解缓冲区

6.1自主封装FILE

7.理解文件系统

7.1硬链接

7.2软链接

7.3如何创建软链接和硬链接

8.动态库和静态库

8.1静态库的创建和使用

"ar -rc"命令

8.2动态库的创建和使用

8.1编译选项 -fPIC选项

8.2编译选项 -shared选项

1.环境变量

2.软链接方案

3.配置文件方案

8.3库的加载

1.C文件相关操作(IO)

写文件

#include <stdio.h>
#include <string.h>
int main()
{
    // w: 以写的方式打开文件,如果文件不存在,就创建它
	// 1. 默认如果只是打开,文件内容会自动被清空
	// 2. 同时,每次进行写入的时候,都会从最开始进行写入
	FILE* fp = fopen("myfile", "w");//
	if (!fp) {
		printf("fopen error!\n");
	}
    
	const char* msg = "hello bit!\n";
	int count = 5;
	while (count--) {
		fwrite(msg, strlen(msg), 1, fp);//二进制输入
	}
	fclose(fp);
	return 0;
}

读文件

#include <stdio.h>
#include <string.h>
int main()
{
    //r:以只读的方式打开现有文件。
	FILE* fp = fopen("myfile", "r");
	if (!fp) {
		printf("fopen error!\n");
	}
	char buf[1024];
	const char* msg = "hello bit!\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;
}

ssize_t 是一个有符号整数类型,用于表示读取或写入的字节数。它是在<unistd.h>头文件中定义的。通常情况下,ssize_t的大小与size_t相同,都是根据平台决定的。在大多数情况下,ssize_t类型的变量用于存储readwrite函数返回的字节数。

stdin & stdout & stderr

C默认会打开三个输入输出流,分别是stdin, stdout, stderr ,仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针(这些指针指向不同的文件)。

在C语言中,stdin、stdout和stderr是三个预定义的文件指针,分别用于标准输入、标准输出和标准错误输出。

  • stdin(标准输入):它是一个文件指针,用于从标准输入设备(通常是键盘)读取数据。可以使用scanf等函数从stdin读取用户的输入(标准输入会自动把读取的数据回显到显示器上,这是让用户可以看见读取的数据)。
  • stdout(标准输出):它也是一个文件指针,用于向标准输出设备(通常是终端(显示器)或控制台)写入数据。
  • stderr(标准错误输出):类似于stdout,stderr也是一个文件指针,用于向标准错误输出设备(通常也是终端或控制台)写入数据。不过,stderr主要用于输出错误信息和警告信息,以便将其与正常的输出(通过stdout)区分开来。通常情况下,stderr的输出会显示为红色或以其他方式与正常输出进行区分。

这些文件指针在程序运行时自动打开,并且在程序开始执行之前就已经存在。它们的定义如下:

#include <stdio.h>

FILE *stdin;  // 标准输入
FILE *stdout; // 标准输出
FILE *stderr; // 标准错误输出

注:Linux下一切皆文件,所以只要是文件都可以对这些文件进行读和写。

文件的打开方式

在C语言中,可以使用以下方式之一打开文件:

  1. 只读方式(Read-only Mode):以只读方式打开现有文件。
FILE *file;
file = fopen("filename", "r");
  1. 只写方式(Write-only Mode):以只写方式打开文件。如果文件不存在则创建,如果文件存在则截断文件为空。
FILE *file;
file = fopen("filename", "w");
  1. 读写方式(Read-write Mode):以读写方式打开文件。如果文件不存在则创建,如果文件存在则保留文件内容。
FILE *file;
file = fopen("filename", "rw");
  1. 追加方式(Append Mode):以追加方式打开文件。如果文件不存在则创建,如果文件存在则在文件末尾追加数据。
FILE *file;
file = fopen("filename", "a");
  1. 二进制模式(Binary Mode):以二进制方式打开文件,默认为文本模式。
FILE *file;
file = fopen("filename", "rb"); // 二进制只读模式
file = fopen("filename", "wb"); // 二进制只写模式
file = fopen("filename", "r+b"); // 二进制读写模式
file = fopen("filename", "ab"); // 二进制追加模式

在以上所有方式中,通过fopen()函数打开文件后,需要使用fclose()函数关闭文件。

fclose(file);

请注意,这些打开方式适用于文本文件和二进制文件。对于二进制文件,可以使用fread()和fwrite()等函数进行读写操作。

2.系统文件I/O

在Linux中,open()函数是用于打开文件的系统调用函数之一。它的原型如下:

#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  • pathname:要打开的文件的路径名。
  • flags(标志):用于指定文件访问模式和打开选项的参数。
  • mode:仅在创建新文件时使用,指定文件权限的参数。(比如创建文件时把权限掩码参数设置为0666)

open()函数返回一个非负整数的文件描述符(file descriptor)作为文件的句柄,若出现错误则返回-1。文件描述符可以用于后续对文件的读取、写入和其他操作。

flags参数用于指定打开文件的模式和选项,可以连续使用多个标志,下面是一些常用的标志:

  • O_RDONLY:以只方式打开文件。
  • O_WRONLY:以只方式打开文件。
  • O_RDWR:以读写方式打开文件。
  • O_CREAT:如果文件不存在,则创建文件。(CREAT:创建)
  • O_TRUNC:如果文件存在,将其截断(清除内容)为空文件。(截断是指当以只写方式打开文件时,如果文件已存在,打开文件时会清空文件内容,即将文件内容清空为一个空文件。)
  • O_APPEND:在文件末尾追加数据。(O_APPEND标志必须与O_WRONLY或O_RDWR标志一起使用,用于指定以只写或读写方式打开文件。它不能单独使用。)
  • O_EXCL:与O_CREAT一起使用,如果文件已经存在,则open调用失败。

OS一般会如何让用户给自己传递标志位的?

1.我们怎么做的? int XXXX(int flag, int flag1,int flag2); 如果我们想同时传递多个标志位呢?通过传递多个参数的方式,如:flag1,flag2,flag3,.........。

⒉系统怎么做的? int YYY(int flag) ; //flag是一个 int类型, 有32个比特位,我们可以用一个比特位表示一个标志位,我们一个int就可以同时至少传递32个标记位,都是够用了!(也就是把int flag看成一个位图)

3.代码

#include <stdio.h>

#define ONE 0x1 //0001
#define TWO 0x2 //0010
#define THREE 0x4 //0100
#define FOUR 0x8 //1000
#define FIVE 0x10 //1010

// 0000 0000 0000 0000 0000 0000 0000 0000
//为了传递多个标志为,每一个标志位不相同,一个标志位占用一个比特位,然后使用 按位或 | 操作符
//把所以的值按位或在一起,一个 整形 就可以表示多(32)个标志位了 (按位或 |:有一则一)
void Print(int flags)
{
    if (flags & ONE) printf("hello 1\n"); //充当不同的行为
    if (flags & TWO) printf("hello 2\n");
    if (flags & THREE) printf("hello 3\n");
    if (flags & FOUR) printf("hello 4\n");
    if (flags & FIVE) printf("hello 5\n");
}

int main()
{
    printf("--------------------------\n");
    Print(ONE);
    printf("--------------------------\n");
    Print(TWO);
    printf("--------------------------\n");
    Print(FOUR);
    printf("--------------------------\n");

    Print(ONE | TWO);
    printf("--------------------------\n");

    Print(ONE | TWO | THREE);
    printf("--------------------------\n");

    Print(ONE | TWO | THREE | FOUR | FIVE);
    printf("--------------------------\n");

    return 0;
}

mode参数仅在使用O_CREAT标志时起作用,它用于指定新创建文件的权限,通常使用数字形式的八进制表示。

读文件操作示例:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE 1024

int main() {
    char buffer[BUFFER_SIZE];

    int fd = open("file.txt", O_RDONLY);//如果文件已经存在,读文件时调用两个参数的函数即可

    if (fd == -1) {
        printf("Failed to open the file.\n");
        return 1;
    }

    //使用系统接口进行IO的时候,一定要注意 '\0' 的问题
    //因为 \0 时C语言的规定,不是文件的规定。
    ssize_t bytesRead = read(fd, buffer, stlren(buffer) - 1);//去掉/0

    if (bytesRead == -1) {
        printf("Failed to read the file.\n");
        close(fd);
        return 1;
    }

    printf("Read %ld bytes: %s\n", bytesRead, buffer);

    close(fd);

    return 0;
}

写文件操作示例:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    char *content = "Hello, world!";

    //写文件需要使用三个参数的函数,因为在写入时有可能文件不存在就需要创建文件,创建文件就需要
    //给文件赋予对应的权限,0644就是创建文件的权限
    umask(0);//把权限掩码设置为0,否则创建文件的权限会对不上,这里的设置只对代码里创建的文件有作用。
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);//O_WRONLY | O_CREAT | O_TRUNC 连续使用多个标志需要使用 | 

    if (fd == -1) {
        printf("Failed to open the file.\n");
        return 1;
    }

    //使用系统接口进行IO的时候,一定要注意 '\0' 的问题
    //因为 \0 时C语言的规定,不是文件的规定。
    ssize_t bytesWritten = write(fd, content, strlen(content));//strlen不需要+1,因为系统IO进行不需要\0

    if (bytesWritten == -1) {
        printf("Failed to write to the file.\n");
        close(fd);
        return 1;
    }

    printf("Written %ld bytes.\n", bytesWritten);

    close(fd);

    return 0;
}

在读文件和写文件操作中,我们可以使用read和write函数来实际读取和写入文件内容,并使用close函数关闭文件描述符。(在使用系统IO时需要注意 \0 问题,写时不需要加1,读时需要减1)

read和write是用于在Linux系统上进行文件读取和写入操作的系统调用函数。

read 函数是用于从文件描述符中读取数据的系统调用函数。它的原型如下:

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

参数说明:

  • fd:文件描述符,指定要读取的文件或套接字。
  • buf:指向存储读取数据的缓冲区的指针。
  • count:要读取的最大字节数。

返回值:

  • 如果成功读取数据,则返回实际读取的字节数(可能小于 count)。
  • 如果已到达文件末尾(EOF),返回值为 0。
  • 如果出现错误,返回值为 -1,并设置 errno 变量来指示具体的错误原因。

read 函数的工作原理是从文件描述符中读取字节流,将数据存储到缓冲区 buf 中,并返回实际读取的字节数。它是一个阻塞调用,即当没有数据可读时,程序会被阻塞,直到有数据可读或出现错误。

write 函数是用于向文件描述符中写入数据的系统调用函数。它的原型如下:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

参数说明:

  • fd:文件描述符,指定要写入的文件或套接字。
  • buf:指向要写入数据的缓冲区的指针。
  • count:要写入的字节数。

返回值:

  • 如果成功写入数据,则返回实际写入的字节数(通常为 count)。
  • 如果出现错误,返回值为 -1,并设置 errno 变量来指示具体的错误原因。

write 函数的工作原理是将缓冲区 buf 中的数据写入到文件描述符所指向的文件或套接字。它是一个阻塞调用,即当写入的数据无法立即写入时,程序会被阻塞,直到能够写入数据或出现错误。

以下是使用read和write函数读取和写入文件的示例:

#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("file.txt", O_RDONLY);

    if (fd == -1) {
        printf("Failed to open the file.\n");
        return 1;
    }

    char buffer[1024];
    ssize_t bytesRead = read(fd, buffer, sizeof(buffer));

    if (bytesRead == -1) {
        printf("Failed to read the file.\n");
        close(fd);
        return 1;
    }

    printf("Read %ld bytes: %s\n", bytesRead, buffer);

    close(fd);

    char *content = "Hello, world!";
    ssize_t bytesWritten = write(fd, content, strlen(content));

    if (bytesWritten == -1) {
        printf("Failed to write to the file.\n");
        close(fd);
        return 1;
    }

    printf("Written %ld bytes.\n", bytesWritten);

    close(fd);

    return 0;
}

注意,在写文件之前,我们需要以写入模式打开文件。如果文件不存在,可以使用O_CREAT标志来创建文件。

3.open、write、read、close、lseek ,类比C文件相关接口

在C语言中,文件相关接口用于打开、读取、写入、关闭和移动文件指针。以下是与C文件相关的类比接口:

  1. 打开文件:fopen函数用于打开一个文件,并返回一个指向文件的指针。类比函数为 open。
  2. 读取文件:fread函数用于从文件中读取指定数量的数据。类比函数为 read。
  3. 写入文件:fwrite函数用于向文件中写入指定数量的数据。类比函数为 write。
  4. 关闭文件:fclose函数用于关闭先前打开的文件。类比函数为 close。
  5. 移动文件指针:fseek函数用于在文件中移动文件指针的位置。类比函数为 lseek。

这些类比接口在C语言中用于处理文件操作,对于每个接口,可以使用类似的函数来实现相似的功能。

这些文件相关接口在C语言中实际上是对底层系统I/O函数的封装。因为语言或者库函数不能直接去访问硬件设备,只能通过操作系统提供的系统调用接口来拿到数据,比如下图中用户部分就是我们使用语言的所在位置:

编程语言或者库函数不能直接访问硬件设备,因此它们需要通过操作系统提供的系统调用接口来进行文件的访问和操作。这些系统调用接口是操作系统提供给应用程序使用的接口,它们允许应用程序与底层硬件设备或者文件系统进行交互。

为了提供更简单和统一的文件操作接口,C语言(及其他编程语言)提供了一套高级的文件操作函数,如fopen、fread、fwrite、fclose和fseek。这些函数封装了底层的系统调用,使得文件的读写操作变得更加方便和易于使用。这样,开发者可以更方便地使用这些函数来读取和写入文件,而无需直接与底层的系统I/O函数和系统调用接口交互,当我们调用这些高级文件操作函数时,它们会在内部调用底层的系统I/O函数,通过系统调用接口与操作系统进行通信,实现对文件的读写操作。

因此,我们可以说这些文件相关接口是对底层系统I/O函数的封装,它们隐藏了底层细节,简化了文件操作的过程,使开发者能够更轻松地进行文件的读写操作。

4.open函数返回值

open函数用于打开一个文件,并返回一个整数值作为文件描述符(file descriptor)。文件描述符是一个非负整数,用于标识在进程中打开的文件。

open函数的返回值可以有以下几种情况:

  • 成功打开文件:如果文件打开成功,open函数将返回一个非负整数,该整数表示文件描述符。这个文件描述符可以用于后续的读取、写入和其他文件操作。
  • 打开失败:如果文件打开失败,open函数会返回一个特殊的值-1。这通常表示打开文件时发生了错误,可能是因为文件不存在、权限不足、文件被占用等。

4.1文件描述符 'fd' (返回值)

0 & 1 & 2:
Linux进程默认情况下会有3个默认打开的文件描述符,分别是标准输入0(stdin), 标准输出1(stdout), 标准错误2(stderr)。0,1,2对应的物理设备一般是:键盘,显示器,显示器。(因为Linux下一切皆文件,所以向显示器打印,本质就是向文件写入。)

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访
问的。所以C库当中的FILE结构体内部,必定封装了fd。

5.重定向和文件描述符的分配规

补充:

注:>表示:输入重定向,>>表示:输入追加重定向,<表示:输出重定向。

5.1使用dup2系统调用

像上面的那种做法太过于麻烦,我们可以通过使用dup2完成之前的操作,dup2系统调用是一个在Linux系统中可用的系统调用,它用于复制文件描述符。

系统调用的原型是:

#include <unistd.h>

int dup2(int oldfd, int newfd);

参数oldfd是要复制的文件描述符,参数newfd是新的文件描述符。当成功时,dup2会返回新的文件描述符,如果出现错误,则返回-1。

dup2的作用是将oldfd复制到newfd,并关闭newfd之前指向的文件描述符(如果它已经打开)。这样可以方便地重定向文件描述符,例如将标准输出重定向到文件中。

下面是一个简单的示例,将标准输出重定向到文件中:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    if (dup2(fd, 1) == -1) {
        perror("dup2");
        close(fd);
        return 1;
    }

    printf("This will be written to the file.\n");

    close(fd);
    return 0;
}

6.如何理解缓冲区

一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后。
但是进程退出之后,会统一刷新,写入文件当中。
但是fork的之后,因为父子进程指向的是同一个缓冲区,如果父子进程要退出,谁先退出谁就要对缓冲区进行清空,所以父子进程数据就会发生写时拷贝,谁先退出谁先发生写时拷贝,所以就产生了两份数据。
write 没有变化,说明没有所谓的缓冲

综上: printf和fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,
都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统
调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是
C,所以由C标准库提供。

6.1自主封装FILE

//Makefile
testfile:main.c mystdio.c
		g++ $^ -o $@
.PHONY:clean
clean:
		rm -f testfile
//main.c
#include "mystdio.h"
#include <string.h>
#include <unistd.h>


#define MYFILE "log.txt"

int main()
{
    MY_FILE* fp = my_fopen(MYFILE, "w");
    if (fp == NULL)
        return 1;
    
    const char* str = "hello my fwrite";
    int cnt = 5;
    //操作文件
    while(cnt)
    {
      char buffer[1024];
      snprintf(buffer, sizeof(buffer), "%s:%d\n", str, cnt--);
      size_t size = my_fwrite(buffer, strlen(buffer), 1, fp);
      sleep(1);
      printf("当前成功写入:%lu个字节\n", size);
    }
    
    my_fclose(fp);

  return 0;
}
//mystdio.c
#include "mystdio.h"
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <malloc.h>
#include <unistd.h>
#include <assert.h>


MY_FILE* my_fopen(const char* path, const char* mode)
{
    //1.识别标志位
    int flag = 0;
    if (strcmp(mode, "r") == 0) flag |= O_RDONLY;
    else if(strcmp(mode, "w") == 0) flag |= (O_CREAT | O_WRONLY | O_TRUNC);
    else if(strcmp(mode, "a") == 0) flag |= (O_CREAT | O_WRONLY | O_APPEND);
    else {
      //"r+","w+".....
    }

    //2.尝试打开文件
    mode_t m = 0666;
    int fd = 0;
    if(flag & O_CREAT) fd = open(path, flag, m);//使用系统接口打开文件
    else fd = open(path, flag);

    if (fd < 0) return NULL;//打开文件失败
    
    //3.给用户返回MY_FILE对象,所以需要先进行构建一个对象
    MY_FILE *mf = (MY_FILE*)malloc(sizeof(MY_FILE));
    if(mf == NULL)
    { 
      close(fd);
      return NULL;
    }

    //4.初始化mf对象
    mf->fd = fd;
    mf->flags = 0;
    mf->flags |= BUFF_NONE;//可以直接使用 = 也可以使用 |= ,因为只会出现一种情况。
    memset(mf->outputbuffer, '\0', sizeof(mf->outputbuffer));
    mf->current = 0;
    //5.返回打开的文件
    return mf;
}

size_t my_fwrite(const void* ptr, size_t size, size_t nmemb, MY_FILE* stream)
{
    //1.缓冲区如果已经满了,就直接写入
    if (stream->current == 0) my_fflush(stream);
    
    //2.根据缓冲区剩余的情况,进行数据拷贝即可
    size_t user_size = size * nmemb;
    size_t my_size = NUM - stream->current;
  
    size_t writen = 0;
    //3.更新计数器字段
    if(my_size >= user_size)
    {
      memcpy(stream->outputbuffer + stream->current, ptr, user_size);
      stream->current += user_size;
      writen = user_size;
    }
    else 
    {
      memcpy(stream->outputbuffer + stream->current, ptr, my_size);
      stream->current += my_size;
      writen = my_size;
    }
    
    //4.开始计划刷新
    //不发生刷新的本质,就是不进行写入,不进行IO,不进行系统调用,所以my_fwrite函数调用会非常快,数据会暂时保存在缓冲区中
    //可以在缓冲区中积压多分数据,统一进行刷新写入,本质:就是一次IO可以IO更多的数据,提高IO效率
    if (stream->flags & BUFF_ALL)
    {
        if (stream->current == NUM) my_fflush(stream);
    }
    else if(stream->flags & BUFF_LINE)
    {
        if(stream->outputbuffer[stream->current - 1] == '\n') my_fflush(stream);
    }
    else 
    {
        //TODO
        ;
    }

    return writen;
}

//刷新数据到系统
int my_fflush(MY_FILE* fp)
{
    assert(fp);
    //将用户缓冲区的数据,通过系统调用接口,冲刷给OS
    write(fp->fd, fp->outputbuffer, fp->current);
    fp->current = 0;

    fsync(fp->fd);//fsync函数强制把数据刷新到文件当中,不需要等待系统的刷新策略把数据刷新到文件中。
    //因为数据需要先拷贝到语言的缓冲区中,然后在拷贝到内核的缓冲区,最后等待系统的刷新策略把数据刷新到磁盘的文件当中(最少要拷贝三次),
    //但是如果在没有刷新到磁盘的时候断电了,数据不就没了吗?所以可以通过fsync函数强制把数据刷新到磁盘当中,类似我们在使用一些软件的
    //ctrl+s保存键一样。
    return 0;
}

int my_fclose(MY_FILE* fp)
{
    assert(fp);
    //1.刷新缓冲区
    if(fp->current > 0) my_fflush(fp);
    //2.关闭文件
    close(fp->fd);
    //3.释放堆空间
    free(fp);
    //4.指针置为NULL
    fp = NULL;

    return 0;
}
//mystdio.h
#pragma once 

#include <stdio.h>

#define NUM 1024
#define BUFF_NONE 0x1//无缓冲
#define BUFF_LINE 0x2//行缓冲
#define BUFF_ALL  0x4//全缓冲


typedef struct MY_FILE
{
    int fd;
    char outputbuffer[NUM];//提供缓冲区
    int flags;
    int current;
} MY_FILE;


MY_FILE* my_fopen(const char* path, const char* mode);
size_t my_fwrite(const void* ptr, size_t size, size_t nmemb, MY_FILE* stream);
int my_fclose(MY_FILE* fp);
int my_fflush(MY_FILE* fp);

关于缓冲区,通过自主封装的FILE,我们历史所谈的用户级缓冲区是由语言提供的。

7.理解文件系统

我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据。

[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out
-rw-r--r--. 1 root root 654 "9月 13 14:56" test.c

每行包含7列:
模式、硬链接数、文件所有者、组、大小、最后修改时间、文件名。

其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息

[root@localhost linux]# stat test.c
File: "test.c"
Size: 654 Blocks: 8 IO Block: 4096 普通文件
Device: 802h/2050d Inode: 263715 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800

上面的执行结果有几个信息需要解释清楚
inode
为了能解释清楚inode我们先简单了解一下文件系统

Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的boot block(块,俗称:分区)。一个boot block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中的 块(Boot Block)的大小是确定的。

1.Block Group:Boot Block(块)又会被ext2文件系统会根据分区的大小划分为数个Block Group(分组),而每个Block Group都有着相同的结构组成,并且一个Block Group是从Block Group 0 到 Block Group n(数组下标),所以他们的管理方式都是相同的。

注:文件 = 内容 + 属性,而Linux是将内容和属性分离的,分为以下这几块:
2.超级块(Super Block简称SB):存放文件系统本身的所有属性结构信息(1.文件系统的类型。2.整个分组的情况)。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。SB在各个分组里面可能都会存在,而且是统一更新的,而这么做是为了防止SB区域坏掉,如果出现故障,整个分区就不可以被使用!需要做好备份!(Boot Block是一个区里的第一个块,而Super Block又是每一个Boot Block组里面的首块)。
3.Group Descriptor Table简称GDT:组描述符,描述组属性信息,用来改变组内的详细统计等属性信息。
4.块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用(每个bit表示一个Data blocks是否空闲可用)。
5.inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
6.i节点表(inode Table):存放文件属性 如 文件大小,所有者,最近修改时间等。一般而言,一个文件,内部所有属性的集合,inode节点(128字节),一个文件,一个inode,其中即便是一个分区,内部也会存在大量的文件,即会存在大量的inode节点,一个group,需要有一个区域来专门保存该group内的所有文件的inode节点(inode table),inode表分组内部,可能会存在多个inode需要将inode区分开来,每一个inode都会有自己的inode编号,inode编号也有属于对应的属性id,Linux查找一个文件,是要根据inode编号,来进行文件查找的,包括读取内容。一个inode对应一个文件,而改文件inode属性和改文件对应的数据块,是有映射关系的。
7.数据区(Data blocks):存放文件内容。而文件的内容是变化的,我们是用数据块来进行文件内容来保存的,所以一个有效文件,要保存内容,就需要[1,n]数据库,如果有多个文件呢?需要更多的数据块,Date Blocks。

注:

查找一个文件

1.inode vs 文件名:Linux系统只认识inode号,文件的inode属性中,并不存在文件名,文件名是给用户的

2.重新认识目录:目录是文件吗?是的,目录有inode吗?有的,有内容吗、有,内容是什么?

3.任何一个文件,一定在一个目录内部,所以目录的内容是什么?需要数据块,目录的数据块里面保存的是该目录下文件名和文件inode编号对应的映射关系,而且,在目录内,文件名和inode(内容)互为key值

4.当我们访问一个文件的时候,我们是在特定的目录下访问的,cat log.txt:

1.先要在当前的目录下,找到log.txt的inode编号

2.一个目录也是一个文件。也一定隶属于一个分区,结合inode,在该分区中找到分组,在该分组中inode table中,找到文件的inode。

3.通过inode和对应的data blocks的映射关系,找到该文件的数据块,并加载到OS,并完成显示到显示器的工作。

删除一个文件

1.找到根据文件名 -> inode number(编号)

2.inode number -> inode 属性中的映射关系,设置 block bitmap 对的比特位,置为0即可。

3.inode number 设置 inode bitmap 对应的比特位设置为0。

删除文件只需要修改问题即可。

将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看是如何工作的。

[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
263466 abc//abc 的inode

为了说明问题,我们将上图简化:

创建一个新文件主要有一下4个操作:
1. 存储属性
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
2. 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
3. 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
4. 添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

注:内核缓冲区的第一块数据指的是要存储在新文件中的数据。当创建一个新文件时,需要将文件内容写入磁盘,而这些内容首先会存储在内核的缓冲区中。

内核缓冲区是内核在内存中维护的一个区域,用于暂时存储文件系统的读写操作。当需要将数据写入磁盘时,内核将数据从应用程序或操作系统的缓冲区复制到内核缓冲区,然后再由内核将数据从内核缓冲区写入磁盘。

在创建一个新文件并向其中写入数据时,首先需要将文件数据存储在内核缓冲区中。然后,内核根据文件系统的分配策略将数据写入磁盘的相应块中。在这个过程中,内核缓冲区的第一块数据指的是要存储到文件中的第一块数据。

请注意,这里的内核缓冲区是指操作系统内核维护的一块内存区域,用于临时缓存文件系统的读写数据,而不是特指某个具体的数据内容。

7.1硬链接

硬链接(Hard Link)是一种特殊类型的文件链接,它创建一个与原始文件具有相同索引节点(Inode)的新文件,它们共享同一个数据块。与软链接不同,硬链接不创建一个指向目标文件的路径,而是直接将新文件与源文件关联在一起。

硬链接的作用是创建一个指向同一数据块的文件副本,当访问硬链接时,实际上是访问与之关联的原始文件。在文件系统中,源文件和硬链接之间没有区别,它们是对同一个文件的不同名称。

硬链接有以下特点:

  1. 硬链接只能链接到文件,不能链接到目录。
  2. 硬链接只能链接到同一个文件系统中的文件。
  3. 硬链接与源文件具有相同的权限、时间戳和文件内容。
  4. 硬链接的大小与源文件大小相同,共享同一份数据。
  5. 删除源文件或硬链接中的任意一个都不会影响其他链接文件,因为它们共享相同的数据。

硬链接的使用场景包括:

  1. 创建文件的备份,确保即使源文件被删除,仍然存在一个链接文件。
  2. 在多个位置引用同一个文件,以节省存储空间。
  3. 使得对一个文件的修改可以通过不同的名称访问。
  4. 在一些应用程序中,如版本控制系统,使用硬链接来跟踪文件的版本变化。

7.2软链接

软链接(Symbolic Link),也被称为符号链接或软连接,是一种特殊类型的文件,它指向另一个文件或目录。软链接可以在不移动或复制源文件的情况下引用它,它类似于Windows中的快捷方式。

软链接的作用是创建一个指向目标文件或目录的路径,当访问软链接时,实际上是访问目标文件或目录。软链接可以跨越文件系统边界,可以链接到不同的文件系统或分区。

软链接有以下特点:

  1. 软链接可以链接到文件或目录。
  2. 软链接可以跨越文件系统边界,可以链接到不同的文件系统或分区。
  3. 软链接可以被删除而不影响源文件,但如果源文件被删除,软链接将失效。
  4. 软链接的大小只占用一个文件块的空间,而不是源文件实际大小。
  5. 软链接可以创建循环链接,即链接到自身或链接链中的其他链接。

软链接的使用场景包括:

  1. 创建方便的快捷方式,使得访问某个文件或目录更加方便。
  2. 在不移动或复制源文件的情况下,在不同的目录中引用同一个文件。
  3. 在系统中创建其他名称的文件或目录的别名。

7.3如何创建软链接和硬链接

要创建软链接和硬链接,可以使用ln命令。以下是创建软链接和硬链接的示例:

  1. 创建软链接:
ln -s [源文件] [链接文件]
    • [源文件]:要创建链接的源文件或目录。
    • [链接文件]:要创建的软链接文件的名称。

例如,要创建一个名为link的软链接,指向/path/to/file.txt文件,可以使用以下命令:

ln -s /path/to/file.txt link
  1. 创建硬链接:
ln [源文件] [链接文件]
    • [源文件]:要创建链接的源文件。
    • [链接文件]:要创建的硬链接文件的名称。

例如,要创建一个名为link的硬链接,指向/path/to/file.txt文件,可以使用以下命令:

ln /path/to/file.txt link

需要注意的是,硬链接只能链接到同一文件系统中的文件,而软链接可以跨越文件系统边界。另外,无论是软链接还是硬链接,只要源文件被删除,链接文件将失效

注:

在Linux文件系统中,每个文件和目录都有一个链接计数器(link count)。当创建一个新目录时,其链接计数器的初始值是2(指的就是硬链接数)。

这是由于Linux文件系统的设计原理。在Linux中,目录本身也是一个特殊类型的文件。每个目录都包含至少两个链接:

  1. "."(当前目录)链接:每个目录都包含一个指向自身的链接,即当前目录。这个链接会增加当前目录的链接计数器。
  2. ".."(上级目录)链接:每个目录还包含一个指向上级目录的链接,即上级目录。这个链接同样会增加目录的链接计数器,但是不是增加当前目录的硬链接计数器,因为它指向的是上一个目录。

可以使用指令ls -di 查看 "." 和 ".." 的inode编号,然后在查看 当前目录(文件名) 和 上级目录(文件名) ,当前目录和"."的inode是一样的,而".."和上级目录的inode是一样的。

因此,当创建一个新目录时,这两个链接会自动添加到目录中,只有"."会导致当前目录链接计数器增加,所以初始值为2,一个是代表目录自身,一个是".",而".."会增加上级目录的链接计数器。

当向目录中添加其他文件或子目录时,目录的链接计数器会相应增加。当删除目录中的文件或子目录时,目录的链接计数器会相应减少。只有当链接计数器的值减少到0时,才表示该目录真正被删除,释放其占用的磁盘空间。

需要注意的是,硬链接计数器不包括符号链接(软链接)。符号链接是一种特殊类型的文件,它只包含一个链接,不会影响目录的链接计数器。

8.动态库和静态库

1.静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
2.动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
3.一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。(机器码(Machine code),也称为目标代码(Object code)或二进制代码(Binary code),是计算机能够直接执行的最底层的指令集表示形式。机器码是由一系列二进制位组成的,用于告诉计算机的处理器具体要执行的操作。)
4.在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
5.动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

//测试程序
/add.h/
#ifndef __ADD_H__
#define __ADD_H__
int add(int a, int b);
#endif // __ADD_H__
/add.c/
#include "add.h"
int add(int a, int b)
{
    return a + b;
}
/sub.h/
#ifndef __SUB_H__
#define __SUB_H__
int sub(int a, int b);
#endif // __SUB_H__
/add.c/
#include "add.h"
int sub(int a, int b)
{
    return a - b;
}
///main.c
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main( void )
{
    int a = 10;
    int b = 20;
    printf("add(10, 20)=%d\n", a, b, add(a, b));
    a = 100;
    b = 20;
    printf("sub(%d,%d)=%d\n", a, b, sub(a, b));
}

库(Library)的存在有以下几个重要原因:

  1. 代码复用:库中包含了一组已经实现的函数、变量和数据结构,开发者可以在自己的程序中重复使用这些代码。通过使用库,开发者无需重新编写已经存在的功能,可以节省开发时间和工作量。库提供了一种模块化的方式来组织和管理代码,使得代码更易于维护和重用。
  2. 提高开发效率:使用库可以加速开发过程,尤其是对于常见的功能和任务。库中的函数经过优化和测试,可以提供高效和可靠的功能。开发者可以基于库来快速构建应用程序,而无需从头开始编写所有的代码。
  3. 共享和交互:库是一种共享代码的机制,多个开发者可以共享和访问同一个库。这样,在开发社区中可以共享和交流代码,并且能够更容易地整合不同的模块和组件。库还可以提供一种标准化的接口,使得开发者可以在不同的应用程序中使用同一个库来实现相同的功能。
  4. 抽象和封装:库通过提供抽象和封装的接口,隐藏了底层的实现细节。开发者可以通过库的接口来使用功能,而无需了解底层的实现和细节。这种封装可以提高代码的可维护性和可扩展性,使得开发者能够更专注于应用程序的逻辑和功能实现。

总而言之,库的存在可以提供代码复用、开发效率、共享和交互等好处。它们是一种重要的工具,帮助开发者构建高效、可靠和可维护的应用程序。

8.1静态库的创建和使用

"ar -rc"命令

"ar -rc"是一条用于创建或修改静态库(archive)的Unix/Linux命令。

具体的命令格式为:

ar -rc <archive_name> <file1> <file2> ...
  • "ar"是用来创建或修改Unix/Linux静态库的命令。它是“archive”的缩写。
  • "-rc"是命令的选项参数,其中:
    • "-r"表示将文件追加到已存在的静态库中,或者创建新的静态库。
    • "-c"表示创建一个新的静态库,如果静态库已存在,则会被覆盖。
  • "archive_name"是要创建或修改的静态库的名称。
  • "file1", "file2", ... 是要添加到静态库中的目标文件的名称,可以是多个文件。

例如,要创建一个名为"mylib.a"的静态库,并将"file1.o"和"file2.o"添加到其中,可以使用以下命令:

ar -rc mylib.a file1.o file2.o

这将创建一个名为"mylib.a"的静态库,并将"file1.o"和"file2.o"添加到其中。如果已经存在同名的静态库"mylib.a",那么它将被覆盖。

演示:

[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
//可以不输入 -o xxx.o 这部分,gcc -c xxx.c会自动生成同名的.o文件
生成静态库
[root@localhost linux]# ar -rc libmymath.a add.o sub.o

查看静态库中的目录列表
[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 
//注:. 和 mymath可以不使用空格和选项参数隔开,例如:gcc -o mytest main.c -L . -l mymath
-L 指定库路径
-l 指定库名
测试目标文件生成后,静态库删掉,程序照样可以运行。

在创建的静态库的名字前面加上lib是什么意思?可以不加吗?为什么编译链接时要去掉前面的lib部分和 . 后面的部分?

在创建的静态库的名字前面加上"lib"是一种命名规范,它是为了表明该文件是一个库文件。这是常见的命名约定,以帮助开发者识别一个文件是否是一个库文件。

虽然这种规范是推荐的,但并不是强制的,你可以自由选择是否在静态库的名字前加上"lib"。不过,如果你决定不按照这个规范命名,建议在使用该静态库时,保持一致性并清晰地标明该文件是一个库文件,以避免命名冲突或混淆。

在编译链接时,为了指定库文件的名称,通常需要使用编译器选项参数。为了正确地指定静态库的名称,需要去掉库文件名字前面的"lib"部分和后面的文件扩展名,以及在指定库文件时不需要包含文件的完整路径,只需要指定库文件名字即可(如:mymath才是真正的库名)。这是因为编译器在搜索库文件时会按照一定的规则进行搜索。

例如,如果你的静态库名字是"libmymath.a",则在编译链接时,使用-lmymath来指定要链接的静态库。编译器会在默认的库搜索路径中查找名字为"libmymath.a"的静态库文件。

需要注意的是,具体的编译器和操作系统可能会有一些差异,因此在使用时需要查看具体的编译器文档或相关说明来了解正确的选项和命名规则。

使用自己创建的静态库时,通常需要注意以下几点:

  1. 包含头文件:在使用静态库的源代码中,需要包含静态库提供的头文件,以便使用其中的函数、结构体等定义。通常可以通过#include预处理指令来包含头文件。
  2. 编译链接:在编译源代码时,需要通过编译器的选项参数指定要链接的静态库文件。例如,使用 -l 选项指定静态库名称,使用 -L 选项指定静态库的搜索路径。
  3. 链接顺序:如果你的源代码依赖于多个静态库,确保按照正确的顺序链接它们。一般来说,将依赖的静态库放在被依赖的静态库的后面进行链接。
  4. 兼容性问题:如果你的静态库在不同的平台或操作系统上使用,需要注意兼容性问题。确保静态库在目标平台上能够正常链接和运行。
  5. 版本管理:如果你对静态库进行了更新或修复,需要确保使用了新版本的静态库。避免使用旧版本的静态库,以免引发错误或安全问题。
  6. 构建和发布:在将自己的静态库提供给其他开发者或用户使用时,需要提供适当的文档和示例代码,以便他们能够正确地使用和链接你的静态库。

需要注意的是,静态库在编译时会被完整地复制到可执行文件中,因此会增加可执行文件的大小。同时,静态库无法在运行时更新,如果需要更新功能或修复问题,需要重新编译使用了静态库的程序。因此,在使用静态库时,需要权衡代码重用性、可维护性和可执行文件的大小等因素。

第三方库的使用方法:

如果需要和默认的库一样,自动搜索头文件和库,可以把存放头文件的目录拷贝到 /usr/include/ 目录底下,/usr/include/ 是系统默认存放头文件的地方,把存放库文件(目标文件)的库拷贝到 /lib64/ 目录底下 /lib64/ 是系统默认存放库的地方,然后我们就可以直接使用 g++ main.c 直接编译生成可执行程序了吗?还不可以,因为我们的是第三方库,所以必须要要使用 -l 指明具体库的名称,也就是不能直接使用 g++ main.c,写法要改成g++ main.c -l mymath 的形式才可以执行使用第三方库进行编译。

接上图中的第三方库的使用:

3.我们将下载下来的库(第三方库),拷贝到系统默认路径底下的这种方式就是在Linux下载安装库!那么卸载呢?就是把它从系统默认路径底下删除!对于任何软件而言,安装和卸载的本质就是拷贝到系统特定的路径下!

4.如果我们安装的库是第三方的库,要正常使用,即便是已经安装到了系统中,gcc/g++进行编译时必须用 -l 指明具体库的名称!

理解现象:

无论你是从网络中未来直接下好的库,或者是源代码(编译方法) --- make、install安装的命令 -- cp,安装到系统中,我们安装大部分指令,库等等都是需要sudo的或者超级用户操作!

8.2动态库的创建和使用

8.1编译选项 -fPIC选项

在Linux中,"-fPIC"选项用于编译生成位置无关代码(Position Independent Code,PIC)。位置无关代码是指在内存中可以加载到任意位置而无需修改其代码的代码。(说明白了就是它里面的代码和数据与静态库的地址排布是有差别的)

使用"-fPIC"选项编译代码时,生成的目标文件中的代码被设计为可以在内存中加载到任意位置。这对于生成动态库或共享库非常重要,因为这些库可以被加载到不同的内存地址并与其他库共享。而如果不使用"-fPIC"选项,则生成的代码将是位置相关的,只能加载到特定的内存地址。

位置无关码(Position Independent Code,PIC)是一种编程技术,用于生成可在内存中的任意位置加载和执行的代码。位置无关码是一种特殊的代码形式,其执行不依赖于在内存中的具体位置。

在传统的编程模型中,代码在编译时通常会使用绝对地址,这意味着代码中的指令和数据引用使用的是固定的内存地址。然而,如果将这样的代码移动到不同的内存地址上运行,由于地址发生变化,代码的指令和数据引用可能会指向错误的位置,导致运行错误。

位置无关码通过使用相对地址或特殊的加载和访问方式,使得代码的指令和数据引用不依赖于具体的内存位置。这样,在代码被加载到内存中的时候,可以通过修正相对地址或使用特殊的访问方式,使代码在任意位置正确运行。

位置无关码主要用于以下场景:

  1. 动态链接库(共享库):位置无关码可以在不同的进程之间共享,因为它们不依赖于具体的内存位置。
  2. 内存虚拟化:在虚拟化环境中,位置无关码可以在不同的虚拟机之间迁移,并在不同的内存地址上正确运行。
  3. 随机化布局:位置无关码可以用于增加系统的安全性,通过随机化加载地址,增加攻击者对于特定地址的预测难度。

编写位置无关码需要考虑以下方面:

  1. 使用相对地址:相对地址可以通过偏移量计算得到,而不是直接使用绝对地址。
  2. 使用特殊的访问方式:例如,可以使用全局偏移表(GOT)和过程链接表(PLT)来解决函数和变量的引用问题。

位置无关码是一种在编译器生成的目标文件中使用的代码格式。它的主要特点是不依赖于特定的绝对内存地址,而是使用相对地址和基址寄存器来定位和访问代码和数据。

位置无关码的主要作用包括:

  1. 共享性:位置无关码使得目标文件在内存中加载时可以被多个进程或程序共享使用。因为相对地址和基址寄存器的使用,使得目标文件中的代码和数据能够在内存中以位置无关的方式加载,不会与其他库或可执行文件的地址空间冲突,从而实现了共享性。
  2. 加载的方便性:位置无关码的目标文件可以在程序运行时动态加载到内存中,并进行必要的地址重定位。由于不依赖于特定的绝对内存地址,加载时可以将库文件加载到任意合适的内存位置,而不需要修改可执行文件的代码。这样,系统在加载时只需要简单地将目标文件加载到内存中,并进行基址的设定和必要的地址重定位,就能够正确执行目标文件中的代码和访问数据。
  3. 跨平台移植性:位置无关码可以在不同的操作系统或不同的内存布局下正确加载和运行。由于使用相对地址和基址寄存器,目标文件可以在不同的平台上以及在不同的内存布局情况下都能保持一致的代码执行逻辑,提高了跨平台移植性。这使得开发人员可以更方便地将代码和库文件移植到不同的操作系统或平台上,而不需要重新编写和调整代码。

总结来说,位置无关码的主要作用是实现共享性、加载方便性和跨平台移植性。通过使用相对地址和基址寄存器来定位和访问代码和数据,目标文件可以在内存中以位置无关的方式加载和运行,提高了库文件的灵活性和可移植性。

地址重定位是指在将位置无关码的目标文件加载到内存并执行时,根据实际的加载地址对其中的地址引用进行调整,使得代码和数据能够正确地访问。

地址重定位通常由操作系统的链接器(或加载器)在目标文件加载到内存时完成。下面是一般的地址重定位过程:

  1. 加载目标文件:操作系统的链接器将位置无关码的目标文件加载到内存中的一个合适的地址空间。
  2. 计算基址:链接器为目标文件分配一个基址(Base Address),即目标文件加载到内存中的起始地址。
  3. 重定位:链接器遍历目标文件的重定位表(Relocation Table),定位需要进行重定位的地址引用,并根据基址和目标文件中存储的相对偏移量进行调整。这样,通过将基址与相对偏移量相加,可以得到正确的绝对内存地址。
  4. 修改引用:链接器将经过重定位的地址引用修改为正确的绝对地址。这样,目标文件中的指令和数据就可以正常地访问所需的内存位置。
  5. 执行程序:加载器完成地址重定位后,将控制权交给程序的入口点,使其开始执行。

需要注意的是,地址重定位是在目标文件被加载到内存并执行之前进行的。在加载过程中,链接器根据目标文件中的重定位表和基址信息计算出正确的地址,并将其修改到目标文件中的相应引用位置。这样,目标文件中的代码和数据就可以正确地访问所需的内存位置,从而使得程序能够正常执行。

总结来说,地址重定位是通过计算基址和相对偏移量的组合来调整位置无关码目标文件中的地址引用,使得代码和数据能够正确地访问内存。这一过程由操作系统的链接器在目标文件加载到内存时进行。

综上所述,位置无关码是一种在内存中的任意位置加载和执行的代码,可以用于实现动态链接库、内存虚拟化和随机化布局等场景。

例如,使用以下命令编译生成位置无关代码:

gcc -c -fPIC myfile.c

该命令将编译myfile.c文件并生成位置无关的目标文件myfile.o

在创建动态库时,通常需要使用"-fPIC"选项编译生成位置无关代码的目标文件,然后将这些文件链接到动态库中。

请注意,"-fPIC"选项只适用于生成位置无关代码的情况。

8.2编译选项 -shared选项

在Linux中,"-shared"选项用于告诉编译器创建一个共享库(Shared Library)。

共享库(动态库)是一种可以在多个程序之间共享使用的库,也称为动态链接库(Dynamic Linking Library)。与静态库不同,共享库在运行时动态加载到内存中,可以被多个程序共享使用,因此可以减小可执行文件的大小,并且允许库的更新和升级。

使用"-shared"选项编译时,需要提供使用 gcc -c -fPIC 生成的包含所需函数和数据结构的目标文件(通常是通过使用"-c"选项单独编译源文件生成的),然后将这些目标文件链接成一个共享库。

例如,使用以下命令创建共享库:

gcc -shared -o libmyfile.so myfile1.o myfile2.o

该命令将生成一个名为"libmyfile.so"的共享库,其中包含了目标文件"myfile1.o"和"myfile2.o"中的代码。

创建共享库后,可以将其和头文件放置在系统默认的共享库搜索路径上这样就可以自动搜索(例如"/usr/lib" 和 "/usr/include"),或者在编译和链接程序时使用"-L"选项指定库文件的搜索路径,和需要使用"-l"选项指定需要链接的共享库。例如:

gcc -o mytest main.c -L. -lmyfile
//整体来说使用方法和静态库是一样的,如个头文件不在当前路径底下,也是要使用 -I 指明头文件路径的

该命令将编译并链接名为"main.c"的源文件和共享库"libmyfile.so"。

通过使用"-shared"选项,可以方便地创建共享库,并在编译和链接程序时使用这些共享库。

当我们使用命令生成可执行程序并执行程序时:

解决方法:

1.环境变量

在Linux中,可以通过将库路径添加到LD_LIBRARY_PATH环境变量中来指定库文件的搜索路径。

  1. 临时添加库路径:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/library
//注:$LD_LIBRARY_PATH和/path/to/library均可以放在 (:) 前后
//注:/path/to/library 这一部分表示自己库所在的路径

以上命令将/path/to/library添加到LD_LIBRARY_PATH中,并将其设为库文件的搜索路径。注意,这种方法只在当前终端会话中有效。

  1. 永久添加库路径(对所有用户有效): 将上述命令添加到/etc/profile文件中。
sudo vim /etc/profile

在文件末尾添加以下行:

$LD_LIBRARY_PATH:LD_LIBRARY_PATH=/path/to/library
export LD_LIBRARY_PATH

保存并关闭文件。重新登录系统,环境变量将会生效。

  1. 永久添加库路径(对特定用户有效): 将上述命令添加到特定用户的~/.bashrc或~/.bash_profile文件中,其中~表示用户的主目录。
vim ~/.bashrc

vim ~/.bash_profile

在文件末尾添加以下行:

LD_LIBRARY_PATH=/path/to/library:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH

保存并关闭文件。然后执行以下命令使修改生效:

source ~/.bashrc

source ~/.bash_profile

这样,特定用户登录后,库路径将自动添加到LD_LIBRARY_PATH中。

请注意,修改环境变量时需谨慎操作,确保只添加信任的库路径。错误的环境变量设置可能导致系统问题。

2.软链接方案

在系统的默认路径底下创建一个软链接指向我们 .so 文件

3.配置文件方案
1.在这个/etc/ld.so.conf.d/路径底下创建一个 XXX.conf的文件
2.使用vim编辑器打开这个 XXX.conf,然后在里面写入你的库所在路径
比如:/root/Day8/otherPerson/lib/
3.在命令行中输入ldconfig即可
ldconfig是一个用于配置动态链接器运行时的命令。
动态链接器是一个运行时库加载器,用于在程序运行时查找并加载共享库(动态链接库)。

8.3库的加载

动态库和静态库是两种不同的库文件类型,它们的加载方式和机制也有所不同。

  1. 静态库(Static Library): 静态库是在编译时期与可执行文件进行静态链接的库。当使用静态库时,编译器将静态库中的代码和数据复制到最终生成的可执行文件中。因此,静态库的加载是在编译时期完成的。

静态库的加载过程包括以下几个步骤:

  • 预处理:处理源代码的预处理指令,并将源代码转化为预处理后的代码。
  • 编译:将预处理后的代码编译成目标文件(通常是以.o或.obj为扩展名)。
  • 链接:将目标文件与静态库进行链接,将库中的代码复制到生成的可执行文件中。在静态链接时,整个库文件都被复制到可执行文件中,包含库中所有的函数和数据。

由于静态库的代码被复制到可执行文件中,所以可执行文件独立于库文件运行,无需依赖外部的库文件。这使得程序的部署和发布更简单,但也导致可执行文件的体积较大。

  1. 动态库(Dynamic Library): 动态库是在运行时期加载到内存(共享区),并与可执行文件进行动态链接的库。当使用动态库的函数时,可执行文件在运行时期才会去加载和链接库中的代码和数据。动态库独立于可执行文件存在,可被多个可执行文件共享,因为动态库已经被加载到了物理内存中,当其他进程需要去加载动态库时,发现动态库已经被加载直接使用即可,并且只要我们把库加载到了内存,通过页表映射到进程的地址空间中之后,我们的代码执行库中的方法,就依旧还是在自己的地址空间内进行函数跳转即可,因为在自己的地址空间中遇到需要执行库中的方法时通过映射关系找到库中对应的方法(并不会直接把整个库直接进行映射,因为有可能库非常的大呢?),再把方法映射到自己的地址空间中,然后代码继续向下指向,如果再碰到需要执行同一个方法的代码,只需要在自己的地址空间内进行函数跳转即可

动态库的加载过程包括以下几个步骤:

  • 加载:当可执行文件被启动时,系统会根据指定的动态库路径查找并加载所需的动态库文件,将其加载到内存中。
  • 符号解析:系统会对可执行文件中对动态库的引用进行符号解析,找到动态库中对应的函数和数据。
  • 运行:一旦动态库被加载和链接,执行流程就会进入到动态库中的代码中,并与可执行文件共享库中的函数和数据。

动态库的优势在于共享和动态更新。多个可执行文件可以共享同一个动态库,减少了存储空间和内存的占用。而且,当动态库更新时,不需要重新编译和链接可执行文件,只需替换动态库文件即可。

总结来说,静态库在编译时期与可执行文件进行静态链接,加载到可执行文件中,而动态库是在运行时期动态加载到内存,并与可执行文件进行动态链接。

注:

1.动态库和静态库同时存在,系统默认采用动态链接。

2.编译器,在链接的时候,如果提供的库既有动,又有静,优先动。只有静,没法,只能静态链接。

(C/C++的静态库是默认没有安装的)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值