【操作系统】总结篇

【C++语言部分】总结篇
【计算机网络】总结篇
【数据库(SQL)】总结篇

本文目录

1. 常用的 Linux 命令

  1. 文件和目录操作
  • ls:列出当前目录下的文件和子目录。
  • cd:进入指定的目录。
  • mkdir:创建一个新目录。
  • cp:复制一个或多个文件或目录。
  • mv:移动或重命名一个或多个文件或目录。
  • rm:删除一个或多个文件或目录。
  • touch:创建一个新文件或修改文件的时间戳。
  • cat:将一个或多个文件内容输出到终端。
  • more 或 less:逐页显示文本文件内容。
  • pwd:用于显示工作目录。
  1. 命令和程序操作
  • which:查找指定命令的路径。
  • man:查看命令的手册页。
  • grep:在文件中搜索指定的文本模式。该命令常用于分析一行的信息,若当中有我们所需要的信息,就将该行显示出来,该命令通常与管道命令一起使用,用于对一些命令的输出进行筛选加工。
  • ps:查看当前运行的进程和进程状态。
  • kill:终止一个指定的进程。
  • tar:打包和压缩文件和目录。
  • ssh:远程登录到其他计算机终端。
  • scp:安全复制文件和目录到其他计算机。
  1. 系统操作
  • uname:显示系统的基本信息。
  • top 或 htop:实时监测系统的资源使用情况。
  • free:查看系统的内存使用情况。
  • df 或 du:查看磁盘空间的使用情况。
  • ifconfig:查看网络接口的配置。
  • ping:测试网络连接是否正常。
  • systemctl:管理系统的服务和进程。

2. Linux 中查看进程运行状态的指令、查看内存使用情况的指令、tar 解压文件的参数

  1. Linux中查看进程运行状态的指令
$ ps aux | grep PID

该命令将显示当前正在运行的所有进程的详细信息,包括进程的PID、运行时间、CPU利用率、内存利用率等信息。

如果需要特定进程的状态,则可以通过进程的PID来查找。

$ ps -p <PID>

以上命令将显示指定PID的进程的详细信息。

  1. Linux中查看内存使用情况的指令

在Linux中,我们可以通过以下命令查看系统内存使用情况:

$ free -m

该命令将显示当前系统的内存总量、已使用内存、可用内存和缓冲区和缓存使用情况。

如果需要更详细的信息,可以使用以下命令查看系统内存使用情况:

$ cat /proc/meminfo

该命令将显示有关系统内存的详细信息,包括内核使用的内存、进程使用的内存、缓冲区和缓存使用情况等。

  1. tar解压文件的参数

tar 命令是Linux中用于归档和压缩文件和目录的命令。以下是 tar 命令中一些常用的解压参数:

$ tar -xzvf file.tar.gz

解压缩 .tar.gz 或 .tgz 文件。

$ tar -xjvf file.tar.bz2

解压缩 .tar.bz2 或 .tbz 文件。

$ tar -xJvf file.tar.xz

解压缩 .tar.xz 文件。

以上命令中,-x 表示解压缩,-v 表示显示详细信息,-z 表示使用 gzip 解压缩,-j 表示使用 bzip2 解压缩,-J 表示使用 xz 解压缩,-f 指定待解压的文件名。

五个命令中必选一个
	-c: 建立压缩档案
	-x:解压
	-t:查看内容
	-r:向压缩归档文件末尾追加文件
	-u:更新原压缩包中的文件
这几个参数是可选的
	-z:有gzip属性的
	-j:有bz2属性的
	-Z:有compress属性的
	-v:显示所有过程
	-O:将文件解开到标准输出
//ps使用示例
//显示当前所有进程
ps -A
//与grep联用查找某进程
ps -aux | grep apache

//查看进程运行状态、查看内存使用情况的指令均可使用top指令。
top

3. 文件权限怎么修改

在Linux系统中,修改文件权限使用 chmod 命令。

在Linux中,文件权限被分为三类:所有者(owner)、所属组(group)和其他用户(others)。每个用户都有三种文件访问权限:读(read)、写(write)和执行(execute)。

下面是chmod命令的一些常用选项:

  • chmod ugo+rwx file:给所有者(u)、所属组(g)和其他用户(o)添加读、写和执行权限。
  • chmod a+rwx file:给所有用户添加读、写和执行权限。
  • chmod u-rwx file:将所有者的读、写和执行权限删除。
  • chmod go-rwx file:将所属组和其他用户的读、写和执行权限删除。

在这些命令中,使用类似u、g、o和a的参数来指定需要修改权限的用户类别,使用类似+rwx、-rwx这样的参数来指定你要添加或删除的权限等。

4. 如何以 root 权限运行某个程序

在Linux中,root是系统管理员账号,可以执行任何操作。如果需要以root权限运行某个程序,你可以使用以下方法:

sudo chown root app(文件名)
sudo chmod u+s app(文件名)

5. 软链接和硬链接的区别

在Linux系统中,链接是用于连接文件的一种机制。软链接和硬链接是两种不同类型的链接。

  1. 软链接(symbolic link)

软链接也被称为符号链接。创建软链接时,新文件路径是指向源文件路径的一个指针,也就是说,软链接是一个指向另一个文件的快捷方式。当源文件被删除时,软链接将失效。

创建软链接的命令是ln -s,格式为:

ln -s 源文件名 链接名

例如,在/tmp目录下创建一个指向/home/user1目录的软链接:

ln -s /home/user1 /tmp/user1
  1. 硬链接(hard link)

硬链接创建时,实际上是创建了一个新的文件名,并在文件系统中为该文件名分配一个i-node号,新文件名和源文件共享相同的i-node号。可以把硬链接看作对同一文件内容的另一个名字。当源文件被删除时,硬链接并不会被删除。

硬链接只能链接到同一文件系统中的文件,不能链接到不同文件系统的文件。
创建硬链接的命令是ln,格式为:

ln 源文件名 链接名

例如,在/tmp目录下创建一个指向/home/user1目录的硬链接:

ln /home/user1 /tmp/user1
  1. 两者主要的区别在于:
  • 软链接保存的是源文件的路径,而硬链接保存的是源文件的 i-node 号。
  • 软链接是一个指针,而硬链接是一个与源文件完全相同的文件名。
  • 软链接可以跨越文件系统,而硬链接不能跨越文件系统。
  • 当源文件被删除时,软链接失效,而硬链接不会被删除。

6. 静态库和动态库如何制作及使用、区别是什么

静态库和动态库是两种库文件,用于程序的链接和运行。两者主要的区别在于,静态库是在编译时链接到程序中,而动态库是在程序运行时才链接到程序中。

  1. 静态库

静态库是在编译时链接到程序中的库文件。它的制作和使用相对比较简单。静态库的制作流程如下:

  • 编写源代码。
  • 编译源代码生成目标文件。
  • 将目标文件打包成.a文件。

制作好静态库后,可以将其链接到程序中。链接静态库时需要使用-l选项,例如:

gcc -o main main.c -lmylib

其中,-lmylib表示链接名为libmylib.a的静态库文件。

  1. 动态库

动态库是在程序运行时才链接到程序中的库文件。相较于静态库,动态库的使用更加灵活,可执行程序的体积也更小。动态库的制作流程如下:

  • 编写源代码。
  • 编译源代码生成目标文件。
  • 将目标文件编译生成共享库.so文件。

Linux下常见的共享库有静态库和动态库两种,制作动态库和静态库命令格式几乎相同,只是要使用-fPIC选项确保生成的目标代码是可以被共享的位置无关代码。如下:

gcc -fPIC -c foo1.c foo2.c
gcc -shared -o libfoo.so foo1.o foo2.o

制作好动态库后,可以通过以下方式将其链接到程序中:

gcc -o main main.c -L. -lfoo

其中,-L选项指定链接库的搜索路径,-l选项指定要链接的库文件名(不带前缀"lib"和后缀".so")。

总的来说,静态库和动态库的区别是:

  • 静态库在编译时链接到程序中,而动态库在运行时动态链接到程序中。
  • 静态库的大小比较大,浪费空间,但因为编译时已经链接到程序中,所以程序运行时速度较快。相反,动态库的大小比较小,但会增加程序运行时的链接时间和内存开销。
  • 静态库升级或更新时需要重新编译程序,而动态库则只需要替换库文件即可。

7. GDB常见的调试命令,什么是条件断点,多进程下如何调试

GDB是GNU调试器(GNU Debugger)的缩写,是用于调试程序的常用工具。

下面简要介绍一些常用的GDB调试命令

  • quit:退出gdb,结束调试。
  • run:运行被调试的程序。如果程序需要输入参数,可以在命令后面加上参数。
  • break:设置断点。常用的断点有以下几种:
    • break [function]:在函数开始执行的第一行设置断点。
    • break [line number]:在指定的行数设置断点。
    • break [filename:line number]:在指定的文件和行数设置断点。
  • continue:从当前位置继续执行程序,直到下一个断点或程序结束。
  • step:单步执行代码,进入函数内部(如果有的话)。
  • next:单步执行代码,执行函数内部代码,但不进入函数内部。
  • print:打印表达式的值,可以使用简写 p。
  • backtrace:显示当前的函数调用栈信息,可以使用简写 bt。
  • watch:设置观察点,当指定变量的值发生变化时会停在观察点处。
  • info:显示当前程序的信息,包括断点、源文件、线程等。
  • list:查看程序源代码。
    • list 5, 10:显示5到10行的代码。
    • list test.c:5, 10: 显示源文件5到10行的代码,在调试多个文件时使用。
    • list get_sum: 显示get_sum函数周围的代码。
    • list test, c get_sum: 显示源文件get_sum函数周围的代码,在调试多个文件时使用。
  • reverse-search:字符串用来从当前行向前查找第一个匹配的字符串。

条件断点是一种在特定条件下暂停程序执行的断点。在使用 break 命令时,可以指定一个条件,在满足条件时才会停在断点处。例如,可以使用 break filename.c:20 if i == 10 设置在文件filename.c的第20行处的断点,只有当变量i的值等于10时才会停在该断点。

多进程下调试程序时,可以使用GDB的多进程调试功能。首先需要使用 set follow-fork-mode [mode] 命令设置子进程的行为,mode可以为 parent(仅调试父进程)、child(仅调试子进程)或 fork(同时调试父进程和子进程)。使用 attach [pid] 命令可以调试一个已经运行的进程。在多进程调试时,使用 info inferiors 命令可以查看当前程序的所有进程,使用 inferior [n] 命令可以切换到指定进程进行调试。其他的调试命令在多进程调试时与单进程调试相同。

8. 什么是大端小端、如何判断大端小端

大端和小端是用于描述数据在内存中的存储方式的概念。在计算机系统中,数据都是以二进制的形式存储的,而一个数据的高位和低位的存储方式就是大端和小端的区别。

  • 大端模式:也称为高位在前模式,是指数据的高位字节存储在内存的低地址处,而数据的低位字节存储在内存的高地址处。
  • 小端模式:也称为低位在前模式,是指数据的低位字节存储在内存的低地址处,而数据的高位字节存储在内存的高地址处。

以十六进制数0x1234为例:

  • 在大端模式下,0x12存储在地址低位,0x34存储在地址高位。
  • 在小端模式下,0x34存储在地址低位,0x12存储在地址高位。

在实际应用中,需要判断系统采用的是大端模式还是小端模式,常用的方法有以下几种:

  1. 判断的方法:
  • 创建一个整形变量,存储值为0x01。
  • 将这个整形变量的地址强制类型转换为char*类型的指针。
  • 判断这个指针所指的值是0x01还是0x00。如果是0x01,说明是小端模式;如果是0x00,说明是大端模式。
  1. 通过系统调用判断:
  • 在Linux下使用命令 uname -a 查看系统信息,可以得到CPU的架构信息。
  • 根据CPU的架构信息,可以判断系统采用的是大端模式还是小端模式。

大端模式和小端模式的区别涉及到系统底层实现,不同的处理器和操作系统可能会采用不同的存储方式。在进行数据处理和通信传输时,需要考虑数据的存储方式,以保证数据的正确性。

9. 进程调度算法有哪些

在操作系统中,进程调度算法是决定哪个进程优先执行的策略。常用的进程调度算法有以下几种:

  1. 先来先服务调度算法(First Come First Served, FCFS):该算法按照进入就绪队列的顺序进行调度,即根据进程的到达时间先后来确定执行顺序。适用于CPU利用率不高的场景。
  2. 短作业优先调度算法(Shortest Job First, SJF):该算法按照作业的执行时间来决定执行顺序,具有最小平均等待时间和最小平均周转时间等优点。缺点是需要预先知道每个作业的执行时间,而现实中很难做到。
  3. 优先级调度算法(Priority Scheduling):该算法按照进程的优先级来决定执行顺序,具有灵活性和可控性,可以根据不同场景设置不同的优先级,但可能出现饥饿现象,优先级较低的进程可能一直无法得到CPU调度。
  4. 时间片轮转调度算法(Round Robin, RR):该算法是将CPU时间分配成若干个时间片,每次执行一个时间片的任务,每个进程获得一个时间片后就重新排队,可以解决进程运行时间比较短的情况下,多个进程争抢CPU资源的问题。
  5. 多级反馈队列调度算法(Multi-Level Feedback Queues, MLFQ):该算法是将所有可执行的进程分成多个不同的队列,每个队列采用不同的调度策略,进程在队列之间进行调度。优先级高的进程优先执行,而长时间没有获得CPU时间的进程会被提高优先级。可以兼顾响应时间和吞吐量的要求。总的来说就是综合前面多种调度算法。

在这些调度算法中,有抢占式和非抢占式的区别。

  • 非抢占式优先权算法:在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。
  • 抢占式优先权调度算法:在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程 i 时,就将其优先权Pi与正在执行的进程 j 的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使 i 进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。
  • 两者的区别
    • 非抢占式(Nonpreemptive):让进程运行直到结束或阻塞的调度方式,容易实现,适合专用系统,不适合通用系统。
    • 抢占式(Preemptive):允许将逻辑上可继续运行的在运行过程暂停的调度方式可防止单一进程长时间独占,CPU系统开销大(降低途径:硬件实现进程切换,或扩充主存以贮存大部分程序)。

10. 操作系统如何申请以及管理内存

操作系统如何管理内存:

  • 物理内存:物理内存有四个层次,分别是寄存器、高速缓存、主存、磁盘。
    • 寄存器:速度最快、量少、价格贵。
    • 高速缓存:次之。
    • 主存:再次之。
    • 磁盘:速度最慢、量多、价格便宜。

操作系统会对物理内存进行管理,有一个部分称为内存管理器(memory manager),它的主要工作是有效的管理内存,记录哪些内存是正在使用的,在进程需要时分配内存以及在进程完成时回收内存。

  • 虚拟内存:操作系统为每一个进程分配一个独立的地址空间,但是虚拟内存。虚拟内存与物理内存存在映射关系,通过页表寻址完成虚拟地址和物理地址的转换。

操作系统如何申请内存:brk()和mmap()是Linux系统中用于动态分配内存的系统调用函数。

brk()函数将进程数据段的边界移动到指定位置,可以用来扩大或缩小进程的数据段。当进程需要动态分配一块内存时,它可以通过brk()函数将数据段边界向高地址方向移动,从而腾出一块新的内存空间。下面是使用brk()函数分配内存的简单示例:

void *ptr = sbrk(1024); // 将进程数据段向高地址扩展1KB
if (ptr == (void *)-1) {
    
    
    // 内存分配失败
} else {
    
    
    // 内存分配成功
}

mmap()函数则可以将一个文件映射到进程的地址空间中,也可以直接分配一块匿名内存空间供进程使用。mmap()函数返回一个指向新分配内存的指针,可以用来访问这块内存。下面是使用mmap()函数分配内存的简单示例:

void *ptr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
    
    
    // 内存分配失败
} else {
    
    
    // 内存分配成功
}

在使用mmap()函数时,需要指定分配的内存大小,以及内存的保护模式(例如读/写或只读)。还需要指定内存映射的类型(例如使用匿名映射,或者使用文件映射)。如果使用文件映射,还需要指定要映射的文件的路径和相应的打开标志。

总的来说,brk()函数适合用于分配小块内存,而mmap()函数适合用于分配大块内存或需要进行文件映射的情况。

11. Linux 系统态与用户态、什么时候会进入系统态

Linux系统分为用户态和内核态,用户态是指应用程序在运行过程中所处的状态,而内核态是指Linux内核所处的状态。

在用户态中,应用程序可以直接进行计算、操作和访问各种资源和设备等,同时由于安全和稳定性等考虑,内核态的资源和功能通常是不可直接访问的。应用程序需要通过系统调用(是主动的)来请求内核的资源或者服务,进入内核态。

当应用程序需要进行一些特殊操作,例如访问受保护的硬件设备、进行内存映射或者创建新的进程等,便需要通过系统调用来请求内核的协助。此时,CPU就会从用户态切换到内核态,运行由应用程序发起的系统调用,内核态的代码会为应用程序提供服务或者处理请求,直到请求处理完毕后再切换回用户态,应用程序继续执行。

当进程在用户态执行的时候,发生某些特殊事件,比如异常或中断(是被动的),或者进程主动发起一个系统调用请求时,CPU 会切换进入内核态 ,此时程序的运行权和控制权都被操作系统所接管。当操作系统处理完相应的事件或系统调用请求后,会将控制权返回用户态,进程再次恢复运行。

总之,用户态是应用程序所处的状态,可以直接访问应用程序的资源;而系统态是内核所处的状态,只有在应用程序需要访问内核资源或者处理系统事件的时候才会进入系统态。

12. LRU算法及其实现方式

LRU(Least Recently Used)算法是一种常见的页面置换算法,用于在一段有限的内存块中缓存更频繁使用的页面。LRU算法给每个页面设置一个访问时间戳,当内存空间不足时,会优先淘汰最久时间未被访问过的页面。

实现LRU算法的一种有效方式是使用双向链表和哈希表结合的数据结构。双向链表中,每个节点表示一个页面,按照访问时间的先后顺序排列,最久未被访问的页面在链表的末尾,最近被访问的页面在链表的头部。哈希表用于快速查找某个页面是否已经在缓存中。

具体实现过程如下:

  1. 定义一个双向链表,链表节点包含页面的键值(用于哈希表的查找),页面内容,以及前驱和后继指针。
  2. 定义一个哈希表,用于快速查找页面是否在缓存中。哈希表中每个节点包含页面的键值、指向链表节点的指针以及指向哈希表下一个节点的指针。
  3. 当需要访问一个页面时,首先在哈希表中查找该页面是否已经被缓存。如果已经被缓存,则将其从链表中删除,并插入到链表头部;否则,新建一个节点,将其插入到链表头部,并在哈希表中添加一个对应的节点。
  4. 当需要淘汰一个页面时,将链表末尾的节点删除,并在哈希表中也删除对应的节点。

下面是一个用C++实现的LRU算法示例:

#include <unordered_map>
using namespace std;

class Node {
    
    
public:
    int key;
    int value;
    Node* prev;
    Node* next;
};

class LRUCache {
    
    
public:
    LRUCache(int capacity) {
    
    
        _capacity = capacity;
        _size = 0;
        head = new Node();
        tail = new Node();
        head->next = tail;
        tail->prev = head;
    }
    
    int get(int key) {
    
    
        if (cache.find(key) != cache.end()) {
    
    
            Node* node = cache[key];
            moveToHead(node);
            return node->value;
        } else {
    
    
            return -1;
        }
    }
    
    void put(int key, int value) {
    
    
        if (cache.find(key) != cache.end()) {
    
    
            Node* node = cache[key];
            node->value = value;
            moveToHead(node);
        } else {
    
    
            Node* node = new Node();
            node->key = key;
            node->value = value;
            cache[key] = node;
            addToHead(node);
            ++_size;
            if (_size > _capacity) {
    
    
                Node* removed = removeTail();
                cache.erase(removed->key);
                delete removed;
                --_size;
            }
        }
    }

private:
    void addToHead(Node* node) {
    
    
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }
    void removeNode(Node* node) {
    
    
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }
    void moveToHead(Node* node) {
    
    
        removeNode(node);
        addToHead(node);
    }
    Node* removeTail() {
    
    
        Node* node = tail->prev;
        removeNode(node);
        return node;
    }

    int _capacity;
    int _size;
    Node* head;
    Node* tail;
    unordered_map<int, Node*> cache;
};

在上面的示例中,LRUCache类用于实现LRU算法。cache哈希表用于快速查找缓存中的页面,head和tail分别为双向链表的头部和尾部。addToHead、removeNode、moveToHead和removeTail等函数用于操作双向链表的节点,将其添加到头部、从中间删除、移动到头部或者删除末尾节点。

13. 一个线程占多大内存

一个Linux线程的内存占用大小并不固定,它会受到多种因素的影响,包括线程堆栈大小、线程使用的共享库的大小以及线程使用的资源等。

默认情况下,Linux线程的堆栈大小是8MB。在64位系统上,堆栈大小可能会更大。如果线程使用了大量的共享库,则线程的内存占用也会比较大。此外,如果线程访问了大量的资源,例如文件、网络连接或共享内存,那么它的内存占用也会随之增加。

另外需要注意的是,线程和进程的内存占用量并不是一一对应的。在Linux中,进程可以创建多个线程,并且这些线程可以共享进程的虚拟地址空间,这意味着它们可以访问相同的内存,而不需要为每个线程分配独立的内存空间。因此,Linux操作系统会使用一些高级的内存管理技术,如写时复制,来优化进程和线程的内存使用情况。

综上所述,Linux线程的内存占用量很难给出一个具体的统一的数字,它会受到多种因素的影响。 线程默认的堆栈大小是比较大的,除此之外,具体的内存占用量还取决于线程使用的共享库和资源。

14. 什么是页表,为什么要有页表

页表(Page Table),是计算机操作系统中用于管理虚拟内存的重要数据结构之一。它的作用是将虚拟内存地址映射到实际物理内存地址。

在操作系统中,进程经常会向操作系统请求分配内存,但实际内存的大小可能无法满足所有进程的要求。为了解决这个问题,操作系统提供了虚拟内存技术,它将每个进程需要的内存空间虚拟化成连续且相同大小的“页”,这些页可以存储在物理内存或外部设备的交换空间中。当进程访问虚拟内存页时,操作系统会将该页从物理内存或交换空间中加载到内存中进行访问。

为了在物理内存或交换空间中正确地找到虚拟内存页,操作系统需要维护一个页表映射,其中存储了每个虚拟页与实际物理页的对应关系。当进程访问虚拟地址时,操作系统会通过页表查找对应的物理页,并将其加载到内存中。

因此,页表是实现虚拟内存和保证操作系统高效管理内存的重要机制之一。

15. 操作系统中的缺页异常和缺页中断

  • 缺页异常(Page Fault Exception):malloc和mmap函数在分配内存时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常,引发缺页中断。
  • 缺页中断(Page Fault Interrupt):是操作系统响应缺页异常的一种机制,当缺页异常发生时,硬件会自动发送一个中断给操作系统,通知其发生了缺页异常。然后操作系统会调度相应的处理程序来处理缺页异常,找到缺失的物理页面并将其加载到内存中,最后返回到进程继续执行。

16. 说说虚拟内存分布,什么时候会由用户态陷入内核态

虚拟内存分布:
在这里插入图片描述
用户空间:

  • 代码段.text:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
  • 数据段.data:存放程序中已初始化的全局变量和静态变量的一块内存区域。
  • BSS 段.bss:存放程序中未初始化的全局变量和静态变量的一块内存区域。
  • 可执行程序在运行时又会多出两个区域:堆区和栈区。
    • 堆区:动态申请内存用。堆从低地址向高地址增长。
    • 栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
  • 最后还有一个共享区,位于堆和栈之间。

内核空间:DMA区、常规区、高位区。

什么时候进入内核态:共有三种方式:系统调用、异常、设备中断。其中,系统调用是主动的,另外两种是被动的。

17. 简述一下虚拟内存和物理内存,为什么要用虚拟内存,好处是什么

虚拟内存和物理内存是计算机中两种不同的内存形式。

  • 物理内存:指的是计算机实际的内存硬件,它由一组可寻址的存储单元组成,可以直接在CPU中访问。物理内存的大小直接决定了计算机可以存储的数据量和程序的规模。
  • 虚拟内存:是一种从逻辑上扩展了物理内存大小的技术。它利用计算机的硬盘或其他存储设备来模拟物理内存,将进程所需的部分存储在物理内存中,而将其余部分存储在硬盘或其他存储设备上。进程访问虚拟内存时,操作系统会根据需要调入或调出某些数据或指令,以满足进程的需要。
  • 为什么要用虚拟内存:因为早期的内存分配方法存在以下问题:
    • 进程地址空间不隔离。会导致数据被随意修改。
    • 内存使用效率低。
    • 程序运行的地址不确定。操作系统随机为进程分配内存空间,所以程序运行的地址是不确定的。
  • 使用虚拟内存的好处
    • 扩大地址空间。每个进程独占一个4G空间,虽然真实物理内存没那么多。
    • 内存保护:防止不同进程对物理内存的争夺和践踏,可以对特定内存地址提供写保护,防止恶意篡改。
    • 可以实现内存共享,方便进程通信。
    • 可以避免内存碎片,虽然物理内存可能不连续,但映射到虚拟内存上可以连续。
  • 使用虚拟内存的缺点
    • 虚拟内存需要额外构建数据结构,占用空间。
    • 虚拟地址到物理地址的转换,增加了执行时间。
    • 页面换入换出耗时。
    • 一页如果只有一部分数据,浪费内存。

18. 虚拟地址到物理地址怎么映射的

在计算机系统中,通过页表来实现虚拟地址到物理地址的映射。页表是操作系统维护用来记录虚拟页面(Page)和物理页面(Frame)之间映射关系的数据结构。当进程产生一个虚拟地址时,系统会检查相应的页表项,以确定该虚拟地址所对应的物理地址。

具体的映射流程如下:

  1. 进程产生一个虚拟地址(Virtual Address)。
  2. 操作系统根据虚拟地址的高位(Page Index)查找页表,以确定对应的物理页面(Physical Page)。
  3. 计算出物理地址(Physical Address),即把页内偏移量和物理地址对应的页面地址相加。
  4. 根据物理地址找到对应内存数据并返回给进程。

19. 说说堆栈溢出是什么,会怎么样

堆栈溢出就是不顾堆栈中分配的局部数据块大小,向该数据块写入了过多的数据,导致数据越界。常指调用堆栈溢出,本质上一种数据结构的满溢情况。堆栈溢出可以理解为两个方面:堆溢出和栈溢出。

  • 堆溢出:比如不断地 new 一个对象,一直创建新的对象,而不进行释放,最终导致内存不足。将会报错:OutOfMemory Error。
  • 栈溢出:一次函数调用中,栈中将被依次压入参数、返回地址等,而方法如果递归比较深或进去死循环,就会导致栈溢出。将会报错:StackOverflow Error。

20. 简述操作系统中 malloc 的实现原理

malloc 底层实现:当开辟的空间小于 128K 时,调用 brk() 函数;当开辟的空间大于 128K 时,调用 mmap()。malloc 采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。

21. 说说进程空间从高位到低位都有些什么

进程的空间通常包含以下几个部分,从高位到低位依次为:用户空间、堆、栈、内核空间。

  • 用户空间:是进程可用的最高地址空间,它用于存放进程代码、数据和共享库等用户态程序数据。用户空间通常是进程最大的地址空间,因此是进程空间的最高位。
  • 堆:堆区是进程用来动态分配内存的部分。它是由操作系统通过brk或mmap系统调用在进程空间中分配的,它的大小取决于进程的内存需求和操作系统的内存管理能力。堆区通常位于低于用户空间的一段地址范围内。
  • 栈:栈是进程用来维护函数调用关系的一段内存区域。栈由操作系统在进程空间中预留的一定大小的内存空间,用于存储函数调用时的局部变量和临时数据。栈通常位于低于堆区的一段地址范围内,并在进程创建时就被初始化为一个较小的固定值,栈的大小通常较小,通常只有数百K或更少。
  • 内核空间:是操作系统内核专用的地址空间,包含操作系统内核的代码和数据、设备驱动程序等。它通常是操作系统优先级最高的一段地址空间,无法被用户空间的程序访问。

22. 32位系统能访问4GB以上的内存吗

正常情况下是不可以的。因为在32位系统架构下,CPU寻址空间大小为232,也就是4GB。这意味着32位操作系统最多只能访问4GB的内存空间。然而,在实际情况中,32位操作系统中并非所有的4GB地址空间都可以用来访问内存。原因如下:

  • 操作系统及设备驱动程序也需要内存空间。32位操作系统需要一定的内存地址空间来存储内核、驱动程序和其他系统数据结构等,在此基础上,内存映射等机制也需要占用一部分地址空间。
  • 一些硬件设备请求使用一部分地址空间。例如,显卡、网络接口卡或其他外设可能需要从寻址空间中获取内存空间。

综上所述,即使在32位系统中,实际可用的地址空间与可寻址的地址空间存在差异,可能会导致可用内存空间少于4GB。因此,在32位系统中要访问4GB以上的内存,则需要使用物理地址扩展(PAE)等技术来支持。 PAE是一种利用硬件机制来扩展可寻址物理内存空间的技术,它将32位操作系统的物理寻址从32位扩展到36位或更高位,从而使操作系统可以访问4GB以上的内存空间。

23. 说说并发和并行,以及它们的区别

  • 并发:指两个或多个任务在同一时间段内被执行,但这些任务之间可能是相互独立或者互相依赖的。在并发中,每个任务看起来都在同时执行,但实际上是通过任务切换机制“交替”执行的。
  • 并行:指两个或多个任务在同一时间点上同时被执行。在并行中,每个任务都在独立的CPU处理器上执行,这些CPU可以是多个处理器在同一时间点上执行,也可以是一台计算机上多个核心同时执行。在并行中,任务之间不存在像并发中那样的任务切换。
  • 两者的区别:
    • 目的不同:并发的目的是实现多个任务的相互配合和协调,提高系统效率和响应时间;而并行的目的是通过将多个任务同时处理来提高处理能力和系统的吞吐量。
    • 执行方式不同:并行中的多个任务是同时执行的,而并发中的多个任务是交替执行的。
    • 硬件支持不同:并行需要多个处理器或多个CPU支持,并且需要专门的硬件电路;而并发需要操作系统调度和管理多个任务。

24. 说说进程、线程、协程是什么,区别是什么

  • 进程:程序是指令、数据及其组织形式的描述,而进程则是程序的运行实例,包括程序计数器、寄存器和变量的当前值。
  • 线程:微进程,一个进程里更小粒度的执行单元。一个进程里包含多个线程并发执行任务。
  • 协程:协程是微线程,在子程序内部执行,可在子程序内部中断,转而执行别的子程序,在适当的时候再返回来接着执行。

区别:

  • 线程与进程的区别
    • 一个线程从属于一个进程;一个进程可以包含多个线程。
    • 一个线程挂掉,对应的进程挂掉;一个进程挂掉,不会影响其他进程。
    • 进程是系统资源调度的最小单位;线程CPU调度的最小单位。
    • 进程系统开销显著大于线程开销;线程需要的系统资源更少。
    • 进程在执行时拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。
    • 进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈,线程切换时只需要切换硬件上下文和内核栈。
    • 通信方式不一样。
    • 进程适应于多核、多机分布;线程适用于多核。
  • 线程与协程的区别
    • 协程执行效率极高。协程直接操作栈基本没有内核切换的开销,所以上下文的切换非常快,切换开销比线程更小。
    • 协程不需要多线程的锁机制,因为多个协程从属于一个线程,不存在同时写变量冲突,效率比线程高。
    • 一个线程可以有多个协程。

25. 说说Linux中 fork 的作用

在Linux系统中,fork()是一个非常重要的系统调用,它的作用是创建一个新的进程,新进程与原进程完全独立。具体来说,fork()调用会创建一个新的进程(称为子进程),并且子进程的地址空间、数据、内存和文件描述符等都与父进程完全独立,不会相互影响。同时,子进程会继承父进程的环境变量、工作目录、主机名等环境变量和一些信号处理方式等属性。

fork()系统调用返回两次:在父进程中返回子进程的PID,在子进程中返回0。父进程可以使用返回值来识别子进程的PID,并从子进程中获得必要的状态信息。

fork()系统调用的使用非常普遍,经常用于以下几个方面:

  • 创建子进程:fork()被广泛地用于创建子进程,从而使得进程得以并行执行,从而提高系统的响应时间和效率。
  • 简单的多任务处理:在一个进程中调用多次fork(),可以创建多个子进程,这样每个子进程就可以承担不同的任务,实现简单的多任务处理。
  • 创建新的进程空间:fork()可以用于创建一个与父进程分离的新进程,从而获得一个新的地址空间,新的资源管理方式以及新的执行环境。
  • 实现进程拷贝:fork()也可以用于实现一个进程或进程组的拷贝,这使得相关任务可以同时进行并且保持相互独立,避免资源之间的共享和冲突等问题。

26. 说说什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程

孤儿进程和僵尸进程都是与进程状态相关的概念。

  • 孤儿进程:指子进程在父进程结束后,仍然在系统中运行的进程。因为父进程的退出导致它失去了父进程的引用,从而成为“孤儿进程”。孤儿进程会被init进程接管,并由init进程将其退出状态传递给父进程,从而结束进程。
  • 僵尸进程:指一个进程已经结束(调用exit()函数),但其父进程尚未对其进行善后处理(调用wait()等函数来获取结束状态信息),从而导致该进程的进程描述符仍然保存在进程表中,称为“僵尸进程”。

解决僵尸进程的方式主要有两种:

  • 父进程调用wait()等函数来获取子进程的结束状态,从而清除子进程的进程描述符。使用这种方式,父进程会阻塞,直到获取到子进程的结束状态或者发生信号中断。
  • 父进程利用信号处理机制,在收到SIGCHLD信号时调用wait()等函数来获取子进程的结束状态。使用这种方式,父进程可以继续执行,不需要等待子进程的结束状态,当收到SIGCHLD信号时再执行wait()等函数来获取子进程的结束状态。

另外,我们可以在父进程中使用fork()函数创建子进程,并在子进程中执行需要长时间执行的操作,然后在子进程中调用exit()函数退出。这样,父进程会立即收到子进程的结束状态,从而可以避免子进程成为僵尸进程。当然,为了避免出现孤儿进程,父进程需要在创建子进程后等待子进程结束,并处理子进程的结束状态。

27. 说说什么是守护进程,如何实现

  • 守护进程:守护进程是运行在后台的一种生存期长的特殊进程。它独立于控制终端,处理一些系统级别任务。
  • 如何实现
    • 调用fork()函数:守护进程的实现开始于创建一个子进程。这通常通过调用fork()系统函数来实现。创建子进程,终止父进程。
    • 调用setsid()函数:子进程在执行fork()之后,需要使用setsid()系统函数创建一个新的会话。
    • 改变进程的根目录:守护进程需要将当前工作目录修改为根目录,以防止影响其他进程的正常运行。
    • 重设文件权限掩码:文件权限掩码是指屏蔽掉文件权限中的对应位。
    • 关闭文件描述符:守护进程需要关闭其打开的文件描述符,以便系统可以释放它们,并防止它们在守护进程后继续占用内存等资源。

实现代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#define MAXFILE 65535

int main() {
    
    
	// 第一步:创建进程
	int pid = fork();
	if (pid > 0)
		exit(0);// 结束父进程
	else if (pid < 0) {
    
    
		printf("fork error!\n");
		exit(1);//fork失败,退出
	} 
	// 第二步:子进程成为新的会话组长和进程组长, 并与控制终端分离
	setsid();
	// 第三步:改变工作目录到
	chdir("/");
	// 第四步:重设文件创建掩模
	umask(0);
	// 第五步:关闭打开的文件描述符
	for (int i = 0; i < MAXFILE; ++i){
    
    
		close(i);
		sleep(2);
	}
	return 0;
}

28. 说说进程通信的方式有哪些

进程通信(Inter-Process Communication, IPC)是指两个或多个进程之间传递信息或交换数据的过程。进程通信的方式有以下几种:

  • 管道(Pipe):是一种半双工的通信方式,只能在具有亲缘关系的进程之间使用,且数据只能在一个方向上流动。
  • 命名管道(Named Pipe):也是一种半双工的通信方式,它可以在不具有亲缘关系的进程之间使用。
  • 信号(Signal):是一种比较简单的通信方式,可以在任何两个进程之间使用,用于通知一个进程发生了某个事件。
  • 共享内存(Shared Memory):是一种高效的进程间通信方式,可以在多个进程之间共享同一块物理内存区域。不过它需要对共享内存的访问进行同步控制,以避免数据竞争等问题。
  • 消息队列(Message Queue):是一种消息传递机制,在不同进程之间传递一个消息。可以用于通知进程某个事件已经发生,或者将某个进程需要处理的数据传送给其他进程等。
  • 信号量(Semaphore):是一种在多个进程之间控制共享资源访问的通信方式。它可以用于同步进程的动作,并保证在同时访问共享资源时的正确性。
  • 套接字(Socket):是一种在任意两个进程之间通信的方法,通过网络协议(例如TCP/IP)在不同的计算机之间通信。

上述进程通信方式各具特点,选择何种方式要根据具体情况而定,例如通信的双方关系、数据传输的大小及时效性等等。

29. 说说进程同步的方式

进程同步是指在多个进程之间协调执行的一种机制,目的是确保它们按照指定的先后顺序执行,防止因并发执行而产生错误或冲突。常见的进程同步方式有以下几种:

  • 信号量(Semaphore):是进程间进行同步操作的一种机制。它通常使用一种名为信号量的变量来进行进程之间的同步。信号量的值可以用来表示某个资源可用的数量。当进程需要使用某个共享资源时,它就会等待信号量。如果信号量值大于零,则将其减1并继续执行。如果信号量值为零,则暂停进程的执行直到信号量变为非零。
  • 互斥锁(Mutex):是一种轻量级的同步机制,可以保护多个进程或多个线程同时访问的共享资源。它通常实现为一个二值信号量,当锁被某个线程持有时,其他线程就不能获取锁,只有等待锁的线程放弃锁后,其他线程才能获取锁。
  • 条件变量(Condition Variable):是一种在多个进程之间进行同步操作的一种机制。使用条件变量,进程可以等待某个条件的发生,并在条件发生时被唤醒,从而继续执行。
  • 同步屏障(Barrier):是一种同步机制,用于确保多个进程在某个点上同时执行。当所有进程都到达同步点后,它们将等待其他进程到达。一旦所有进程都到达同步点,它们都将继续执行。

上述是常见的进程同步方式。在多进程程序中,使用这些同步方式能够避免多进程间发生数据竞争和死锁等问题,从而保证程序能够正确执行。

30. 说说进程有多少种状态

一个进程的状态是指它在执行过程中所处的不同状态。一个进程创建后,被放入队列处于就绪状态,等待操作系统调度执行,执行过程中可能切换到阻塞状态(并发),任务完成后,进程销毁终止。通常,一个进程可能处于以下状态中的一种或多种:

  • 创建状态(New):一个应用程序从系统上启动,首先就是进入创建状态,需要获取系统资源创建进程管理块(PCB:Process Control Block)完成资源分配。
  • 就绪状态(Ready):在创建状态完成之后,进程已经准备好,处于就绪状态,但是还未获得处理器资源,无法运行。
  • 运行状态(Running):获取处理器资源,被系统调度,当具有时间片开始进入运行状态。如果进程的时间片用完了就进入就绪状态。
  • 阻塞状态(Blocked):在运行状态期间,如果进行了阻塞的操作,如耗时的I/O操作,此时进程暂时无法操作就进入到了阻塞状态,在这些操作完成后就进入就绪状态。等待再次获取处理器资源,被系统调度,当具有时间片就进入运行状态。
  • 终止状态(Terminated):进程完成了所有的工作或由于某些原因被系统终止,进入终止状态。

相互转换如图:
在这里插入图片描述

31. 进程通信中的管道实现原理是什么

管道(Pipe)是一种进程间通信的方式,也称为匿名管道。管道有两个端点,一个读端口和一个写端口,通过管道可以实现两个进程之间的单向数据传输。

在实现管道时,操作系统内核会创建一个管道缓冲区,用于存放要传输的数据。当一个进程写入数据到管道缓冲区时,数据会被存储在缓冲区中,同时被标记为可读。当另一个进程请求从管道中读取数据时,操作系统就从缓冲区中读取数据,并将其标记为可写。这个过程可以循环进行,从而实现数据的传输。

在Linux系统中,管道的实现使用了文件描述符(File Descriptor)的概念。当创建管道时,操作系统会向进程返回两个文件描述符,一个用于读取数据,一个用于写入数据。管道的传输方向从写入端口到读取端口。

管道的缺点是它只能在具有亲缘关系的进程之间使用,且只能实现单向数据传输。如果需要在不具有亲缘关系的进程之间进行双向数据传输,则需要使用命名管道(Named Pipe)或其他的进程间通信机制。

32. 简述 mmap 的原理和使用场景

原理:mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read, write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。如下图:
在这里插入图片描述
mmap 的使用场景主要有以下两个:

  • 文件的高效读取:对于一些特别大的文件,直接使用 read、write 等文件操作可能会很耗时,而 mmap 可以通过直接将文件映射到内存中的方式,实现高效的随机读取和写入。
  • 进程间通信:mmap 也可以用作进程间通信的一种方式,例如通过映射共享内存区域来实现多个进程之间数据的共享与同步。

33. 互斥量能不能在进程中使用

互斥量(Mutex)是线程间进行同步操作的一种机制,利用互斥量可以保护多个线程同时访问的共享资源,从而避免数据竞争和死锁等问题。互斥量主要用于线程之间的同步,不能直接在进程中使用。
不过,在进程间通信(Inter-Process Communication,IPC)的场景下,也可以通过使用系统提供的可以跨进程使用的 Mutex 实现进程间同步机制,这种互斥量称为进程间互斥量(Process Mutex)或跨进程互斥量(Cross-Process Mutex)。
进程间互斥量可以通过系统提供的函数(如 POSIX 标准中的 sem_open() 函数)在进程中创建,并可以通过名称在不同进程之间共享。进程间互斥量与线程间的互斥量类似,都可以通过 lock 和 unlock 操作来实现与共享资源的保护。
需要注意的是,由于进程间互斥量需要使用系统调用和内核资源进行创建和管理,因此相比于线程间互斥量,进程间互斥量的性能会有所下降,而且使用起来也需要特别注意一些与进程间同步相关的问题,例如死锁的情况等。

34. 协程是轻量级线程,轻量级表现在哪里

协程(Coroutine)是一种轻量级的线程,相比于传统线程,它支持更多的并发执行和任务切换,同时具有更轻量、更快速的特性。

协程的轻量级表现主要体现在以下方面:

  • 协程调用跟切换比线程效率高:协程执行效率极高。协程不需要多线程的锁机制,可以不加锁的访问全局变量,所以上下文的切换非常快。
  • 协程占用内存少:执行协程只需要极少的栈内存(大概是4~5KB),而默认情况下,线程栈的大小为1MB。
  • 切换开销更少:协程直接操作栈基本没有内核切换的开销,所以切换开销比线程少。

35. 说说常见信号有哪些,表示什么含义

在计算机系统中,信号(Signal)是一种异步处理机制,用于将某些事件传递给进程或线程。当事件(如错误、警告等)发生时,操作系统可以向进程发送一个信号,进程可以根据接收到的信号进行对应的处理。以下是几个常见的信号及其含义:

  • SIGINT:中断信号。通常由用户在控制台上按下 Ctrl + C 时发送给前台进程,用于中断当前进程的执行。
  • SIGKILL:杀死信号。该信号的作用是立即停止进程的执行,并且无法被进程捕获或者忽略。可用于强制关闭不响应的进程。
  • SIGTERM:终止信号。通常用于请求进程正常终止。该信号会被发送给进程,让进程知道需要结束处理并退出。
  • SIGSEGV:段错误信号。通常表示进程尝试访问未分配给它的内存区域,触发了保护错误。
  • SIGBUS:总线错误信号。通常表示进程尝试访问未对齐的内存区域,或访问不存在的 I/O 设备等导致的错误。
  • SIGALRM:闹钟信号。通常用于定时器等周期性任务的处理,当设置的时间到期时,操作系统会向进程发送该信号。

36. 说说线程间通信的方式有哪些

线程间的通信方式包括临界区、互斥量、信号量、条件变量、读写锁。

  • 临界区:每个线程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个线程使用的共享资源)。每次只准许一个线程进入临界区,进入后不允许其他线程进入。不论是硬件临界资源,还是软件临界资源,多个线程必须互斥地对它进行访问。
  • 互斥量:采用互斥对象机制,只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
  • 信号量:计数器,允许多个线程同时访问同一个资源。
  • 条件变量:通过条件变量通知操作的方式来保持多线程同步。
  • 读写锁:读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高。

37. 说说线程同步方式有哪些

线程间的同步方式包括互斥锁、信号量、条件变量、读写锁。

  • 互斥锁:采用互斥对象机制,只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
  • 信号量:计数器,允许多个线程同时访问同一个资源。
  • 条件变量:通过条件变量通知操作的方式来保持多线程同步。
  • 读写锁:读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高。

38. 说说什么是死锁,产生的条件,如何解决

死锁(Deadlock)是指两个或多个进程或线程被永久地阻塞,因为它们都在等待对方持有的资源,而对方也在等待自己持有的资源。如果发生死锁,那么这些进程或线程将一直等待下去,直到人为干预才能解除阻塞。

产生死锁的必要条件:

  • 互斥条件:进程或线程对资源进行排它性控制,即一次只能有一个进程或线程访问该资源。
  • 请求与保持条件:进程或线程持有至少一个资源,并且在等待其他的资源。
  • 不剥夺条件:即已经分配给进程或线程的资源不能被强制剥夺,只能自己释放。
  • 环路等待条件:进程或线程之间形成了一种环形等待的关系,即每个进程或线程都在等待相邻进程或线程所持有的资源。

解决死锁的常见方法:

  • 防止死锁的发生:通过破坏死锁产生的必要条件之一,可以防止死锁的发生。例如,可以破坏环路等待条件,在资源分配时,按照某个顺序请求资源,或者限制每个进程或线程最多可以持有的资源数目等。
  • 避免死锁:采用安全状态法(Safety Algorithm)进行资源的请求和分配,只有当一个进程或线程可以安全地请求资源时,才能进行资源的分配。例如,银行家算法(Banker’s algorithm)就是一种避免死锁的算法,能够预测在什么情况下会发生死锁,从而防止死锁的发生。
  • 检测和解除死锁:周期性地对系统资源进行检测,发现死锁后采取解除死锁的措施。例如,操作系统可以设置超时时间,在一段时间内如果没有响应就认为发生了死锁,然后通过召唤进程或者抢夺资源等方式解除死锁。

39. 有了进程,为什么还要有线程

  • 原因:进程在早期的多任务操作系统中是基本的执行单元。每次进程切换,都要先保存进程资源然后再恢复,这称为上下文切换。但是进程频繁切换将引起额外开销,从而严重影响系统的性能。为了减少进程切换的开销,人们把两个任务放到一个进程中,每个任务用一个更小粒度的执行单元来实现并发执行,这就是线程。
  • 线程与进程对比:
    • 进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。但多个线程共享进程的内存,如代码段、数据段、扩展段,线程间进行信息交换十分方便。
    • 调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销依然不菲。但创建线程比创建进程通常要快 10 倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表。

40. 单核机器上写多线程程序,是否要考虑加锁,为什么

在单核机器上写多线程程序,仍然需要线程锁。

原因:因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突。

41. 说说多线程和多进程的不同

  • 一个线程从属于一个进程;一个进程可以包含多个线程。
  • 一个线程挂掉,对应的进程挂掉,多线程也挂掉;一个进程挂掉,不会影响其他进程,多进程稳定。
  • 进程系统开销显著大于线程开销;线程需要的系统资源更少。
  • 多个进程在执行时拥有各自独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。
  • 多进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈;多线程切换时只需要切换硬件上下文和内核栈。
  • 通信方式不一样。
  • 多进程适应于多核、多机分布;多线程适用于多核。

42. 简述互斥锁的机制,互斥锁与读写锁的区别

互斥锁机制:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。

互斥锁和读写锁的区别

  • 读写锁区分读者和写者,而互斥锁不区分。
  • 互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。

43. 说说什么是信号量,有什么作用

信号量(Semaphore)是一种计数器,用于多线程环境下的进程同步和互斥。信号量可以被用来控制对共享资源的并发访问,并协调多个线程的执行顺序。

信号量通常包含两个基本操作:P和V。P操作(也称作wait()操作)会将信号量计数器减1,如果计数器当前值为0,则当前线程被阻塞,等待其他线程使用V操作(也称作signal()操作)将信号量计数器加1。V操作会将信号量计数器加1,并唤醒一个或多个阻塞的线程,使得它们可以继续执行。因此,信号量可以用来协调多个线程之间的执行顺序,确保共享资源的访问是有序的,避免数据竞争和死锁等问题。

信号量的作用:用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。

44. 进程、线程的中断切换的过程是怎样的

上下文切换指的是内核(操作系统的核心)在CPU上对进程或者线程进行切换。

  1. 进程上下文切换
  • 保护被中断进程的处理器现场信息
  • 修改被中断进程的进程控制块有关信息,如进程状态等
  • 把被中断进程的进程控制块加入有关队列
  • 选择下一个占有处理器运行的进程
  • 根据被选中进程设置操作系统用到的地址转换和存储保护信息
    切换页目录以使用新的地址空间
    切换内核栈和硬件上下文(包括分配的内存,数据段,堆栈段等)
  • 根据被选中进程恢复处理器现场
  1. 线程上下文切换
  • 保护被中断线程的处理器现场信息
  • 修改被中断线程的线程控制块有关信息,如线程状态等
  • 把被中断线程的线程控制块加入有关队列
  • 选择下一个占有处理器运行的线程
  • 根据被选中线程设置操作系统用到的存储保护信息
    切换内核栈和硬件上下文(切换堆栈,以及各寄存器)
  • 根据被选中线程恢复处理器现场

45. 简述自旋锁和互斥锁的使用场景

自旋锁和互斥锁都是用于保护共享资源的同步机制,它们都可以避免多个线程同时访问和修改共享资源,从而避免数据竞争和不一致性问题。

自旋锁和互斥锁的使用场景如下:
自旋锁(Spin Lock):自旋锁主要适用于对于锁保持的时间很短且线程短时间内不会被阻塞的情况。自旋锁使用轮询的方式等待锁被释放,在锁被释放前,线程会一直占用CPU资源,因此不适用于锁保持时间较长或者存在大量等待线程的情况。

一般来说,自旋锁在以下情况下使用较为合适:

  • 对于锁保持时间极短:自旋锁不会引起线程的上下文切换,因此在锁保持时间非常短的情况下,使用自旋锁可以提高性能。
  • 访问共享资源的频率很高:由于自旋锁不需要进行阻塞和唤醒操作,因此可以避免上下文切换的开销,适用于对共享资源访问频率很高的情况。
  • 多处理器系统中:自旋锁主要用于多处理器环境中,可以避免不同CPU之间的指令竞争,提高系统的并发性能。

互斥锁(Mutex):互斥锁适用于对于锁保持时间较长或存在大量等待线程的情况。互斥锁的特点是当资源被占用时,会将线程阻塞,直到锁被释放,因此不会产生大量线程轮询资源的情况。

一般来说,互斥锁在以下情况下使用较为合适:

  • 锁保持时间较长:在锁保持时间较长的情况下,使用自旋锁会引起大量上下文切换和CPU资源浪费,适用于使用互斥锁保护资源的情况。
  • 等待线程较多:当存在大量等待线程的情况下,自旋锁会引起大量CPU资源浪费,而互斥锁能够及时阻塞线程并释放CPU资源,避免大量等待线程的情况。
  • 锁冲突较多:当存在大量线程竞争同一个锁的情况下,使用互斥锁可以避免竞争和数据不一致的问题。

46. 说说线程有哪些状态,相互之间怎么转换

线程状态通常包括以下几种:

  • 新建(New):线程被创建但尚未启动执行;
  • 就绪(Ready):线程已经创建并准备执行,等待分配CPU时间片;
  • 运行(Running):线程正在执行,占用CPU资源;
  • 阻塞(Blocked):线程被挂起,等待IO、网络、信号等事件处理完毕后再继续执行;
  • 终止(Terminated):线程执行结束,结束其生命周期。

线程状态之间的转换通常由操作系统的线程调度器来控制,主要包括以下几种情况:

  • 从新建状态到就绪状态:线程被创建后,等待分配CPU时间片,进入就绪状态;
  • 从就绪状态到运行状态:当CPU时间片被分配给某个线程,该线程将从就绪状态转换为运行状态;
  • 从运行状态到就绪状态:当一个线程所占用的时间片结束或者主动放弃CPU资源,进入就绪状态等待下一次CPU调度;
  • 从运行状态到阻塞状态:线程需要等待某些事件完成后才能继续执行,进入阻塞状态,这时CPU将调度其他就绪状态的线程执行;
  • 从阻塞状态到就绪状态:当等待的事件完成后,线程进入就绪状态等待CPU调度。
  • 从运行状态到终止状态:线程执行结束,进入终止状态,结束其生命周期。
    在这里插入图片描述

47. 多线程和单线程有什么区别,多线程编程要注意什么,多线程加锁需要注意什么

区别:

  • 多线程从属于一个进程,单线程也从属于一个进程;一个线程挂掉都会导致从属的进程挂掉。
  • 一个进程里有多个线程,可以并发执行多个任务;一个进程里只有一个线程,就只能执行一个任务。
  • 多线程并发执行多任务,需要切换内核栈与硬件上下文,有切换的开销;单线程不需要切换,没有切换的开销。
  • 多线程并发执行多任务,需要考虑同步的问题;单线程不需要考虑同步的问题。

多线程编程需要考虑同步的问题。线程间的同步方式包括互斥锁、信号量、条件变量、读写锁。

多线程加锁,主要需要注意死锁的问题。破坏死锁的必要条件从而避免死锁。

48. 说说sleep和wait的区别

Sleep和Wait都是用于线程等待的方法,但是它们的使用场景和语法略有不同。

Sleep是Thread类中的一个静态方法,它会让当前线程休眠一段时间,单位是毫秒。Sleep方法不会释放线程所持有的锁,也就是说,当一个线程在调用Sleep方法时,它仍然占用着共享资源的锁资源,其他线程无法访问该资源。需要注意的是,Sleep方法是不可打断的,只能等待指定时间后才能继续执行。

Wait是Object类中的一个实例方法,它会让当前线程等待另一个线程发出的通知或者等待一段时间。Wait方法必须在synchronized代码块或者方法中使用,并且在调用Wait方法时需要先获得锁资源。当一个线程调用Wait方法时,它会释放锁资源,并进入等待队列,等待其他线程调用Notify或者NotifyAll方法来唤醒它。需要注意的是,Wait方法是可打断的,也就是说,当其他线程调用了中断方法(Thread.Interrupt)或者超时时间到时,Wait方法就会抛出InterruptedException异常或者在等待一段时间后自动唤醒。

综上所述,Sleep方法是让线程休眠一段时间,不释放锁资源,不可打断;而Wait方法是在锁资源保护下让线程等待通知或超时,释放锁资源,可打断。一般来说,当我们需要等待某个条件满足时,应该使用Wait方法;而当我们需要让线程休眠一段时间后再继续执行时,应该使用Sleep方法。

49. 说说线程池的设计思路,线程池中线程的数量由什么确定

线程池是一种常用的多线程并发处理方式,通过事先创建一定数量的线程,然后让这些线程在需要时执行任务,而不是每次任务到来时都创建一个新线程。线程池的设计思路主要包括以下几个方面:

实现线程池有以下几个步骤:

  • 设置一个生产者消费者队列,作为临界资源。
  • 初始化 n 个线程,并让其运行起来,加锁去队列里取任务运行。
  • 当任务队列为空时,所有线程阻塞。
  • 当生产者队列来了一个任务后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通知阻塞中的一个线程来处理。

线程数量和哪些因素有关

  • CPU资源:线程池中的线程数量应该与CPU资源的数量相匹配,以达到最优的并发效果。如果线程数量过多,会增加CPU调度的负担,降低系统的性能。如果线程数量过少,会导致任务等待时间过长,降低系统的并发效率。
  • 内存资源:线程池中的线程数量应该保证在可用内存资源范围内,避免因线程过多导致内存资源不足。
  • 任务特性:任务类型和任务数量也可以对线程数量进行调整。如果任务是计算密集型的,线程数量可以配置得比较大;如果任务是IO密集型的,线程数量应该配置得比较小。
  • 系统负载情况:线程池中的线程数量应该根据系统的负载情况和处理任务的实时需求进行动态调整,以保证系统的性能和响应能力。
如果是CPU密集型应用,则线程池大小设置为:CPU数目+1
如果是IO密集型应用,则线程池大小设置为:2*CPU数目+1
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

所以线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

「补充」:

  • 为什么要创建线程池:创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。同时线程池也是为了提升系统效率。
  • 线程池的核心线程与普通线程
    任务队列可以存放100个任务,此时为空,线程池里有10个核心线程,若突然来了10个任务,那么刚好10个核心线程直接处理;若又来了90个任务,此时核心线程来不及处理,那么有80个任务先入队列,再创建核心线程处理任务;若又来了120个任务,此时任务队列已满,不得已,就得创建20个普通线程来处理多余的任务。
    以上是线程池的工作流程。

50. 进程和线程相比,为什么慢

  • 进程创建和销毁开销大:进程的创建和销毁相对于线程开销更大。进程需要分配并初始化一段独立的虚拟内存空间;而线程只需要更小的栈区和其他寄存器的存储空间,需要的系统资源更少。。
  • 进程切换开销比线程大。多进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈;多线程切换时只需要切换硬件上下文和内核栈。
  • 进程间通信复杂:不同进程之间通信的机制相对于线程间通信更为复杂。进程间通信需要使用操作系统提供的IPC(Inter-Process Communication)机制,包括命名管道、共享内存、消息队列、信号量等,而线程间通信可以使用更加简单的共享内存,条件变量等。
  • 进程更易受限制:操作系统通常会限制一个进程可以占用的系统资源,如CPU时间片、内存空间等,进程要求更多的系统资源相对于线程更容易受到限制。

51. 简述什么是Linux零拷贝,它的好处及原理

什么是零拷贝

Linux零拷贝是一种通过操作系统内核技术实现高效数据传输和高并发的技术。传统的数据传输通常采用用户空间内存到内核空间内存,再到网卡设备的缓冲区,整个过程中需要经过多次内存拷贝,而零拷贝技术则充分利用了操作系统内核提供的DMA(Direct Memory Access)功能,避免了不必要的内存拷贝,实现在内核空间和I/O设备之间直接传输数据,从而使数据传输更加高效。

零拷贝的好处

  • 节省了 CPU 周期,空出的 CPU 可以完成更多其他的任务。
  • 减少了内存区域之间数据拷贝,节省内存带宽。
  • 减少用户态和内核态之间数据拷贝,提升数据传输效率。
  • 应用零拷贝技术,减少用户态和内核态之间的上下文切换。

零拷贝的原理

零拷贝技术的原理主要是通过操作系统的DMA技术实现的。操作系统内核提供了DMA的机制,可以直接将数据从内核空间复制到设备缓冲区,由I/O设备负责数据的传输,避免了数据在内核和用户空间之间的多次复制。常见的使用零拷贝技术的应用场景包括网络数据传输、文件拷贝、音视频处理等。

52. 简述epoll和select的区别,epoll为什么高效

epoll和select都是Linux系统中的多路复用机制,用于实现高效的IO多路复用和事件处理。

它们的主要区别有以下几个方面:

  • 每次调用 select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;而 epoll 保证了每个fd在整个过程中只会拷贝一次。
  • 每次调用 select 都需要在内核遍历传递进来的所有fd;而 epoll 只需要轮询一次fd集合,同时查看就绪链表中有没有就绪的fd就可以了。
  • select 支持的文件描述符数量太小了,默认是1024;而 epoll 没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048。

epoll为什么高效

  • select、poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而 epoll 只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。
  • select、poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把当前进程往设备等待队列中挂一次,而 epoll 只要一次拷贝,而且把当前进程往等待队列上挂也只挂一次,这也能节省不少的开销。

53. 说说多路IO复用技术有哪些,区别是什么

select,poll,epoll 都是IO多路复用的机制,I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。

区别

  • poll 与 select 不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。
  • select,poll 实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而 epoll 只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。
  • select,poll 每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把当前进程往设备等待队列中挂一次,而 epoll 只要一次拷贝,而且把当前进程往等待队列上挂也只挂一次,这也能节省不少的开销。

54. 简述socket中select,epoll的使用场景和区别,epoll水平触发与边缘触发的区别

select、epoll的使用场景:都是IO多路复用的机制,应用于高并发的网络编程的场景。I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。

select,epoll的区别

  • 每次调用 select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;而 epoll 保证了每个fd在整个过程中只会拷贝一次。
  • 每次调用 select 都需要在内核遍历传递进来的所有fd;而 epoll 只需要轮询一次fd集合,同时查看就绪链表中有没有就绪的fd就可以了。
  • select 支持的文件描述符数量太小了,默认是1024;而 epoll 没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048。

epoll水平触发与边缘触发的区别

  • 水平触发(LT):水平触发模式是只要有数据就能够通知应用程序。如果读写缓冲区中还有未读写的数据,只要是有数据,就能够触发事件通知,需要应用程序自己处理没有读写的数据,因此处理方式较为繁琐,适合处理大文件或者数据流的传输。
  • 边缘触发(ET):边缘触发模式是只有在IO事件发生时才会激发一次,不管缓冲区是否有未读写的数据,即只在状态发生改变时通知应用程序,因此处理方式比较高效,适合于处理短小的数据传输,在特定场景下可以优化性能。

55. 说说Reactor、Proactor模式

在高性能的I/O设计中,有两个比较著名的模式:Reactor 和 Proactor 模式,其中 Reactor 模式用于同步I/O,而 Proactor 运用于异步I/O操作。

  • Reactor 模式:Reactor模式应用于同步I/O的场景。Reactor中读操作的具体步骤如下:
    • 应用程序注册读就需事件和相关联的事件处理器
    • 事件分离器等待事件的发生
    • 当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器
    • 事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
  • Proactor 模式:Proactor模式应用于异步I/O的场景。Proactor中读操作的具体步骤如下:
    • 应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
    • 事件分离器等待读取操作完成事件
    • 在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。
    • 事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。
  • 区别:从上面可以看出,Reactor 中需要应用程序自己读取或者写入数据,而 Proactor 模式中,应用程序不需要用户再自己接收数据,直接使用就可以了,操作系统会将数据从内核拷贝到用户区。

56. 简述同步与异步的区别,阻塞与非阻塞的区别

同步与异步的区别:

  • 同步:是所有的操作都做完,才返回给用户结果。即写完数据库之后,再响应用户,用户体验不好。
  • 异步:不用等所有操作都做完,就响应用户请求。即先响应用户请求,然后慢慢去写数据库,用户体验较好。

阻塞与非阻塞的区别:

  • 阻塞:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
  • 非阻塞:非阻塞等待,每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。

57. BIO、NIO有什么区别

BIO(Blocking I/O):阻塞IO。调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停地检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。

NIO(New I/O):同时支持阻塞与非阻塞模式,NIO的做法是叫一个线程不断的轮询每个IO的状态,看看是否有IO的状态发生了改变,从而进行下一步的操作。

58. 请介绍一下5种IO模型

  1. 阻塞IO:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
  2. 非阻塞IO:非阻塞等待,每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。
  3. 信号驱动IO:Linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进程收到SIGIO信号,然后处理IO事件。
  4. IO多路复用:Linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检查。知道有数据可读或可写时,才真正调用IO操作函数。
  5. 异步IO:Linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。用户可以直接去使用数据。

前四种模型–阻塞IO、非阻塞IO、多路复用IO和信号驱动IO都属于同步模式,因为其中真正的IO操作(函数)都将会阻塞进程,只有异步IO模型真正实现了IO操作的异步性。

异步和同步的区别就在于,异步是内核将数据拷贝到用户区,不需要用户再自己接收数据,直接使用就可以了,而同步是内核通知用户数据到了,然后用户自己调用相应函数去接收数据。

59. socket 网络编程中客户端和服务端用到哪些函数

在Socket网络编程中,客户端和服务端都需要用到一些函数来完成其通信的过程,主要包括以下几种:

  • socket():用于创建一个socket描述符,客户端和服务端都需要使用该函数来创建socket对象。
  • bind():用于服务器端将socket与指定的IP地址和端口绑定,通常在服务器端调用该函数以监听客户端的请求。
  • listen():用于服务器端将指定的socket设置为监听状态,以等待客户端的连接请求。
  • accept():用于服务器端接收客户端连接请求,并返回一个新的socket描述符,用于服务器与客户端的数据交换。
  • connect():客户端使用该函数连接服务器。 connect() 函数将socket对象与服务器地址和端口绑定。
  • send():用于发送数据到服务器或客户端,被发送的数据通常包括报文头和报文体。
  • recv():用于接收从服务器或客户端返回的数据,通常在一个循环中调用 recv() 函数,逐个接收完整的报文,然后进行解析。
  • close():用于关闭打开的socket描述符,释放操作系统所使用的相关资源。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_51913750/article/details/130400262
今日推荐