Linux操作系统【整理版】

Linux问题整理2021年7月27日

操作系统综述

Linux的运行级别

0: 系统停机(关机)模式,系统默认运行级别不能设置为0,否则不能正常启动,一开机就自动关机。
1:单用户模式,root权限,用于系统维护,禁止远程登陆,就像Windows下的安全模式登录。
2:多用户模式,没有NFS网络支持。
3:完整的多用户文本模式,有NFS,登陆后进入控制台命令行模式。
4:系统未使用,保留一般不用,在一些特殊情况下可以用它来做一些事情。例如在笔记本电脑的电池用尽时,可 以切换到这个模式来做一些设置。
5:图形化模式,登陆后进入图形GUI模式或GNOME、KDE图形化界面,如X Window系统。
6:重启模式,默认运行级别不能设为6,否则不能正常启动,就会一直开机重启开机重启。

异常和中断

中断:是指由于外部设备事件所引起的中断,如通常的磁盘中断、打印机中断等;
异常:是指由于 CPU 内部事件所引起的中断,如程序出错(非法指令、地址越界)。

异常是由于执行了现行指令所引起的。由于系统调用引起的中断属于异常。而中断则是由于系统中某事件引起的,该事件与现行指令无关。

内核态与用户态的区别?

用户空间:指的就是用户可以操作和访问的空间,这个空间通常存放我们用户自己写的数据等。

内核空间:是系统内核来操作的一块空间,这块空间里面存放系统内核的函数、接口等。

用户态和内核态是操作系统的两种运行级别,两者最大的区别就是特权级不同。用户态拥有最低的特权级,内核态拥有较高的特权级。运行在用户态的程序不能直接访问操作系统内核数据结构和程序。内核态和用户态之间的转换方式主要包括:系统调用,异常和中断。

在这里插入图片描述

为什么需要内核态和用户态

为了安全性。在cpu的一些指令中,有的指令如果用错,将会导致整个系统崩溃。分了内核态和用户态后,当用户需要操作这些指令时候,内核为其提供了API,可以通过系统调用陷入内核,让内核去执行这些操作。

从用户空间到内核空间有以下触发手段:
1.系统调用:用户进程通过系统调用申请使用操作系统提供的服务程序来完成工作,比如read()、fork()等。系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现的。

2.中断:当外围设备完成用户请求的操作后,会想CPU发送中断信号。这时CPU会暂停执行下一条指令(用户态)转而执行与该中断信号对应的中断处理程序(内核态)

3.异常:当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

在这里插入图片描述

进程管理

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

线程产生的原因
进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;但是其具有一些缺点:

进程在同一时间只能干一件事

进程在执行的过程中如果阻塞,整个进程就会挂起,即使进程中有些工作不依赖于等待的资源,仍然不会执行。

因此,操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时空开销,提高并发性。和进程相比,线程的优势如下:

从资源上来讲,线程是一种非常"节俭"的多任务操作方式。在linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。

从切换效率上来讲,运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间。据统计,一个进程的开销大约是一个线程开销的30倍左右。(

从通信机制上来讲,线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进城下的线程之间贡献数据空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。

操作系统中的页表寻址

页式内存管理,内存分成固定长度的一个个页片。操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。页表中的每一项都记录了这个页的基地址。通过页表,由逻辑地址的高位部分先找到逻辑地址对应的页基地址,再由页基地址偏移一定长度就得到最后的物理地址,偏移的长度由逻辑地址的低位部分决定。一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。页式内存管理的优点就是比较灵活,内存管理以较小的页为单位,方便内存换入换出和扩充地址空间。

虚拟地址

为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。

虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。

请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。

虚拟内存的好处:

1.扩大地址空间;

2.内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。

3.公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。

4.当进程通信时,可采用虚存共享的方式实现。

5.当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存

6.虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高

7.在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片

虚拟内存的代价:

1.虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存

2.虚拟地址到物理地址的转换,增加了指令的执行时间。

3.页面的换入换出需要磁盘I/O,这是很耗时的

4.如果一页中只有一部分数据,会浪费内存。

虚拟内存到物理内存的对应

第一步:CPU段式管理中——逻辑地址转线性地址
CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址。

第一步:页式管理——线性地址转物理地址
再利用其页式内存管理单元,转换为最终物理地址。

共享储存映射

概述

存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。
在这里插入图片描述
于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。

共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式, 因为进程可以直接读写内存,而不需要任何数据的拷贝。

存储映射函数

#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
功能:
    一个文件或者其它对象映射进内存
参数:
    addr :  指定映射的起始地址, 通常设为NULL, 由系统指定
    length:映射到内存的文件长度
    prot:  映射区的保护方式, 最常用的 :
        a) 读:PROT_READ
        b) 写:PROT_WRITE
        c) 读写:PROT_READ | PROT_WRITE
    flags:  映射区的特性, 可以是
        a) MAP_SHARED : 写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享。
        b) MAP_PRIVATE : 对映射区的写入操作会产生一个映射区的复制(copy - on - write), 对此区域所做的修改不会写回原文件。
    fd:由open返回的文件描述符, 代表要映射的文件。
    offset:以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射
返回值:
    成功:返回创建的映射区首地址
    失败:MAP_FAILED宏

关于mmap函数的使用总结:

  1. 第一个参数写成NULL
  2. 第二个参数要映射的文件大小 > 0
  3. 第三个参数:PROT_READ 、PROT_WRITE
  4. 第四个参数:MAP_SHARED 或者 MAP_PRIVATE
  5. 第五个参数:打开的文件对应的文件描述符
  6. 第六个参数:4k的整数倍,通常为0

munmap函数

#include <sys/mman.h>int munmap(void *addr, size_t length);
功能:
    释放内存映射区
参数:
    addr:使用mmap函数创建的映射区的首地址
    length:映射区的大小
返回值:
    成功:0
    失败:-1

两种通信方式:
共享映射:父子进程和不同进程
匿名映射:父子进程,不需要借助文件

同样需要借助标志位参数flags来指定。

缺页中断

malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。
缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存是,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。

缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:

1、保护CPU现场
2、分析中断原因
3、转入缺页中断处理程序进行处理
4、恢复CPU现场,继续执行

缺页置换算法

作系统最常采用的缺页置换算法如下:
先进先出(FIFO)算法:置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。按照进入内存的先后次序排列成队列,从队尾进入,从队首删除。

最近最少使用(LRU)算法: 置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。
2、LFU(最不经常访问淘汰算法):每个数据块一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。每次淘汰队尾数据块。

当前最常采用的就是LRU算法。

信号

每个信号必备4要素,分别是:
1)编号 2)名称 3)事件 4)默认处理动作

信号的产生

a) 当用户按某些终端键时,将产生信号。
终端上按“Ctrl+c”组合键通常产生中断信号 SIGINT,程序终止信号。
终端上按“Ctrl+\”键通常产生中断信号 SIGQUT,进程收到该信号退出时会产生core文件,类似于程序错误信号。
终端上按“Ctrl+z”键通常产生中断信号 SIGSTOP ,停止进程执行。
SIGCHLD:子进程结束,父进程收到。如果子进程结束时,父进程不等待或不处理,子进程会变为僵尸进程。

b) 硬件异常将产生信号。
除数为 0,无效的内存访问等。这些情况通常由硬件检测到,并通知内核,然后内核产生适当的信号发送给相应的进程。

c) 软件异常将产生信号。
当检测到某种软件条件已发生(如:定时器alarm),并将其通知有关进程时,产生信号。

d) 调用系统函数(如:kill、raise、abort)将发送信号。
注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。

e) 运行 kill /killall命令将发送信号。
此程序实际上是使用 kill 函数来发送信号。也常用此命令终止一个失控的后台进程。

信号产生函数

  • kill函数:功能:给指定进程发送指定信号(不一定杀死)
  • raise函数:功能:给当前进程发送指定信号(自己给自己发),等价于 kill(getpid(), sig)
  • abort函数:功能:给自己发送异常终止信号 6) SIGABRT,并产生core文件,等价于kill(getpid(), SIGABRT);
  • alarm函数:功能:设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器。
  • setitimer函数:功能:设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。

SIGCHLD信号产生的条件

  • 子进程终止时
  • 子进程接收到SIGSTOP信号停止时
  • 子进程处在停止态,接受到SIGCONT后唤醒时

如何向其他进程发送信号

(1)用Kill向进程发信号
kill -9 里面的 -9 是信号的一种,kill 命令会向进程发送一个信号,-9代表 SIGKILL 之意,用于强制终止某个进程,当然这是一种无情地,野蛮地方式干掉进程。
(2)kill函数系统调用一个进程给另一个进程发送信号
两个参数:需要接收信号的进程PID(进程id) ;需要发送给进程的信号。调用成功,kill命令返回0。
(3)通过键盘给进程发送信号
按“Ctrl + C”杀死正在运行的前台进程;

接收到的信号挂起

忽略

sigsuspend–让进程挂起,等到特定的信号(而int pause(void)等的是任意的信号)才继续执行,就是先不动再动;

signal函数相反–运行等到特定信号去运行信号处理函数,就是先动再不动。

不忽略

nohup可以使程序能够忽略挂起信号,继续运行。用户退出时会挂载,而nohup可以保证用户退出后程序继续运行。

进程组

代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。

当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID为第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID为其进程ID

可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死:

进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。

进程线程

进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;

线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。

进程与线程的区别

从以下六个方面进行讲解

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
  • 进程是资源分配的最小单位,线程是CPU调度的最小单位。
  • 共享的变量与私有的变量。同一进程的不同线程会共享进程内存空间中的全局区和堆,进程私有的是栈和寄存器。因此,局部变量都是线程私有的。全局变量、静态变量、分配于堆的变量都是共享的。
  • 通信。进程间通信是IPC。包括:管道(pipe)与命名管道(named pipe)、消息队列(message queue)、信号量(signal)、共享内存(share memory)、套接口(socket)。线程因为共享全局变量、静态变量、堆,可以直接通过这些变量通信。
  • 多进程的创建、销毁、上下文切换都比较复杂,速度慢。多线程的创建、销毁、上下文切换相对比较简单,速度快。
  • 可靠性。多进程之间相互不影响。多线程中的一个线程挂掉会导致整个进程挂掉。
  • 分布式。多进程可以多核分布式、多机器分布式。多线程只能多核分布式,不能多机器分布。

进程锁和线程锁的区别

守护进程

守护进程是脱离终端并且在后台运行的进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
守护进程的创建步骤:
1.创建子进程,父进程退出(必须)

  • 所有工作在子进程中进行形式上脱离了控制终端。

2.在子进程中创建新会话(不是必须)

  • setsid()函数
  • 使子进程完全独立出来,脱离控制

进程组:是一个或多个进程的集合。进程组有进程组ID来唯一标识。除了进程号(PID)之外,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID。且该进程组ID不会因组长进程的退出而受到影响。

会话周期:会话期是一个或多个进程组的集合。通常,一个会话开始与用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。

接下来就可以具体介绍setsid的相关内容:

setsid函数作用:
setsid函数用于创建一个新的会话,并担任该会话组的组长。调用setsid有下面的3个作用:

  • 让进程摆脱原会话的控制
  • 让进程摆脱原进程组的控制
  • 让进程摆脱原控制终端的控制

在创建守护进程时为什么要调用setsid函数呢? 由于创建守护进程的第一步调用了fork函数来创建子进程,再将父进程退出。由于在调用了fork函数时,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,还还不是真正意义上的独立开来,而setsid函数能够使进程完全独立出来,从而摆脱其他进程的控制。

3.改变当前目录为根目录(不是必须)

  • chdir()函数。chdir("\")
  • 防止占用可卸载的文件系统
  • 也可以换成其它路径

使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入用户模式)。

4.重设文件权限掩码(不是必须)
例如:掩码:002,最大文件权限:664,最大目录权限:775
文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常的使用方法为umask(0)。

5.关闭文件描述符(不是必须)

close(STDIN_FILEND) // 标准输入
close(STDOUT_FILEND) // 标准输出
close(STDERR_FIEND)  //标准错误输出

同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。

6.开始执行守护进程核心工作

在这里插入图片描述

孤儿进程与僵尸进程

孤儿进程:孤儿进程是父进程退出后它的子进程还在执行,这时候这些子进程就成为孤儿进程。孤儿进程会被init进程收养并完成状态收集。

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

僵尸进程的危害

如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。

如何避免僵尸进程

  • 最简单的方法,父进程通过 wait() 和 waitpid() 等函数等待子进程结束,但是,这会导致父进程挂起。
  • 子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。
  • fork两次,原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。

进程的创建:fork()

fork()系统调用包含两个重要的事件,一个是将 task_struct 结构复制一份并且初始化,另一个是试图唤醒新创建的子进程。

系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。


#include <sys/types.h>
#include <unistd.h>
​
pid_t fork(void);
功能:
    用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
参数:
    无
返回值:
    成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为整型。
    失败:返回-1。
    失败的两个主要原因是:
        1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。
        2)系统内存不足,这时 errno 的值被设置为 ENOMEM。

示例代码

int main()
{
   
    
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值