0
点赞
收藏
分享

微信扫一扫

你不可不知的Linux知识总结(一)

文章目录


前言

UNIX 是一个交互式系统,用于同时处理多进程和多用户同时在线。为什么要说 UNIX,那是因为 Linux 是由 UNIX 发展而来的,UNIX 是由程序员设计,它的主要服务对象也是程序员。Linux 继承了 UNIX 的设计目标。从智能手机到汽车,超级计算机和家用电器,从家用台式机到企业服务器,Linux 操作系统无处不在。

一、Linux 系统设计的简单性

不能想要实现顺序存取、随机存取、按键存取、远程存取就单独再设计一个文件系统,像大型机一样设计。

如前言所说,Linux 是一个继承了 UNIX 的设计目标的操作系统。同时,大多数程序员都喜欢让系统尽量简单,优雅并具有一致性。举个例子,从最底层的角度来讲,一个文件应该只是一个字节集合。为了实现顺序存取、随机存取、按键存取、远程存取只能是妨碍你的工作。相同的,如果命令ls A*意味着只列出以 A 为开头的所有文件,那么命令rm A*会移除所有以 A 为开头的文件而不是只删除文件名是 A* 的文件。这个特性也是最小吃惊原则(principle of least surprise)意思是在设计中该功能或者特征应该符合用户的预期,不应该使用户感到惊讶和震惊。设计 Linux 的一个基本目标是每个应用程序只做一件事情并把他做好。所以编译器只负责编译的工作,编译器不会产生列表,因为有其他应用比编译器做的更好。

二、Linux简介

1.Linux接口

Linux是一种金字塔模式的系统,如下图所示:
请添加图片描述
Linux 具有三种不同的接口:
系统调用接口(linux操作系统,如进程管理、内存管理、文件系统、IO等)
库函数接口(标准库,open、close、read、却也、fork等)
应用程序接口(标准实用程序,如shell、编译器、编辑器等)
Note:trap指令切换用户态至内核态

2.Linux 组成部分

Linux 操作系统可以由下面这几部分构成:

引导程序(Bootloader):引导程序是管理计算机启动过程的软件,对于大多数用户而言,只是弹出一个屏幕,但其实内部操作系统做了很多事情
内核(Kernel):内核是操作系统的核心,负责管理 CPU、内存和外围设备等。
初始化系统(Init System):操作系统的实模式转向保护模式,就像硬件在启动的时候要进行初始化,一样的道理,软件要驾驭硬件,就像飞机起飞一样要检查哥哥模块功能是否正常
后台进程(Daemon):顾名思义就是在后台运行的程序,比如打印、声音、调度等,它们可以在引导过程中启动,也可以在登录桌面后启动
图形服务器(Graphical server):这是在监视器上显示图形的子系统。通常将其称为 X 服务器或 X。
桌面环境(Desktop environment):这是用户与之实际交互的部分,有很多桌面环境可供选择,每个桌面环境都包含内置应用程序,比如文件管理器、Web 浏览器、游戏等
应用程序(Applications):桌面环境不提供完整的应用程序,但就像 Windows 和 macOS 一样,Linux 提供了成千上万个可以轻松找到并安装的高质量软件。

3.Shell (command-line interface)

尽管 Linux 应用程序提供了 GUI ,但是大部分程序员仍偏好于使用命令行(command-line interface),称为shell。通常在 GUI 中启动一个 shell 窗口然后就在 shell 窗口下进行工作。
下面会介绍一些最简单的 bash shell。当 shell 启动时,它首先进行初始化,在屏幕上输出一个 提示符(prompt),通常是一个%或者$,等待用户输入
等用户输入一个命令后,shell 提取其中的第一个词,这里的词指的是被空格或制表符分隔开的一连串字符。假定这个词是将要运行程序的程序名,那么就会搜索这个程序,如果找到了这个程序就会运行它。然后 shell 会将自己挂起直到程序运行完毕,之后再尝试读入下一条指令。shell 也是一个普通的用户程序。它的主要功能就是读取用户的输入和显示计算的输出。shell 命令中可以包含参数,它们作为字符串传递给所调用的程序。比如 ls *.c,其中*可以匹配一个或多个可能的字符串,被称为通配符*(wild cards)*

4.Linux应用程序

Linux 的命令行也就是 shell,它由大量标准应用程序组成。这些应用程序主要有下面六种:

  • 文件和目录操作命令
  • 过滤器
  • 文本程序
  • 系统管理
  • 程序开发工具,例如编辑器和编译器
  • 其他
    除了这些标准应用程序外,还有其他应用程序比如 Web 浏览器、多媒体播放器、图片浏览器、办公软件和游戏程序等
    下面列出了 POSIX 的一些标准应用程序:
    在这里插入图片描述

5.Linux内核结构

在上面我们看到了 Linux 的整体结构,下面我们从整体的角度来看一下 Linux 的内核结构:
在这里插入图片描述
内核直接坐落在硬件上,内核的主要作用就是 I/O 交互、内存管理和控制 CPU 访问。上图中还包括了 中断调度器,中断是与设备交互的主要方式,中断出现时调度器就会发挥作用。进程调度也会发生在内核完成一些操作并且启动用户进程的时候,在该图中的调度器是 dispatcher。

我们把内核系统分为三部分:

  • I/O 部分负责与设备进行交互以及执行网络和存储 I/O 操作的所有内核部分
  • I/O 右边的是内存部件,程序被装载进内存,由 CPU 执行,这里会涉及到虚拟内存的部件,页面的换入和换出是如何进行的,坏页面的替换和经常使用的页面会进行缓存
  • 进程模块负责进程的创建和终止、进程的调度、Linux 把进程和线程看作是可运行的实体,并使用统一的调度策略来进行调度

在内核最顶层的是系统调用接口,所有的系统调用都是经过这里,系统调用会触发一个 trap,将系统从用户态转换为内核态,然后将控制权移交给上面的内核部件。

三、Linux进程管理

1.Linux内核结构

首先,我们都知道进程是操作系统资源调度的基本单位,线程是任务的调度执行的基本单位。在Linux中每个进程都会运行一段独立的程序,并且在初始化的时候拥有一个独立的控制线程,换句话说,每个进程都会有一个自己的程序计数器,这个程序计数器用来记录下一个需要被执行的指令。Linux 允许进程在运行时创建额外的线程。
请添加图片描述
Linux是一个多道程序设计系统,因此系统中存在彼此相互独立的进程同时运行,其中有一种特殊的守护进程被称为 计划守护进程(Cron daemon) ,计划守护进程可以每分钟醒来一次检查是否有工作要做,做完会继续回到睡眠状态等待下一次唤醒。

在 Linux 系统中,进程通过非常简单的方式来创建,fork 系统调用会创建一个源进程的拷贝(副本),调用 fork 函数的进程被称为父进程(parent process),使用 fork 函数创建出来的进程被称为子进程(child process)。当子进程结束运行时,父进程会得到子进程的 PID,因为一个进程会 fork 很多子进程,子进程也会 fork 子进程,所以 PID 是非常重要的。我们把第一次调用 fork 后的进程称为 原始进程,一个原始进程可以生成一颗继承树。
请添加图片描述

2.Linux进程间通信

Linux 进程间的通信机制通常被称为 Inter-Process communication,IPC 下面我们来说一说 Linux 进程间通信的机制,大致来说,Linux 进程间的通信机制可以分为 6 种:
请添加图片描述
接下来我们就一一介绍这六种通信机制,

1. 信号 singnal

Linux 支持信号机制,通过向一个或多个进程发送异步事件信号来实现,信号可以从键盘或者访问不存在的位置等地方产生,信号通过 shell 将任务发送给子进程。
你可以在 Linux 系统上输入 kill - 1 来列出系统使用的信号,如下(来自leetcode):
请添加图片描述
进程可以选择忽略发送过来的信号,但是有两个是不能忽略的:SIGSTOPSIGKILL 信号。SIGSTOP 信号会通知当前正在运行的进程执行关闭操作,SIGKILL 信号会通知当前进程应该被杀死,其他信号就不一一介绍了,感兴趣可以自行了解。

2. 管道 pipe

在两个进程之间,可以建立一个通道,一个进程向这个通道里写入字节流,另一个进程从这个管道中读取字节流。管道是同步的,当进程尝试从空管道读取数据时,该进程会被阻塞,直到有可用数据为止。shell 中的 管线 pipelines 就是用管道实现的,当 shell 发现输出sort <f | head它会创建两个进程,一个是 sort,一个是 head,sort,会在这两个应用程序之间建立一个管道使得 sort 进程的标准输出作为 head 程序的标准输入。sort 进程产生的输出就不用写到文件中了,如果管道满了系统会停止 sort 以等待 head 读出数据,如下图:
请添加图片描述
管道实际上就是 |,两个应用程序不知道有管道的存在,一切都是由 shell 管理和控制的。

3. 共享内存 shared memory

两个进程之间还可以通过共享内存进行进程间通信,其中两个或者多个进程可以访问公共内存空间。两个进程的共享工作是通过共享内存完成的,一个进程所作的修改可以对另一个进程可见(很像线程间的通信)。
请添加图片描述
在使用共享内存前,需要经过一系列的调用流程,流程如下

  • 创建共享内存段或者使用已创建的共享内存段(shmget())
  • 将进程附加到已经创建的内存段中(shmat())
  • 从已连接的共享内存段分离进程(shmdt())
  • 对共享内存段执行控制操作(shmctl())

4. 先入先出队列 FIFO

先入先出队列 FIFO 通常被称为 命名管道(Named Pipes),命名管道的工作方式与常规管道非常相似,但是确实有一些明显的区别。未命名的管道没有备份文件:操作系统负责维护内存中的缓冲区,用来将字节从写入器传输到读取器。一旦写入或者输出终止的话,缓冲区将被回收,传输的数据会丢失。相比之下,命名管道具有支持文件独特 API ,命名管道在文件系统中作为设备的专用文件存在。当所有的进程通信完成后,命名管道将保留在文件系统中以备后用。命名管道具有严格的 FIFO 行为请添加图片描述
如图,写入的第一个字节是读取的第一个字节,写入的第二个字节是读取的第二个字节,依此类推。

5. 消息队列 message Queue

消息队列是用来描述内核寻址空间内的内部链接列表。可以按几种不同的方式将消息按顺序发送到队列并从队列中检索消息。每个消息队列由 IPC 标识符唯一标识。消息队列有两种模式,一种是 严格模式, 严格模式就像是 FIFO 先入先出队列似的,消息顺序发送,顺序读取。还有一种模式是 非严格模式,消息的顺序性不是非常重要。

6. 套接字 socket

还有一种管理两个进程间通信的是使用 socket,socket 提供端到端的双相通信。一个套接字可以与一个或多个进程关联。就像管道有命令管道和未命名管道一样,套接字也有两种模式,套接字一般用于两个进程之间的网络通信,网络套接字需要来自诸如 TCP(传输控制协议) 或较低级别 UDP(用户数据报协议) 等基础协议的支持。
套接字有以下几种分类:

  • 数据报套接字(SOCK_DGRAM):传输层基于udp协议,不需要创建并维护一个稳定的连接,数据报套接字所占用的计算机和系统资源较小。
  • 流式套接字(SOCK_STREAM):传输层基于tcp协议,可以在任何使用TCP/IP协议的网络上运行,提供双向可靠的数据流。
  • 底层套接字(STOCK_RAW):主要用于一些协议的开发,可以进行比较底层的操作。

7. Linux 中进程管理系统调用

我们常说的上下文切换指的就是内核态模式和用户态模式的频繁切换。而系统调用指的就是引起内核态和用户态切换的一种方式,系统调用通常在后台静默运行,表示计算机程序向其操作系统内核请求服务。下面是一些与进程管理相关的最主要的系统调用:

  • fork
    fork 调用用于创建一个与父进程相同的子进程,创建完进程后的子进程拥有和父进程一样的程序计数器、相同的 CPU 寄存器、相同的打开文件。
  • exec
    调用 exec 后,会将旧文件或程序替换为新文件或执行,然后执行文件或程序。新的执行程序被加载到相同的执行空间中,因此进程的 PID 不会修改,因为我们没有创建新进程,只是替换旧进程。但是进程的数据、代码、堆栈都已经被修改。如果当前要被替换的进程包含多个线程,那么所有的线程将被终止,新的进程映像被加载执行。
    **Note:什么是进程映像(process image)呢?**进程映像是执行程序时所需要的可执行文件,通常会包括代码段(code_segment),数据段(data_segment),bss段(bass_segment),栈(stack),堆(heap),下面是这些区域的构成图:请添加图片描述
  • waitpid
    等待子进程结束或终止
  • exit
    在许多计算机操作系统上,计算机进程的终止是通过执行 exit 系统调用命令执行的。0 表示进程能够正常结束,其他值表示进程以非正常的行为结束。

其他一些常见的系统调用如下:
在这里插入图片描述

2.Linux进程间通信

1. Linux 进程

首先,在 Linux 系统中,进程和线程几乎没有区别。对于操作系统,进程就是一个数据结构,我们直接来看Linux的源码:

struct task_struct {
    // 进程状态
    long              state;
    // 虚拟内存结构体
    struct mm_struct  *mm;
    // 进程号
    pid_t             pid;
    // 指向父进程的指针
    struct task_struct __rcu  *parent;
    // 子进程列表
    struct list_head        children;
    // 存放文件系统信息的指针
    struct fs_struct        *fs;
    // 一个数组,包含该进程打开的文件指针
    struct files_struct     *files;
};

task_struct就是 Linux 内核对于一个进程的描述,也可以称为「进程描述符」。
其中比较有意思的是mm指针和files指针。mm指向的是进程的虚拟内存,也就是载入资源和可执行文件的地方;files指针指向一个数组,这个数组里装着所有该进程打开的文件的指针。
对于文件描述符,先说files,它是一个文件指针数组。一般来说,一个进程会从files[0]读取输入,将输出写入files[1],将错误信息写入files[2]。因此每个进程被创建时,files的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。我们常说的「文件描述符」就是指这个文件指针数组的索引,所以程序的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。如下图:
请添加图片描述
对于一般的计算机,输入流是键盘,输出流是显示器,错误流也是显示器,所以现在这个进程和内核连了三根线。因为硬件都是由内核管理的,我们的进程需要通过「系统调用」让内核进程访问硬件资源。
如果我们写的程序需要其他资源,比如打开一个文件进行读写,进行系统调用,让内核把文件打开,这个文件就会被放到files的第 4 个位置
请添加图片描述
明白了这个原理,输入重定向就很好理解了,程序想读取数据的时候就会去files[0]读取,所以我们只要把files[0]指向一个文件,那么程序就会从这个文件中读取数据,而不是从键盘:
请添加图片描述
同理,输出重定向就是把files[1]指向一个文件,那么程序的输出就不会写入到显示器,而是写入到这个文件中:
请添加图片描述
错误重定向也是一样的,就不再赘述。

管道符其实也是异曲同工,把一个进程的输出流和另一个进程的输入流接起一条「管道」,数据就在其中传递:

$ cmd1 | cmd2 | cmd3

请添加图片描述

到这里,你可能也看出「Linux 中一切皆文件」设计思路的高明了,不管是设备、另一个进程、socket 套接字还是真正的文件,全部都可以读写,统一装进一个简单的files数组,进程通过简单的文件描述符访问相应资源,具体细节交于操作系统,有效解耦,优美高效。

2. Linux 线程

为什么说 Linux 中线程和进程基本没有区别呢,因为从 Linux 内核的角度来看,并没有把线程和进程区别对待。我们知道系统调用fork()可以新建一个子进程,函数pthread()可以新建一个线程。但无论线程还是进程,都是用task_struct结构表示的,唯一的区别就是共享的数据区域不同,换句话说,线程看起来跟进程没有区别,只是线程的某些数据区域和其父进程是共享的,而子进程是拷贝副本,而不是共享。就比如说,mm结构和files结构在线程中都是共享的,如图:
请添加图片描述
请添加图片描述
所以说,我们的多线程程序要利用锁机制,避免多个线程同时往同一区域写入数据,否则可能造成数据错乱。
那么既然进程和线程差不多,而且多进程数据不共享,即不存在数据错乱的问题,为什么多线程的使用比多进程普遍得多呢?
---------因为现实中数据共享的并发更普遍呀,比如十个人同时从一个账户取十元,我们希望的是这个共享账户的余额正确减少一百元,而不是希望每人获得一个账户的拷贝,每个拷贝账户减少十元。
当然,必须要说明的是,只有 Linux 系统将线程看做共享数据的进程,不对其做特殊看待,其他的很多操作系统是对线程和进程区别对待的,线程有其特有的数据结构,增加了系统的复杂度。在 Linux 中新建线程和进程的效率都是很高的,对于新建进程时内存区域拷贝的问题,Linux 采用了 copy-on-write 的策略优化,也就是并不真正复制父进程的内存空间,而是等到需要写操作时才去复制。所以Linux 中新建进程和新建线程都是很迅速的。

以上就是今天要讲的内容,本文仅仅简单介绍了Linux的基本概念,后续会继续更新补充有关于内存管理,I/O管理以及Linux文件系统,文章内容参考来自Leetcode和知乎,感谢所有作者的支持!!

举报

相关推荐

0 条评论