操作系统
从应用程序或者应用开发者的角度来看,操作系统是计算机系统的核心软件,它为应用程序提供运行环境和基础服务。
操作系统提供了以下基本服务:
- 进程管理:操作系统负责创建、管理和终止应用程序进程。
- 内存管理:操作系统负责分配和回收应用程序的内存。
- 设备管理:操作系统负责管理计算机的硬件设备,如磁盘、打印机等。
- 文件系统:操作系统负责管理计算机的文件系统。
- 网络通信:操作系统负责提供网络通信服务。
操作系统封装了以下硬件资源:
- 处理器:操作系统负责管理处理器的资源,如执行时间、内存等。
- 内存:操作系统负责管理内存的资源,如地址空间、数据存储等。
- 设备:操作系统负责管理设备的资源,如读写速度、存储容量等。
操作系统解决了以下问题:
- 资源共享:操作系统负责管理计算机的资源,使多个应用程序可以共享这些资源。
- 并发性:操作系统负责管理多个应用程序的并发执行,使它们可以同时运行。
- 异常处理:操作系统负责处理计算机的异常,如内存访问错误、硬件故障等。
操作系统为应用程序提供了以下便利:
- 应用程序可以专注于自己的业务逻辑,而不需要考虑硬件资源的管理。
- 应用程序可以并发运行,提高了计算机的利用率。
- 操作系统提供了异常处理机制,保障了应用程序的稳定性。
- 操作系统是应用程序开发的重要基础。
应用程序开发人员需要了解操作系统的基本原理和功能,才能开发出高效、稳定的应用程序,那么对于操作系统来说最核心的就是操作系统内核,从Unix,Linux,Windows以及MacOS,包括工业系统以及嵌入式的RTOS,甚至是航天器使用的VxWorks 都需要使用操作系统.
尽管不同类型的操作系统内核的设计也可能有所不同。但是所有的操作系统内核的通用设计基本包括以下几个方面:
- 进程管理:操作系统内核负责创建、管理和终止应用程序进程。进程是操作系统的基本运行单元,每个进程都有独立的地址空间、内存、寄存器等资源。操作系统内核需要对进程进行调度、同步、通信等管理。
- 内存管理:操作系统内核负责分配和回收应用程序的内存。内存是计算机系统的重要资源,操作系统内核需要对内存进行合理分配和管理,以提高内存的利用率。
- 设备管理:操作系统内核负责管理计算机的硬件设备。计算机系统中包含各种硬件设备,如磁盘、打印机、网卡等。操作系统内核需要提供对这些设备的访问接口,并负责设备的驱动和管理。
- 文件系统管理:操作系统内核负责管理计算机的文件系统。文件系统是计算机系统中存储数据的一种方式。操作系统内核需要提供对文件系统的访问接口,并负责文件的创建、删除、读写等操作。
- 网络通信管理:操作系统内核负责提供网络通信服务。网络通信是计算机系统的重要功能。操作系统内核需要提供对网络通信的接口,并负责网络协议的实现。 除了上述通用设计之外,操作系统内核还可能包含其他功能,如安全性、虚拟化、并行处理等。
内核态和用户态
为了保护操作系统内核的安全性和稳定性,操作系统通常将内核和用户程序分为两个不同的运行状态,即内核态和用户态。
- 内核态是操作系统内核运行的状态。在内核态下,操作系统内核具有最高的权限,可以直接访问计算机的所有资源。
- 用户态是用户程序运行的状态。在用户态下,用户程序的权限受到限制,只能访问特定的资源。
内核态和用户态的区分主要是为了以下两个目的:
- 安全性:内核态下,操作系统内核具有最高的权限,可以直接访问计算机的所有资源。如果用户程序在用户态下可以直接访问这些资源,可能会对计算机系统的安全性造成威胁。
- 稳定性:内核态下,操作系统内核负责管理计算机的关键资源。如果用户程序在用户态下可以直接访问这些资源,可能会导致操作系统内核的崩溃,从而影响计算机系统的稳定性。
在操作系统内核的设计中,通常会使用特权级来区分内核态和用户态。在 x86 架构下,内核态的特权级为 0,用户态的特权级为 3。当程序运行在内核态时,CPU 将切换到特权级 0。当程序运行在用户态时,CPU 将切换到特权级 3。
内核态和用户态之间的切换是由操作系统来控制的。用户程序在需要访问内核资源时,需要通过系统调用的方式向内核请求。用户进程完成一次系统调用的过程可以分为以下几个步骤:
- 用户进程通过系统调用号和参数调用系统调用函数。
- 操作系统内核将当前进程切换到内核态。
- 内核根据系统调用号调用相应的系统调用处理程序。
- 系统调用处理程序执行相应的操作,并将结果返回给用户进程。
- 操作系统内核将当前进程切换回用户态。
具体来说,用户进程可以通过调用系统调用号和参数来调用系统调用函数
- 当用户进程调用 open() 系统调用时,内核将调用 do_open() 系统调用处理程序。
- do_open() 系统调用处理程序将打开指定的文件,并将文件描述符返回给用户进程。
最后,操作系统内核将当前进程切换回用户态。用户进程可以使用文件描述符来访问打开的文件。在 x86 架构下,用户进程可以通过以下汇编代码来调用 open() 系统调用,例如:
代码语言:txt复制mov eax, 5 ; 系统调用号
mov ebx, filename ; 文件名
mov ecx, flags ; 打开方式
int 0x80 ; 调用系统调用
用户进程完成一次系统调用的过程是一个复杂的过程,涉及操作系统内核的多个模块。了解系统调用的过程可以帮助我们更好地理解操作系统内核与用户进程之间的关联关系,做出更佳的软件设计。
内核态和用户态的区别
特征 | 内核态 | 用户态 |
---|---|---|
权限 | 最高权限 | 受限权限 |
可访问资源 | 所有资源 | 操作系统提供的资源 |
代码执行 | 直接执行 | 通过系统调用执行 |
应用程序 | 操作系统内核 | 用户应用程序 |
解释
- 权限:内核态具有最高权限,可以直接访问计算机的所有资源,包括内存、硬件设备等。用户态的权限受到限制,只能访问操作系统提供的资源。
- 可访问资源:内核态可以直接访问计算机的所有资源,包括内存、硬件设备等。用户态只能访问操作系统提供的资源,如文件系统、网络通信等。
- 代码执行:内核态的代码可以直接在 CPU 上执行,无需经过用户态的检查。用户态的代码需要通过系统调用的方式向内核请求执行权限。
- 应用程序:内核态运行的是操作系统内核,用户态运行的是用户应用程序。
内存管理
计算机启动时,处于实模式。实模式下,计算机的地址空间只有 1 MB,并且所有的内存都是直接可访问的。以X86指令集的CPU为例,在实模式下,BIOS 会加载内核到内存的
内存管理
计算机启动时,处于实模式。实模式下,计算机的地址空间只有 1 MB,并且所有的内存都是直接可访问的。以X86指令集的CPU为例,在实模式下,BIOS 会加载内核到内存的 0x0000:0x7C00 处。内核是一个特殊的程序,它负责启动操作系统。
加载完毕内核后,BIOS会把对硬件的控制健全交给内核,内核会通过 lgdt 和 lidt 指令来切换到保护模式,从而完成实模式到保护模式的切换,在保护模式下,计算机的地址空间可以大于 1 MB,并且内存被分为两部分:内核空间和应用空间。
内核空间
内核空间是操作系统运行的区域,应用程序无法直接访问内核空间。内核空间中的内存由操作系统管理,应用程序无法直接分配或释放内核空间中的内存。
物理地址是内存中一个存储单元的实际地址,是内核直接访问和管理的空间
用户空间
- 虚拟地址是指进程视角看到的操作为期提供的内存地址。在保护模式下,每个进程都有自己的虚拟地址空间。虚拟地址空间是通过分页来实现的。
- 分页是将虚拟内存划分为大小相同的页面,每个页面大小通常为 4 KB 或 8 KB。每个进程都有 自己的页表,用于将虚拟地址映射到物理地址。当进程需要访问一个页面时,操作系统会首先检查该页面是否在物理内存中。如果在物理内存中,则操作系统将直接将虚拟地址转换为物理地址,并将数据返回给进程。如果不在物理内存中,则操作系统会将该页面从磁盘中加载到物理内存中。
- 交换是将进程的页面从内存中换出到磁盘上,以释放内存空间。交换通常用于以下情况: 当进程的页面数超过物理内存的大小时。 当进程的页面长时间没有被访问时。
堆和栈
堆和栈都是用户空间的内存。
- 堆是用于动态内存分配的区域,应用程序可以通过 malloc() 等 API 申请堆内存。堆内存的大小是可变的,可以随时增加或减少。应用程序必须使用 free() 等 API 来释放不再使用的堆内存。
- 栈是用于线程私有数据的区域,应用程序可以通过 push() 和 pop() 等指令操作栈。栈内存用于存储函数调用时使用的参数、返回地址和局部变量。栈内存是按 LIFO(后进先出)顺序分配和释放的。
buffer和cache
- buffer 是指内存中的一个区域,用于存储临时数据。 buffer 可以位于内核空间,用于存储操作系统使用的内存数据,例如文件数据、网络数据和设备数据。buffer 也可以位于用户空间,用于存储应用程序使用的内存数据,例如图像数据、音频数据和文本数据。buffer 是指内存中的一个区域,用于存储临时数据。
* 提高 I/O 性能:buffer 可以缓冲 I/O 操作的数据,从而减少 I/O 操作的次数,提高 I/O 性能。
* 提高 CPU 性能:buffer 可以缓冲 CPU 需要的数据,从而减少 CPU 访问内存的次数,提高 CPU 性能。cache 是指内存中的一个区域,用于存放经常访问的数据。cache 可以提高数据读取的速度,从而提高系统性能。cache 可以位于内核空间,用于存储操作系统经常访问的数据,例如文件数据和设备数据。cache 也可以位于用户空间,用于存储应用程序经常访问的数据,例如图像数据和音频数据。buffer 和 cache 都是用来提高系统性能的技术。buffer 用于存储临时数据,而 cache 用于存储经常访问的数据。buffer 可以用于各种目的,而 cache 通常用于提高数据访问的速度。
在 Linux 64 位内核中,堆和栈都位于用户空间,地址从高地址向低地址增长。堆的起始地址由操作系统分配,在应用程序运行时可以动态增长。栈的起始地址由操作系统分配,在函数调用时分配,在函数返回时释放。
也有例外情况。例如,应用程序可以通过 mmap() 系统调用将内核空间的内存映射到用户空间。在这种情况下,应用程序可以直接访问内核空间中的内存,包括 buffer 和 cache
进程,线程,协程
- 进程:进程是操作系统中资源分配的最小单位,每个进程都有自己的独立的内存空间、代码空间、数据空间、堆栈空间等。进程的特点是独立性强,但切换效率低。
- 线程:线程是进程内的一个执行实体,每个线程都有自己的堆栈空间,但共享进程的内存空间、代码空间等。线程的特点是切换效率高,但独立性弱。
- 协程:协程是轻量级的线程,每个协程都有自己的堆栈空间和局部变量,但共享进程的内存空间、代码空间等。协程的特点是切换效率高,且可以实现并发执行。
- 进程间通信:进程间通信(IPC)是指两个或多个进程之间相互发送数据或信号的过程。常见的IPC方式包括管道、消息队列、共享内存等。
- 多线程编程:多线程编程是指在一个进程中同时运行多个线程的过程。多线程编程可以提高程序的并发性和响应速度。
Linux IO
Linux IO 是 Linux 操作系统中用于处理输入输出请求的机制。Linux IO 主要包括以下几个部分:
- 设备驱动程序:设备驱动程序是用于与特定设备进行通信的软件。设备驱动程序将设备的硬件接口转换为操作系统可以理解的接口。
- 内核空间 IO 子系统:内核空间 IO 子系统是操作系统用于处理 IO 请求的核心组件。内核空间 IO 子系统负责将 IO 请求发送到设备驱动程序,并将设备驱动程序的响应返回给应用程序。
- 用户空间 IO 库:用户空间 IO 库是用于简化应用程序对 IO 的访问的软件。用户空间 IO 库将应用程序的 IO 请求转换为内核空间 IO 子系统可以理解的接口。
Linux IO 主要有以下几种模型:
- 阻塞 IO:阻塞 IO 是指应用程序在发出 IO 请求后会被阻塞,直到 IO 请求完成。阻塞 IO 模型简单易用,但会导致应用程序的响应性下降。
- 非阻塞 IO:非阻塞 IO 是指应用程序在发出 IO 请求后不会被阻塞,而是会立即返回。应用程序可以通过轮询或信号来检测 IO 请求是否完成。非阻塞 IO 模型可以提高应用程序的响应性,但需要应用程序自己来处理 IO 请求的完成情况。
- 异步 IO:异步 IO 是指应用程序在发出 IO 请求后会立即返回,IO 请求的完成由操作系统通知应用程序。异步 IO 模型可以最大限度地提高应用程序的响应性,但需要应用程序自己来处理 IO 请求的完成情况。
Linux IO 还支持以下几种优化技术:
- 缓存:缓存可以提高 IO 的性能。操作系统可以将经常访问的数据缓存在内存中,从而减少对设备的访问次数。
- 预读:预读可以提高 IO 的性能。操作系统可以预先读取应用程序可能需要的数据,从而减少应用程序的等待时间。
- 集合 IO:集合 IO 可以提高 IO 的性能。操作系统可以将多个 IO 请求合并成一个请求,从而减少对设备的访问次数。
从应用开发的视角,Linux IO 库可以分为以下几类:
- 文件 IO 库:用于操作文件的库,例如 open()、read()、write()、close() 等系统调用。
- 网络 IO 库:用于操作网络的库,例如 socket()、connect()、send()、recv() 等系统调用。
- 设备 IO 库:用于操作设备的库,例如 ioctl()、mmap()、munmap() 等系统调用。
以下是一些常用的 Linux IO 库:
- 标准 C 库:标准 C 库提供了基本的 IO 操作,例如 open()、read()、write()、close() 等系统调用。
- POSIX 库:POSIX 库提供了更高级的 IO 操作,例如 fopen()、fread()、fwrite()、fclose() 等系统调用。
- GNU 库:GNU 库提供了更强大的 IO 操作,例如 gopen()、gread()、gwrite()、gclose() 等系统调用。
- 第三方库:第三方库提供了各种各样的 IO 操作,例如 libcurl()、libuv()、libevent() 等库。
以 C、 Python 、 Go 、 Rust 、 JavaScript 为例
类别 | C | Python | Go | Rust | JavaScript |
---|---|---|---|---|---|
进程 | fork | multiprocessing / multiprocessing.Process() | os / os.Fork() | std::process::Fork() | child_process / child_process.fork() |
线程 | pthread_create | threading / threading.Thread() | sync / sync.NewThread() | std::thread::spawn() | threads / threads.spawn() |
协程 | goroutine | asyncio / asyncio.get_event_loop().run_until_complete() | go / go func() | std::future::spawn() | async / async function() |
消息 | queue | queue / queue.Queue() | sync / sync.NewWaitGroup() | std::sync::mpsc::channel() | queue / queue() |
共享内存 | shmget | multiprocessing / multiprocessing.shared_memory.SharedMemory() | sync / sync.Map() | std::sync::mpsc::shared_queue() | shared / shared() |
管道 | pipe | multiprocessing / multiprocessing.Pipe() | os / os.Pipe() | std::os::unix::net::pipe() | stream / stream() |
同步IO | read | io / io.open() | io / io.Read() | std::io::Read() | fs / fs.readFile() |
异步IO | asyncio.open | asyncio / asyncio.open() | io / io.Read() | std::io::Read() | fs / fs.readFile() |
多路复用 | select | select | select / std::net::poll() / net / net.select() | select / select / epoll.epoll() / std::net::epoll() / net / net.epoll() | |
高性能多路复用 | epoll / epoll / epoll / std::net::epoll() / net / net.epoll() | epoll / epoll / epoll / std::net::epoll() / net / net.epoll() |
说明
- 表格中标记为 stdlib.h 的表示标准库头文件,需要包含到程序中。
- 表格中标记为 import 的表示第三方库,需要先安装。
- 表格中标记为 typing 的表示 Python 的类型注解,可以不用。
附加说明
select()
和epoll()
是两种不同的多路复用方式,select()
是传统的多路复用方式,效率较低;epoll()
是高性能的多路复用方式,效率较高。- 表格中
select()
和epoll()
的语法分别使用了两种展示方式,一种是使用:
来分隔参数,另一种是使用/
来分隔参数。这两种方式都是正确的,可以根据自己的喜好选择
开发调试命令工具参考
以服务端应用运行的Linux系统为背景,对于大多数应用开发者来说,了解 Linux 命令的目的是为了更好地利用 Linux 系统,提高开发效率和应用程序的质量。掌握 Linux 命令并不意味着要成为操作系统开发者。操作系统开发是一项复杂且具有挑战性的任务,需要扎实的计算机科学基础和丰富的经验。
应用开发者可以利用 Linux 命令提高开发效率和应用程序质量的例子:
- 使用 ps 命令查看系统中正在运行的进程,帮助应用开发者识别和解决应用程序性能问题。
- 使用 gdb 命令调试应用程序,帮助应用开发者定位和解决应用程序中的错误。
- 使用 perf 命令分析应用程序的性能,帮助应用开发者优化应用程序的性能。
对于这些应用,了解 Linux 命令的基本使用方法即可满足需求。如果应用开发者需要更深入地了解 Linux 系统,可以考虑进一步学习操作系统相关的知识。掌握 Linux 命令对于应用开发者来说是必不可少的,但并不意味着要成为操作系统开发者。
应用开发者需要比使用者更加了解操作系统,熟悉操作系统带来的收益 :
- 提高开发效率:Linux 命令可以帮助应用开发者快速完成日常任务,如部署应用程序、调试应用程序、分析应用程序性能等。
- 提高应用程序质量:Linux 命令可以帮助应用开发者更好地了解和管理系统资源,从而避免应用程序出现性能问题和安全问题。
- 扩展开发技能:掌握 Linux 命令可以帮助应用开发者更深入地了解操作系统,从而为未来的职业发展打下基础。
以下是 Linux CLI 程序中用于查看进程、内存、文件系统、设备以及开发调试应用使用的命令:
- 查看进程
- ps 命令可以列出所有进程。
- top 命令可以实时显示系统的运行状态,包括进程列表。
- htop 命令是 top 命令的增强版,提供更丰富的功能。
- dstat 命令可以显示系统的 CPU、内存、IO 等使用情况。
- 查看内存
- free 命令可以显示系统的内存使用情况。
- vmstat 命令可以显示系统的虚拟内存使用情况。
- 查看文件系统
- ls 命令可以列出目录中的文件和子目录。
- du 命令可以显示文件或目录的大小。
- df 命令可以显示文件系统的使用情况。
- 查看设备
- lsblk 命令可以列出所有块设备。
- lspci 命令可以列出所有 PCI 设备。
- 查看 IO
- fio 命令可以用于生成 IO 负载。
- iostat 命令可以显示系统的 IO 使用情况。
- iotop 命令是 iostat 命令的增强版,提供更丰富的功能。
- vmstat 命令可以显示系统的虚拟内存使用情况,其中包括 IO 使用情况。
- 查看网络
- netstat 命令可以显示系统的网络连接情况。
- ip/ifconfig 命令可以显示系统的网络接口信息。
- tcpdump 命令可以捕获网络数据包。
- mtr 命令可以用于ping 多个主机,并显示网络延迟和丢包情况。
- traceroute 命令可以用于跟踪网络路由。
- 开发过程调试分析应用性能
- gdb 命令可以调试 C/C 程序。
- valgrind 命令可以检测内存泄漏和其他错误。
- perf 命令可以分析应用程序的性能。
- perf record 命令可以记录应用程序的性能数据。
- perf report 命令可以分析应用程序的性能数据。
- gdbserver 命令可以将 C/C 程序转换为远程调试程序。
以下是一些具体的例子:
- 要查看当前正在运行的所有进程,可以使用 ps 命令: ps -ef
- 要查看系统的内存使用情况,可以使用 free 命令:free
- 要查看某个文件或目录的大小,可以使用 du 命令:du -h /path/to/file
- 要查看系统的文件系统使用情况,可以使用 df 命令:df -h
- 要查看某个设备的信息,可以使用 lsblk 命令:lsblk -l /dev/sda
- 要调试 C/C 程序,可以使用 gdb 命令:gdb /path/to/program
- 要检测内存泄漏,可以使用 valgrind 命令:valgrind ./program
- 要分析应用程序的性能,可以使用 perf 命令: perf record -e cycles ./program
- 要查看系统的 IO 使用情况,可以使用 iostat 命令:iostat -d
- 要查看某个进程的 IO 使用情况,可以使用 iotop 命令:iotop -p 1234
- 要查看系统的网络连接情况,可以使用 netstat 命令:netstat -an
- 要查看某个网络接口的信息,可以使用 ifconfig 命令:ifconfig eth0
- 要捕获网络数据包,可以使用 tcpdump 命令:tcpdump -i eth0