Bootstrap

嵌入式Linux八股(二)——Linux

二、Linux

01.Linux系统编程

01.Linux系统文件类型: 7/8 种

  1. 普通文件:-

  2. 目录文件:d

  3. 字符设备文件:c

  4. 块设备文件:b

  5. 软连接:l

  6. 管道文件:p

  7. 套接字:s

  8. 未知文件

文件权限说明

chmod 操作码 filename  直接用操作码修改文件权限rwx-->421
-rwxrw-r--
.421421421
普通文件所有者读写执行权限(7),同组用户读写权限(6),其他人读权限(4)

02.Linux常用命令

  1. find:在特定的目录下 搜索 符合条件的文件

    //按名字查找 -name
    find . -name "*.c*" //在当前目录(包括子目录)中查找所有以 .c 或 .c 开头的文件
    //find按文件大小查找 -size
    find /path/to/directory -size 100c  //查找大小为 100 字节的文件
    find /path/to/directory -size +1M   //查找大于 1M 的文件
    find /path/to/directory -size -100k //查找小于 100k 的文件
    //按文件类型查找 -type d:目录
    find . -type f  //在当前目录(包括子目录)中查找所有的普通文件,并输出符合条件的文件路径

  2. tar 是 Linux 中最常用的 备份工具,此命令可以 把一系列文件 打包到 一个大文件中,也可以把一个 打包的大文件恢复成一系列文件

    # 打包文件
    tar -cvf archive.tar *.c directory/
    # 解包文件
    tar -xvf archive.tar
    #列出归档文件中的内容
    tar -tf archive.tar
    #解压归档文件到指定目录
    tar -xvf archive.tar -C /target/directory

  3. targzip 命令结合可以使用实现文件 打包和压缩。tar 只负责打包文件,但不压缩,用 gzip 压缩 tar 打包后的文件,其扩展名一般用 xxx.tar.gz

    # 压缩文件
    tar -zcvf 打包文件.tar.gz 被压缩的文件/路径...
    # 解压缩文件
    tar -zxvf 打包文件.tar.gz
    # 解压缩到指定路径
    tar -zxvf 打包文件.tar.gz -C 目标路径
    ​
    #tar 与 bzip2 命令结合可以使用实现文件 打包和压缩(用法和 gzip 一样)
    # 压缩文件
    tar -jcvf 打包文件.tar.bz2 被压缩的文件/路径...
    # 解压缩文件
    tar -jxvf 打包文件.tar.bz2

  4. grep允许对文本文件进行 模式查找,所谓模式查找,又被称为正则表达式

    #在多个文件中搜索指定字符串
    grep "pattern" file1.txt file2.txt
    #忽略大小写进行搜索
    grep -i "pattern" file.txt
    #使用"-o"选项, 可以值显示被匹配到的关键字, 而不是讲整行的内容都输出.
    #-n 显示所在行数
    #显示出文章中有多少行有a
    grep "a" test.txt -c

  5. 管道 |:将 一个命令的输出 可以通过管道 做为 另一个命令的输入

    #查找包含 "main" 关键字的进程
    ps aux | grep main

  • tail 是一个在 Unix/Linux 系统中常用的命令行工具,用于显示文件的末尾内容。通常用于查看日志文件或其他可能在文件末尾持续更新的文本文件。tail 命令默认显示文件的末尾 10 行,但也可以通过参数来指定显示的行数或其他选项。

    基本用法是在终端中输入 tail 命令,后跟要查看的文件名,例如:

    tail filename.txt

    如果你想查看文件末尾的 20 行,可以使用 -n 选项:

    tail -n 20 filename.txt

    tail 还可以与 f 选项结合使用,实现实时监视文件内容的功能。例如:

    tail -f filename.txt

    这会持续输出文件的末尾内容,并在文件有更新时实时显示新内容。

  • netstat 是一款命令行工具,可用于列出系统上所有的网络套接字连接情况,包括 tcp, udp 以及 unix 套接字,另外它还能列出处于监听状态(即等待接入请求)的套接字。

    • netstat -tuln:显示所有 TCP 连接和监听端口。

    • netstat -rn:显示路由表。

    • netstat -i:显示网络接口信息。

  • nc 是一个简单而强大的网络工具,也称为 netcat。它可以在网络上读取和写入数据,因此可以用于多种目的,包括端口扫描、端口监听、文件传输等。在不同的操作系统中,nc 的用法略有不同。

  1. 监听端口: 使用 nc -l <port> 命令可以监听指定的端口,等待连接。

  2. 连接到远程主机: 使用 nc <host> <port> 命令可以连接到指定的主机和端口。

  3. 文件传输: nc 可以用于简单的文件传输,例如 nc -l <port> > file.txt 可以接收文件,而 nc <host> <port> < file.txt 则可以发送文件。

  4. 端口扫描: nc 可以用于快速进行端口扫描,例如 nc -zv <host> <start-port>-<end-port>

在Linux系统中,route命令用于显示和操作IP路由表。它允许你查看系统当前的路由信息,并且可以用于添加、删除和修改路由。

以下是一些常用的route命令选项及其功能:

  1. 显示当前路由表

    route -n

    或者

    route -rn

    这会以数字形式显示当前系统的路由表,包括目标网络、网关、接口和其他相关信息。

  2. 添加路由

    route add -net 目标网络 netmask 子网掩码 gw 网关

    这个命令将指定的目标网络添加到路由表中,通过指定的网关进行访问。

  3. 删除路由

    route del -net 目标网络 netmask 子网掩码 gw 网关

    这个命令将从路由表中删除指定的目标网络。

  4. 修改默认网关

    route add default gw 网关

    这个命令将系统的默认网关设置为指定的网关地址。

  5. 临时改变路由

    route add -net 目标网络 netmask 子网掩码 gw 网关 metric 数值

    这个命令可以在不修改配置文件的情况下,临时修改某个路由的优先级(metric值越小,优先级越高)。

  6. 清空所有路由

    route flush

    这个命令会清空系统的所有路由表项。

以上只是route命令的一些常见用法,你也可以通过man route命令来查看更详细的帮助信息。

03.Linux查看内存使用情况

  • free 命令用于显示系统的内存使用情况,包括物理内存和交换空间的情况。下面是 free 命令的输出示例:

free              
              total        used        free      shared  buff/cache   available
Mem:       32825356     1821324    29717784      221628     1275248    30482484
Swap:       2097148           0     2097148
​
free -h
              total        used        free      shared  buff/cache   available
Mem:           3.8G        1.5G        523M        3.3M        1.8G        2.0G
Swap:          8.4G          0B        8.4G
  • top用于实时监视系统的运行情况,包括 CPU 使用率、内存使用情况、进程状态等

TOP命令参数详解---10分钟学会top用法_top详解-CSDN博客

  • cat /proc/meminfo 命令用于查看系统中有关内存的详细信息,包括内存总量、空闲内存、缓冲区和缓存等。

    /proc 目录是一个特殊的虚拟文件系统,它提供了关于当前运行中的 Linux 内核和进程的信息。这个目录中包含了大量的文件和子目录,每个文件和子目录都代表着不同的系统信息。下面是一些 /proc 目录中常见的内容:

    Linux下的/proc目录介绍 - 头痛不头痛 - 博客园 (cnblogs.com)

    • /proc/cpuinfo: 包含有关 CPU 的信息,如型号、频率等。

    • /proc/meminfo: 包含有关内存的信息,如总内存、空闲内存等。

    • /proc/loadavg: 包含系统负载平均值的信息。

    • /proc/PID: 包含有关进程 PID 的信息,每个运行中的进程都有一个对应的目录,其中包含有关该进程的各种信息,如命令行参数、状态等。

  • ps 命令用于查看当前系统中运行的进程信息

  1. 显示当前用户的所有进程:

# 查看系统中所有进程,使用BSD操作系统格式
ps aux
选项:
a:显示一个终端的所有进程,除了会话引线
u:显示进程的归属用户及内存的使用情况
x:显示没有控制终端的进程

04.gcc编译四步骤

GCC | 爱编程的大丙 (subingwen.cn)

使用 GCC 编译 C 语言程序通常需要四个步骤,包括预处理、编译、汇编和链接。下面是 GCC 编译 C 语言程序的四个步骤:

  1. 预处理(Preprocessing): 在这个阶段,预处理器会处理源文件,包括展开宏定义、处理条件编译指令等。预处理后的代码通常保存在一个中间文件中(通常以 .i 结尾),我们可以使用 -E 选项告诉 GCC 只执行预处理步骤,并输出预处理后的代码,例如:

gcc -E main.c -o main.i
  1. 编译(Compiling): 在这个阶段,编译器会将预处理后的代码翻译成汇编代码。编译后的代码通常保存在一个汇编文件中(通常以 .s 结尾),我们可以使用 -S 选项告诉 GCC 只执行编译步骤,并输出汇编代码,例如:

gcc -S main.i -o main.s
  1. 汇编(Assembling): 在这个阶段,汇编器将汇编代码翻译成机器可执行的目标代码。汇编后的对象文件通常保存在一个目标文件中(通常以 .o 结尾),我们可以使用 -c 选项告诉 GCC 只执行汇编步骤,并输出目标文件,(汇编过程是将汇编代码转化成目标文件同时生成符号表,方便链接器的运行。)例如:

gcc -c main.s -o main.o
  1. 链接(Linking): 在这个阶段,链接器将目标文件及其依赖的库文件链接在一起,生成最终的可执行文件。我们可以直接调用 GCC 来完成整个编译过程,例如:

gcc main.c -o main

程序的运行过程(详解)_写出程序的解释执行过程-CSDN博客

链接过程中会进行合并段表和符号表的合并和重定位

在系统上运行程序的链接过程(详细)_程序链接阶段使用的技术-CSDN博客

  • 相似段合并:对于输入的多个目标文件,链接器一般采用“相似段合并”的方法将相同性质的段合并到一起

  • 符号地址的确定:当合并相似段之后,链接器开始计算各个符号的虚拟地址,由于各个符号在段内的相对位置是固定的,所以链接器只需要给每一个符号加上一个偏移量,使得它们能够调整到正确的虚拟地址上。

  • 链接器解析多重定义的全局符号

05.静态库和动态库

Linux 静态库和动态库 | 爱编程的大丙 (subingwen.cn)

静态库

  • ar rcs 命令用于创建静态库(archive),将一组目标文件(.o 文件)打包成一个静态库文件(.a 文件)。

    例如,如果要将一组目标文件 file1.ofile2.ofile3.o 打包成一个名为 libexample.a 的静态库,可以使用以下命令:

    ar rcs libexample.a file1.o file2.o file3.o

    这将创建一个名为 libexample.a 的静态库文件,并将 file1.ofile2.ofile3.o 这三个目标文件添加到该静态库中。

  • 在Linux中静态库以lib作为前缀, 以.a作为后缀, 中间是库的名字自己指定即可, 即: libxxx.a

  • 在Windows中静态库一般以lib作为前缀, 以lib作为后缀, 中间是库的名字需要自己指定, 即: libxxx.lib

  • 发布和使用静态库

    # 发布静态库
        1. 提供头文件 **.h
        2. 提供制作出来的静态库 libxxx.a
    # 4. 编译的时候指定库信息
        -L: 指定库所在的目录(相对或者绝对路径)
        -l: 指定库的名字, 掐头(lib)去尾(.a) ==> calc
    # -L -l, 参数和参数值之间可以有空格, 也可以没有  -L./ -lcalc
    $ gcc main.c -o app -L ./ -l calc
    ​
    # 查看目录信息, 发现可执行程序已经生成了
    $ tree
    .
    ├── app         # 生成的可执行程序
    ├── head.h
    ├── libcalc.a
    └── main.c

动态库

  • 将源文件进行汇编操作, 需要使用参数 -c, 还需要添加额外参数 -fpic / -fPIC

    # 得到若干个 .o文件
    #表示生成位置无关代码(Position Independent Code),通常用于动态链接库的编译
    $ gcc 源文件(*.c) -c -fpic
    #-shared 指定生成动态库
    gcc -shared 与位置无关的目标文件(*.o) -o 动态库(libxxx.so)
  • 在Linux中动态库以lib作为前缀, 以.so作为后缀, 中间是库的名字自己指定即可, 即: libxxx.so

  • 在Windows中动态库一般以lib作为前缀, 以dll作为后缀, 中间是库的名字需要自己指定, 即: libxxx.dll

静态库优缺点

  • 优点:

    • 静态库被打包到应用程序中加载速度快

    • 发布程序无需提供静态库,移植方便

  • 缺点:

    • 相同的库文件数据可能在内存中被加载多份, 消耗系统资源,浪费内存

    • 库文件更新需要重新编译项目文件, 生成新的可执行程序, 浪费时间。

img

动态库优缺点

  • 优点:

    • 可实现不同进程间的资源共享

    • 动态库升级简单, 只需要替换库文件, 无需重新编译应用程序

    • 程序猿可以控制何时加载动态库, 不调用库函数动态库不会被加载

  • 缺点:

    • 加载速度比静态库慢, 以现在计算机的性能可以忽略

    • 发布程序需要提供依赖的动态库

img

06.软连接和硬链接

#创建硬链接,创建硬链接后,文件的硬链接计数+1
ln /home/book/Desktop/test.txt hard_link
#创建软链接
ln -s /home/book/Desktop/test.txt soft_link
​
book@100ask:~/Desktop$ ls -l test.txt 
-rw-rw-r-- 2 book book 11 Feb 26 16:38 test.txt
​
book@100ask:~/Desktop/linuxCMD$ ls -l
total 4
-rw-rw-r-- 2 book book 11 Feb 26 16:38 hard_link
lrwxrwxrwx 1 book book 27 Feb 26 16:39 soft_link -> /home/book/Desktop/test.txt

链接:是给系统中已有的某个文件指定另外一个可用于访问它的名称,链接也可以指向目录。即使我们删除这个链接,也不会破坏原来的文件或目录。 硬链接是指多个文件名指向同一个物理文件。当创建硬链接时,不会在磁盘上创建新的数据块,而是将已有文件的索引节点(inode)复制一份,新文件名指向该索引节点。因此,多个硬链接文件实际上是同一个文件,它们在磁盘上占用的空间是相同的。硬链接只能针对文件,不能针对目录。 软链接又称符号链接,是指一个文件名指向另一个文件名,而不是物理文件。创建软链接时,在磁盘上创建一个新的数据块,其中包含指向目标文件名的路径信息。因此,软链接实际上是一个文件,它的内容是目标文件的路径软链接可以针对文件或目录与硬链接不同,软链接是一个新文件,在磁盘上占用的空间比较小,但是因为需要额外的寻址操作,访问速度相对较慢。同时,当目标文件被删除或移动时,软链接会失效。

硬链接不是一个独立文件,他和目标文件使用的是同一个inode

软连接是一个独立文件,有自己独立的inode和inode编号

07.目录项和inode

目录项、inode、数据块 - Dazzling! - 博客园 (cnblogs.com)

大部分的Linux文件系统(如ext2、ext3)规定,一个文件由目录项、inode和数据块组成:

  • 目录项:包括文件名和inode节点号,用于建立文件名和文件的 inode 号之间的映射关系

  • inode:又称文件索引节点,包含文件的基础信息(如文件类型、权限、所有者、所属组、大小、创建时间、修改时间等)以及数据块的指针。

  • 数据块:包含文件的具体内容。

inode和硬链接:一般情况下,文件名和inode号码是"一一对应"关系,每个inode号码对应一个文件名。但是,Unix/Linux系统允许,多个文件名指向同一个inode号码。这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(hard link)。

inode和软连接:文件A和文件B的inode号码虽然不一样,但是文件A的内容是文件B的路径。读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的"软链接"(soft link)或者"符号链接(symbolic link)。这意味着,文件A依赖于文件B而存在,如果删除了文件B,打开文件A就会报错:"No such file or directory"。这是软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B的inode号码,文件B的inode"链接数"不会因此发生变化。

在 Linux 中,文件的删除是通过删除该文件的目录项实现的。当一个文件被删除时,文件系统会将其对应的目录项从目录中删除,并将该文件的链接数减少 1。只有在该文件的链接数降为 0 之后,操作系统才会将该文件的数据块从磁盘上彻底清除所谓的删除文件,就是删除inode,但是数据其实还是在硬盘上,以后会覆盖掉。

img

08.进程地址空间

img

09.进程之父子进程的关系

在 Unix/Linux 系统编程中,fork() 函数是一个创建新进程的系统调用。调用 fork() 函数时,操作系统会复制当前进程(称为父进程),并创建一个新的子进程。父进程和子进程在调用 fork() 函数后会继续执行下面的代码,但是它们各自拥有自己独立的内存空间和资源。

具体来说,fork() 函数的行为如下:

  • 在父进程中,fork() 返回新创建子进程的进程 ID(PID),这个 PID 是一个正整数;

  • 在子进程中,fork() 返回 0;

  • 如果 fork() 失败,则返回一个负值。

父子进程相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式 父子进程不同之处: 进程ID、fork返回值、父进程ID,进程运行时间、定时器、未决槽信号 父子进程间遵循读时共享写时复制的原则,节省内存开销。

#include <stdio.h>
#include <unistd.h>
​
int main() {
    int x = 10;
    pid_t pid = fork();
​
    if (pid == 0) {
        // 子进程
        printf("Child process: x = %d\n", x);
        x = 20;
        printf("Child process: Modified x = %d\n", x);
    } else if (pid > 0) {
        // 父进程
        printf("Parent process: x = %d\n", x);
        x = 30;
        printf("Parent process: Modified x = %d\n", x);
    } else {
        // fork() 失败
        fprintf(stderr, "Fork failed.\n");
        return 1;
    }
    return 0;
}
/*
Parent process: x = 10
Parent process: Modified x = 30
Child process: x = 10
Child process: Modified x = 20
*/

10.孤儿进程、僵尸进程和守护进程

孤儿进程、僵尸进程和守护进程是操作系统中常见的进程状态,它们分别具有不同的特征和含义。

  1. 孤儿进程:当一个子进程的父进程退出或者意外终止,而子进程本身还在运行,此时子进程就成为孤儿进程。孤儿进程会被 init 进程(在 Unix 系统中通常是 PID 为 1 的进程)接管,并由 init 进程负责回收其所占用的系统资源。

  2. 僵尸进程所谓僵尸进程,就是当子进程退出时,父进程尚未结束,而父进程又没有对已经结束的子进程进行回收。此时,这样的子进程就成了僵尸进程。僵尸进程会占用系统资源,因此需要及时被回收。父进程可以通过 wait() 或 waitpid() 等系统调用来等待子进程退出并回收其资源,防止子进程成为僵尸进程。

  3. 守护进程:守护进程是在后台运行的一种特殊进程,通常用于在系统启动时就开始运行,并在系统关闭时停止运行。守护进程通常脱离终端控制,以避免受用户登录或注销的影响。经典的例子包括网络服务进程、系统监控进程等。

11.wait()/waitpid()

wait()waitpid() 是用于等待子进程结束并获取其终止状态的系统调用。它们可以避免僵尸进程的产生,确保子进程的资源得到正确回收。

wait() 是最简单的等待子进程结束的系统调用,其原型如下:

pid_t wait(int *status);
  • 参数 status 是一个指向整型的指针,用于存储子进程的终止状态信息。如果不关心子进程的终止状态,可以将 status 设置为 NULL。

  • 返回值是终止的子进程的进程 ID,如果没有子进程或者出错,则返回 -1。

当调用 wait() 后,如果有一个或多个子进程已经终止,那么它会立即返回并回收其中一个已终止子进程的资源,并将子进程的终止状态存储在 status 中。如果没有已终止的子进程,那么调用进程会被阻塞,直到有一个子进程终止为止。

waitpid() 具有比 wait() 更灵活的特性,可以指定等待的子进程和等待的选项,其原型如下:

pid_t waitpid(pid_t pid, int *status, int options);
  • 参数 pid 指定要等待的子进程的进程 ID,具体取值意义如下:

    • pid > 0:等待进程 ID 为 pid 的子进程。

    • pid == -1:等待任意子进程。

    • pid == 0:等待与调用进程属于同一进程组的任意子进程。

    • pid < -1:等待进程组 ID 等于 pid 绝对值的任意子进程。

  • 参数 status 用于存储子进程的终止状态信息。

  • 参数 options 可以指定一些附加选项,如 WNOHANG(非阻塞)、WUNTRACED(包括被暂停的子进程)等。

  • 返回值与 wait() 类似,为终止的子进程的进程 ID。

waitpid() 提供了更细粒度的控制,可以选择等待特定的子进程,也可以设置非阻塞模式,以及等待被暂停的子进程等。

12.线程与进程

线程 | 爱编程的大丙 (subingwen.cn)

线程和进程之间的主要区别是,进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。。进程可以包含多个线程,但每个线程都有自己的执行上下文和调用栈,使得它们可以并行执行不同的任务。这使得线程更加轻量级和高效,因为它们不需要像进程那样在内存中维护独立的地址空间和系统资源。

  • 进程有自己独立的地址空间, 有独立的 pcb,多个线程共用同一个地址空间

  • 在一个地址空间中多个线程独享: 每个线程都有属于自己的栈区, 寄存器(内核中管理的)

  • 在一个地址空间中多个线程共享: 代码段, 堆区, 全局数据区, 打开的文件(文件描述符表)都是线程共享的

进程与线程的选择取决以下几点: 1、需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。 2、线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应。 3、多进程可以使用在多机分布式系统,需要扩展到其他机器上,使用多进程,多线程适用于多核处理机。 4、需要更稳定安全时,适合选择进程;需要速度时,选择线程更好

进程有独立的堆区和栈区,线程共享进程的堆区但拥有独立的栈区,协程是一种用户态的轻量级线程。在一个用户线程上可以跑多个协程,这样就提高了单核的利用率。协程不是被操作系统内核所管理,而完全是由程序所控制。

为什么有了进程还需要线程和协程

尽管进程提供了资源隔离和独立的执行环境,但它们的创建和管理相对较重,不适用于需要频繁交互或共享状态的场景。线程作为轻量级的进程,提供了更快的上下文切换和高效的资源共享,使得在同一进程内可以有多个并发执行流。然而,线程的调度仍然受操作系统控制,可能涉及到用户态和内核态的切换开销

协程进一步降低了并发编程的复杂度和开销,因为它完全是在用户态下使用的,尤其在 I/O 密集型任务和微服务架构中表现出色,它们提供了更细粒度的操作和更高效的CPU利用率。由于这些优势,现代编程语言和框架越来越多地采用协程来处理并行和异步任务。

14.什么是 inode、block、sector

Sector(扇区):文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。

block(块):操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小,最常见的是4KB,即连续八个 sector 组成一个 block。

inode(索引):文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做 inode,中文译名为"索引节点"。

每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。

15.linux内核同步方式

在 Linux 内核中,为了实现多个进程或线程之间的同步,以及对共享资源的访问控制,提供了多种同步方式。以下是一些常见的 Linux 内核同步方式:

  1. 互斥锁(Mutex):互斥锁是最基本的同步原语之一,用于保护共享资源免受并发访问的影响。只有一个进程或线程可以持有互斥锁,其他进程或线程必须等待锁的释放才能访问共享资源。在 Linux 内核中,互斥锁由 mutex 结构体表示,可以使用 mutex_lock()mutex_unlock() 函数来获取和释放锁。

  2. 读写锁(ReadWrite Lock):读写锁允许多个读操作同时进行,但只允许一个写操作进行。这样可以提高读操作的并发性能。在 Linux 内核中,读写锁由 rwlock_t 结构体表示,可以使用 read_lock()read_unlock()write_lock()write_unlock() 函数来获取和释放锁。

  3. 自旋锁(Spin Lock):自旋锁是一种忙等待的锁,当无法获取锁时,进程或线程会一直循环尝试获取锁,而不会睡眠。自旋锁适用于锁的持有时间很短的情况下。在 Linux 内核中,自旋锁由 spinlock_t 结构体表示,可以使用 spin_lock()spin_unlock() 函数来获取和释放锁。

  4. 信号量(Semaphore):信号量是一种计数器,用于控制对共享资源的并发访问。在 Linux 内核中,信号量由 struct semaphore 结构体表示,可以使用 sema_init()down()up() 函数来初始化、获取和释放信号量。

  5. 睡眠与唤醒机制:内核中的进程或线程可以通过调用 sleep() 或者等待某个条件满足来睡眠,然后通过调用 wake_up() 或者满足条件时唤醒其他进程或线程。这种方式适用于需要等待某个事件发生的场景。

16.进程间通信方式

  1. 管道 (Pipe)

    • 无名管道(匿名管道):主要用于有亲缘关系的进程之间的通信(例如,父子进程)。数据是单向流动的。

    • 命名管道(FIFO):允许无亲缘关系的进程之间通信,它在文件系统中有一个名字。

  2. 信号 (Signal):一种用于通知接收进程某个事件已经发生的简单机制。

  3. 消息队列 (Message Queue):允许一个或多个进程向另一个进程发送格式化的数据块。数据块在消息队列中按照一定的顺序排列。

  4. 共享内存 (Shared Memory):让多个进程共享一个给定的存储区,是最快的IPC方式,因为数据不需要在进程间复制。

  5. 信号量 (Semaphore):主要用于同步进程间的操作,而不是传递数据,但通过控制资源的访问,它可以作为通信的一种手段。

  6. 套接字 (Socket):提供网络通信的机制,可用于不同机器上的进程间通信,也可以在同一台机器上的进程之间进行通信。

17.线程间通信方式

  1. 共享内存:线程可以直接访问进程的内存空间。共享数据的访问通常需要同步机制来防止出现竞态条件。

  2. 互斥锁:用于控制对共享资源的访问,保证在同一时间只有一个线程访问共享资源。

  3. 读写锁:允许多个线程同时读取一个资源,但写入时需要独占访问。

  4. 条件变量:允许一个或多个线程在某个条件发生前处于睡眠状态,等待另一个线程在该条件上发出通知或广播。

  5. 信号量:可以用于限制对共享资源的访问,也用于线程间的同步。

18.Linux信号

Linux操作系统中常见的信号有

  1. SIGHUP:挂起进程

  2. SIGINT:中断进程

  3. SIGQUIT:退出进程和生成核心文件

  4. SIGILL:非法指令

  5. SIGTRAP:跟踪/断点陷阱

  6. SIGABRT:异常终止

  7. SIGBUS:总线错误

  8. SIGFPE:浮点异常

  9. SIGKILL:ss进程,该信号不能被阻塞,处理或忽略,一旦接收就会ss进程

  10. SIGUSR1、SIGUSR2:用户自定义信号

  11. SIGSEGV:无效内存引用

  12. SIGPIPE:管道破碎:写到一个没有读者的管道

  13. SIGALRM:实时定时器超时

  14. SIGTERM:终止进程

  15. SIGCHLD:子进程已经停止或终止

  16. SIGCONT:如果进程已经停止,那么继续运行进程

  17. SIGSTOP:停止执行进程

  18. SIGTSTP、SIGTTIN、SIGTTOU:停止进程的运行

一个进程接收到一个信号后,可以有三种方式处理

  1. 忽略这个信号。

  2. 捕捉这个信号。一旦一个进程决定要捕捉某种信号,就需要提供一个函数,这个函数被称为信号处理程序。当这种信号发给该进程时,内核就运行该信号处理程序。

  3. 执行默认操作。

系统如何将一个信号通知到进程

  1. 内核会修改进程上下文信息,并设置标识表明收到信号。

  2. 当进程再次被调度执行时,它会先检查是否有未处理的信号,如果有,就调用相应的信号处理函数。

  3. 如果没有为该信号指定处理函数或者信号被阻塞,那么就执行系统默认的操作,可能是忽略、停止进程或者终止进程等。

19.标准库和系统调用

  1. 来源:

    • 系统调用:这些函数来自操作系统内核。它们是操作系统提供给应用程序直接访问硬件和系统资源的基础界面,例如读写文件、发送网络数据、创建进程等。

    • 标准库函数:这些函数是由C语言(或其他语言)运行时环境提供的,例如printf、strcpy、malloc等。

  2. 实现:

    • 系统调用:由操作系统内核代码实现,当一个进程执行系统调用时,它需要切换到内核模式来运行特权代码。

    • 标准库函数:通常使用用户模式下的普通代码实现,并且可能在其内部使用系统调用以提供其功能。

  3. 性能:

    • 系统调用:因为涉及用户空间到内核空间的上下文切换,所以相比于标准库函数,系统调用通常会有更高的开销。

    • 标准库函数:也可能引入一定的性能开销,如果它们内部使用了系统调用,但如果只是在用户空间进行计算,那么它们的开销就会小得多。

  4. 可移植性:

    • 系统调用:通常依赖于特定的操作系统,所以在不同操作系统之间的可移植性较差。

    • 标准库函数:大多数情况下,标准库函数在各种平台上的行为都是一致的,所以具有更好的可移植性。

    • 库函数在用户地址空间执行,系统调用是在内核地址空间执行,库函数运行时间属于用户时间,系统调用属于系统时间,库函数开销较小,系统调用开销较大

    • 库函数是有缓冲的,系统调用是无缓冲的

    • 库函数并不依赖平台,库函数调用与系统无关,不同的系统,调用库函数,库函数会调用不同的底层函数实现,因此可移植性好。系统调用依赖平台

20.什么是PCB

PCB 它是操作系统中用于存储关于进程信息的一个重要数据结构。PCB 是操作系统用来管理和跟踪进程状态的一种方式,确保进程能够有序地执行和切换。

通常包含以下信息:

  1. 进程标识符(PID):一个唯一的标识号,用于区分不同的进程。

  2. 进程状态:如就绪、运行、等待、终止)等。

  3. 程序计数器:指向进程下一个要执行的指令地址。

  4. CPU 寄存器信息:保存进程被中断或切换时,CPU 寄存器中的数据,以便恢复时可以继续执行。

  5. CPU 调度信息:包括进程优先级、调度队列指针和其他调度参数。

21.进程终止方式

进程终止通常有以下几种方式:

  1. 正常退出(自愿):当进程完成其任务后,它会自动结束并释放其占用的资源。这是最常见的进程结束方式。

  2. 错误退出(自愿):如果进程在执行过程中遇到无法处理的错误情况,比如除零操作、访问非法内存地址等,它可能会选择主动终止。

  3. 致命错误(强制):当进程发生严重错误,如段错误(segmentation fault),或者操作系统检测到一个不能允许进程继续运行的状态(例如保护性错误)时,操作系统将强制结束这个进程。

  4. 被其他进程杀死(强制):在UNIX/Linux系统中,进程可以接收到来自其他进程的信号,其中一些信号可以导致进程结束,如SIGKILL和SIGTERM。管理员或具有足够权限的用户可以使用kill命令发送这样的信号以结束进程。

  5. 父进程终止(强制):在某些系统中,如果父进程结束,那么它的所有子进程也将被终止

22.什么是线程池

线程池是一种常见的多线程并发编程技术,它是一组线程的集合,这些线程预先创建并初始化,并被放入一个队列中等待任务。当有新任务到来时,线程池中的某个线程会被唤醒并处理该任务,任务处理完后,线程又会回到线程池中等待下一次任务。通过线程池技术,我们可以实现高效、可伸缩的并发处理,提高系统的并发处理能力,降低系统的开销和复杂度。 线程池的主要组成部分包括任务队列、线程池管理器和工作线程。任务队列用于存储所有需要处理的任务,线程池管理器用于管理线程池的创建、销毁和线程的调度等操作,工作线程则是线程池中的执行单位,它们从任务队列中取出任务并执行任务。线程池通常采用预创建线程的方式,通过线程复用的方式避免了线程频繁创建和销毁所带来的开销。

23.分页和分段、内存碎片

4.1 为什么要有虚拟内存? | 小林coding (xiaolincoding.com)

内存分段

程序是由若干个逻辑分段组成的,由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来。

分段机制下的虚拟地址由两部分组成,段选择因子段内偏移量

 分段内存缺点

  • 内存碎片(外部碎片),内存分段管理可以做到段根据实际需求分配内存,所以有多少需求就分配多大的段,所以不会出现内部内存碎片;但是由于每个段的长度不固定,所以多个段未必能恰好使用所有的内存空间,会产生了多个不连续的小物理内存,导致新的程序无法被装载,所以会出现外部内存碎片的问题。
  • 内存交换的效率低。为了解决「外部内存碎片」的问题使用内存交换,但是硬盘的访问速度太慢。
内存分页

为了解决内存分段「外部内存碎片和内存交换的空间太大」的问题,分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫Page)。在 Linux 下,每一页的大小为 4KB。虚拟地址与物理地址之间通过页表来映射。

采用了分页,页与页之间是紧密排列的,所以不会有外部碎片。但是,因为内存分页机制分配内存的最小单位是一页,即使程序不足一页大小,我们最少只能分配一个页,所以页内会出现内存浪费,所以针对内存分页机制会有内部内存碎片的现象。

多级页表

对于单页表的实现方式,在 32 位和页大小 4KB 的环境下,一个进程的页表需占用 4MB 大小的空间。如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表

TLB

多级页表降低了地址转换的速度,也就是带来了时间上的开销。在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为页表缓存、转址旁路缓存、快表等

如何理解虚拟地址空间? - 知乎 (zhihu.com)

内存碎片:是指分布在内存中的未被充分利用的零散内存块。它可能出现在动态内存分配过程中,导致内存利用率降低,甚至影响系统性能。内存碎片分为两种类型外部碎片和内部碎片

外部碎片:是指由于动态内存分配和释放过程中,导致剩余的未分配内存块被零散占据,无法满足大块内存的需求。虽然总的空闲内存足够,但无法分配连续的内存空间。

内部碎片:是指已经分配给进程的内存块中,存在着未被充分利用的空间。

内存碎片的产生原因:

  1. 频繁的内存分配和释放:过度频繁的内存分配和释放操作会导致内存块的零散分布,增加外部碎片的概率。

  2. 内存对齐要求:某些系统和硬件要求内存地址对齐,导致分配的内存块大小超过实际需要,产生内部碎片。

  3. 内存泄漏:未释放的内存占用会导致内存的不连续分布,增加外部碎片。

预防和处理内存碎片

  1. 使用对象池或内存池:对象池是一种预分配一定数量的对象并重复使用的技术。通过避免频繁的内存分配和释放,可以降低内存碎片的产生。

  2. 合理选择内存分配策略:根据应用场景和数据特点,选择合适的内存分配策略,例如使用固定大小的内存块或动态调整内存块大小。

  3. 避免频繁的内存分配和释放:尽量减少不必要的内存分配和释放操作,可以通过对象复用、对象缓存等方式来减少内存碎片的产生。

  4. 使用内存池和自定义内存管理器:通过自定义内存管理器,可以实现更加灵活和高效的内存分配和释放策略,从而降低内存碎片的风险

  5. 解决外部内存碎片的问题就是内存交换。可以把音乐程序占用的那 256MB 内存写到硬盘上,然后再从硬盘上读回来到内存里。不过再读回的时候,我们不能装载回原来的位置,而是紧紧跟着那已经被占用了的 512MB 内存后面。这样就能空缺出连续的 256MB 空间,于是新的 200MB 程序就可以装载进来。硬盘的访问速度要比内存慢太多

24.Linux虚拟内存管理

如何理解虚拟地址空间? - 知乎 (zhihu.com)

Linux 内存管理 详解(虚拟内存、物理内存,进程地址空间)_linux内存管理-CSDN博客

Linux内核学习笔记3——分段机制和分页机制 - LOSER Z - 博客园 (cnblogs.com)

在Linux中,通过分段和分页的机制,将物理内存划分为4k大小的内存页(page),并且将作为物理内存分配与回收的基本单位。通过分页机制我们可以灵活的对内存进行管理。

如果直接使用物理内存,通常都会面临以下几种问题

  • 内存缺乏访问控制,安全性不足

  • 各进程同时访问物理内存,可能会互相产生影响,没有独立性

  • 物理内存极小,而并发执行进程所需又大,容易导致内存不足

  • 进程所需空间不一,容易导致内存碎片化问题。

基于以上几种原因,Linux通过mm_struct结构体来描述了一个虚拟的,连续的,独立的地址空间,也就是我们所说的虚拟地址空间。

在内存管理中,常见的几种技术包括内存分页、内存分段以及它们的结合形式——段页式。

25.GDB

  1. GDB 编译

    ​
    gcc  -g   program.c  -o  program 
  2. GDB 启动、退出

    #启动语法: gdb + 可执行文件
    gdb   program 
    #退出
    quit/q
  3. GDB 调试命令

    start      #程序停在第一行
    run        #遇到断点才停
    c/continue #继续运行
    n/next     #逐行运行(不会进入函数体)
    ​
    #逐行调试(遇函数进入函数体),在 GDB 中,step 命令用于执行程序的下一步,并且如果当前行是一个函数调用,则会进入到该函数内部执行。与 next 命令不同,step 命令会进入到函数内部执行,并逐行执行其中的代码。
    s/step   
    finish(跳出函数体)
    ​
    list: 显示当前执行点周围的代码。
    list <function_name>: 显示指定函数的代码。
    list <line_number>: 显示指定行号的代码
    ​
    b <line_number>: 在指定行号处设置断点。
    b <function_name>: 在指定函数的入口处设置断点。
    b <file_name>:<line_number>: 在指定文件的指定行号处设置断点。
    ​
    info breakpoints 命令(简写为 i b)用于显示当前设置的所有断点信息,包括断点号、断点位置、断点类型、是否启用、条件等。
    ​
    #使用 GDB 调试 core 文件
    #确保生成了 core 文件: 在程序崩溃时,通常会生成一个 core 文件,其中包含了程序崩溃时的内存状态。确保你的程序生成了 core 文件,否则你将无法使用 GDB 进行调试。你可以通过设置 ulimit -c unlimited 来确保生成 core 文件,或者在程序中使用 setrlimit 函数进行设置
    #启动 GDB: 在终端中启动 GDB,并指定要调试的可执行文件和 core 文件,例如:
    gdb /path/to/your/executable /path/to/corefile
    ​
    ​
    #gdb调试正在运行的程序
    ​
    #1、获取正在运行程序的进程ID(PID)
    #2、启动 GDB 并附加到进程:gdb -p PID。
    ​
    #3、开始调试: 一旦 GDB 附加到了正在运行的程序的进程上,你就可以像平常一样使用 GDB 进行调试了。你可以设置断点、查看变量、单步执行等操作,来分析程序的行为和调试问题。
    ​
    #4、分离 GDB: 在调试结束后,你可以使用 detach 命令将 GDB 从正在运行的程序的进程上分离出来,让程序继续正常运行。命令格式为 detach。

26.grep/sed/awk

  1. grep

    #在文件中搜索包含指定字符串的行
    grep "pattern" file.txt
    #忽略大小写进行搜索
    grep -i "pattern" file.txt
    #显示匹配行的行号
    grep -n "pattern" file.txt
    #统计匹配的行数
    grep -c "pattern" file.txt
    #-e :实现多个选项间的逻辑or 关系
    -  -A<显示行数>:除了显示符合范本样式的那一列之外,并显示该行之后的内容。
    -  -B<显示行数>:除了显示符合样式的那一行之外,并显示该行之前的内容。
    -  -C<显示行数>:除了显示符合样式的那一行之外,并显示该行之前后的内容。
  2. sed

    sed 是一个流编辑器,用于对文本进行处理和转换。它主要用于对文件中的文本进行替换、删除、插入等操作。以下是一些 sed 命令的常见用法示例:

    1. 替换文本

      sed 's/pattern/replacement/g' file.txt

      这将在文件 file.txt 中查找匹配字符串 "pattern" 的所有实例,并用 "replacement" 替换它们。

    2. 删除行

      sed '/pattern/d' file.txt

      这将删除文件 file.txt 中包含字符串 "pattern" 的所有行。

    3. 插入行

      sed '3i\new_line' file.txt

      这将在第三行之前插入新行 "new_line"。

    4. 打印指定行

      sed -n '5p' file.txt

      这将打印文件 file.txt 中的第五行。

    5. 批量处理多个文件

      sed -i 's/pattern/replacement/g' *.txt

      这将在当前目录下的所有 .txt 文件中查找匹配字符串 "pattern" 的所有实例,并用 "replacement" 替换它们。

  3. awk

    awk 的基本语法结构如下:

    awk 'pattern { action }' input-file
    • pattern 模式部分用于筛选要处理的行,类似于条件语句。如果省略模式部分,则所有行都会被匹配。

    • { action } 动作部分定义了对匹配行执行的操作,包括打印、计算、赋值等。

    • input-file 是要处理的输入文件名。

    awk 中,还可以使用以下特殊变量:

    • $0:代表整个当前行。

    • $1, $2, ...:代表当前行的第一个、第二个字段,依此类推。

    • NR:代表当前处理的行号。

    • NF:代表当前行的字段数量。

    除了基本语法外,awk 还支持各种内置函数和运算符,可以进行字符串操作、数学计算等。例如:

    • 字符串连接:$1 $2

    • 算术操作:$1 + $2

    • 内置函数:length($1)(返回字段 $1 的长度)

    #!/bin/bash
    ​
    # 获取本地 IP 地址
    ip_address=$(hostname -I | awk '{print $1}')
    ​
    search_text="local_ip ="
    replace_text="local_ip = "$ip_address
    file_path="/home/pi/frp_0.24.1_linux_arm/frpc.ini"
    ​
    echo $file_path
    # 使用 sed 命令替换文件中包含指定字符的一行
    sed -i "s/.*$search_text.*/$replace_text/g" $file_path
    ​
    current_time=$(date +"%Y-%m-%d %H:%M:%S")
    echo "#"$current_time >> $file_path 

27.shell

Shell 脚本是一种用来编写一系列 Shell 命令的脚本文件,通常以 .sh 为扩展名。Shell 脚本可以在命令行中执行,用于自动化完成各种任务。下面是一些 Shell 脚本的基本语法要点:

  1. 指定 Shell 解释器

在 Shell 脚本的第一行通常需要指定要使用的 Shell 解释器,例如:

#!/bin/bash

这行代码告诉系统使用 Bash 解释器来执行该脚本。

  1. 环境变量

可以使用 # 符号开始的行来添加注释,这些注释会被解释器忽略。例如:

# Shell常见的变量之一系统变量,主要是用于对参数判断和命令返回值判断时使用,系统变量详解如下:
​
$0      当前脚本的名称;
$n      当前脚本的第n个参数,n=1,2,…9;
$*      当前脚本的所有参数(不包括程序本身);
$#      当前脚本的参数个数(不包括程序本身);
$?      令或程序执行完后的状态,返回0表示执行成功;
$$      程序本身的PID号。
​
#Shell常见的变量之二环境变量,主要是在程序运行时需要设置,环境变量详解如下:
​
PATH        命令所示路径,以冒号为分割;
HOME        打印用户家目录;
SHELL       显示当前Shell类型;
USER        打印当前用户名;
ID          打印当前用户id信息;
PWD         显示当前所在路径;
TERM        打印当前终端类型;
HOSTNAME    显示当前主机名;
PS1         定义主机命令提示符的;
HISTSIZE    历史命令大小,可通过 HISTTIMEFORMAT 变量设置命令执行时间;
RANDOM      随机生成一个 0 至 32767 的整数;
HOSTNAME    主机名
#!/bin/bash
#""和''都可以
echo 'hello world'
​
#可以使用 '$'符号来引用变量的值
num=8848
#相当于字符串拼接
echo "num="$num
​
#可以使用反引号'' 或 $() 来执行命令并获取其输出
cur_time=$(date +"%Y-%m-%d %H:%M:%S")
echo $cur_time
​
#当前脚本的名称
echo 'cur shell name: '$0
#环境变量,必须大写
echo 'cur pwd:'$PWD
echo 'cur user:'$USER
echo 'cur hostname:'$HOSTNAME
echo 'cur shell:'$SHELL
​

if语句

#!/bin/bash
​
SCORE=85
# If条件判断语句,通常以if开头,fi结尾。也可加入else或者elif进行多条件的判断
if [ $SCORE -ge 90 ]
then
    echo "excellent"
elif [ $SCORE -ge 80 ]
then
    echo "good"
elif [ $SCORE -ge 60 ]
then
    echo "pass"
else
    echo "fail"
fi

for语句

#!/bin/bash
#1到10 步长为2
for i in {1..10..2}
do
    echo "Number:$i"
done
#1到10 步长为2
for i in $(seq 1 2 10)
do
    echo "Number: $i"
done

while

#!/bin/bash
i=1
while [ $i -le 10 ]
do
    echo "Number: $i"
    #let 命令用于执行算术运算,并更新变量的值
    let i=i+1
done

28.vim

#打开文件,并将光标置于第 n 行的首部
vim +n a.c
#打幵文件,并将光标置于第一个与 pattern 匹配的位置
vim +/pattern a.c
#快速格式化代码
gg=G
#复制光标所在行,此命令前可以加数字 n,可复制多行
n yy
#以光标所在行为准(包含当前行),向下剪切指定行数
n dd
#p  将剪贴板中的内容粘贴到光标后
#P(大写)  将剪贴板中的内容粘贴到光标前
​
#查找:“/关键词”
/abc    #从光标所在位置向前查找字符串 abc
/^abc   #查找以 abc 为行首的行
/abc$   #查找以 abc 为行尾的行

29.快表

快表,又称联想寄存器(TLB),是一种访问速度比内存快很多的高速缓冲存储器,用来存放当前访问的若干页表项,以加速地址变换的过程。

多级页表虽然解决了空间占用大的问题,但是由于其复杂化了地址的转换,因此也带来了大量的时间开销,使得地址转换速度减慢。我们将最常访问的几个页表项存储到TLB中,在之后进行寻址时,CPU就会先到TLB中进行查找,如果没有找到,这时才会去查询页表。

30.dup/dup2

dup函数的原型为int dup(int oldfd);

     该函数的作用是,返回一个新的文件描述符(可用文件描述符的最小值)newfd,并且新的文件描述符newfd指向oldfd所指向的文件表项。如以下调用形式:int newfd  =   dup(oldfd)

up2函数原型为int dup2(int oldfd,int newfd);

    dup函数是返回一个最小可用文件描述符newfd,并让其与传入的文件描述符oldfd指向同一文件表项;
​
    而dup2则是直接让传入的参数newfd与参数oldfd指向同一文件表项,如果newfd已经被open过,那么就会先将newfd关闭,然后让newfd指向oldfd所指向的文件表项,如果newfd本身就等于oldfd,那么就直接返回newfd。因此,传入的newfd既可以是open过的,也可以是一个任意非负整数,总之,dup2函数的作用就是让newfd重定向到oldfd所指的文件表项上,如果出错就返回-1,否则返回的就是newfd。

31.锁

互斥锁(Mutex Lock)是一种用于保护共享资源不被并发访问的同步机制。当线程需要访问共享资源时,首先会尝试获取互斥锁。如果该互斥锁已被其他线程获取,则线程会被阻塞,直到该互斥锁被释放为止。一旦线程获取到了互斥锁,就可以访问共享资源,并在完成操作后释放互斥锁,以允许其他线程访问该资源。

读写锁(Read-Write Lock)是一种特殊的锁机制,它允许多个线程同时读取共享资源,但在有线程写入时会进行排他性访问。这种锁的目的是提高并发性能,因为在大多数情况下,数据被读取的频率远远高于被写入的频率。读写锁分为读锁和写锁两种状态,多个线程可以同时持有读锁,但只有一个线程能够持有写锁。

自旋锁(Spin Lock)是一种基于忙等待的同步机制,在尝试获取锁时,线程会反复检查锁是否可用,而不是立即进入睡眠状态。这样可以避免线程进入睡眠状态带来的上下文切换开销,适用于临界区很小且线程持有锁的时间很短的情况。然而,如果临界区很大或者锁被长时间占用,自旋锁会造成大量的CPU资源浪费。

总结一下:

  • 互斥锁适用于临界区较大的情况,它可以确保同时只有一个线程访问共享资源。

  • 读写锁适用于读取操作远远多于写入操作的场景,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。

  • 自旋锁适用于临界区很小,且线程持有锁的时间很短的情况,它可以避免线程进入睡眠状态所带来的开销。

32.原子操作

单核,多核CPU的原子操作 - LeonGo - 博客园 (cnblogs.com)

33.匿名管道

  • 管道是半双工通信

  • 管道生命随进程而终止

  • 命名管道任意多个进程间通信

  • 管道提供的是流式数据传输服务

  • 管道自带 同步与互斥 机制

在C语言中,pipe() 函数用于创建一个管道,并返回两个文件描述符。这个函数的原型如下:

#include <unistd.h>
int pipe(int pipefd[2]);// 其本质是一个伪文件(实为内核缓冲区)

pipefd[0] 将会指向管道的读取端,而 pipefd[1] 将会指向管道的写入端。如果 pipe() 函数成功执行,它将返回0,并将管道的文件描述符保存在 pipefd 数组中;否则,它将返回-1,并设置适当的错误码来指示错误的原因。

以下是一个简单的示例,演示如何使用 pipe() 函数创建一个管道:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//这个示例中,父进程向子进程发送了一条消息,并等待子进程读取并处理该消息。
int main() {
    int pipefd[2];
    char buf[20];
    pid_t pid;
​
    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
​
    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
​
    if (pid == 0) { // 子进程
        close(pipefd[1]);  // 关闭写入端
        read(pipefd[0], buf, sizeof(buf));  // 从管道读取数据
        printf("Child process received: %s\n", buf);
        close(pipefd[0]);  // 关闭读取端
        exit(EXIT_SUCCESS);
    } else { // 父进程
        close(pipefd[0]);  // 关闭读取端
        write(pipefd[1], "Hello, child process!", 22);  // 向管道写入数据
        close(pipefd[1]);  // 关闭写入端
        wait(NULL);  // 等待子进程结束
        exit(EXIT_SUCCESS);
    }
​
    return 0;
}

管道的局限性:

① 数据自己读不能自己写。

② 数据一旦被读走,便不在管道中存在,不可反复读取。

③ 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。

④ 只能在有公共祖先的进程间使用管道。

34.有名管道

有名管道(named pipe),也称为FIFO(First In, First Out),是一种特殊类型的文件,用于进程间通信。相对于匿名管道,有名管道具有以下特点:

  1. 可以用于无血缘关系的进程间通信。

  2. 自带同步与互斥机制、数据单向流通

使用有名管道的基本步骤如下:

  1. 创建有名管道:使用 mkfifo() 函数创建一个有名管道,并指定一个文件路径作为参数。

  2. 打开管道:使用 open() 函数打开有名管道。打开管道时需要指定读取模式或写入模式。

  3. 读取和写入数据:对于读取端,使用 read() 函数从管道中读取数据;对于写入端,使用 write() 函数向管道中写入数据。

  4. 关闭管道:使用 close() 函数关闭管道。

以下是一个简单的示例,展示了如何使用有名管道进行进程间通信:

进程A:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
​
int main() {
    // 创建有名管道
    mkfifo("/tmp/myfifo", 0666);
​
    // 打开管道进行写入
    int fd = open("/tmp/myfifo", O_WRONLY);
​
    // 写入数据
    char *message = "Hello from Process A";
    write(fd, message, sizeof(message));
    close(fd);
​
    return 0;
}

进程B:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
​
#define BUFFER_SIZE 1024
​
int main() {
    // 打开管道进行读取
    int fd = open("/tmp/myfifo", O_RDONLY);
​
    // 读取数据
    char buffer[BUFFER_SIZE];
    read(fd, buffer, BUFFER_SIZE);
    printf("Received message: %s\n", buffer);
    close(fd);
​
    return 0;
}

在这个示例中,进程A创建了一个名为 "/tmp/myfifo" 的有名管道,并向管道中写入一条消息。进程B打开同样的管道,并从管道中读取并打印这条消息。

需要注意的是,有名管道的读取和写入是阻塞的操作。如果没有数据可读,读取操作将会被阻塞,直到有数据可读取。类似地,如果管道已满,写入操作将会被阻塞,直到有空间可以写入数据。因此,在实际使用中,需要合理处理管道的读取和写入操作,以避免阻塞导致的问题。

35.mmap

mmap 是一种在内存和文件之间创建映射的系统调用,它允许程序直接使用文件的内容,而无需进行显式的读取和写入操作。mmap 函数将文件映射到调用进程的地址空间,使得文件中的数据可以像访问内存一样被访问。这样做的好处是可以提高文件的访问效率,特别是对于大文件或需要随机访问的文件而言

在使用 mmap 函数时,需要指定文件描述符、映射区域的大小、映射区域的权限和映射区域在进程地址空间中的位置等参数。mmap 函数的原型如下:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • addr:指定映射区域在进程地址空间中的起始地址,通常设置为 NULL,由系统自动选择合适的地址。

  • length:指定映射区域的大小,以字节为单位。

  • prot:指定映射区域的保护方式,包括 PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)等。

  • flags:指定映射的类型和其他标志,通常设置为 MAP_SHARED修改会反映到磁盘上,多个进程可以通过共享映射的方式,来共享同一个文件。这样一来,一个进程对该文件的修改,其他进程也可以观察到,这就实现了数据的通讯。)或 MAP_PRIVATE修改不反映到磁盘上)。

  • fd:指定要映射的文件描述符。

  • offset:指定文件中的偏移量,从该偏移量开始映射,必须是 4096 的整数倍。(MMU 映射的最小单位 4k )。

调用成功时,mmap 函数返回映射区域的起始地址;失败时,返回 MAP_FAILED

下面是一个简单的示例,展示了如何使用 mmap 函数将文件映射到内存中,并对文件进行读取操作:

#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
​
int main() {
    int fd;
    struct stat sb;
    char *file_data;
​
    // 打开文件
    fd = open("example.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
​
    // 获取文件信息
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        return 1;
    }
​
    // 将文件映射到内存
    file_data = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (file_data == MAP_FAILED) {
        perror("mmap");
        return 1;
    }
​
    // 输出文件内容
    printf("File content:\n%s\n", file_data);
​
    // 释放映射区域
    if (munmap(file_data, sb.st_size) == -1) {
        perror("munmap");
        return 1;
    }
​
    // 关闭文件
    if (close(fd) == -1) {
        perror("close");
        return 1;
    }
​
    return 0;
}

在这个示例中,程序打开名为 "example.txt" 的文件,使用 mmap 函数将文件映射到内存中,并输出文件的内容。最后,程序使用 munmap 函数释放映射区域,并关闭文件。

02.Linux网络编程

01.网络字节序

在计算机内部,数据的表示可以使用两种字节序,即大端字节序和小端字节序。大端字节序是指数据的高位字节存放在内存的低地址中,而小端字节序是指数据的低位字节存放在内存的低地址中。为了在网络上传输时保证数据的正确性,所有计算机都必须使用相同的字节序。在网络编程中,网络字节序被规定为大端字节序,无论计算机的实际字节序是大端还是小端,都必须将数据转换为网络字节序后再进行传输。

在网络编程中,常用的网络字节序是大端存储(Big Endian),也被称为网络字节序(Network Byte Order)。为了在不同字节序的系统之间进行通信,可以使用一些函数来进行字节序转换。

在C语言中,可以使用以下函数将本地字节序和网络字节序相互转换:

  1. htons():将一个无符号短整型(16位)从主机字节序转换为网络字节序。

  2. htonl():将一个无符号长整型(32位)从主机字节序转换为网络字节序。

  3. ntohs():将一个无符号短整型从网络字节序转换为主机字节序。

  4. ntohl():将一个无符号长整型从网络字节序转换为主机字节序。

以下是一个简单的示例代码,演示如何使用这些函数进行字节序转换:

#include <stdio.h>
#include <arpa/inet.h>
​
int main() {
    unsigned short host_short = 0x1234;
    unsigned long host_long = 0x12345678;
    unsigned short network_short;
    unsigned long network_long;
​
    network_short = htons(host_short);
    network_long = htonl(host_long);
​
    printf("Host short: 0x%x\n", host_short);
    printf("Network short: 0x%x\n", network_short);
​
    printf("Host long: 0x%lx\n", host_long);
    printf("Network long: 0x%lx\n", network_long);
​
    return 0;
}

02.socket编程

TCP编程流程

TCP客户端编程流程
1、cfd=socket();
2、connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));//连接服务器
3、send()/recv()
4、close()
TCP服务器编程流程
1、lfd=socket()//创建套接字
2、bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));//给服务器socket绑定地址结构(IP+port)
3、listen(lfd,128)//监听套接字 创建连接队列
4、cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len); // 阻塞等待客户端连接请求
5、send()/recv()
6、close

//server.c
#define SERV_PORT 9527
#define BUF_SIZE 1024
int main(int argc, char *argv[])
{
    struct sockaddr_in serv_addr, clit_addr;  // 定义服务器地址结构 和 客户端地址结构
    
    serv_addr.sin_family = AF_INET;             // IPv4
    serv_addr.sin_port = htons(SERV_PORT);      // 转为网络字节序的 端口号
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 获取本机任意有效IP
​
    int lfd = socket(AF_INET, SOCK_STREAM, 0);      //创建一个 socket
​
    bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));//给服务器socket绑定地址结构(IP+port)
​
    listen(lfd, 128);                   //  设置监听上限
​
    socklen_t clit_addr_len = sizeof(clit_addr);    //  获取客户端地址结构大小
​
    int cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len);   // 阻塞等待客户端连接请求
    char buf[BUF_SIZE];
    printf("client ip:%s port:%d\n", 
            inet_ntop(AF_INET, &clit_addr.sin_addr.s_addr, buf, sizeof(buf)), 
            ntohs(clit_addr.sin_port));         // 根据accept传出参数,获取客户端 ip 和 port
​
    while (1) {
        memset(buf,0,sizeof(buf));
        int ret = read(cfd, buf, sizeof(buf));      // 读客户端数据
        printf("recv len=%d:",ret,buf);         // 写到屏幕查看
    }
​
    close(lfd);
    close(cfd);
    return 0;
}
//client.c
#define SERV_PORT 9527
#define BUF_SIZE  1024
#define SERV_ADDR 127.0.0.1
int main(int argc, char *argv[])
{    
    struct sockaddr_in serv_addr;          //服务器地址结构
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERV_PORT);
    //inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
    inet_pton(AF_INET,SERV_ADDR, &serv_addr.sin_addr);
​
    int cfd = socket(AF_INET, SOCK_STREAM, 0);
​
    connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    
    while (1) {
        write(cfd, "hello\n", 6);
        sleep(1);
    }
    close(cfd);
    return 0;
}

UDP

03.多进程与多线程并发服务器

多进程并发服务器:每个客户端请求都将创建一个新的进程,这个进程负责处理该客户端的请求。因为每个进程都是独立的,所以它们之间的内存空间是隔离的。这意味着在进程间共享数据需要使用进程间通信(IPC)技术,如管道、信号、共享内存、套接字等。由于进程切换的开销比较大因此多进程并发服务器的性能通常比多线程并发服务器要差

多线程并发服务器:在多线程并发服务器中,每个客户端请求都将创建一个新的线程,这个线程负责处理该客户端的请求。由于所有线程都属于同一个进程,它们共享同一个地址空间,可以轻松地共享数据,不需要进行进程间通信。由于线程切换的开销比进程切换的开销小得多,因此多线程并发服务器的性能通常比多进程并发服务器要好。但是,多线程编程需要注意线程安全问题,例如数据共享、竞态条件、死锁等。

04.IO多路复用

IO多路复用是一种高效的IO操作方式,它可以同时监听多个文件描述符(socket)的可读、可写、异常等事件,并在有事件发生时通知应用程序进行处理。常见的IO多路复用机制有select、poll、epoll等。 在传统的IO模型中,每个文件描述符都需要对应一个线程来处理,这会导致系统资源的浪费和线程切换的开销。而使用IO多路复用机制,可以将多个文件描述符的IO事件集中到一个线程中处理,减少了系统调用和线程切换的次数,提高了系统的吞吐量和响应性能。

05.select()/poll()/epoll()

I/O 多路复用之select()、poll()、epoll()详解_io多路复用select poll epoll-CSDN博客

select/poll/epoll的相关面试题_select/poll/epoll面试题-CSDN博客

  1. select模型:这是最古老的一种IO多路复用模型。它的主要功能是监视多个文件描述符(在网络编程中,文件描述符通常代表一个socket连接),直到其中一个文件描述符准备好进行某种IO操作(如读或写)为止。使用select模型的优点是跨平台性好,基本上所有的操作系统都支持。但是它有一些明显的缺点,如单个进程能够监视的文件描述符数量有限(通常是1024),处理效率较低(每次调用select都需要遍历所有的文件描述符),以及它不能随着连接数的增加而线性扩展。

  2. poll模型poll模型和select模型非常相似,但它没有最大文件描述符数量的限制。和select一样,poll每次调用时也需要遍历所有的文件描述符,同样不能随着连接数的增加而线性扩展。

  3. epoll模型:这是一个在Linux 2.6及以后版本中引入的新型IO多路复用模型。与select和poll相比,epoll在处理大量并发连接时更高效。它默认使用了一个事件驱动的方式(ET),以红黑树作为底层的数据结构,只有当某个文件描述符准备好进行IO操作时,它才会将这个文件描述符添加到就绪列表中,这避免了遍历所有文件描述符的开销。另外,epoll没有最大文件描述符数量的限制,因此它可以处理更多的并发连接。

epoll相关函数

epoll 是 Linux 下一种高性能的事件通知机制,通常用于处理大量的文件描述符,比如网络套接字。它相比于传统的 selectpoll 等方法,在处理大量文件描述符时有更好的性能表现。

以下是 epoll 相关的主要函数:

  1. int epoll_create(int size)

    • 创建一个 epoll 实例,并返回一个文件描述符用于后续操作。

    • size 参数表示要监听的文件描述符数量的一个建议值,它并不是一个严格的限制。

  2. int epoll_create1(int flags)

    • 类似于 epoll_create,但可以通过 flags 参数来设置一些额外的选项。

    • 目前主要用于设置 EPOLL_CLOEXEC 标志,以在执行 exec 系列函数时自动关闭文件描述符。

  3. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

    • epfd 所指代的 epoll 实例进行控制操作,例如添加、修改或删除要监听的文件描述符。

    • op 参数表示操作类型,可以是 EPOLL_CTL_ADDEPOLL_CTL_MODEPOLL_CTL_DEL

    • fd 参数是要操作的文件描述符。

    • event 参数是一个 struct epoll_event 结构体指针,用于指定要监听的事件类型。

  4. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

    • 等待 epfd 所指代的 epoll 实例上的事件发生,并将发生的事件填充到 events 数组中。

    • maxevents 参数表示 events 数组的最大容量,即最多可以返回多少个事件。

    • timeout 参数表示等待事件的超时时间,单位为毫秒,传入 -1 表示无限等待。

    • 函数返回发生事件的文件描述符数量,或者在超时时返回 0,出错时返回 -1。

  5. struct epoll_event 结构体

    • 用于描述一个事件,定义如下:

      struct epoll_event {
          uint32_t events;  // 表示要监听的事件类型,如 EPOLLIN、EPOLLOUT 等
          epoll_data_t data;  // 与事件相关的数据,是一个联合体,可以是文件描述符或指针
      };

在使用 epoll 的时候,有两种工作模式:水平触发(LT,Level-Triggered)和边缘触发(ET,Edge-Triggered)。

  1. 水平触发(LT)

    • 在水平触发模式下,如果文件描述符上有数据可读或可写,epoll_wait 将会返回并报告这个事件。

    • 如果文件描述符还有数据未读取完,或者还有缓冲区可写,则下次调用 epoll_wait 时会再次返回就绪事件。

    • 水平触发模式下,只要文件描述符处于就绪状态,epoll_wait 就会返回就绪事件,即使应用程序没有处理完就绪事件。

  2. 边缘触发(ET)

    • 在边缘触发模式下,只有当文件描述符状态变化时,epoll_wait 才会返回并报告这个事件。

    • 一旦文件描述符处于就绪状态并且应用程序已经对这个就绪事件进行了处理,下次调用 epoll_wait 时,不会再次返回这个就绪事件,直到文件描述符状态再次发生变化。

    • 边缘触发模式要求应用程序在处理就绪事件时必须尽快将文件描述符的数据读取完毕,或者将需要写入的数据写入完毕,否则下次调用 epoll_wait 时可能不会再次返回就绪事件。

通常情况下,边缘触发模式比水平触发模式效率更高,因为它可以减少不必要的事件通知。但是,边缘触发模式要求应用程序对就绪事件进行更为及时和精确的处理,因为它不会保留就绪事件,而是只在状态变化时通知应用程序。

03.Linux内核

01.Linux内核5大功能

1、管理进程:内核负责创建和销毁进程, 并处理它们与外部世界的联系(输入和输出),不同进程间通讯(通过信号,管道,或者进程间通讯原语)对整个系统功能来说是基本的,也由内核处理。 另外, 调度器, 控制进程如何共享CPU,是进程管理的一部分。更通常地,内核的进程管理活动实现了多个进程在一个单个或者几个CPU 之上的抽象。

2、管理内存:计算机的内存是主要的资源, 处理它所用的策略对系统性能是至关重要的。内核为所有进程的每一个都在有限的可用资源上建立了一个虚拟地址空间。内核的不同部分与内存管理子系统通过一套函数调用交互,从简单的malloc/free对到更多更复杂的功能。

3、文件系统:Unix 在很大程度上基于文件系统的概念;几乎Unix中的任何东西都可看作一个文件。内核在非结构化的硬件之上建立了一个结构化的文件系统,结果是文件的抽象非常多地在整个系统中应用。另外,Linux 支持多个文件系统类型,就是说,物理介质上不同的数据组织方式。例如,磁盘可被格式化成标准Linux的ext3文件系统,普遍使用的FAT文件系统,或者其他几个文件系统。

4、设备控制:几乎每个系统操作终都映射到一个物理设备上,除了处理器,内存和非常少的别的实体之外,全部中的任何设备控制操作都由特定于要寻址的设备相关的代码来进行。这些代码称为设备驱动。内核中必须嵌入系统中出现的每个外设的驱动,从硬盘驱动到键盘和磁带驱动器。内核功能的这个方面是本书中的我们主要感兴趣的地方。

5、网络管理:网络必须由操作系统来管理,因为大部分网络操作不是特定于某一个进程: 进入系统的报文是异步事件。报文在某一个进程接手之前必须被收集,识别,分发,系统负责在程序和网络接口之间递送数据报文,它必须根据程序的网络活动来控制程序的执行。另外,所有的路由和地址解析问题都在内核中实现。

02.linux内核态和用户态

一、内核态、用户态概念

内核态:也叫内核空间,是内核进程/线程所在的区域。主要负责运行系统、硬件交互。

用户态:也叫用户空间,是用户进程/线程所在的区域。主要用于执行用户程序。

二、内核态和用户态的区别 内核态:运行的代码不受任何限制,CPU可以执行任何指令。

用户态:运行的代码需要受到CPU的很多检查,不能直接访问内核数据和程序,也就是说不可以像内核态线程一样访问任何有效地址。

操作系统在执行用户程序时,主要工作在用户态,只有在其执行没有权限完成的任务时才会切换到内核态。

三、为什么要区分内核态和用户态 保护机制。防止用户进程误操作或者是恶意破坏系统。内核态类似于C++的私有成员,只能在类内访问,用户态类似于公有成员,可以随意访问。

四、用户态切换到内核态的方式 1、系统调用(主动)

由于用户态无法完成某些任务,用户态会请求切换到内核态,内核态通过为用户专门开放的中断完成切换。

2、异常(被动)

在执行用户程序时出现某些不可知的异常,会从用户程序切换到内核中处理该异常的程序,也就是切换到了内核态。

3、外围设备中断(被动)

外围设备发出中断信号,当中断发生后,当前运行的进程暂停运行,并由操作系统内核对中断进程处理,如果中断之前CPU执行的是用户态程序,就相当于从用户态向内核态的切换。

03.什么是MMU?为什么要用MMU?

MMU(Memory Management Unit)是一种硬件设备,主要用于实现虚拟内存管理。它的作用是将进程所使用的虚拟地址转换成对应的物理地址,并进行内存保护。

在没有MMU的系统中,所有进程共享同一块物理内存,因此进程间需要通过约定好的内存地址来进行通信,容易导致地址冲突和安全问题。而有了MMU之后,每个进程都有自己的虚拟地址空间,不会互相干扰。MMU还可以根据进程的访问权限,对虚拟地址空间进行访问控制和内存保护。此外,MMU还可以通过虚拟地址和物理地址的映射关系,实现了虚拟内存技术,使得进程能够访问大于物理内存的虚拟地址空间,从而提高了内存利用率和系统性能。

04.交叉编译

交叉编译是指将源代码从一种计算机架构编译为另一种计算机架构的过程,在一个操作系统上编译针对另一个操作系统或硬件平台的程序。 需要进行交叉编译的原因通常是: 1.目标平台和开发平台不同:在开发软件时,开发者可能需要将软件运行在一个与其开发机器不同的目标平台上,如编写针对嵌入式设备的应用程序时,开发者通常需要在 PC 上编译,然后将其部署到嵌入式设备中。 2.硬件架构不同:在不同的硬件架构之间进行编译时需要进行交叉编译。例如,将 ARM 架构的应用程序编译为 x86 架构的应用程序。 3.系统库不同:不同的操作系统有不同的系统库,编译程序时需要使用适当的系统库。交叉编译可以使开发者在开发机器上使用开发者熟悉的库,在目标平台上使用目标平台的库。 交叉编译需要考虑多种因素,例如处理器架构、操作系统、编译器版本和编译选项等。因此,需要仔细配置编译工具链,以确保生成的可执行文件或库能够在目标平台上运行。

05.Linux驱动编译两种方法

  1. 编译成模块.ko文件

    使用insmod加载驱动,rmmod移除驱动

  2. 编译进内核

    做成镜像

06.设备驱动分类

  1. 字符设备驱动:字符设备指那些必须按字节流传输,以串行顺序依次进行访问的设备。它们是我们日常最常见的驱动了,像鼠标、键盘、打印机、触摸屏,还有点灯以及I2C、SPI、音视频都属于字符设备驱动。

  2. 块设备驱动:存储器设备的驱动,eMMC、NAND、SD

  3. 网络设备驱动

07.字符设备框架

//1.定义自己的file_operations结构体
static struct file_operations led_drv = {
    .owner   = THIS_MODULE,
    .open    = led_drv_open,
    .read    = led_drv_read,
    .write   = led_drv_write,
    .release = led_drv_close,
};
//2.实现对应的open/read/write等函数,填入file_operations结构体 
static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    return 0;
}
​
//3.把file_operations结构体告诉内核:注册驱动程序
//4.谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 
static int __init led_init(void)
{
    //注册字符设备驱动,它会返回一个主设备号 major
    major = register_chrdev(0, "100ask_led", &led_drv);  /* /dev/led */
    led_class = class_create(THIS_MODULE, "100ask_led_class");
    device_create(led_class, NULL, MKDEV(major, 0), NULL, "100ask_led0"); //* /dev/100ask_led0  
    return 0;
}
​
/* 5. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
static void __exit led_exit(void)
{
    int i;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
​
    for (i = 0; i < LED_NUM; i++)
        device_destroy(led_class, MKDEV(major, i)); /* /dev/100ask_led0,1,... */
​
    device_destroy(led_class, MKDEV(major, 0));
    class_destroy(led_class);
    unregister_chrdev(major, "100ask_led");
}
​
​
/* 6. 其他完善:提供设备信息,自动创建设备节点                                     */
​
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
​

08.Linux启动流程

【Linux基础】1. Linux 启动过程_linux关机流程会先后停什么服务-CSDN博客

Linux系统的启动过程分为 5个阶段

  1. 内核的引导:当计算机打开电源后,首先是 BIOS(Basic Input Output System,基本输入输出系统)开机自检,按照 BIOS 中设置的启动设备(通常是硬盘),操作系统接管硬件以后,首先 读入 /boot 目录下的内核文件

  2. 运行 init:读取配置文件 /etc/inittab,去运行需要开机启动的程序。Linux系统有7个运行级别 0-6

  3. 系统初始化:用执行了/etc/rc.d/rc.sysinit,而 rc.sysinit 是一个bash shell的脚本,它主要是完成一些系统初始化的工作,rc.sysinit 也是 每一个运行级别都要首先运行的重要脚本,它主要完成的工作有:激活交换分区,检查磁盘,加载硬件模块以及其它一些需要优先执行任务。

  4. 建立终端 :init接下来会 打开6个终端

  5. 用户登录系统。

;