-
大家好!我是大聪明-PLUS!有一天,我上班时接到一个任务,需要在单板电脑上构建并运行 Linux。作为一名微控制器软件开发人员,我有点不知所措——这个任务显然不能通过安装 IDE 并点击“构建项目”来解决。谷歌帮我找到了一个叫做 Buildroot 的工具。相关资料让整个过程看起来非常简单:下载、配置、运行几个命令,将结果上传到单板电脑,就可以开始了!所以,这个过程不会比在普通电脑上安装 Linux 或 Windows 发行版复杂多少?当然不会。网上有很多信息,但都很零散,而且大多是英文的。要了解各个功能以及在哪里配置,需要翻阅大量的资料。而且,你很难保证所有东西都能在脑子里拼凑起来。因此,在经历了漫长而艰辛的知识积累、系统化和实践应用的旅程后,我决定撰写这一系列文章。在本系列文章中,我将尽可能清晰、透彻地解释嵌入式 Linux 以及 Buildroot 的具体内容。我将描述一个基于嵌入式 Linux (EL)的通用片上系统(SoC)场景,避免涉及具体芯片的具体细节。由于这些文章面向的是 Linux 初学者,我事先并不了解他们的 Linux 水平,因此我可能会对某些要点进行过于详细的解释。 1. 简介 1.1 什么是片上系统?简单来说,SoC就是包含处理器、接口和硬件组件的微芯片。熟悉微控制器的人自然会问:“那有什么区别?微控制器不是一回事吗?”事实上,任何微控制器都具备上述所有特性。但主要区别不在于硬件,而在于编程方法和开发人员使用的抽象层次。为了理解微控制器和SoC的区别,让我们比较一下创建嵌入式解决方案的三种主要方法——从最简单到最复杂:无操作系统的微控制器这是最便宜、最简单的解决方案,非常适合高度专业化的任务,例如控制伺服驱动器或从传感器读取数据。这些系统的特点是功耗极低、响应速度快。这类微控制器的编程几乎总是低级的:直接访问寄存器,即使有封装器,也只需要极少的封装。有时,你唯一可用的就是.h描述寄存器的头文件和数据手册。每个特定的微控制器型号都有自己的一套外设和寄存器,因此代码通常高度依赖于平台。这里的一个例外是,例如 STM 的 HAL,它允许在 STM32 系列内从一个芯片到另一个芯片进行相对轻松的代码传输。带 RTOS 的微控制器下一步是使用实时操作系统 (RTOS),例如 FreeRTOS 或 SYS-BIOS。这将引入多任务处理(不要与多线程混淆)、驱动程序和硬件抽象。这样的系统可以以更少的开销执行“繁重”的任务,因为操作系统的存在允许开发更复杂、更灵活的代码。例如,与传感器的交互可以分解为由任务调度程序管理的不同任务。代码不再局限于特定平台——只要 RTOS 支持,大部分代码都可以在不同的微控制器上复用。然而,局限性仍然存在:资源仍然有限,通常内存空间有限,而且没有完善的文件系统。片上系统在这个层面上,真正的“成人”生活开始了:多核处理器、兆字节的 RAM、千兆字节的存储空间、硬件视频处理模块、各种接口(如 HDMI、蓝牙和 USB)——所有这些都集成在一个芯片中。从外部来看,SoC 可能看起来只是一个“强大的微控制器”,但实际上它已经是一台可以安装嵌入式 Linux 的功能齐全的计算机。从这一点开始,开发人员不再直接使用寄存器,而是通过 Linux 内核、驱动程序、DeviceTree 文件和大量工具(从编译器到包管理器)进行操作。尤其重要的是,代码尽可能地可移植。任何与特定芯片不直接相关的功能(例如 GPIO 控制)都可以在不同的 SoC 之间保持一致。硬件差异在 Linux 内核和设备树层面被抽象化。不可移植的部分通常是制造商的 SDK——一组用于处理特定 SoC 的独特硬件块的特定库。需要澄清的是,我们讨论的是专为运行成熟操作系统而设计的 SoC。当然也有一些更简单的 SoC,例如 ESP32,其本质更接近微控制器,并且通常无需 Linux 即可运行。因此,SoC 不仅仅是一个“高级微控制器”,而是一个完全不同架构的设备类别。它能够解决需要资源、多线程、成熟操作系统和复杂数据处理的任务。主要区别在于与硬件“捆绑”的软件级别。如果微控制器是程序员直接控制硬件的设备,那么 SoC 就是程序员通过成熟的操作系统和驱动程序模型工作的平台。 1.2 什么是嵌入式Linux?EL 不是一个特定的发行版,甚至不是一个产品。它是一个完整的概念:使用 Linux 内核及其相关的嵌入式软件堆栈来管理除传统计算机之外的设备的想法。通常 EL 包括:装载机Linux内核根文件系统库、服务最终结果是一个独立的操作系统,但它是专门针对特定设备的需求而定制的——无论是路由器、工业控制器,还是智能扬声器。一个合乎逻辑的问题出现了:如果可以使用像 Debian 或 Ubuntu 这样的现成发行版,为什么还要手动构建呢?以下是主要区别:硬件平台支持并非所有 SoC 都提供现成的 Linux 镜像。即使你找到了兼容的镜像,也并不意味着它能开箱即用或与你的硬件完美兼容。硬件功能可能不会被披露许多 SoC 都有特定的硬件组件:视频加速器、神经网络协处理器、硬件加密模块等等。在标准发行版中,这些功能通常根本不启用或根本不受支持。冗余和资源强度桌面发行版包含大量的后台服务、通用驱动程序和库。所有这些都会消耗资源——内存、CPU 时间和电量。然而,嵌入式解决方案的资源几乎总是有限的。EL 允许您删除所有不必要的组件,只留下真正必要的组件。因此,嵌入式 Linux 与 Linux 相同,但并非通用发行版,而是为特定任务组装的套件。它是一个一切都在掌控之中的系统:从内核到最后的启动脚本。它专为特定设备打造,运行高效、稳定,并且完全按照设计目标运行——没有任何额外功能。然而,这并不意味着所有 EL 组件都与传统 PC 中使用的组件完全相同。例如,在典型系统中,启动是通过 BIOS 和 GRUB 进行的,而在 SoC 中,启动是通过一组专门的引导加载程序进行的。因此,为了了解这种系统的结构以及其中包含的组件,让我们先来了解一下它的组件以及启动过程的工作原理。 2. 嵌入式Linux的内部结构是怎样的? 2.1. 开机到登录的启动流程也许我会延续比较的传统——这样差异就会变得特别明显。 2.1.1. PC启动流程让我们从许多人都熟悉的 PC 启动过程开始:通电后,基本固件(BIOS,现代平台上称为 UEFI)会启动。它会检查关键组件(例如处理器、RAM 等)是否存在以及功能是否正常。接下来,它会轮询连接到主板的设备。如果在其中一个设备上检测到引导扇区,则该设备会被添加到引导队列中。BIOS(或 UEFI)将引导扇区(例如 MBR)从第一个合适的设备复制到 RAM 中,并将控制权移交给该扇区中包含的代码。这段代码称为第一阶段引导加载程序。该引导加载程序知道其延续位置(通常在/boot),并加载主引导加载程序 — 通常是 GRUB。GRUB 显示一个菜单(如果指定)并将控制权转移到配置中指定的 Linux 内核。内核挂载启动参数中指定的根文件系统(RootFS),并启动init进程。从此时直到登录屏幕出现为止发生的一切都取决于具体的分布和设置:它可能涉及设置网络、启动图形环境和其他任务。 2.1.2. SoC启动过程现在我们来看一下基于SoC的嵌入式系统的启动:通电后,主程序加载器 (PPL)(位于 ROM 中的引导加载程序)启动。它会初始化基本外设,例如静态 RAM (SRAM),并开始搜索下一阶段——辅助程序加载器 (SPL)。如果找到,则将其代码上传到 SRAM,然后 PPL 将控制权移交给它。第一阶段的引导加载程序没有特定的术语。它被称为引导 ROM、ROM 代码、内部引导加载程序和 PPL。不过,从现在开始我将使用 PPL 这个术语。SPL 会初始化更复杂的外设,尤其是 RAM。然后,它会搜索下一阶段——第三程序加载器 (TPL),或者更确切地说,是内核。找到的代码会被加载到 RAM 中,并将控制权移交给内核。TPL(如果存在)会执行额外的硬件初始化并搜索内核。一旦找到,它会将其加载到 RAM 中并转移控制权。内核根据设备树重新配置外设,挂载 RootFS,并启动 Init 进程。与标准 PC 的其他区别在于 RootFS 和 Init 进程中具体包含的内容。需要注意的是,每个阶段并不会触发下一个阶段,而是会转移控制权。无法回退——你只能重启整个设备,重新开始。 2.1.3 相似之处和主要区别乍一看,加载过程很相似,而且似乎同样令人困惑。某种程度上来说,确实如此。但是!在 PC 中,微控制器负责处理许多任务:例如,内存和总线会自动初始化——BIOS 仅负责协调它们的运行。每个系统组件都是独立的,并包含各自的固件。在这种情况下,启动操作系统更多的是组织独立组件之间的交互,而不是手动启动每个组件。SoC 的情况则有所不同。它是一种单片芯片,没有单独的控制器或不必要的空间——一切都必须尽可能紧凑、可靠且直观。唤醒系统的任务完全落在 SoC 的肩上。因此需要级联的引导加载程序,每个程序都执行各自的任务。PPL是 ROM 中最小、高度可靠且不可变的代码。它仅执行基本初始化并启动下一阶段。其紧凑的尺寸使其易于验证且可靠。损坏 PPL 意味着摧毁整个系统:芯片将变砖。SPL 的功能略有增加:特别是它可以初始化 RAM。然而,SRAM 太小,无法容纳复杂的逻辑,因此 SPL 通常仅作为下一步的桥梁。TPL已经在完整的 RAM 中运行,并且可以提供丰富的脚本:例如,在哪里加载操作系统、如何显示启动过程、如何响应连接的设备等。这种架构不会使事情变得复杂——它能提供保护。如果 SPL 或 TPL 级别发生故障,可以拦截启动过程,从备份驱动器启动,并恢复系统。关键在于预见这种可能性。然而,PPL 发生故障则意味着设备完全失效。因此,它是不可改变的,并且极其简单。 2.2. 嵌入式Linux组成现在,我们已经确定了要使用的内容,让我们仔细看看每个组件。 2.2.1. 引导加载程序U-Boot 是一款功能强大且用途广泛的开源引导程序,常用于在 SoC 上加载操作系统。我们将要使用的就是 U-Boot。它的优势如下:支持多种架构和SoC平台允许您从各种来源启动内核和 RootFS:USB、SD 卡、SATA 驱动器、网络、NAND/NOR 内存可以运行脚本:例如,先尝试从 USB 启动,然后从 eMMC 启动,最后才从网络启动支持交互模式(通过控制台)和计时器自动加载它可以通过环境变量进行灵活的配置。您可以从官方存储库下载 U-Boot 。如果 SPL 不适合 SRAM,它可以被“拆分”成多个部分 - 有关这方面的更多信息请参阅 SoC 加载部分 2.2.2. 内核这就是 Linux 本身——控制硬件、抽象资源并确保程序之间交互的内核。您可以从官方存储库或针对特定 SoC 定制的分支下载内核。 2.2.3. 根文件系统RootFS 不仅仅是目录的集合。它是一个完整的环境,包括:实用程序,例如 init、sh、ifconfig配置文件运行程序的依赖项与内核和引导加载程序不同,RootFS 不是以二进制文件的形式构建的,而是以目录结构的形式创建的。您可以手动创建 RootFS(例如,从主机系统复制必要的二进制文件和库),但这需要谨慎操作并透彻理解系统结构。 2.3. 嵌入式 Linux 构建工具和辅助工具至此,我们已经介绍了“代表”。有些人甚至可能已经开始查看源代码了。但我不建议你着急,因为我们还需要掌握构建工具和媒体。 2.3.1. 工具链工具链是一组特定于目标架构的编译器、头文件和实用程序,即:交叉编译器 gcc、g++、ld、as — 构建代码所需的一切Linux 内核头文件库和头文件 libc、libstdc++、libm 和其他系统库调试实用程序、分析器、系统助手工具链的主要要求是符合主机(编译发生的地方)和目标(代码运行的地方)系统的架构。您可以下载现成的解决方案,也可以自行构建(例如,使用 CrossTool-NG)。后者值得另写一篇系列文章来阐述,因此我们假设只能使用现成的解决方案。它们可以从您的设备制造商或第三方工具链制造商的网站下载。嗯,你仍然可以自己安装它......等等,我不是说过我们不会这么做吗?这么说吧,我撒谎了。我们会构建和配置工具链,但不是自己做的;我们会让构建工具来帮我们做。 2.3.2. 装配工具为了避免手动构建所有内容,通常使用专门的构建系统。其中最受欢迎的两个是 Buildroot 和 Yocto 项目。Yocto 项目Yocto 是一个强大且灵活的嵌入式 Linux 构建环境。它允许您:构建内核、引导加载程序、RootFS 和工具链在系统中启用对包管理器(opkg、rpm、dpkg)的支持精确管理版本、补丁和配置然而,Yocto 的学习曲线相当高。它使用自己的配方系统 (bitbake) 以及多层和多个概念。作为交换,您可以获得最大的灵活性和控制力。BuildrootBuildroot 更简单:它由一组 Makefile、Kconfig 和 bash 脚本组成,用于构建一个极简但可立即使用的 Linux 系统。与 Yocto 类似,它允许您构建内核、引导加载程序、RootFS 和工具链。优点:进入门槛低简单的界面(menuconfig)系统受到运行时保护,不会被更改——RootFS 默认为只读。虽然可以毫不费力地嵌入包管理器,但操作极其困难,因此不建议这样做。本指南将使用 Buildroot。您可以从官方仓库下载。至此,我们可以完成介绍部分并从理论转向实践。 结果因此,我们已经介绍了基础知识:嵌入式 Linux 是什么、它与常规发行版有何不同、它由什么组成、如何启动以及可以使用哪些工具来构建它。在下一篇文章中,我们将使用 Docker 准备工作环境,检查 U-Boot、Linux、Buildroot 及其外层的结构,并在我们的板上配置、构建和运行嵌入式 Linux。因此我强烈建议购买某种带有 SoC 的主板。感谢您的时间!再见!————————————————版权声明:本文为CSDN博主「大聪明-PLUS」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/2501_93209230/article/details/153827065
-
Nginx 是一款高性能的 Web 服务器和反向代理服务器,其配置灵活、性能卓越,被广泛应用于现代 Web 架构中。在 Nginx 的配置文件中,location 指令是实现请求路由的核心机制之一。正确理解和使用 location,对于构建高效、安全、可维护的服务至关重要。一、location 基本语法location 指令用于定义如何处理特定 URI(统一资源标识符)的请求。其基本语法如下:location [modifier] pattern { # 配置指令}其中:modifier 是可选的修饰符,用于指定匹配方式;pattern 是要匹配的 URI 路径或正则表达式;{} 内是具体的配置指令,如 proxy_pass、root、rewrite 等。二、location 的五种匹配类型Nginx 支持以下五种主要的 location 匹配类型,每种有不同的优先级和用途:1. 精确匹配(=)location = /index.html { ...}特点:完全匹配 URI,不进行任何前缀或正则扩展。优先级最高。适用于高频访问且路径固定的资源(如首页),能提升性能。示例:当请求为 /index.html 时匹配,但 /index.html?a=1 也会匹配(Nginx 在匹配时忽略查询字符串)。2. 前缀匹配(无修饰符)location /images/ { ...}特点:匹配以指定字符串开头的 URI。属于“普通前缀匹配”,优先级低于 = 和 ^~。可被正则匹配覆盖(除非使用 ^~)。3. 最长前缀匹配(^~)location ^~ /static/ { ...}特点:也是前缀匹配,但一旦匹配成功,不再检查正则 location。优先级高于普通前缀匹配,但低于 =。适用于静态资源目录,避免不必要的正则计算。4. 正则匹配(~ 和 ~*)location ~ \.php$ { ...}location ~* \.(jpg|jpeg|png)$ { ...}~:区分大小写的正则匹配;~*:不区分大小写的正则匹配。按配置文件中出现的顺序依次匹配,一旦匹配成功即停止。优先级高于普通前缀匹配,但低于 = 和 ^~。5. 通用匹配(/)location / { ...}匹配所有请求,通常作为兜底规则。优先级最低。三、location 匹配优先级总结Nginx 处理请求时,location 的匹配顺序如下:精确匹配(=)最长前缀匹配(^~)正则匹配(~ / ~*) —— 按配置顺序普通前缀匹配 —— 选择最长匹配的规则通用匹配(/) 注意:普通前缀匹配虽然先被扫描,但只有在没有更高优先级规则匹配时才会生效。Nginx 会先找出所有前缀匹配中最长的一个,然后判断是否有 ^~ 或 =,如果没有,则继续尝试正则匹配。四、示例示例 1:静态资源与动态请求分离server { listen 80; server_name example.com; # 精确匹配首页 location = / { root /var/www/html; index index.html; } # 静态资源,使用 ^~ 避免正则检查 location ^~ /static/ { alias /var/www/static/; expires 30d; } # PHP 动态请求 location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name; } # 兜底规则 location / { try_files $uri $uri/ /index.html; }}示例 2:API 路由代理location /api/v1/ { proxy_pass http://backend_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr;}注意:proxy_pass 结尾是否带 / 会影响 URI 重写行为。proxy_pass http://backend/; → /api/v1/user → 后端收到 /userproxy_pass http://backend; → /api/v1/user → 后端收到 /api/v1/user 掌握 Nginx 的 location 匹配机制,是构建高性能 Web 服务的基础。理解其优先级规则、合理使用修饰符、结合实际业务场景设计路由逻辑,不仅能提升系统性能,还能增强安全性和可维护性。
-
在 Linux 中监控 CPU、内存、磁盘、网络的指令非常丰富,以下是 最常用、实用的监控指令,按 “指标分类 + 指令详解 + 核心用法” 整理,新手也能快速上手:一、CPU 监控(核心:查看负载、使用率、进程占用)1. top(实时监控,最常用)功能:实时显示系统整体 CPU 负载、进程 CPU 占用排名(默认每 3 秒刷新)。核心用法:top # 直接运行,进入实时监控界面 界面关键信息:第一行:Cpu(s): 20.0%us(用户态 CPU 使用率)、5.0%sy(内核态 CPU 使用率)、75.0%id(空闲 CPU 使用率)。第三行:%Cpu0~%CpuN(多核心 CPU 各自使用率)。进程列:%CPU(单个进程占用 CPU 百分比)。常用快捷键:P:按 CPU 使用率排序(默认正序)。1:显示所有 CPU 核心的详细使用率。q:退出监控。2. htop(top 增强版,更直观)功能:比 top 界面更友好,支持鼠标操作、颜色区分进程状态。核心用法: # 先安装(Ubuntu/Debian):sudo apt install htop# 先安装(CentOS/RHEL):sudo yum install htophtop # 运行后直接查看,默认按 CPU 排序 3. mpstat(查看多 CPU 核心详情)功能:专门统计单个 / 所有 CPU 核心的使用率,适合分析 CPU 负载不均衡问题。核心用法mpstat # 查看所有 CPU 平均使用率mpstat -P ALL # 显示每个 CPU 核心的详细数据(0 代表第一个核心,1 代表第二个,以此类推)mpstat 1 5 # 每 1 秒刷新一次,共刷新 5 次(适合持续监控) 4. pidstat(查看单个进程的 CPU 占用)功能:精准定位某个进程的 CPU 使用情况(避免 top 刷屏)。核心用法: pidstat -u 1 3 # 每 1 秒统计一次所有进程的 CPU 使用率,共 3 次pidstat -u -p 1234 1 3 # 只监控 PID=1234 的进程,每 1 秒刷新,共 3 次 二、内存监控(核心:查看总内存、已用 / 空闲内存、进程内存占用)1. free(快速查看内存使用概况)功能:显示物理内存(RAM)和交换分区(Swap)的总容量、已用、空闲数据。核心用法: free -h # -h:以人类可读单位(GB/MB)显示(推荐)free -m # 以 MB 为单位显示 输出示例: total used free shared buff/cache availableMem: 15Gi 2.3Gi 10Gi 342Mi 3.1Gi 12GiSwap: 19Gi 0B 19Gi 关键指标:available(实际可分配给新进程的内存,含空闲 + 缓存可释放部分),比 free 更能反映真实内存状态。2. top/htop(实时查看进程内存占用)核心用法:运行 top 或 htop 后,关注进程列:%MEM:进程占用物理内存的百分比。VIRT:进程虚拟内存大小(含共享库、交换空间)。RES:进程实际占用的物理内存大小(不含共享库,核心指标)。快捷键:M(按内存使用率排序)。3. vmstat(监控内存 + CPU+IO 整体状态)功能:综合监控内存交换、页面调度、CPU 负载,适合排查内存瓶颈。核心用法: vmstat 1 5 # 每 1 秒刷新一次,共 5 次 关键输出:si(Swap in):从交换分区读入内存的数据量(越大说明内存不足,频繁使用 Swap)。so(Swap out):从内存写入交换分区的数据量(同上,si/so 长期非 0 代表内存紧张)。三、磁盘监控(核心:查看磁盘容量、IO 负载、分区使用)1. df(查看磁盘分区容量)功能:显示所有挂载分区的总容量、已用空间、空闲空间、使用率。核心用法: df -h # -h:人类可读单位(GB/MB),推荐df -T # 显示分区文件系统类型(ext4、xfs 等)df -h /home # 只查看 /home 分区的容量情况 2. du(查看目录 / 文件占用磁盘空间)功能:统计单个目录或文件的磁盘占用大小(df 看分区整体,du 看具体文件 / 目录)。核心用法: du -sh /var/log # 统计 /var/log 目录总占用(-s:只显示总和,-h:人类可读)du -h --max-depth=1 /home # 显示 /home 下一级目录的占用情况(不递归子目录)du -h /home/user/*.log # 统计指定类型文件的占用 3. iostat(监控磁盘 IO 负载)功能:查看磁盘的读写速度、IO 等待时间(判断磁盘是否繁忙)。核心用法: iostat -x 1 5 # -x:显示详细 IO 指标,每 1 秒刷新,共 5 次iostat -x -d sda 1 5 # 只监控 sda 磁盘(如 /dev/sda)的 IO 状态 关键指标:%util:磁盘 IO 使用率(接近 100% 说明磁盘繁忙,可能是瓶颈)。tps:每秒 IO 请求数(读 + 写)。rMB/s/wMB/s:每秒读 / 写数据量(MB)。4. iotop(磁盘 IO 进程排名)功能:类似 top,但按磁盘 IO 使用率排序,精准定位 “谁在占用磁盘 IO”。核心用法: # 安装:sudo apt install iotop(Ubuntu)/ sudo yum install iotop(CentOS)iotop # 运行后查看,默认按 IO 使用率排序iotop -o # 只显示正在进行 IO 操作的进程(过滤空闲进程,更清晰) 四、网络监控(核心:查看网络连接、带宽占用、网卡状态)1. ifstat(查看网卡带宽占用)功能:实时显示每个网卡的收发带宽(字节 / 秒、包 / 秒)。核心用法: # 安装:sudo apt install ifstat(Ubuntu)/ sudo yum install ifstat(CentOS)ifstat # 实时监控所有网卡带宽ifstat -i eth0 1 5 # 只监控 eth0 网卡,每 1 秒刷新,共 5 次 输出示例: eth0 wlan0 KB/s in KB/s out KB/s in KB/s out0.00 0.00 0.00 0.001.20 3.40 0.00 0.00 # 实时收发速率 2. iftop(网络带宽进程排名)功能:按进程 / IP 的网络带宽占用排序,直观查看 “谁在占用网络”。核心用法: # 安装:sudo apt install iftop(Ubuntu)/ sudo yum install iftop(CentOS)iftop -i eth0 # 监控 eth0 网卡的带宽占用 界面快捷键:N:显示 IP 地址(默认显示主机名,按 N 切换)。P:显示端口号(如 80、443)。q:退出。3. netstat(查看网络连接状态)功能:列出所有网络连接(TCP/UDP)、监听端口、进程 PID。核心用法(常用组合参数): netstat -tuln # 查看所有监听的 TCP/UDP 端口(-t:TCP,-u:UDP,-l:监听,-n:数字显示端口)netstat -anp # 查看所有网络连接(含 ESTABLISHED 连接)及对应进程 PID(-a:所有连接,-p:显示进程)netstat -anp | grep 80 # 过滤端口 80 的连接(排查 HTTP 服务) 4. ss(netstat 替代版,更快更高效)功能:与 netstat 功能一致,但性能更好(大并发连接下不卡顿),推荐优先使用。核心用法: ss -tuln # 等价于 netstat -tuln(查看监听端口)ss -anp # 等价于 netstat -anp(查看所有连接+进程)ss -anp | grep 443 # 过滤 HTTPS 端口(443)的连接 5. ping(测试网络连通性)功能:测试与目标 IP / 域名的网络连通性(基于 ICMP 协议)。核心用法: ping baidu.com # 持续 ping 百度,测试连通性ping -c 4 baidu.com # 只 ping 4 次(避免持续刷屏) 五、综合监控工具(一次性监控所有指标)如果想同时监控 CPU、内存、磁盘、网络,推荐用以下工具:1. glances(全能监控工具)功能:一站式监控所有系统指标,支持 Web 界面、远程监控。核心用法: # 安装:sudo apt install glances(Ubuntu)/ sudo yum install glances(CentOS)glances # 本地实时监控(界面含 CPU、内存、磁盘、网络、进程排名)glances -w # 启动 Web 服务,浏览器访问 http://服务器IP:61208 查看监控 总结:常用指令速查表监控指标快速查看指令详细分析指令CPUtop / htopmpstat、pidstat -u内存free -htop(按 M 排序)、vmstat磁盘容量df -hdu -sh 目录磁盘 IOiostat -xiotop网络带宽ifstatiftop网络连接ss -tuln / ss -anpnetstat -anp综合监控glances 根据需求选择即可:快速排查用 top+free -h+df -h+ss;精准定位问题用 pidstat+iotop+iftop。
-
在众多编程语言中,Lua(发音为 /ˈluːə/,意为“月亮”)以其轻量、高效、可嵌入性强的特性,在游戏开发、嵌入式系统、Web 服务器扩展、脚本自动化等领域占据着不可替代的地位。它被广泛应用于 《魔兽世界》、《愤怒的小鸟》、Redis、Nginx (OpenResty) 等知名项目中。 什么是 Lua?Lua 是一种用标准 C 语言编写的开源、轻量级、可扩展的脚本语言,由巴西里约热内卢天主教大学(PUC-Rio)的 Roberto Ierusalimschy 等人于 1993 年开发。核心设计理念:轻量:整个解释器核心仅几百 KB,适合嵌入资源受限的设备。高效:执行速度快,接近 Python、JavaScript 等主流脚本语言。可嵌入:设计初衷就是作为“胶水语言”,无缝集成到 C/C++ 等宿主程序中。可扩展:通过 C 扩展模块,轻松调用底层功能。定位:Lua 不是用于构建大型独立应用的“主语言”,而是作为嵌入式脚本语言,为宿主程序提供灵活的逻辑扩展能力。Lua 的核心特性特性说明轻量级解释器体积小(~200KB),内存占用低速度快基于寄存器的虚拟机,执行效率高动态类型变量无需声明类型,运行时确定自动内存管理内置垃圾回收(GC)机制过程式 + 函数式支持函数式编程(函数是一等公民)协程(Coroutine)轻量级线程,支持协作式多任务无内置复杂类型仅提供 表(table) 作为唯一数据结构,但功能强大Lua 基础语法速览1. 变量与数据类型Lua 有 8 种基本类型:nil, boolean, number, string, function, thread, table, userdata。-- 变量声明(无需类型)name = "Alice" -- stringage = 25 -- numberis_active = true -- booleanscore = nil -- nil-- 动态类型x = 10x = "hello" -- 合法,类型在运行时改变2. 表(Table)—— Lua 的“瑞士军刀”table 是 Lua 中唯一的数据结构,可实现数组、字典、对象、模块等多种功能。-- 作为数组(索引从 1 开始!)fruits = {"apple", "banana", "orange"}print(fruits[1]) --> "apple"-- 作为字典(键值对)person = { name = "Bob", age = 30, city = "Shanghai"}print(person["name"]) --> "Bob"print(person.age) --> 30-- 混合使用mixed = {10, "hello", x = 1, y = 2}print(mixed[1]) --> 10print(mixed.x) --> 13. 控制结构-- if 语句if age > 18 then print("Adult")elseif age > 13 then print("Teen")else print("Child")end-- for 循环for i = 1, 5 do print(i)end-- while 循环local i = 1while i <= 3 do print(i) i = i + 1end4. 函数(Function)函数是“一等公民”,可赋值给变量、作为参数传递。-- 定义函数function greet(name) return "Hello, " .. name .. "!"end-- 调用print(greet("Alice")) --> "Hello, Alice!"-- 函数作为变量local sayHi = function(name) return "Hi, " .. nameend-- 闭包(Closure)function makeCounter() local count = 0 return function() count = count + 1 return count endendcounter = makeCounter()print(counter()) --> 1print(counter()) --> 2Lua 的核心优势:可嵌入性Lua 的最大价值在于它可以被轻松嵌入到 C/C++ 程序中,通过 Lua C API 实现双向调用。典型嵌入流程:宿主程序(C/C++) 调用 Lua 脚本。Lua 脚本 调用宿主程序暴露的 C 函数。数据交换:通过 Lua 栈(Stack)传递参数和返回值。C 代码调用 Lua 示例:#include <lua.h>#include <lualib.h>#include <lauxlib.h>int main() { lua_State *L = luaL_newstate(); // 创建 Lua 状态机 luaL_openlibs(L); // 加载标准库 // 执行 Lua 脚本 if (luaL_dofile(L, "script.lua") != 0) { fprintf(stderr, "Error: %s\n", lua_tostring(L, -1)); } lua_close(L); return 0;}Lua 调用 C 函数:C 代码注册函数后,Lua 脚本可直接调用:-- Lua 脚本中result = my_c_function(42, "hello")Lua 的典型应用场景1. 游戏开发《魔兽世界》:UI 界面和插件系统使用 Lua。《愤怒的小鸟》:游戏逻辑由 Lua 驱动。Cocos2d-x:跨平台游戏引擎,支持 Lua 脚本开发。✅ 优势:热更新、快速迭代、非程序员(策划、美术)也能参与逻辑编写。2. Web 服务器扩展OpenResty:基于 Nginx 和 Lua 的高性能 Web 平台。在 Nginx 中直接运行 Lua 脚本,实现 API 网关、限流、鉴权、缓存等。性能远超传统 PHP/Python 后端。location /api { content_by_lua_block { local redis = require("resty.redis") local red = redis:new() red:connect("127.0.0.1", 6379) local data = red:get("user:123") ngx.say(data) }}3. 数据库脚本Redis:使用 Lua 脚本实现原子性操作(EVAL 命令)。多条命令打包执行,避免网络开销。保证操作的原子性。-- Redis Lua 脚本:原子性递增并返回local current = redis.call("GET", KEYS[1])if current then current = tonumber(current) + 1 redis.call("SET", KEYS[1], current) return currentelse return nilend4. 嵌入式系统与 IoT资源受限设备(如路由器、智能家居)中,Lua 用于配置脚本、业务逻辑扩展。NodeMCU:基于 ESP8266 的 IoT 固件,支持 Lua 编程。5. 配置与自动化脚本作为配置文件语言(比 JSON 更灵活)。自动化工具脚本(如 Wireshark 的插件)。 Lua 虽然小众,但其在特定领域的不可替代性使其历久弥新。它证明了“少即是多”的设计哲学——一个简单的 table 和强大的 C API,足以支撑起无数复杂系统的灵活扩展。
-
Nginx(发音为 "engine-x")已成为高并发、高性能 Web 服务的代名词。作为全球最受欢迎的 Web 服务器之一,Nginx 不仅被 Google、Facebook、Netflix、淘宝、京东等大型互联网公司广泛采用,更是微服务、负载均衡、API 网关等现代架构的核心组件。 一、什么是 Nginx?Nginx 是一个开源的 高性能 HTTP 服务器和反向代理服务器,同时也是一个 IMAP/POP3/SMTP 邮件代理服务器。它由俄罗斯程序员 Igor Sysoev 于 2004 年发布,最初为解决 C10K 问题(单机支持 1 万并发连接)而设计。核心特点:特性说明高性能异步非阻塞事件驱动架构,资源消耗低,支持高并发高可靠性进程模型稳定,即使高负载下也不会崩溃热部署支持不停机更新配置、升级版本模块化设计功能通过模块扩展,灵活可定制反向代理与负载均衡支持多种负载均衡算法静态资源服务高效处理 HTML、CSS、JS、图片等静态文件二、Nginx 架构原理:为什么这么快?1. 事件驱动 + 异步非阻塞与传统 Apache 的 多进程/多线程模型(每个连接占用一个进程/线程)不同,Nginx 采用 事件驱动的异步非阻塞 I/O 模型。Master 进程:管理进程,不处理请求。Worker 进程:每个 Worker 采用单线程 + 事件循环(Event Loop)处理成千上万个并发连接。I/O 多路复用:使用 epoll(Linux)、kqueue(BSD)等机制,一个进程可监听多个 socket。✅ 优势:内存占用少,上下文切换开销小,轻松应对数万并发连接。三、Nginx 的核心应用场景1. 静态 Web 服务器Nginx 是服务静态资源的绝佳选择,性能远超应用服务器(如 Tomcat)。server { listen 80; server_name www.example.com; location / { root /var/www/html; # 静态文件目录 index index.html; } # 缓存静态资源 location ~* \.(jpg|jpeg|png|css|js)$ { expires 1y; add_header Cache-Control "public, immutable"; }}2. 反向代理(Reverse Proxy)Nginx 作为“门面”,接收客户端请求,转发给后端应用服务器(如 Java、Python、Node.js),并返回响应。server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:8080; # 转发到本地 8080 端口 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}✅ 优势:隐藏后端服务器真实 IP统一入口,便于管理提升安全性3. 负载均衡(Load Balancing)Nginx 可将请求分发到多个后端服务器,实现横向扩展与高可用。配置示例:upstream backend { # 负载均衡算法 least_conn; # 最少连接 # round-robin; # 轮询(默认) # ip_hash; # IP 哈希(会话保持) # hash $request_uri; # 一致性哈希 server 192.168.1.10:8080 weight=3; # 权重 3 server 192.168.1.11:8080; server 192.168.1.12:8080 backup; # 备用服务器}server { listen 80; location / { proxy_pass http://backend; }}4. SSL/TLS 加密(HTTPS)Nginx 可作为 SSL 终端,处理 HTTPS 请求并解密后转发给后端 HTTP 服务。server { listen 443 ssl http2; server_name www.example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/private.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512; location / { proxy_pass http://backend; }}✅ 推荐:使用 Let's Encrypt 免费证书 + Certbot 自动续期。5. 缓存加速Nginx 支持反向代理缓存,减少后端压力,提升响应速度。# 定义缓存区proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;server { location / { proxy_cache my_cache; proxy_pass http://backend; proxy_cache_valid 200 302 10m; # 缓存 10 分钟 add_header X-Cache-Status $upstream_cache_status; }}缓存命中时,X-Cache-Status 返回 HIT,否则为 MISS。6. URL 重写与重定向# 301 永久重定向rewrite ^/old-page$ /new-page permanent;# 条件重写if ($http_user_agent ~* "bot|spider") { rewrite ^/.*$ /robots.txt break;}# 伪静态rewrite ^/article/(\d+)\.html$ /article.php?id=$1 last;四、常用配置指令详解指令作用listen监听端口和 IPserver_name匹配域名location定义 URL 路由规则root / alias文件路径映射proxy_pass反向代理目标upstream定义后端服务器组try_files尝试多个文件路径(常用于 SPA 路由)gzip启用 Gzip 压缩SPA 应用路由支持(如 Vue、React)location / { root /var/www/app; try_files $uri $uri/ /index.html;}确保前端路由刷新不 404。五、性能优化建议1. Worker 进程优化worker_processes auto; # 通常设置为 CPU 核心数worker_connections 1024; # 每个 Worker 最大连接数worker_rlimit_nofile 65535; # 提升文件描述符限制2. 开启 Gzip 压缩gzip on;gzip_types text/plain text/css application/json application/javascript text/xml application/xml;3. 启用 HTTP/2listen 443 ssl http2;减少延迟,提升加载速度。4. 静态资源缓存location ~* \.(css|js|jpg|png|gif)$ { expires 1y; add_header Cache-Control "public, immutable";}六、安全加固1. 隐藏 Nginx 版本号server_tokens off;2. 防止点击劫持add_header X-Frame-Options SAMEORIGIN;3. 防止 XSS 攻击add_header X-Content-Type-Options nosniff;add_header Content-Security-Policy "default-src 'self'";4. 限制请求频率limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;location /api/ { limit_req zone=api burst=20 nodelay;}
-
前言在现代计算机系统中,并发执行已成为提升资源利用率与系统吞吐量的核心机制。当多个进程/线程共享有限的系统资源(如处理器、内存、I/O 设备等)时,如何协调它们的执行顺序以避免冲突、保证数据一致性,成为操作系统设计的关键挑战 —— 这正是进程同步与互斥问题的核心议题。本文将系统梳理进程同步与互斥的理论基础,深入分析典型问题的内在逻辑,探讨各类解决方案的适用场景与局限性。本文分为6个部分:模拟多线程访问共享资源出错现象;访问共享资源出错原因;互斥锁解决共享资源访问问题;互斥锁的原理;线程同步互斥周边概念;线程同步如何实现。一. 多线程访问出错下面先通过一个实验看一下什么是多线程访问错误:模拟抢票的逻辑,使用5个线程模拟用户,用户要抢500张票。准备工作:设置一个结构体,存储线程的ID和线程的名字,用来打印抢票数据和回收线程:int tickets = 1000;struct ThreadData{ std::string thread_name_; pthread_t thread_id_;};编写抢票逻辑void* Get_Ticket(void* args){ ThreadData* pdata = static_cast<ThreadData*>(args); // 进行抢票 while(tickets > 0) // 还有票,可以抢 { // 打印出线程名以及抢到的票编号 std::cout << pdata->thread_name_ << " is getting a ticket , the numberr of tickets is " << tickets << std::endl; --tickets; usleep(rand() % 5); // 模拟延时 } return nullptr;}编写主线程,创建新线程和回收新线程:int main(){ // 模拟10个人抢票的逻辑 // 即10个线程一起抢票, --tickets表示抢票 std::vector<ThreadData*> threads; for(int i = 0 ; i < 5 ; i++) { ThreadData* pdata = new ThreadData(); pdata->thread_name_ = "Thread-" + std::to_string(i + 1); pthread_create(&pdata->thread_id_, NULL, Get_Ticket, pdata); threads.push_back(pdata); } // 等待所有线程结束 for(int i = 0 ; i < threads.size() ; i++) { pthread_join(threads[i]->thread_id_, NULL); delete threads[i]; } return 0;}输出现象:因为输出太长,此处直接去最后几行:根据上面的现象可以看到,有线程抢到了同一编号的票,并且有线程抢到了标号为负数的票。这就是多线程并发访问导致的数据不一致问题,下面围绕这一现象进行详细解释。二. 访问共享资源出错的原因对于全局变量的++/–操作是否是安全的???在上面代码中多个线程都会对同一个全局变量进行–操作,那么这一过程是否安全。此处需要了解以下,CPU是如何处理加减操作的,具体过程如下:先将tickets的数据存放到CPU的寄存器中;将寄存器中的值-1;将寄存器中的数据拷贝回内存中。具体实现如下:mov eax,tickets // 将tickets加载到eax寄存器dec eax // 将eax寄存器的值减1mov tickets,eax // 将结果存储到变量tickets中我们知道操作系统上的线程是并发运行的,随时有可能被操作系统切换;所以有没有一种情况:线程A执行到dec eax后,突然被切换了,然后将这些上下文数据存储到线程中;线程B在CPU上多跑一会,抢到了很多票,执行了多次--tickets操作,后也被切换了;此时A又被调度了,它下一步要执行mov tickets,eax,但是寄存器eax中的值是多少,是线程B前往票后的值吗?并不是线程A中的值,是线程B还没抢之前的值,所以此时将这个值返回tickets中就会导致,tickets值变大,从而导致多个线程抢到同一张票。同理如果一个线程C在while(tickets > 0)判断完后被切换,其他线程执行将票抢完了,已经是0了,没有票了;此时线程C再次被切换进来,因为之前判断是允许抢票的,所以其已经在循环内了,从而导致线程抢到负数的票。此处引入一个新的概念:原子性(Atomicity): 是指一个操作或一组操作具有 “不可分割” 的特性 —— 要么完整地执行完毕,要么完全不执行,在执行过程中不会被任何外部因素(如其他进程、线程、中断等)打断,也不会向外界暴露中间状态。一般只有一条汇编语句的代码就是具有原子性的。所以也就是说:对tickets的加减是不原子的,所以导致抢到了相同的票;进入循环执行代码也是不原子的,所以导致抢到了负票。如何解决这种数据不一致问题?如何保证对共享数据的访问,任何时候只有一个执行流在访问共享资源???下面介绍互斥锁来解决数据不一致问题。三. 互斥锁在介绍互斥锁之前,先介绍几个概念:临界资源:多线程执行流共享的资源就叫做临界资源;临界区:每个线程内部,访问临界资源的代码,就叫做临界区;互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。锁是对临界区资源访问的一种限制操作,其保证在任何一个时间点上只有一个线程在访问临界资源。示意图如下: 锁一共有4个操作,初始化锁,销毁锁,加锁和解锁。初始化锁:int pthread_mutex_init(pthread_mutex_t *restrict mutex , const pthread_mutexattr_t *restrict attr):参数1:需要进行初始化的锁;参数2:定制互斥锁的行为特性,一般不进行定制,使用NULL;返回值:0表示成功,失败返回错误码。销毁锁:int pthread_mutex_destory(pthread_mutex_t *mutex):参数与返回值与上面类似。加锁:int pthread_mutex_lock(pthread_mutex_t* mutex);解锁:int pthread_mutex_unlock(pthread_mutex_t* mutex)。此时就可以对上面抢票代码进行重写,此时因为不能在循环外进行加锁,因此要把判断能否抢票的逻辑放在循环里面:pthread_mutex_t lock;void *Get_Ticket(void *args){ ThreadData *pdata = static_cast<ThreadData *>(args); // 进行抢票 while (1) { // 先上锁 pthread_mutex_lock(&lock); if (tickets > 0) // 还有票,可以抢 { // 打印出线程名以及抢到的票编号 std::cout << pdata->thread_name_ << " is getting a ticket , the numberr of tickets is " << tickets << std::endl; --tickets; pthread_mutex_unlock(&lock); // 解锁 } else { pthread_mutex_unlock(&lock); // 解锁 break; } // 解锁 usleep(getpid() % 100); // 让线程休眠一会,防止一个线程申请锁的能力太强 } return nullptr;}以上代码中解锁后如果没有代码,就会导致刚解锁的进程又会立即拿到锁,就会导致锁的分配不合理,容易导致进程饥饿问题;上面代码逻辑本身也是不对了,在解锁后,肯定还有其他操作要进行,比如保存用户信息,将票号从总票数中移除等,一次上面使用usleep()来模拟该操作。锁本身也是共享资源,所以申请锁和释放锁本身在设计的时候就是原子的。在临界区中,线程可以被切换吗???可以被切换,只不过在切换时会将锁也带走,在此期间其他线程依旧没有办法访问临界区。补充:还有一种初始化锁的方式:pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER,通过该方法进行初始化的锁,必须定义成全局的,并且不需要销毁。四. 互斥锁的原理为了实现互斥锁操作,大多数的CPU体系结构都提供了swap和exchange指令,该指令的作用是把寄存器和内存单元的数据相符交换,由于只有一条指令,保证了原子性。加锁&&解锁示意图如下:在加锁的时候,将寄存器中的al先进行初始化,再将mutex中的值交换过去,相当于将锁给线程,如果mutex中的锁已经被拿走了,就会将线程挂起,直到锁归还。解锁是,再见mmutex置为1,表示规范锁。所以,交换锁的本质就是:把内存中的数据交换到CPU寄存器中,把数据交换到线程的上下文中。mutex只有一个,其值子啊内存和寄存器之间进行交换;将共享的锁,以一条汇编的方式交换到线程的上下文,代表着该线程拿到了锁,其他线程就拿不到了。五. 线程同步互斥周边概念5.1 重入与线程安全重入:对于一个函数来说,当有多个执行流同时在函数内执行,就成该函数为重入函数;重入函数又可以分为:可重入函数和不可重入函数。线程安全:当多个线程并发式的访问同一段代码的时候,不会出现不同的结果,就称为线程安全。我们经常调用的库函数/系统调用基本上都是不可重入的。重入与线程安全的关联:一个函数是可重入的,那么一定是线程安全的;一个函数是不可重入的,可能会出现线程不安全。5.2 死锁死锁:当一个线程占有一把锁A,正在获取另一把锁B时,另一个线程持有锁B,但是同时也在请求锁A,造成两个进程都在阻塞是等待的情况。死锁的必要条件:互斥条件,一个资源每次只能有一个进程访问;请求与保持:执行流因为请求资源——锁而阻塞,对已有资源——锁不进行释放;不掠夺条件:资源——锁不会被抢走;循环等待:拥有锁的双方都在请求对方的锁。根据死锁的必要条件,可以提出对应的解决方案:对代码结构重构,打破互斥条件;在申请锁失败阻塞的时候将自己拥有的锁释放,可以使用int pthread_mutex_trylock()尝试申请锁,如果失败返回-1,此时可以选择将自己的锁释放;并不能抢其他线程的锁,所以无法掠夺;打破循环条件,将两个锁同时进行申请,同时释放。死锁还有些相关算法:1)死锁检测算法;2)银行家算法。六. 线程同步在保证资源安全的前提下,我们希望我买了的线程访问资源具有一定的顺序性。线程同步就是为了解决不同线程竞争资源的能力不同,导致的饥饿问题;简单说就是解决线程排队的优先级,避免一个线程的优先级高并且一直在访问共享资源,导致其他线程都访问不到的问题。线程同步是使用条件变量来实现的:可以将条件变量理解为一个队列,所有要访问临界资源的线程都要想到队列中进行排队;以下是条件变量的接口:初始化一个条件变量:int pthread_cond_init(pthread_cond_t *restrict cond , const pthread_condatte_t *restrictatta):第一个参数要进行初始化的条件变量;第二个参数,可以指定条件变量的一些属性,一般不进行指定,使用NULL;返回值0表示成功,失败返回错误码。条件变量也可以先锁一样定义为全局的形式:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;销毁条件变量:int pthread_cond_destroy(pthread_cond_t *cond)。让一个线程在队列中进行等待:int pthread_cond_wait(pthread_cond_t *cond , pthread_mutex_t *mutex):参数一,要在哪一个队列中等待;参数二,对应的锁;条件变量让进程进行等待是因为,临界资源没有就绪,所以才需要进行等待,但是我们如果想要直到临界资源是否就绪,就需要访问临界资源,因此条件变量的使用要在加锁和解锁之间,当线程在条件变量中等待的时候,会将对应的锁释放。线程在条件变量中等待,就必须能够被唤醒,线程库中也提供了将线程唤醒的接口:int ptherad_cond_signal(pthread_cond_t *cond):唤醒条件变量中的一个线程;int ptherad_cond_broadcast(pthread_cond_t *cond):唤醒条件变量中的所有线程;下面写一个demo代码让主线程负责唤醒一个个新线程:先定义锁和条件变量,以及一个存储线程数据的结构体:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义锁pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 定义条件变量struct ThreadData{ std::string thread_name_; pthread_t thread_id_;};定义新线程调用的函数,只需要打印出每次访问临界区的线程名称即可:void *thread_func(void *args){ pthread_detach(pthread_self()); ThreadData *data = static_cast<ThreadData *>(args); while (1) { pthread_mutex_lock(&mutex); pthread_cond_wait(&cond, &mutex); std::cout << data->thread_name_ << " wake up" << std::endl; pthread_mutex_unlock(&mutex); } return nullptr;}编写主线程,负责发送信号唤醒条件队列,以及创建新线程:int main(){ std::vector<ThreadData *> threads; for (int i = 0; i < 5; i++) { ThreadData *pdata = new ThreadData(); pdata->thread_name_ = "Thread-" + std::to_string(i + 1); pthread_create(&pdata->thread_id_, NULL, thread_func, pdata); threads.push_back(pdata); usleep(500); } while(1) { pthread_cond_signal(&cond); sleep(1); } return 0;}运行结果:可以看到线程确实是一次访问临界资源的。综上所述:cond就相当于一个队列,要访问临界资源就需要先排队,而pthread_cond_signal和pthread_cond_broadcast相当于信号,告诉队列可以访问临界资源了。———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/2401_87944878/article/details/150649808
-
在 Linux C/C++ 开 发 中,库 是 代 码 复 用 和 工 程 化 的 核 心。不 少 开 发 者 会 遇 到 源 码 泄 露、编 译 “找 不 到 头 文 件 / 库”、动 态 库 运 行 加 载 失 败 等 问 题,本 质 是 对 库 的 流 程 不 熟 悉。本 文 从 背 景 切 入,先 讲 静 态 库 的 原 理、制 作 与 使 用,再 讲 动 态 库 的 实 战 技 巧,帮 你 掌 握 库 的 全 流 程 应 用。 一、静 态 库1、背 景 设 计 一 个 静 态 库 并 将 已 经 写 好 的 代 码 给 别 人 用。有 两 种 方 法: 把 源 文 件 给 他把 源 代 码 打 包 成 库,必 须 提 供 头 文 件。头 文 件 的 本 质 是 库 文 件 的 说 明 书。libxxx.a - - - 静 态 链 接libxxx.so - - - 动 态 链 接 2、原 理 3、静 态 库 的 流 程(1)编 写 者编 写 源 代 码(不 包 括 main 函 数)。mymath.h #pragma once #include<stdio.h> extern int myerrno;int add(int x,int y);int sub(int x,int y);int mul(int x,int y);int div(int x,int y); mymath.c #include"mymath.h" int myerrno = 0;int add(int x,int y){ return x + y;}int sub(int x,int y){ return x - y;}int mul(int x,int y){ return x * y;}int div(int x,int y){ if(y == 0) { myerrno = 1; return -1; } return x / y;} makefile lib=libmymath.a$(lib):mymath.oar -rc $@ $^mymath.o:mymath.cgcc -c $^.PHONY:cleanclean:rm -rf *.o *.a lib.PHONY:outputoutput:mkdir -p lib/includemkdir -p lib/mymathlibcp *.h lib/includecp *.a lib/mymathlib 这 里 不 写 $@ 的 原 因 是 因 为 gcc 可 以 将 mymath.h 编 译 成 和 源 文 件 名 字 相 同 的 .o 文 件。 ar 是 生 成 静 态 库 的 1 个 命 令,可 以 将 所 有 的 .o 文 件 打 包 形 成 .a 文 件,-rc 表 示 将 所 有 的 .o 放 在 目 标 文 件 .a 中,如 果 不 存 在 就 创 建,如 果 存 在 就 替 换。 编 译 生 成 .a 和 .o 文 件。 将 生 成 的 文 件 打 包 进 文 件 夹 中。将 生 成 的 lib 文 件 夹 打 包 压 缩。(2)使 用 者下 载 并 解 压 静 态 库 的 压 缩 包编 写 main 函 数。#include "mymath.h"int main(){ int n = div(10,0); printf("10/0=%d,errno=%d\n",n,myerrno); return 0;} 编 译 代 码没 有 找 到 头 文 件 原 因 编 译 器 会 在 默 认 路 径 和 当 前 目 录 下(和 源 代 码 在 同 一 级 路 径 下) 寻 找 头 文 件。 解 决 方 法方 法 1gcc main.c -I + 目 录:编 译 器 会 去 指 定 目 录 下 寻 找 头 文 件。方 法 2#include "lib/include/mymath.h" 可 以 在 代 码 中 包 含 路 径。这 里 推 荐 使 用 方 法 1。 链 接 错 误(找 不 到 静 态 库) 下 图 中 的 错 误 是 链 接 错 误,原 因 是 因 为 以 .o 结 尾 的 一 般 是 链 接 错 误。 -c:编 译 到 目 标 代 码 gcc -c 编 译 通 过,只 能 说 明 代 码 在 语 法 和 基 本 编 译 规 则 上 没 有 问 题,它 不 检 查 链 接 错 误。 -L 静 态 库 的 存 储 路 径。 没 有 包 含 .a 文 件 -l 找 到 .a 文 件,这 里 是 静 态 库 的 真 实 名 字,建 议 -l 和 库 的 真 实 名 字 相 连 接,二 者 之 间 没 有 空 格。 库 的 真 实 名 字去 掉 前 缀 和 后 缀。 (3)简 化 gcc 编 译 选 项直 接 将 头 文 件 和 静 态 库 放 进 系 统 文 件 夹。这 是 库 的 安 装。这 里 不 建 议 不 要 将 头 文 件 和 静 态 库 放 入 系 统 中。 需 要 指 明 静 态 库 的 真 实 名 字,才 能 编 译 成 功。 使 用 软 链 接 使 用 makefilemain:main.cgcc main.c -I ./lib/include/ -L ./lib/mymathlib/ -lmymath -o main.PHONY:cleanclean:rm -f main (4)myerrno 问 题 上 面 的 errno 不 是 -1 的 原 因 是 因 为 c 语 言 的 实 例 化 是 从 右 向 左 实 例 化 的。即 printf("10/0=%d,errno=%d\n",div(10,0),myerrno); 这 句 代 码 中 先 调 用 myerrno 然 后 使 用 div。调 用 的 顺 序 错 误 正 确 的 顺 序 是 在 调 用 myerrno 时 应 使 用 div,然 后 使 用 myerrno。修 改 后 的 代 码 #include "mymath.h"int main(){ int n = div(10,0); printf("10/0=%d,errno=%d\n",n,myerrno); return 0;} (5)结 论第 3 方 库 使 用 的 时 候 必 须 使 用 -l 选 项 ldd可 以 查 看 是 否 是 动 态 链 接 还 是 静 态 链 接。如 下 图 gcc 采 用 的 是 动 态 链 接,因 为 文 件 是 以 .so 结 尾 的。 gcc 默 认 是 动 态 链 接,如 果 只 提 供 静 态 链 接,gcc 只 能 对 该 库 使 用 静 态 链 接。ldd 的 输 出 反 映 的 是 整 个 程 序 是 否 依 赖 动 态 库,而 非 单 个 库 的 链 接 方 式。上 面 使 用 的 是 静 态 库,gcc 对 静 态 库 采 用 静 态 链 接,这 里 没 有 显 示 静 态 库 的 链 接。但 对 可 执 行 程 序 来 讲,程 序 中 还 有 其 他 库(如 C 标 准 库 libc)使 用 了 动 态 链 接(默 认 行 为),整 个 程 序 仍 然 是 “动 态 链 接 的 可 执 行 文 件”。 当 同 一 库 的 动 态 版 本(.so)和 静 态 版 本(.a)同 时 存 在 时,gcc 默 认 会 优 先 选 择 动 态 链 接 方 式。 若 要 强 制 使 用 静 态 链 接,可 在 编 译 时 添 加 -static 选 项。不 过 需 要 注 意,-static 并 非 绝 对 强 制 的 指 令 - - - 它 的 作 用 是 告 知 gcc 尽 量 采 用 静 态 链 接,但 在 某 些 情 况 下,gcc 可 能 仍 无 法 实 现 完 全 的 静 态 链 接。 二、动 静 态 库1、编 写 者编 写 代 码gccmylog.h #pragma once //防止头文件被重复包含#include<stdio.h>void log(const char*); mylog.c #include"mylog.h"void log(const char* info){ printf("Warning:%s\n",info);} myprint.c #include "myprint.h"void Print(){ printf("hello world!\n"); printf("hello world!\n"); printf("hello world!\n"); printf("hello world!\n");} myprint.h #pragma once //防止头文件被重复包含#include<stdio.h>void Print(); makefile dy-lib=libmymethod.sostatic-lib=libmymath.a # 同时生成动态库和静态库.PHONY:allall: $(dy-lib) $(static-lib) # 生成静态库$(static-lib):mymath.oar -rc $@ $^mymath.o:mymath.cgcc -c $^ # 生成动态库$(dy-lib):mylog.o myprint.ogcc -shared -o $@ $^mylog.o:mylog.cgcc -fPIC -c $^myprint.o:myprint.cgcc -fPIC -c $^ # 清理静态库文件.PHONY:cleanclean:rm -rf *.o *.a *.so mylib # 打包.PHONY:outputoutput:mkdir -p mylib/includemkdir -p mylib/libcp *.h mylib/includecp *.a mylib/lib cp *.so mylib/lib 编 译 代 码,将 .c 文 件 编 译 成 .o 文 件。fPIC:产 生 位 置 无 关 码。 生 成 动 态 库 .so 表 示 动 态 库。 -shared 此 选 项 表 示 不 生 成 可 执 行 程 序,将 尽 量 使 用 动 态 库,所 以 生 成 文 件 比 较 小,但 是 需 要 系 统 由 动 态 库 -O0、-O1、-O2、-O3 编 译 器 的 优 化 选 项 的 4 个 级 别,-O0 表 示 没 有 优 化,-O1 为 缺 省 值,-O3 优 化 级 别 最 高。 2、使 用 者编 译 代 码(包 含 动 静 态 库) 运 行 代 码 有 ldd 和 a.out 可 以 看 出 生 成 的 可 执 行 程 序 为 动 态 链 接 的。 gcc 编 译 时 的 路 径 是 编 译 器,还 需 要 让 系 统(加 载 器) 明 白 动 态 库 在 哪 里。3、加 载 找 不 到 动 态 库将 动 态 库 拷 贝 到 系 统 路 径(/lib64 或 者 /usr/lib64/) 下。实 际 情 况,我 们 使 用 的 库 都 是 别 人 成 熟 的 库,都 采 用 直 接 安 装 到 系 统 的 方 式。 建 立 软 链 接 将 自 己 的 库 所 在 的 路 径 添 加 到 系 统 的 环 境 变 量 中。使 用 echo $LD_LIBRARY_PATH 搜 索 用 户 自 定 义 的 库 路 径。 xshell 每 次 重 启 都 会 重 新 加 载 环 境 变 量,可 以 将 LD_LIBRARY_PATH 这 个 环 境 变 量 添 加 到 vim ~/.bash_profile,来 解 决 这 个 问 题。 在 /etc/ld.so.conf.d 这 是 系 统 维 护 动 态 库 时 放 的 路 径,建 立 自 己 的 动 态 库 路 径 的 配 置 文 件,然 后 使 用 ldconfig 重 新 加 载 即 可。 (1)切 换 到 root 身 份 并 进 入 到 /etc/ld.so.conf.d 这 个 路 径 下,创 建 以 .conf 结 尾 的 文 件 并 添 加 路 径,这 种 方 法 和 xshell 是 否 关 闭 没 有 影 响。 (2)使 用 ldconfig 重 新 加 载 文 件(3)成 功 执 行 可 执 行 程 序 (4)如 果 不 想 使 用 这 种 方 法,可 以 删 除 刚 才 新 建 的 文 件,然 后 使 用 ldconfig 重 新 加 载 文 件 即 可。 三、总 结 本 文 覆 盖 了 静 态 库(.o 打 包、Makefile 构 建、-I/-L/-l 链 接)与 动 态 库(-fPIC/-shared 编 译、4 种 加 载 问 题 解 决 方 案)的 核 心 内 容。静 态 库 嵌 入 程 序、独 立 但 体 积 大,动 态 库 共 享 模 块、轻 量 但 需 依 赖 系 统,需 按 需 选 择。后 续 可 尝 试 封 装 通 用 模 块 为 库,或 研 究 版 本 管 理,进 一 步 提 升 开 发 效 率。———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/2301_78847073/article/details/151329450
-
一、Linux 内核支持调度策略主要分为实时调度策略和普通(非实时)调度策略,以及一种特殊的限期调度策略。 先进先出调度(SCHED_FIFO),非抢占式的实时调度策略,没有时间片。一直占用 CPU,直到主动放弃 CPU,或者被更高优先级的实时进程抢占。 轮流调度(SCHED_RR),抢占式的实时调度策略,有时间片。 限期调度策略(SCHED_DEADLINE),Linux 3.14 引入的一种基于 Earliest Deadline First (EDF) 和 Constant Bandwidth Server (CBS) 算法的实时调度策略。 用不同的调度策略调度实时进程。普通进程支持两种调度策略: 标准轮流分时(SCHED_NORMAL):默认的调度策略,调度大多数非实时、通用目的的进程。用完全公平调度器(CFS),为所有进程提供公平的 CPU 时间分配,同时考虑进程的 nice 值。 批处理调度(SCHED_BATCH ):调度普通的非实时进程,针对 CPU 密集型、非交互式任务进行了优化。 空闲(SCHED_IDLE):最低优先级的调度策略。只有系统没有其他任何可运行的进程,SCHED_IDLE 进程才会被调度运行。 二、进程优先级限期进程的优先级比实时进程高,实时进程的优先级比普通进程高。 优先级层次: 限期进程 (SCHED_DEADLINE) 有最高的优先级,根据截止时间进行调度,在所有其他进程之前完成。 实时进程 (SCHED_FIFO, SCHED_RR) 的优先级次之,高于所有普通进程。 普通进程 (SCHED_NORMAL, SCHED_BATCH) 有相对较低的优先级。 空闲进程 (SCHED_IDLE) 有最低的优先级。 限制进程的优先级是-1。实时进程的实时优先级是 1-99,优先级数值越大,表示优先级 越高。普通进程的静态优先级是 100-139,优先级数值越小,表示优先 级越高,可通过修改 nice 值改变普通进程的优先级,优先级等于 120 加上 nice 值。 优先级数值: 限期进程:优先级由截止时间决定,截止时间越早,逻辑优先级越高。 实时进程:优先级范围是 1 到 99。数值越大,优先级越高。 普通进程:静态优先级范围是 100 到 139。数值越小,优先级越高。普通进程的优先级可以修改 nice 值来调整。nice 值的范围是 -20 到 +19。静态优先级计算公式:静态优先级 = 120 + nice 值。 空闲进程:优先级最低,内部表示为 -1。 prio是调度优先级,数值越小,优先级越高;多数情况为normal_prio。 优先级 限期进程 普通进程 实时进程 prio normal_prio normal_prio normal_prio static_prio 0 120 加上 nice 值,数值越小优先级越高。 0 normal_prio -1 static_prio 99至rt_priority rt_priority 0 0 1至99,数值越大优先级越高 在 task_struct 结构体中,4 个成员和优先级有关如下: // include/linux/sched.hint prio;int static_prio;int normal_prio;unsigned int rt_priority;AI写代码三、公平调度 CFS 与其它调度3.1、调度类Linux 内核 sched_class 调度器有五种类型: dl_sched_class:限期调度类。 rt_sched_class:实时调度类。 stop_sched_class:停机调度类。 idle_sched_c lass。 fair_sched_class。 其中每种调度类都有自己的调度策略。主要是为方便添加新的调度策略 ,Linux 内核抽象一 个调 度类 sched_class。其调试器类型源码如下(kernel/sched/sched.h): extern const struct sched_class dl_sched_classextern const struct sched_class rt_sched_classextern const struct sched_class stop_sched_classextern const struct sched_class idle_sched_c lassextern const struct sched_class fair_sched_classAI写代码stop_sched_class调度类可以抢占其他进程,其他进程不能抢占它;停机调度类是指将处理器停下来做更加紧急的工作,只有迁移线程属于停机调度类,每个CPU有一个迁移线程cpu_id。 dl_sched_class调度类使用红黑树将进程按照绝对截至限期从小到大排序,每次调度的时间选择绝对截止限期最小的进程。 rt_sched_class将每个调度优先级维护一个队列,通过位图bitmap能够快速查找第一个非空队列;DECLARE_BITMAP(bitmap,MAX_RT_PRIO+1)。 调度类优先级,由高到低进行排列:停机调度类>限期调度类> 实时调度类>公平调度类>空闲调度类。 修改时间片(默认是5ms):/proc/sys/kernel/sched_rt_timeslice_ms 3.2、公平调度类 CFS公平调度类应用完全公平调度算法,丢掉时间片和固定调度周 期,在此引入虚拟运行时间。vruntime 的计算公式如下: 虚拟运行时间(vruntime)=实际运行时间*nice0 权重值/进程权重值。 /* * Nice levels are multiplicative, with a gentle 10% change for every * nice level changed. I.e. when a CPU-bound task goes from nice 0 to * nice 1, it will get ~10% less CPU time than another CPU-bound task * that remained on nice 0. * * The "10% effect" is relative and cumulative: from _any_ nice level, * if you go up 1 level, it's -10% CPU usage, if you go down 1 level * it's +10% CPU usage. (to achieve that we use a multiplier of 1.25. * If a task goes up by ~10% and another task goes down by ~10% then * the relative distance between them is ~25%.) */const int sched_prio_to_weight[40] = { /* -20 */ 88761, 71755, 56483, 46273, 36291, /* -15 */ 29154, 23254, 18705, 14949, 11916, /* -10 */ 9548, 7620, 6100, 4904, 3906, /* -5 */ 3121, 2501, 1991, 1586, 1277, /* 0 */ 1024, 820, 655, 526, 423, /* 5 */ 335, 272, 215, 172, 137, /* 10 */ 110, 87, 70, 56, 45, /* 15 */ 36, 29, 23, 18, 15,};AI写代码 完全公平调度算法使用红黑树将进程按照虚拟运行时间从小到大排序,每次调度的时候选择虚拟运行时间最小的那个进程。 进程时间片=调度周期*进程权重 / 运行队列中全部进程的权重之和。 3.3、运行队列每个处理器有一个运行队列,结构体是 rq,定义的全局变量如下: DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);AI写代码rq 是描述就绪队列,其设计是为每一个 CPU 就绪队列,本地进程在 本地队列上排序。 当一个进程被调度到某个处理器上运行时,它会被添加到该处理器的运行队列的末尾。当处理器空闲时,它会从运行队列的头部选择一个进程来运行。 每个运行队列都有一个时间片,这是进程在该处理器上运行之前允许运行的最大时间量。时间片通常为 10 毫秒,但可以根据需要进行配置。 当一个进程用完它的时间片时,它会被移到运行队列的末尾。如果运行队列中还有其他进程,则处理器会从运行队列的头部选择下一个进程来运行。 如果运行队列为空,则处理器将进入空闲状态。当新的进程被调度到该处理器上运行时,处理器将从空闲状态唤醒。 通过使用每个处理器自己的运行队列,Linux 内核可以确保每个处理器上的进程公平地获得 CPU 时间。它还可以防止单个进程独占一个处理器,从而导致其他进程饥饿。 3.4、调度进程主动调度进程的函数是 schedule() ,它会把主要工作委托给 __schedule()去处理。 asmlinkage __visible void __sched schedule(void){struct task_struct *tsk = current;//获取当前进程 sched_submit_work(tsk);//防止进程睡眠时发送死锁do {preempt_disable();//关闭内核抢占__schedule(false);//执行调度的核心函数处理细节sched_preempt_enable_no_resched();//开启内核抢占} while (need_resched());sched_update_worker(tsk);}EXPORT_SYMBOL(schedule);AI写代码 函数__shcedule 的主要处理过程如下: 调用 pick_next_task()以选择下一个进程。 调用 context_switch()以切换进程。 /* * __schedule() is the main scheduler function. * * The main means of driving the scheduler and thus entering this function are: * * 1. Explicit blocking: mutex, semaphore, waitqueue, etc. * * 2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return * paths. For example, see arch/x86/entry_64.S. * * To drive preemption between tasks, the scheduler sets the flag in timer * interrupt handler scheduler_tick(). * * 3. Wakeups don't really cause entry into schedule(). They add a * task to the run-queue and that's it. * * Now, if the new task added to the run-queue preempts the current * task, then the wakeup sets TIF_NEED_RESCHED and schedule() gets * called on the nearest possible occasion: * * - If the kernel is preemptible (CONFIG_PREEMPTION=y): * * - in syscall or exception context, at the next outmost * preempt_enable(). (this might be as soon as the wake_up()'s * spin_unlock()!) * * - in IRQ context, return from interrupt-handler to * preemptible context * * - If the kernel is not preemptible (CONFIG_PREEMPTION is not set) * then at the next: * * - cond_resched() call * - explicit schedule() call * - return from syscall or exception to user-space * - return from interrupt-handler to user-space * * WARNING: must be called with preemption disabled! */static void __sched notrace __schedule(bool preempt){struct task_struct *prev, *next;unsigned long *switch_count;struct rq_flags rf;struct rq *rq;int cpu; cpu = smp_processor_id();rq = cpu_rq(cpu);prev = rq->curr; schedule_debug(prev, preempt); if (sched_feat(HRTICK))hrtick_clear(rq); local_irq_disable();rcu_note_context_switch(preempt); /** Make sure that signal_pending_state()->signal_pending() below* can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)* done by the caller to avoid the race with signal_wake_up().** The membarrier system call requires a full memory barrier* after coming from user-space, before storing to rq->curr.*/rq_lock(rq, &rf);smp_mb__after_spinlock();/* Promote REQ to ACT */rq->clock_update_flags <<= 1;update_rq_clock(rq);switch_count = &prev->nivcsw;if (!preempt && prev->state) {if (signal_pending_state(prev->state, prev)) {prev->state = TASK_RUNNING;} else {deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);if (prev->in_iowait) {atomic_inc(&rq->nr_iowait);delayacct_blkio_start();}}switch_count = &prev->nvcsw;}next = pick_next_task(rq, prev, &rf);clear_tsk_need_resched(prev);clear_preempt_need_resched();if (likely(prev != next)) {rq->nr_switches++;/** RCU users of rcu_dereference(rq->curr) may not see* changes to task_struct made by pick_next_task().*/RCU_INIT_POINTER(rq->curr, next);/** The membarrier system call requires each architecture* to have a full memory barrier after updating* rq->curr, before returning to user-space.** Here are the schemes providing that barrier on the* various architectures:* - mm ? switch_mm() : mmdrop() for x86, s390, sparc, PowerPC.* switch_mm() rely on membarrier_arch_switch_mm() on PowerPC.* - finish_lock_switch() for weakly-ordered* architectures where spin_unlock is a full barrier,* - switch_to() for arm64 (weakly-ordered, spin_unlock* is a RELEASE barrier),*/++*switch_count;trace_sched_switch(preempt, prev, next);/* Also unlocks the rq: */rq = context_switch(rq, prev, next, &rf);} else {rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);rq_unlock_irq(rq, &rf);}balance_callback(rq);}/* * context_switch - switch to the new MM and the new thread's register state. */static __always_inline struct rq *context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next, struct rq_flags *rf){prepare_task_switch(rq, prev, next); /** For paravirt, this is coupled with an exit in switch_to to* combine the page table reload and the switch backend into* one hypercall.*/arch_start_context_switch(prev); /** kernel -> kernel lazy + transfer active* user -> kernel lazy + mmgrab() active** kernel -> user switch + mmdrop() active* user -> user switch*/if (!next->mm) { // to kernelenter_lazy_tlb(prev->active_mm, next); next->active_mm = prev->active_mm;if (prev->mm) // from usermmgrab(prev->active_mm);elseprev->active_mm = NULL;} else { // to usermembarrier_switch_mm(rq, prev->active_mm, next->mm);/** sys_membarrier() requires an smp_mb() between setting* rq->curr / membarrier_switch_mm() and returning to userspace.** The below provides this either through switch_mm(), or in* case 'prev->active_mm == next->mm' through* finish_task_switch()'s mmdrop().*/switch_mm_irqs_off(prev->active_mm, next->mm, next);if (!prev->mm) { // from kernel/* will mmdrop() in finish_task_switch(). */rq->prev_mm = prev->active_mm;prev->active_mm = NULL;}}rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);prepare_lock_switch(rq, next, rf);/* Here we just switch the register state and the stack. */switch_to(prev, next, prev);barrier();return finish_task_switch(prev);}AI写代码 (1)切 换 用 户 虚 拟 地 址 空 间 , ARM64 架 构 使 用 默 认 的switch_mm_irqs_off,其内核源码定义如下: // include/linux/mmu_context.h /* SPDX-License-Identifier: GPL-2.0 */#ifndef _LINUX_MMU_CONTEXT_H#define _LINUX_MMU_CONTEXT_H #include <asm/mmu_context.h> struct mm_struct; void use_mm(struct mm_struct *mm);void unuse_mm(struct mm_struct *mm); /* Architectures that care about IRQ state in switch_mm can override this. */#ifndef switch_mm_irqs_off# define switch_mm_irqs_off switch_mm#endif #endifAI写代码 switch_mm 函数内核源码处理如下: // arch/arm64/include/asm/mmu_context.h static inline void __switch_mm(struct mm_struct *next){unsigned int cpu = smp_processor_id(); /** init_mm.pgd does not contain any user mappings and it is always* active for kernel addresses in TTBR1. Just set the reserved TTBR0.*/if (next == &init_mm) {cpu_set_reserved_ttbr0();return;} check_and_switch_context(next, cpu);} static inline voidswitch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk){if (prev != next)__switch_mm(next); /** Update the saved TTBR0_EL1 of the scheduled-in task as the previous* value may have not been initialised yet (activate_mm caller) or the* ASID has changed since the last run (following the context switch* of another thread of the same process).*/update_saved_ttbr0(tsk, next);}AI写代码 (2)切 换 寄 存 器 , 宏 switch_to 把 这 项 工 作 委 托 给 函 数__switch_to: // include/asm-generic/switch_to.h #ifndef __ASM_GENERIC_SWITCH_TO_H#define __ASM_GENERIC_SWITCH_TO_H #include <linux/thread_info.h> /* * Context switching is now performed out-of-line in switch_to.S */extern struct task_struct *__switch_to(struct task_struct *, struct task_struct *); #define switch_to(prev, next, last) \do { \((last) = __switch_to((prev), (next))); \} while (0) #endif /* __ASM_GENERIC_SWITCH_TO_H */(arch/arm64/kernel/process.c) /* * Thread switching. */__notrace_funcgraph struct task_struct *__switch_to(struct task_struct *prev,struct task_struct *next){struct task_struct *last; fpsimd_thread_switch(next);tls_thread_switch(next);hw_breakpoint_thread_switch(next);contextidr_thread_switch(next);entry_task_switch(next);uao_thread_switch(next);ptrauth_thread_switch(next);ssbs_thread_switch(next); /** Complete any pending TLB or cache maintenance on this CPU in case* the thread migrates to a different CPU.* This full barrier is also required by the membarrier system* call.*/dsb(ish); /* the actual thread switch */last = cpu_switch_to(prev, next); return last;}AI写代码 3.5、调度时机调度进程的时机: 进程主动调用 schedule()函数。 周期性地调度,抢占当前进程,强迫当前进程让出处理器。 唤醒进程的时候,被唤醒的进程可能抢占当前进程。 创建新进程的时候,新进程可能抢占当前进程。 (1)主动调度 :进程在用户模式下运行,无法直接调用 schedule()函数,只能通过系统调用进入内核模式,如果系统调用需要等待某个资源,如互斥锁或信号量,就会把进程的状态设置为睡眠状态,然后调用schedule()函数来调度进程。 (2)周期调度 :周期调度的函数为 scheduler_tick(),调用当前进程所属调度类task_tick()方法。 四、RCU机制与内存屏障(1)RCU(read-copy update)为 Linux 当中的一种同步机制,则为读/拷贝更新。 写者修改对象流程:先复制生成一个副本,然后更新这个副本,最后使用新的对象替换旧的对象,在写者执行复制的时候读者则执行可以读数据。 写者删除对象时,必须等待所有访问对象的访问者都访问结束了才能删除对象。等待所有读者访问结束的时间叫做宽期限。 RCU的读者没有任何的同步开销,不需要获取任何的锁,也不需要执行延迟指令,也不需要执行内存屏障;但是写者的同步开销非常大,需要延迟对象释放时间,复制被修改的对象,写者直接必须使用锁;从某个意义来说也算是RCU的缺点。 (2)内存屏障 :保证内存访问的顺序方法,用来解决编译器编码的时候可能会重新排序汇编指令。 因为为了使编译出来的程序在CPU上运行更快,有时候优化结果可能不符合程序的要求;现在的CPU采用超标量的一个体系结构和lanch技术,能够一个时钟并行执行很多条指令。 内存屏障可分为两种类型:编译器内存屏障和 CPU 内存屏障。 内核支持三种内存屏障:内存映射I/O写屏障、编译器屏障、处理器屏障。 五、总结Linux 内核调度机制是复杂、强大的系统,对进程行为进行精细的控制。理解进程优先级、调度策略和调度类,可以优化系统满足特定应用程序和工作负载的需求。———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/mr_yuanshen/article/details/152028702
-
1. 初识文件系统:硬盘的"整理术"想象你有一个巨大的仓库(硬盘),要存放各种箱子(文件)。如果随便堆放在一起,找东西会很麻烦。文件系统就是给仓库画格子、贴标签的"整理术",让每个文件都有自己的位置和编号。1.1 分区:给仓库画格子硬盘需要先分成多个分区(类似仓库的不同区域),每个分区独立管理。分区后还需要格式化——写入文件系统的"管理规则",包括:数据块(存放文件内容的4KB小格子)inode(文件的身份证,记录属性和数据块位置)超级块(整个分区的"户口本")1.2 数据块:文件内容的"集装箱"所有文件内容都存在数据块(Data Blocks)中,每个块固定4KB大小。小文件可能占1块,大文件会占用多个块,这些块可以分散在磁盘的不同位置。 2. inode:文件的"身份证"每个文件都有一个inode(索引节点),相当于"身份证",包含:文件大小、权限、创建时间等属性指向数据块的指针(告诉系统去哪里找内容)2.1 inode的关键特性唯一编号:每个分区内inode编号唯一,类似身份证号跨区编号:inode和数据块在整个分区内统一编号(如1-10000)不能跨分区:inode编号只在本分区有效,就像小区门牌号不能跨小区使用2.2 查看inode的命令ls -i filename # 查看文件的inode编号stat filename # 查看inode详细属性执行效果:12345 filename # ls -i输出,12345是inode编号3. 目录:文件名的"通讯录"你可能会问:文件名存在哪里?答案是:目录里。目录本身也是一种特殊文件,它的inode指向的数据块中,存储着"文件名→inode编号"的映射表,就像通讯录记录"姓名→电话"。3.1 目录的inode和数据块目录的数据块内容示例:. → inode 100 (当前目录自身).. → inode 50 (父目录)file1.txt → inode 12345doc/ → inode 678903.2 路径解析:从"/"到文件找文件的过程就像查地图:从根目录(/)开始,根目录的inode编号是固定的(通常是2)逐层解析路径(如/home/user/file.txt)每个目录的"通讯录"找到下一级目录/文件的inode最终通过目标文件的inode找到数据块4. 超级块(Super Block):文件系统的"户口本"超级块存储整个分区的关键信息:总块数、空闲块数、inode总数块大小、inode大小、挂载时间4.1 为什么需要备份超级块?超级块一旦损坏,整个分区的数据可能丢失!因此系统会在多个分组中备份超级块,就像重要文件多存几份副本。 4.2 查看超级块信息dumpe2fs /dev/sda1 | grep -i superblock # 查看ext系列文件系统的超级块5. 挂载:给分区"安个门牌号"硬盘分区就像未开封的快递箱,需要挂载到一个目录(挂载点)才能使用,这个目录就成了分区的"门牌号"。5.1 挂载步骤示例# 1. 创建挂载点目录mkdir /mnt/mydisk# 2. 将/dev/sdb1分区挂载到/mnt/mydiskmount /dev/sdb1 /mnt/mydisk# 3. 查看挂载情况df -h # 显示分区使用情况5.2 开机自动挂载修改/etc/fstab文件(需root权限):# 格式:设备路径 挂载点 文件系统类型 选项 备份 自检/dev/sdb1 /mnt/mydisk ext4 defaults 0 2AI写代码bash126. 软硬链接:文件的"分身术"Linux有两种链接方式,就像文件的不同分身术:6.1 软链接(符号链接):快捷方式相当于Windows的"快捷方式",有独立inode内容存储目标文件的路径,删除原文件后链接失效ln -s 原文件路径 软链接名 # 创建软链接ln -s /home/user/file.txt link.txt6.2 硬链接:文件别名没有独立inode,只是给原文件新增一个文件名(共享inode)原文件删除后,别名仍可访问(需引用计数>0)ln 原文件路径 硬链接名 # 创建硬链接ln file.txt hardlink.txt6.3 链接数的奥秘文件的"链接数"就是指向该inode的文件名数量目录默认链接数是2(.和..),创建子目录后会增加ls -l # 查看链接数(第2列数字)# 示例输出:drwxr-xr-x 2 user user 4096 目录(链接数2)7. 内核的小优化:dentry缓存操作系统为了加快文件查找速度,会把常用目录的"文件名→inode"映射缓存到内存中,这就是dentry结构体(目录项缓存)。作用:第二次查找同一目录时,直接从内存读取,无需访问磁盘验证:find命令第一次慢、第二次快,就是因为dentry缓存生效总结:文件系统的核心逻辑存储三要素:分区(容器)、inode(身份证)、数据块(内容箱)查找流程:路径→目录→inode→数据块关键技术:挂载(分区入门)、链接(文件分身)、缓存(加速访问)通过这些机制,Linux能高效管理成千上万的文件,即使你每天用ls、cd命令,背后都藏着这些精妙的设计!1.3 磁盘物理结构与分区表硬盘的物理结构包括盘片、磁道和扇区,每个扇区通常为512字节。分区是将磁盘划分为独立区域的过程,常见的分区表有两种:MBR分区表:最多支持4个主分区每个分区最大容量2TB位于磁盘的第一个扇区(512字节)GPT分区表:支持无限多个分区(取决于操作系统,通常256个)支持大于2TB的分区有备份分区表,提高可靠性使用gdisk工具创建GPT分区表的界面,支持大磁盘和多分区实战代码:创建GPT分区# 查看磁盘信息fdisk -l /dev/sdb# 使用gdisk创建GPT分区表gdisk /dev/sdb# 按提示操作:o(新建GPT)→ n(新建分区)→ 回车(默认起始扇区)→ +10G(分区大小)→ w(保存)2.3 inode的15个指针inode包含15个指针,决定了文件能使用的数据块:前12个直接指针:每个指向一个数据块(4KB),直接访问小文件(≤48KB)一级间接指针:指向一个"指针块",可存储1024个数据块地址(4KB/4B=1024),支持4MB二级间接指针:指向一个"指针块的指针块",支持4GB三级间接指针:指向一个"二级指针块的指针块",支持4TBinode的多级指针结构,使小文件快速访问,大文件无限扩展计算最大文件大小:block_size = 4096 # 4KB/块direct = 12 * block_sizesingle = (block_size // 4) * block_size # 1024块double = (block_size // 4) ** 2 * block_size # 1024²块triple = (block_size // 4) ** 3 * block_size # 1024³块max_size = direct + single + double + tripleprint(f"最大文件大小: {max_size / (1024**4):.2f} TB") # 输出:4.00 TB3.3 块组的6大组成部分每个块组就像一个独立的"小区",包含:Super Block:小区总览图GDT(组描述符表):每个块组的详细信息inode位图:记录哪些inode已使用数据块位图:记录哪些数据块已使用inode表:存储inode的具体内容数据块:实际存储文件内容块组的6个组成部分,每个部分负责不同的管理功能空闲块管理:数据块位图使用1位表示一个块的状态:0:空闲(可用)1:已占用例如,一个4KB的位图可以管理32768个数据块(4KB×8=32768位),轻松定位空闲块。4.3 目录项缓存(dentry cache)Linux内核为加速路径解析,将常用目录项缓存在内存中,称为dentry缓存:结构:哈希表+链表,支持快速查找和LRU淘汰命中率:缓存命中率高时,文件访问速度显著提升dentry结构体包含指向inode的指针和目录项关系验证缓存存在:# 第一次执行,无缓存,较慢time find / -name "passwd"# 第二次执行,利用缓存,较快time find / -name "passwd"5.3 VFS:虚拟文件系统VFS是Linux的"文件系统翻译官",使不同文件系统(ext4、xfs等)呈现统一接口:核心对象:超级块对象、inode对象、文件对象、目录项对象作用:用户无需关心底层文件系统类型,统一使用open/read/write等系统调用VFS位于用户空间和具体文件系统之间,提供统一接口安全挂载选项:# 以只读方式挂载U盘,防止病毒写入mount -o ro /dev/sdb1 /mnt/usb# 禁止在分区上执行程序,提高安全性mount -o noexec /dev/sdb1 /mnt/usb6.3 硬链接数实验创建硬链接后,inode的链接数会增加:# 创建文件,初始链接数1touch file.txtls -li file.txt # 输出:12345 -rw-r--r-- 1 user user ... file.txt# 创建硬链接,链接数变为2ln file.txt link.txtls -li file.txt link.txt # 两者inode相同,链接数2# 删除原文件,链接数变为1,link.txt仍可访问rm file.txtcat link.txt # 仍能读取内容硬链接创建后,链接数从1变为2,删除原文件后链接数减为17.1 Super Block损坏修复Super Block损坏会导致分区无法挂载,可使用备份恢复:# 查看分区的超级块备份位置mke2fs -n /dev/sda1 # 模拟格式化,显示超级块备份信息# 使用备份超级块修复e2fsck -b 32768 /dev/sda1 # 32768是备份超级块的块地址Super Block在多个块组中备份,确保损坏后可恢复———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/wheeldown/article/details/151972370
-
什么是自定义协议?自定义协议 是指根据应用程序的需求,开发者定义的一种数据格式或传输规则。在网络通信中,协议用于描述双方通信时的数据结构、数据格式、传输顺序等。定义:自定义协议是开发者为特定应用或场景设计的协议,用来满足应用之间的数据交换需求。与标准协议(如HTTP、TCP/IP)不同,自定义协议不依赖于任何标准,而是根据具体需求自行设计。用途:例如,在一个客户端与服务器之间进行通信时,客户端发送特定格式的数据请求,服务器根据预定义的协议格式解析并返回响应数据。自定义协议的设计通常需要考虑数据的序列化、反序列化、传输效率和安全性。示例:比如,设计一个简单的数学计算协议,客户端发送请求(比如两个数字和一个运算符),服务器进行处理,返回结果。自定义协议在此场景中的格式可以是:数字1 运算符 数字2。// 请求结构struct Request { int num1; int num2; char op; // 操作符(如 +,-,*,/)}; // 响应结构struct Response { int result; int code; // 0: 成功,1: 错误};AI写代码cpp运行通信双方根据这个格式进行数据的传递和解析,即构成了自定义协议.自定义协议 是属于 应用层 的概念。应用层 是 OSI 模型中的最上层,也是计算机网络协议栈中最接近用户的层。它定义了应用程序之间如何交换数据,涵盖了各种应用协议,如 HTTP、FTP、SMTP,以及你提到的自定义协议。自定义协议是应用层的一部分,通常由应用程序开发者定义,用于特定应用的需求。自定义协议可以定义数据格式、通信规则、如何打包和解包数据等。它通常使用其他协议(如 TCP/IP、UDP)来实现数据的传输。说到自定义协议,我们还要谈到序列化和反序列化这两个概念。序列化与反序列化简介序列化 和 反序列化 是计算机程序在处理数据传输和存储时常用的两个概念。它们用于将数据从一种格式转化为另一种格式,通常在网络通信、文件存储和跨平台数据交换中广泛应用。1. 序列化(Serialization)序列化 是将程序中的对象、数据结构或内存中的数据转换为可存储或传输的格式的过程。常见的格式有 JSON、XML、二进制数据流等。目的:将内存中的复杂数据结构转换为字节流或其他格式,使其能够被存储在文件、数据库中或通过网络传输。应用场景:网络通信:将对象或数据结构转化为字节流或字符串,在客户端与服务器之间传输。数据持久化:将对象序列化为文件或数据库记录,便于后续读取。跨平台通信:将数据转换为跨平台通用格式,支持不同系统之间的通信。2. 反序列化(Deserialization)反序列化 是将序列化后的数据(如字节流或字符串)转换回程序能够理解的对象、数据结构或实体的过程。目的:将传输或存储的字节流转换回原始数据结构,使程序可以对其进行操作。应用场景:网络通信:将接收到的数据字节流还原为原始对象,供程序进一步处理。数据恢复:从存储中恢复对象数据,供程序重新使用。将数据转化为字节流而不是直接传输结构体本身,主要是出于以下几个原因:跨平台兼容性:不同平台的内存布局差异会导致直接传输结构体时的兼容性问题。灵活性:序列化为字节流或标准化格式,可以提高程序的灵活性、可扩展性,并支持版本控制。网络传输效率:通过序列化,你可以优化数据的大小,减少带宽浪费,并更容易适应不同的网络协议。调试与可读性:序列化格式(如 JSON)更加易于调试和查看,对于错误处理和日志记录也更为方便。安全性:序列化后的数据更容易进行验证,保证数据的完整性和安全性。TCP的全双工通信总结全双工通信 是指通信双方可以同时进行数据的发送和接收。在 TCP(传输控制协议)中,全双工通信 是其核心特性之一,支持客户端和服务器之间在同一个连接上同时发送和接收数据。 TCP全双工通信的关键要点:独立的发送与接收缓冲区:每一端(客户端和服务器)都有两个独立的缓冲区:一个用于发送数据(发送缓冲区),另一个用于接收数据(接收缓冲区)。发送和接收操作互不干扰,可以同时进行。同时发送和接收数据:在一个TCP连接上,客户端和服务器可以同时发送和接收数据。例如,客户端可以在发送请求的同时接收服务器的响应,服务器也可以在接收客户端请求时,立即返回数据。数据流双向传输:TCP连接中的数据流是双向的。每个方向上都可以独立进行数据传输,而不会相互阻塞。基于缓冲区的管理:发送数据时,数据从应用层的缓冲区拷贝到 发送缓冲区,然后通过网络传输给接收方。接收方的数据从网络传输到接收缓冲区,应用层可以从接收缓冲区读取数据。这两个缓冲区(发送缓冲区和接收缓冲区)的独立性保证了全双工通信的流畅性。流量控制与拥塞控制:TCP采用 流量控制 和 拥塞控制,确保在通信过程中,双方的发送速率匹配,避免数据丢失或网络拥塞。阻塞I/O操作:TCP的I/O函数(如 read 和 write)通常是阻塞的,确保数据的同步处理。发送和接收操作在每个缓冲区内独立进行,但为了避免数据冲突或丢失,它们通过阻塞机制进行同步。网络版计算器例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.约定方案一:客户端发送一个形如"1+1"的字符串;这个字符串中有两个操作数, 都是整形;两个数字之间会有一个字符是运算符, 运算符只能是 + ;数字和运算符之间没有空格;约定方案二:定义结构体来表示我们需要交互的信息;发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;这个过程叫做 "序列化" 和“反序列化”。1.Socket.hpp#pragma once #include <iostream>#include <string>#include <stdint.h>#include <unistd.h>#include <sys/types.h> /* See NOTES */#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <string.h>#include "log.hpp" const int backlog =5;extern Log lg; enum{ SOCKERR = 1, BINDERR = 2, LISTENERR}; class Sock{public: Sock() { } ~Sock() { } public: void Socket() { sockfd_ = socket(AF_INET, SOCK_STREAM, 0); if (sockfd_ < 0) { lg("fatal", "socket create fail errno:%d strerrno:%s", errno, strerror(errno)); exit(1); } } void Bind(uint16_t port) { struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(port); local.sin_addr.s_addr=INADDR_ANY; socklen_t len = sizeof(local); if (bind(sockfd_, (struct sockaddr *)&local, len) < 0) { lg("fatal", "bind fail errno:%d,strerror:%s\n", errno, strerror(errno)); exit(BINDERR); } } void Listen() // 变为监听窗口,错了就别玩了 { if (listen(sockfd_, backlog) < 0) { lg("fatal", "Listen fail errno:%d,strerror:%s\n", errno, strerror(errno)); exit(LISTENERR); } } int Accpect(string*ip,uint16_t* port)//输出型参数,谁链接的我,对方的端口,ip { struct sockaddr_in temp; socklen_t len=sizeof(temp); int newfd=accept(sockfd_,(struct sockaddr*)&temp,&len); if(newfd<0) { lg("fatal", "accpect fail errno:%d,strerror:%s\n", errno, strerror(errno)); return -1; } *port=ntohs(temp.sin_port); string ipstr=inet_ntoa(temp.sin_addr); *ip=ipstr; return newfd; } bool Connect(string& ip,uint16_t port)//服务器的端口,ip { struct sockaddr_in server; bzero(&server,0); server.sin_family=AF_INET; server.sin_port=htons(port); server.sin_addr.s_addr=inet_addr(ip.c_str()); int ret=connect(sockfd_,(struct sockaddr*)&server,sizeof(server)); if(ret<0) { lg("info", "Connect fail errno:%d,strerror:%s\n", errno, strerror(errno)); return false; } return true; } void Close() { close(sockfd_); }public: int sockfd_;};2.Protocal.hpp#pragma once #include <iostream>#include <string>#include <jsoncpp/json/json.h> // #define Myself 1 //编译的时候可以 用-DMyself using namespace std; const string blank_sep = " ";const string protocal_sep = "\n"; //"x op y" ->//"len"/n"x op y"/nstd::string Encode(string &content){ int len = content.size(); string package = to_string(len); package += protocal_sep; package += content; package += protocal_sep; return package;} //"len"/n"x op y"/n -> 解析可能错误//"x op y"bool Decode(string &package, string *content){ int pos = package.find(protocal_sep); if (pos == string::npos) return false; string len_str = package.substr(0, pos); int len = stoi(len_str); int to_len = len_str.size() + 2 + len; if (package.size() < to_len) return false; *content = package.substr(pos + 1, len); // 位置 加 长度 package.erase(0, to_len); // 解析的这一段,从缓冲区里去掉 return true;} class Request{public: Request(int x, int y, char op) : x_(x), y_(y), op_(op) { } Request() { } ~Request() { } public: bool Serialize(string *content) // 序列化为 字节流 {#ifdef Myself string s = to_string(x_); s += blank_sep; s += op_; s += blank_sep; s += to_string(y_); *content = s; return true;#else Json::Value root; root["x"] = x_; root["y"] = y_; root["op"] = op_; Json::FastWriter w; *content = w.write(root); return true;#endif } bool Deserialize(string &in) // 反序列化 将字节流解析 {#ifdef Myself int l = in.find(blank_sep); if (l == string::npos) return false; string x = in.substr(0, l); // 左闭,右开 int r = in.rfind(blank_sep); if (r == string::npos) return false; string y = in.substr(r + 1); if (l + 2 != r) return false; x_ = stoi(x); y_ = stoi(y); op_ = in[l + 1]; return true;#else Json::Reader R; Json::Value root; bool ret = R.parse(in.c_str(), root); if (!ret) return false; x_ = root["x"].asInt(); y_ = root["y"].asInt(); op_ = root["op"].asInt(); return true; #endif } void Printf() { cout << "任务请求" << x_ << op_ << y_ << "=?" << endl; } public: int x_; int y_; char op_;}; class Respone{public: Respone(int res, int c) : result_(res), code_(c) { } Respone() { } ~Respone() { } public: bool Serialize(string *out) // 序列化 形成字节流 {#ifdef Myself string s = to_string(result_); s += blank_sep; s += to_string(code_); *out = s; return true;#else Json::Value root; root["result"] = result_; root["code"] = code_; Json::FastWriter w; *out = w.write(root); return true;#endif } bool Deserialize(string &in) // 反序列化 字节流解析 {#ifdef Myself int pos = in.find(blank_sep); if (pos == string::npos) return false; string x = in.substr(0, pos); int result = stoi(x); string y = in.substr(pos + 1); int code = stoi(y); result_ = result; code_ = code; return true;#else Json::Reader R; Json::Value root; bool ret = R.parse(in.c_str(), root); if (!ret) return false; result_=root["result"].asInt(); code_=root["code"].asInt(); return true;#endif } void Printf() { cout << "result " << result_ << " code " << code_ << endl; } public: int result_; int code_;};3.Server.cal#pragma once #include <iostream>#include <string>#include <jsoncpp/json/json.h> // #define Myself 1 //编译的时候可以 用-DMyself using namespace std; const string blank_sep = " ";const string protocal_sep = "\n"; //"x op y" ->//"len"/n"x op y"/nstd::string Encode(string &content){ int len = content.size(); string package = to_string(len); package += protocal_sep; package += content; package += protocal_sep; return package;} //"len"/n"x op y"/n -> 解析可能错误//"x op y"bool Decode(string &package, string *content){ int pos = package.find(protocal_sep); if (pos == string::npos) return false; string len_str = package.substr(0, pos); int len = stoi(len_str); int to_len = len_str.size() + 2 + len; if (package.size() < to_len) return false; *content = package.substr(pos + 1, len); // 位置 加 长度 package.erase(0, to_len); // 解析的这一段,从缓冲区里去掉 return true;} class Request{public: Request(int x, int y, char op) : x_(x), y_(y), op_(op) { } Request() { } ~Request() { } public: bool Serialize(string *content) // 序列化为 字节流 {#ifdef Myself string s = to_string(x_); s += blank_sep; s += op_; s += blank_sep; s += to_string(y_); *content = s; return true;#else Json::Value root; root["x"] = x_; root["y"] = y_; root["op"] = op_; Json::FastWriter w; *content = w.write(root); return true;#endif } bool Deserialize(string &in) // 反序列化 将字节流解析 {#ifdef Myself int l = in.find(blank_sep); if (l == string::npos) return false; string x = in.substr(0, l); // 左闭,右开 int r = in.rfind(blank_sep); if (r == string::npos) return false; string y = in.substr(r + 1); if (l + 2 != r) return false; x_ = stoi(x); y_ = stoi(y); op_ = in[l + 1]; return true;#else Json::Reader R; Json::Value root; bool ret = R.parse(in.c_str(), root); if (!ret) return false; x_ = root["x"].asInt(); y_ = root["y"].asInt(); op_ = root["op"].asInt(); return true; #endif } void Printf() { cout << "任务请求" << x_ << op_ << y_ << "=?" << endl; } public: int x_; int y_; char op_;}; class Respone{public: Respone(int res, int c) : result_(res), code_(c) { } Respone() { } ~Respone() { } public: bool Serialize(string *out) // 序列化 形成字节流 {#ifdef Myself string s = to_string(result_); s += blank_sep; s += to_string(code_); *out = s; return true;#else Json::Value root; root["result"] = result_; root["code"] = code_; Json::FastWriter w; *out = w.write(root); return true;#endif } bool Deserialize(string &in) // 反序列化 字节流解析 {#ifdef Myself int pos = in.find(blank_sep); if (pos == string::npos) return false; string x = in.substr(0, pos); int result = stoi(x); string y = in.substr(pos + 1); int code = stoi(y); result_ = result; code_ = code; return true;#else Json::Reader R; Json::Value root; bool ret = R.parse(in.c_str(), root); if (!ret) return false; result_=root["result"].asInt(); code_=root["code"].asInt(); return true;#endif } void Printf() { cout << "result " << result_ << " code " << code_ << endl; } public: int result_; int code_;};4.Tcpserver.hpp#pragma once#include <functional>#include <signal.h>#include "log.hpp"#include "Socket.hpp"#include "ServerCal.hpp"#include "Protocal.hpp" using func_t=function<string(string&)>;extern Log lg; class Server{public: Server(uint16_t port,func_t calback):port_(port) ,calback_(calback) {} bool Init() { listensock_.Socket(); listensock_.Bind(port_); listensock_.Listen(); lg("info","init server .... done"); return true; } void Start() { signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); while(true) { string clientip; uint16_t clientport; int sockfd=listensock_.Accpect(&clientip,&clientport); if(sockfd<0) { lg("fatal","accpect fail"); break; } lg("info","accpect success"); if(fork()==0) { listensock_.Close(); string inbuffer_stream; while(true) { char buffer[1024]; int n=read(sockfd,buffer,sizeof(buffer)); if(n>0) { buffer[n]=0; inbuffer_stream+=buffer; lg("Debug", "debug:\n%s", inbuffer_stream.c_str()); while(true) { string ret=calback_(inbuffer_stream); if(ret.empty()) break; lg("Dedug","debug:\n%s",ret.c_str()); lg("Dedug","debug:\n%s",inbuffer_stream.c_str()); write(sockfd,ret.c_str(),ret.size()); } } else { cout<<"错误了"<<endl; break; } } exit(0); } close(sockfd); } } private: Sock listensock_; uint16_t port_; func_t calback_;};5.Tcpserver.cpp#include "Tcpserver.hpp"#include "ServerCal.hpp" void Usage(string prco){ cout<<prco<<" [1024++]"<<endl;} int main(int argc,char*argv[]){ if(argc!=2) { Usage(argv[0]); exit(0); } uint16_t port=stoi(argv[1]); ServerCal calculator; Server* tsrv=new Server(port,bind(&ServerCal::Calculator,&calculator,placeholders::_1));//第一个是this指针 //不用bind的话可以创建一个ServerCal的对象。 tsrv->Init(); tsrv->Start(); return 0;}的bind 是 C++11 引入的一个函数模板,用于创建一个新的可调用对象,将函数的某些参数“绑定”到具体值,然后返回一个可以稍后调用的函数。基本用法:std::bind(function, arg1, arg2, ..., argN);function:要绑定的函数。arg1, arg2, ...:绑定的参数,可以是具体值或占位符(std::placeholders)。#include <iostream>#include <functional> int add(int x, int y) { return x + y;} int main() { // 绑定第一个参数为 5,第二个参数用占位符 auto add5 = std::bind(add, 5, std::placeholders::_1); std::cout << add5(3) << std::endl; // 输出 8 (5 + 3) return 0;}简化函数调用:std::bind 可以将函数和参数绑定,生成新函数,便于后续调用。灵活性:支持部分绑定函数参数,允许在调用时提供剩余的参数。与 lambda 比较:// 使用 std::bindauto add5 = std::bind(add, 5, std::placeholders::_1); // 使用 lambdaauto add5 = [](int x) { return add(5, x); };———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/2401_83603768/article/details/151154240
-
1. 协议分层问题一:为什么一般的软件要分层?1.层与层之间的耦合度是降低的,更重要的是一但分层,就相当于一个非常大的项目被肢解成了多个子项目,这样的话,在后期的维护上成本会非常低,所以第一点就是为了方便维护2.凡是划分到同一层的内部,其中的代码和逻辑必须是强相关的—高内聚(层内部),低耦合(层与层之间)3.问题是层状的!!!为了解决相应的问题,所以设计的时候也是按分层来设计的 在现实生活中,我们打电话给别人,看似是人与人之间直接进行交流,但是在实际上,是我们载荷电话沟通,然后电话将信息传输到另一台电话上,然后才让别人给听到,但是在逻辑上,人与人是在同一层的,所以我们会认为是人与人在沟通,同层内部高内聚,层与层之间低耦合。这样的好处就在于,某一层出问题,只需要处理某一层的问题就行了,其它层不用变,即使某一层的协议发生了变化,比如,如图的第一层的语言变成了英语协议,但是其下面的电话协议根本不用变,也无法被影响,依然能够正常交流,同理如果其电话机协议变成了无线电协议,其上层的汉语协议也无需变化,只需要处理无线电这一层的协议就行!!!1.1 OSI七层模型OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范;把网络从逻辑上分为了7层. 每一层都有相关、相对应的物理设备,比如路由器,交换机;OSI 七层模型是一种框架性的设计方法,其最主要的功能使就是帮助不同类型的主机实现数据传输;它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整. 通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯;但是, 它既复杂又不实用; 所以我们按照TCP/IP四层模型来讲解.1.2 TCP/IP五层(或四层)模型TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇.TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求.物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等. 集线器(Hub)工作在物理层.数据链路层: 负责设备之间的数据帧的传送和识别. 例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作. 有以太网、令牌环网, 无线LAN等标准. 交换机(Switch)工作在数据链路层.网络层: 负责地址管理和路由选择. 例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由). 路由器(Router)工作在网路层.传输层: 负责两台主机之间的数据传输. 如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机.应用层: 负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等. 我们的网络编程主要就是针对应用层2. 操作系统与网络技术栈问题一:网络协议栈和我们之前学的操作系统有什么关系?数据链路层是软件,是在各种软件内部驱动实现的,其中的传输层和网络层是要在Linux内核中实现的在我们日常的生活中,我们也会刷视频来获取信息,这些信息是怎么来的?就是通过物理层中的网卡获取的!!!所以网络通信的本质也是在访问硬件!!!如果用户想要获取到视频资源,就需要去访问硬件,但是用户并没有资格去直接访问硬件,因为操作系统是硬件的管理者,操作系统不允许用户来访问底层硬件,操作系统并不信任用户,所以用户想要访问到硬件资源就必须要贯穿操作系统!!!用户层、传输层、网络层、数据链路层和物理层,从上往下,依次贯穿!!!网络通信的本质:就是贯穿协议栈的过程此时问题又来了,如果用户层要访问硬件资源,不能直接访问,要依次贯穿网络技术栈,但是操作系统可能让用户去访问、去贯穿网络技术栈吗?这显然不可能,那用户又是如何获取到硬件资源的呢?— ==系统调用接口!!!==通过系统调用接口,用户就可以获取到硬件资源,既然用户无法直接获取,但能让操作系统去获取,并给用户层提供一个接口,让用户层能拿到硬件资源。所以就有了利用系统调用来写出各种协议来供用户层使用!!! 大部分操作系统的内容和系统调用接口都是不一样的,但是网络协议栈是一样的!!!这是因为想要用使用网络,就必须按照网络制定的协议栈,又因为网络通信是要贯穿网络协议栈的(向上贯穿或者向下贯穿),主机1有用户层,主机2也一定会有用户层,同理任意两台主机的网络协议栈一定是是一样的!!!所以同层协议在逻辑上,就是在和对方的同层直接沟通!!!3.网络传输基本流程网络协议栈的层状结构中,由于每一层都有协议,所以每一层都要添加报头(报文 == 报头 + 有效载荷),才会将信息送到下一层。数据的自顶向下的封装的过程:1. 应用层定的协议,要在发送的信息前加一个报头(里面会有版本信息…),消息往下传递给传输层。(将要传输的内容叫做有效载荷)2. 传输层定的协议,再加入一个报头(里面会有信息的序号、源端口号和目的端口号…),消息往下传递给网络层。3. 网络层定的协议,再加入一个报头(里面会有该信息是谁发的(主机),要发生到哪里(主机)…),消息再往下传递给链路层。4. 链路层定的协议,再加入一个报头(里面的信息是从哪里发的(网卡),发送给谁的(网卡)…) 接着,报文就会交给网卡,网卡就会通过以太网,将报文发送给对方的主机的网卡上问题一:为什么远端机器拿到这个数据(报文)的时候一定是网卡先拿到数据?那是因为计算机里面只有一个网络通信的设备就叫做网卡!网卡拿到这个数据(报文)后,肯定要将数据交给操作系统,从硬件外设交到内存里。问题二:为什么网卡拿到数据后一定要将数据(报文)从硬件设备交到内存里?因为这是冯诺依曼体系规定!冯诺依曼体系规定网卡中的数据一定要从硬件设备交给内存里面当对方拿到报头后,是有办法区分报头和有效载荷的,这时候,只需要将报头取出来,就可以将剩下的有效载荷送给上层,同理,由于同层协议既然能给有效载荷添加报头,所以也能获取报头,每一层拿到报文后,都会将报头和有效载荷分离开来,层层传递,直到到了用户层,将信息传递成功。通信的过程:本质就是在不断的封装和解包的过程!除了上述的知识,不同层其实不止一种协议,从上往下其实可以看成是一个倒着的多叉树结构,每一层的协议都要考虑到底是传输给上层的哪种协议封装和解包中的问题(大部分协议的共性,未来学习具体协议的时候,都是这两个问题!!!)1. 几乎任何层的协议,都要提供一种能力,将报头和有效载荷分离的能力2. 几乎任何层的协议,都要在报头中提供,决定将自己的有效载荷交付给上层的哪一个协议只有知道了两个问题,再面对封装和解包的问题就不会困惑,因为这就是大部分协议的共性总结:本文浅谈了OSI七层网络模型和TCP/IP网络模型,主要对操作系统与网络技术栈的关系进行的探讨,并且对网络传输的基本流程进行了深刻的讲解———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/zby2791830200/article/details/151764387
-
一. 信号基本概念信号是一种用于进程间通信或系统通知进程发生特定事件的机制。它可以被视为操作系统向进程发送的“消息”,用于告知进程发生了某种异常等需要处理的事情。信号的生命周期分为三个阶段 信号产生 信号保存 信号处理。接下来我将按照这三个阶段进行一一讲解。首先我们来粗略的了解一下信号的基本概念。信号的产生方式有5种,可以由操作系统,其他进程,自身进程,硬件等不同的发送方。当信号产生之后,并不会立即作出处理,内核会先将其暂存,存入一张未决信号表当中,此时要区分对于该信号是否阻塞,待进程有空后再进行处理。当进程成功接收到信号后,此时有三种处理方式,忽略处理,默认动作,信号捕捉。 我们使用 kill -l 指令可以查看到所有的信号,我们把前三十一个称为普通信号,它们都是大写的宏,旁边的数字就是它们的对应值。 使用man 7 signal 可以查看到里边大多数信号的默认动作,以及使用的默认效果。 二. 信号的产生首先我们来认识一个函数 signalsignal:Linux 操作系统用于处理信号的一个系统调用。它允许用户指定一个函数,当接收到特定信号时进行函数调用。这样我们便可以对信号进行自定义处理。 signum:需要处理的信号类型,可以是宏定义的,也可以是对应的数字。handler:函数指针类型,用于接收需要的信号。参数要包含你一个 int 来接收信号1. 通过终端按键产生信号终端按键可以通过终端驱动程序触发特定信号,如我们熟知的 Ctrl + c ,下面我们介绍几个信号同时演示如何通过终端按键产生信号。1-1 操作演示Ctrl + c (SIGINT)会产生 2 号信号,默认会进行终止进程的处理,终止的是前台进程(死循环)。Ctrl + \ (SIGQUIT)会产生3号信号,除了终止进程,还会生成 core dump 文件,主要用于调试使用。9号信号 (SIGKILL)强制终止进程,该信号不可被捕捉,阻塞。19号信号(SIGSTOP)用于暂停进程执行信号,它相当于“冻结”进程而非终止,具有强制性。下面我们就以 2 号信号作为演示:#include <iostream>#include <unistd.h>#include <signal.h>using namespace std; void handler(int signal){ cout<<"我是信号:"<<signal<<endl;} int main(){ cout<<"我是进程:"<<getpid()<<endl; signal(2,handler); while(true) { cout<<"我是进程:"<<getpid()<<"我正在等待信号"<<endl; sleep(2); } return 0;}AI写代码cpp运行上述代码,将2号信号进行捕捉后特殊处理,我们来看效果。 当我们Ctrl + c 时,进程不会终止,而是执行了我的函数,我们只有使用 9 号信号才能对其进行终止。同理,其他按键也与其类似。1-2 理解OS如何得知键盘数据键盘和其他外部设备都属于硬件,硬件向中断控制器中发送信号,发起中断,这也就是所谓的硬件中断,控制器为其分配一个中断号,确定了唯一标识源。紧接着通知CPU,CPU得知中断获取中断号。CPU 根据中断号,结合中断向量表,执行中断处理方法。 通过上面的过程,我们可以感受到,硬件中断行为和软件中断(信号处理)有着异曲同工之处。其实,信号就是从软件角度,模拟硬件中断的行为。只不过,硬件中断是发送给CPU,软件中断发送给进程。2. 调用系统命令向进程发送信号我们可以通过 kill -信号 进程id 的方法,来对进程发送信号。2-1 操作演示我们还是使用先前的代码#include <iostream>#include <unistd.h>#include <signal.h>using namespace std; void handler(int signal){ cout<<"我是信号:"<<signal<<endl;} int main(){ cout<<"我是进程:"<<getpid()<<endl; signal(2,handler); while(true) { cout<<"我是进程:"<<getpid()<<"我正在等待信号"<<endl; sleep(2); } return 0;}AI写代码cpp运行我们启动进程,当我们使用 kill 对进程发送2号进程时,屏幕打印出信息,执行了函数。 3. 使用函数产生信号kill 命令是调用 kill 函数实现的,kill 函数可以给一个指定的进程发送指定的信号。 参数:pid:发送信号进程的pidsig:发送的信号返回值:成功返回0,失败返回-1raise:给当前进程发送指定的信号 参数:sig:发送的信号返回值:成功返回0,失败返回 non-zeroabort:使当前进程接收到信号而异常终止 3-1 操作演示首先我们写一个系统调用 kill 函数的程序#include <iostream>#include <unistd.h>#include <signal.h>#include <sys/types.h>using namespace std; int main(int argc,char* argv[]){ if(argc != 3) { cerr<<"Usage:"<<argv[0]<<"-signalnumber pid"<<endl; } int number = stoi(argv[1]+1); pid_t id = stoi(argv[2]); int n = kill(id,number); return n;} 接着再来一个测试代码#include <iostream>#include <unistd.h>#include <signal.h>using namespace std; int main(){ while(true) { cout<<"我是进程:"<<getpid()<<endl; sleep(3); } return 0;}我们来看一下运行结果 当我们调用2号信号后,测试用例终止。4. 软件条件产生信号由软件逻辑或用户行为触发的信号。下面我们来介绍一下一些软件条件下产生的信号。13号信号(SIGPIPE),用于处理管道中的写入错误,例如当读端进程关闭,写端进程仍向管道写入,就会触发 SIGPIPE 。14号信号(SIGALRM),用于通知进程的预设时间已到。我们通常用 alarm 函数来触发该信号。alarm:设置一个定时器,second秒后触发信号 参数:seconds:秒数返回值:若先前设置了定时器,返回剩余秒数,否则返回0。4-1 操作演示下面我们来测试一下闹钟我们来看下面的代码#include <iostream>#include <signal.h>#include <cstdlib>#include <unistd.h>#include <sys/types.h>using namespace std; void handler(int signum){ cout << "接收到信号:" << signum << endl; exit(1);} int main(){ int cnt = 1; alarm(1); signal(14, handler); while(true) { cout<<cnt<<endl; cnt++; } return 0;}运行结果: 当闹钟响起时接收到14号信号,最终返回。4-2 理解闹钟在底层,alarm函数与内核定时器机制相关联,当alarm被调用之后,内核会生成一个软定时器。当定时器到期后,内核会发送SIGALRM信号。当进程中多出用到alarm时候,OS 会借助最小堆,判断要先向谁发送信号。5. 硬件异常产生信号硬件异常被硬件以某种方式通过硬件检测通知内核,内核向进程发送适当信号。例如当前进程执行了除0的指令,CPU 运算单元产生异常,内核会将这个异常解释为 SIGFPE 8号信号发送给进程。或者进程访问了野指针,MMU 会产生异常,会向进程发送 SIGSEGV 11号信号。关于野指针问题这个问题与 CPU 内部的MMU,CR2 ,CR3有关联。MMU和页表用来管理虚拟内存从虚拟地址到物理地址的转换。CR3用于切换不同进程的页表,CR2用于存储当前页表的错误虚拟地址。当MMU无法将虚拟地址与物理地址进行关联时,CR2会存储该虚拟地址,并产生异常信号,向当前进程发送 11号信号。6.term 与 coreterm 和 core 是信号默认动作的表示。1. term 是terminate 的缩写,表示默认终止进程。2. core 动作在终止进程的同时,还会生成一个core dump文件,这个文件用于调试。当进程退出时,core dump为0表示没有异常退出,如果是1表示异常退出。 综上所述,产生信号的方法有5种,但本质其实只有三种,硬件触发如键盘或硬件异常等,软件触发如闹钟管道,用户对系统调用kill命令等。所有的信号产生最终都是由 OS 进行执行,产生的信号不会立即作出处理,而是在合适的时候进行。所以说,这些暂时没有执行的信号就会进行保存,那么接下来我们就来理解一下信号的保存。三. 信号的保存操作系统用三张表来对信号进行管理。Block表(阻塞表):一个位图,用于表示哪些信号被阻塞,阻塞为1,未阻塞为0Pending表(未决表):未决即信号已经产生,但是暂时未被进程处理。当信号处于未决时,位图表为1。Handler表(处理函数表):一个函数指针数组,用于表示信号接收后的处理动作。可以是默认函数,忽略函数,自定义函数。有阻塞不一定有未决,但有未决一定是因为阻塞。进程对该信号进行阻塞,若用户一直不传递该信号,该信号就不会进入未决状态。若该信号进入了未决状态,说明该信号已经被阻塞了,正在等待进程处理。操作系统通过两张位图 + 一张函数指针表 ,就完成了让进程识别不同的信号。 1. 信号集 sigset_tsigset_t 是一种数据类型,用于接收阻塞和未决标志专门设置的数据类型。可以用来表示该数据的有效或者无效。2. 信号操作函数sigset_t 存储在内部,从使用者的角度我们不必关心,我们只需要能调用它的封装函数来对 sigset_t 进行操作。#include <signal.h> int sigemptyset(sigset_t *set);int sigfillset(sigset_t *set);int sigaddset(sigset_t *set, int signo);int sigdelset(sigset_t *set, int signo);int sigismember(const sigset_t *set, int signo);参数:set:指向信号集signo:某种信号返回值:成功返回0,失败返回-1sigemptyset:将指向的信号集所有信号对应的bit清空。sigfillset:将指向的信号集所有的信号bit置为1.sigaddset:向信号集某一个信号的bit置为1.sigdelset:向信号集某一个信号的bit置为0.sigimember:判断一个信号集中是否包含某种信号,包含返回1,不包含返回-1sigprocmask:读取或更改进程的信号屏蔽字#include <signal.h>int sigpromask(int how, const sigset_t *set, sigset_t *oset);参数:how:如何修改当前的信息掩码{SIG_BLOCK:将信号集中的所有信号进行阻塞SIG_UNBLOCK:将信号集中的所有信号解除阻塞SIG_SETMASK:将信号集更新为set,并且返回oldset}set:新的更改过的信号集old:更改前的信号集。返回值:成功返回0,失败返回-1.sigpending:读取当前进程的未决信号集,通过set传出#include <signal.h>int sigpending(sigset_t *set);AI写代码cpp运行参数:set:接收的信号集返回值:成功返回0,失败返回-13. 操作演示我们设计一个程序,对2号信号进行阻塞,然后不断打印未决表观察,在10次轮询后,解除对2号信号阻塞,再对未决表进行观察。#include <iostream>#include <signal.h>#include <unistd.h>#include <cstdio>#include <sys/types.h>#include <sys/wait.h>using namespace std; //先屏蔽2号信号,轮询10次解除屏蔽 void PrintPending(sigset_t &pending){ cout<<"["<<getpid()<<"]:"; for(int i = 31;i>=1;i--) { if(sigismember(&pending,i)) { cout<<"1"; } else { cout<<"0"; } } cout<<endl;} void headler(int signo)//打印信号表{ cout<<"接收到信号:"<<signo<<endl; cout<<"----------------------"<<endl; sigset_t pending; sigpending(&pending); PrintPending(pending); cout<<"----------------------"<<endl;} int main(){ //初始化 sigset_t new_set,old_set; signal(2,headler); sigemptyset(&new_set); sigemptyset(&old_set); sigaddset(&new_set,2); //设置阻塞表 sigprocmask(SIG_BLOCK,&new_set,&old_set); int cnt = 10; while(true) { sigset_t pending; sigpending(&pending); PrintPending(pending); if(cnt == 0) { sigprocmask(SIG_SETMASK,&old_set,&new_set); cout<<"解除对2号信号屏蔽"<<endl; } cnt--; sleep(1); } return 0;}AI写代码cpp运行运行结果: 我们发现当我们发送2号信号时,未决表接收到信号,但是因为阻塞所以进程接收不到。当解除信号屏蔽后,先前发送的2号信号就立马被接受到了,此时再发送信号就不会出现阻塞。四. 信号的处理当我们进程接收到信号后,有三种处理方式1. 忽略信号:忽略该信号,不对其做任何处理2. 捕捉信号:进程可以注册一个对于该信号的一个特殊捕捉函数,当该信号触发时,进程接收信号并做出相应处理。3. 默认处理:若进程没有对该信号进行注册,系统会按照默认的方法进行处理。signal(2, handler);signal(2, SIG_IGN);signal(2, SIG_DFL);AI写代码cpp运行handler:自定义函数SIG_IGN:忽略信号SIG_DFL:默认处理我们从上文得知,信号并不会被立即处理,而是会先储存等待合适的时机再进行处理。那合适的时机是什么时候呢?先说结论,是从内核态切换为用户态的时候。简单讲,当我们执行自己的代码,访问自己的数据就是用户态;进入系统调用,以操作系统的身法运行就是内核态。当程序出现中断,异常或者系统调用的时候,我们会从用户态转为内核态。在内核态中以操作系统的身份完成工作后,将会从内核态切换回用户态,此时操作系统会对未决的信号进行检测处理(通过三张表判断),因为信号的三张表存储在PCB(进程控制块)中,需要调用三张表就必须处在内核态。若信号执行的是默认或者忽略,此时已经完成了工作。若信号执行的是自定义函数,此时返回用户态之后会处理信号函数,并且返回sys_sigreturn()使其进入内核态。最后在内核态和用户态切换的间隙处理函数,最后返回到主流程被中断的地方,这就完成了信号的捕捉流程。也就是说,如果信号执行的是自定义函数,那么就需要4次的用户态和内核态的切换 简化一下: 我们再来认识一个函数sigaction:自定义信号处理方式#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction { void (*sa_handler)(int); // 信号处理函数(类似 signal() 的回调) sigset_t sa_mask; // 信号处理期间临时阻塞的信号集 int sa_flags; // 信号处理的标志位(如 SA_RESTART、SA_NODEFER 等) void (*sa_sigaction)(int, siginfo_t *, void *); // 高级处理函数(需配合 SA_SIGINFO 志)};AI写代码cpp运行参数:signum:信号数字act:结构体定义信号行为,传入的新的行为old:旧的行为返回值:成功返回0,失败返回-1.五. 操作系统是如何运行的1. 硬件中断当我们在键盘上输入命令或数据时,键盘的电路会检测按键的按下和释放,并产生对应的电信号。电信号随后转换为中断信号,通过硬件连线传递到CPU的中断控制器。中断控制器根据信号的优先级和当前CPU的状态,决定是否向CPU发送中断请求。CPU通过中断机制来响应中断请求。在保护下,CPU会维护一个中断描述符表,该表包含中断服务相应的地址信息。当中断发生后,会根据该表来找到对应执行方法的地址。一旦CPU接收到来自键盘的中断请求,就会暂停当前正在执行的程序,然后跳转到特定的中断处理服务地址,处理对应的方法。中断处理操作会执行必要的操作,如读取键盘状态,更新数据,发送响应等。处理完中断后,CPU会恢复之前保存的状态,并继续执行原来的程序。2. 时钟中断进程是在操作系统的指挥下被调度运行的,而操作系统是由时钟中断进行管理的。时钟中断源于硬件定时器,有计算机的主板芯片或处理器芯片提供,通过定时器计数器来实现中断功能。功能:1. 维护系统时间:每当时钟中断发生,内核系统会重新维护一个时间,通过更新系统保证时间的准确性,为用户提供可靠的时间信息。2. 任务调度:在多进程中,时钟会为每一进程分配时间片。当一个时间片用完了,内核就会重新选择下一个要运行的进程,并切换上下文,这样就可以保证公平的进行进程调度。3. 计录进程执行时间:每当进程或线程被抢占或者切换时,时钟会记录下抢占时间,方便我们后序了解系统的运行情况。3. 死循环操作系统在启动之后就会变成一个死循环,但这个循环是可控循环。会进行事件驱动,动态调度,资源管理等操作。通过时钟中断,可以为每一个进程进行时间分配,这样可以确保每个进程都有机会获得处理器资源。4. 软中断软中断是软件主动触发“中断”,从而进入内核态执行特权操作。也被称为“陷阱”,被用作内核态与用户态直接的通信桥梁。软硬中断的具有共性,中断之后需要进行状态切换(从用户态到内核态),上下文保存(保存用户态寄存器,计数器等信息),跳转执行(根据中断号,跳转处理),处理返回(执行完内核逻辑后返回)。在x86下32位机器通常用 int 0x80指令实现调用;在x86下64位机器通常用syscall指令执行调用。通过软中断,我们从用户态到了内核态,这样我们就可以实现系统调用。在内核态中,所有的系统调用其实都是被封装好的一个个函数指针数组,我们通过对应的下标,就可以对其进行访问。 这些被系统调用的系统函数会以数组下标的形式存储在 寄存器 中,通过下标找对应的系统调用表找到对应的处理函数并调用。执行完后再从内核态返回用户态,恢复上下文。这样就完成了系统调用。 紧急着,上层对于这些系统调用接口进行封装,这样我们用户就无法直接调用系统接口,保证了安全性。5. 缺页中断,内存碎片处理,除零野指针错误这些问题会被CPU内部转化为软中断,走中断处理例程进行相应处理。六.对内核虚拟地址空间的理解我们这里只对内核的虚拟地址空间进行讨论。内核虚拟地址空间存储在虚拟地址空间的高地址处,且每一份进程共享这一份内核页表,用于对操作系统进行管理。通常用户态在【0-3G】而内核区在【3-4G】处。CPU通过 int 0x80 或 syscall 指令进行用户态到内核态的切换,将当前状态记录在 CPL 中,其中0代表内核,3代表用户。在内核区中存储了内核页表,存储着内核处理中断异常硬件等各种方法的函数指针。 总结:信号是操作系统中一种轻量级的异步通信机制,通过预定义的信号编号传递事件通知,实现进程对异常、外部请求等事件的响应。其核心是 “事件触发 - 处理” 模型,兼具灵活性(自定义处理)和强制性(关键信号不可忽略),是进程间通信和系统异常处理的基础工具。 ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/2401_86449430/article/details/150430290
-
中电科金仓(北京)科技股份有限公司(以下简称“电科金仓”)成立于1999年,是成立最早的拥有自主知识产权的国产数据库企业,也是中国电子科技集团(CETC)成员企业。电科金仓以“提供卓越的数据库产品助力企业级应用高质量发展”为使命,致力于“成为世界卓越的数据库产品与服务提供商”。 电科金仓自成立起始终坚持自主创新,专注数据库领域二十余载,具备出色的数据库产品研发及服务能力,核心产品金仓数据库管理系统KingbaseES(简称“KES”)是面向全行业、全客户关键应用的企业级大型通用数据库。KES产品V9版本已通过国家权威机构认证,产品核心源代码自主率达到100%。2018年,电科金仓申报的“数据库管理系统核心技术的创新与金仓数据库产业化”项目荣获国家科学技术进步二等奖。金仓数据库管理系统KES于2022年入选国务院国资委发布的十项国有企业数字技术典型成果,彰显数据库领域国家队硬实力。继2023年金仓数据库管理系统V8通过第一批《安全可靠测评》后,2024年金仓数据库管理系统V9、金仓分布式HTAP数据库软件集群V3再度入围,至此电科金仓共计2款产品3个版本通过《安全可靠测评》*。 🥇 点击进入金仓数据库专栏,本专栏聚焦金仓数据库(KingbaseES)这一国产企业级融合数据库,为开发者及技术决策者提供从基础操作到架构设计的系统化学习路径。从多语法兼容(Oracle/MySQL/PostgreSQL)、多模数据存储(关系 / 文档 / 时序 / GIS)等功能展开讲解!🌞 正文开始:KingbaseES是一款功能强大的数据库管理系统,支持多种硬件架构和安装方式。本文将详细介绍通过Docker和ISO包两种方式快速安装KingbaseES的步骤,帮助开发者快速部署和体验该数据库。Docker方式快速安装KingbaseES环境准备硬件环境要求KingbaseES Docker软件包支持通用X86_64、龙芯、飞腾、鲲鹏等国产CPU硬件体系架构,满足不同场景的硬件需求。软件环境要求软件包名称 版本说明Docker 20.10.0及以上版本安装步骤创建安装路径使用root用户在宿主机执行以下命令,创建数据库持久化存储路径:mkdir -p /opt/kingbasemkdir -p /opt/kingbase/datachmod -R 755 /opt/kingbase/data导入镜像首先获取KingbaseES的Docker镜像(如kingbase.tar),可通过电科金仓官网、销售人员、售后支持人员或代理商获取。进入软件安装包目录并查看容器镜像:cd /opt/kingbasels -al使用root用户将镜像导入至Docker镜像库:docker load -i /opt/kingbase/kingbase.tar若执行上述命令报错,可尝试使用docker import kingbase.tar命令重新导包。启动实例最小启动命令(未进行数据持久化):docker run -tid --privileged \-p 54321:54321 \--name kingbase \kingbase:v1 /usr/sbin/init数据持久化启动命令:docker run -tid --privileged \-p 54321:54321 \--name kingbase \-v /opt/kingbase/data:/home/kingbase/userdata \kingbase:v1 /usr/sbin/init其中,/home/kingbase/userdata是金仓数据库默认数据文件目录,/opt/kingbase/data是本机存储持久化目录,可根据实际需求修改。安装后检查执行docker ps命令,查看容器运行状态,若能看到创建的kingbase容器信息,则说明安装启动成功。命令行界面访问进入容器并执行bash:docker exec -it kingbase /bin/bash 执行ksql访问命令行界面:ksql -Ukingbase -d testISO包方式快速安装KingbaseES安装步骤概览ISO包安装主要包括:硬件环境检查、创建安装用户、创建安装目录、安装包准备、安装包挂载、安装并启动KingbaseES、安装后检查等步骤。详细步骤硬件环境要求支持架构:通用X86_64、龙芯、飞腾、鲲鹏等国产CPU硬件体系架构。具体要求(标准版/企业版/专业版/开发版):CPU:X86、龙芯、飞腾、鲲鹏内存:512MB以上硬盘:11GB以上空闲空间备注:/tmp目录需要至少10G空间。若安装过程中出现存储空间不足,需先释放足够磁盘空间;硬件配置不满足要求时,需更换硬件设备。创建安装用户使用root用户创建kingbase用户:useradd -u2000 kingbase或若需修改已有用户:kill -9 `lsof -u kingbase`usermod -u 2000 kingbasegroupmod -g 2000 kingbase设置kingbase用户密码(需输入两次且保持一致):passwd kingbase注意:创建安装用户后,后续操作默认使用kingbase用户进行。创建安装目录KingbaseES默认安装目录为/opt/Kingbase/ES/V9,使用root用户创建该目录并设置权限:mkdir -p /opt/Kingbase/ES/V9chmod o+rwx /opt/Kingbase/ES/V9安装包准备获取安装包:通过电科金仓官网、销售人员、售后支持人员或代理商获取对应平台的ISO安装程序。上传安装包至服务器存放路径(示例路径为/opt/software/),并使用root用户创建存放软件路径:mkdir -p /opt/software/ mkdir -p /opt/software/KingbaseESV9 安装包的挂载使用root用户挂载iso文件:cd /opt/software/mount KingbaseES_V009R001C010_Lin64_install.iso ./KingbaseESV9安装KingbaseES使用kingbase用户执行数据库安装命令:cd /opt/software/KingbaseESV9sh setup.sh -i console随后按照安装向导依次进行操作,包括选择安装类型(默认安装新实例)、阅读并接受许可协议、选择安装路径(默认/opt/Kingbase/ES/V9)、选择安装集(默认完全安装)等,直至安装完成。安装后操作安装完成后,可根据提示进行初始化数据库等操作,例如手动初始化数据库命令:/opt/Kingbase/ES/V9/KESRealPro/V009R001/Server/bin/initdb -U "system" -x "12345678ab" -E "UTF-8" --lc-ctype="zh_CN.UTF-8" --lc-collate="zh_CN.UTF-8" -D "/opt/Kingbase/ES/V9/data"AI写代码bash1通过以上两种方式,可快速完成KingbaseES的安装与部署,根据实际应用场景选择合适的安装方式即可。在安装过程中,若遇到问题可参考官方文档或联系技术支持。Windows下载安装下载数据库 金仓官网提供了金仓各产品各版本的安装包、补丁包、对应的工具、接口驱动、授权文件等下载服务。可以通过页面中的筛选栏选择所需产品及版本进行下载,我这里下载的是windows版本的x64完整版金仓数据库。下载地址:https://download.kingbase.com.cn/xzzx/index.htm 这里下载好的是iso镜像文件,直接双击打开,然后把内容复制出来。 安装配置 单击 KINGBASE.EXE 开始安装! 我这里选择默认的中文安装,点击 确定 单击 下一步 单击 下一步 我这里选择的完全安装,点击 下一步 我这里选择试用,点击 下一步 可以自定义安装路径,点击 下一步 点击 安装 默认数据文件就行,点击 下一步 填写相关配置信息,system的密码自定义,字符集编码,选择的是UTF8;数据库兼容模式选择的是MySql;是否大小写敏感选择的是NO,存储块大小选择的是8K,点击 完成 这里数据库就安装完成了,是不是相当简单。 联系博主 xcLeigh 博主,全栈领域优质创作者,博客专家,目前,活跃在CSDN、微信公众号、小红书、知乎、掘金、快手、思否、微博、51CTO、B站、腾讯云开发者社区、阿里云开发者社区等平台,全网拥有几十万的粉丝,全网统一IP为 xcLeigh。希望通过我的分享,让大家能在喜悦的情况下收获到有用的知识。主要分享编程、开发工具、算法、技术学习心得等内容。很多读者评价他的文章简洁易懂,尤其对于一些复杂的技术话题,他能通过通俗的语言来解释,帮助初学者更好地理解。博客通常也会涉及一些实践经验,项目分享以及解决实际开发中遇到的问题。如果你是开发领域的初学者,或者在学习一些新的编程语言或框架,关注他的文章对你有很大帮助。 亲爱的朋友,无论前路如何漫长与崎岖,都请怀揣梦想的火种,因为在生活的广袤星空中,总有一颗属于你的璀璨星辰在熠熠生辉,静候你抵达。 愿你在这纷繁世间,能时常收获微小而确定的幸福,如春日微风轻拂面庞,所有的疲惫与烦恼都能被温柔以待,内心永远充盈着安宁与慰藉。 至此,文章已至尾声,而您的故事仍在续写,不知您对文中所叙有何独特见解?期待您在心中与我对话,开启思想的新交流。 💞 关注博主 🌀 带你实现畅游前后端! 🥇 从零到一学习Python 🌀 带你玩转Python技术流! 🏆 人工智能学习合集 🌀 搭配实例教程与实战案例,帮你构建完整 AI 知识体系 💦 注:本文撰写于CSDN平台,作者:xcLeigh(所有权归作者所有) ,https://xcleigh.blog.csdn.net/,如果相关下载没有跳转,请查看这个地址,相关链接没有跳转,皆是抄袭本文,转载请备注本文原地址。———————————————— 原文链接:https://blog.csdn.net/weixin_43151418/article/details/150473138
-
概念 环境变量:一般指的是在操作系统中用来指定操作系统运行环境的一些参数。 就比如说我们编写的C/C++代码在连接时从来不知道我们所连接的动静态库在哪里,但是照样可以连接成功。其原因就是有环境变量帮助编译器进行查找。 环境变量通常具有某些特殊用途,其在系统中还具有全局特性。命令行参数 我们知道中Linux命令中许多命令都是有对应的选项的,不同的选项对应不同的功能。那这个操作是如何实现的呢?main函数参数 首先,让我们先来讲讲main函数。mian函数可以说是我们接触编程的第一步,但许多人可以对main函数并不了解。 main函数其实是可以有参数的。int main(int argc,char* argv[])AI生成项目 第一个参数,argc:表示命名行参数的数量(包括程序名本身) 第二个参数,argv:是一个字符指针数组,储存了命令行中的每个参数验证:#include<stdio.h> int main(int argc,char* argv[]){ for(int i=0;i<argc;i++) { printf("%s\n",argv[i]); }}AI生成项目 实现指令选项#include<stdio.h>#include<string.h> int main(int argc,char* argv[]){ if(argc==2) { char* x=argv[1]; if(strcmp(x,"-a")==0) printf("执行-a功能\n"); if(strcmp(x,"-b")==0) printf("执行-b功能\n"); if(strcmp(x,"-c")==0) printf("执行-c功能\n"); } else printf("请使用:-a/-b/-c\n");}AI生成项目 进程中有一张表:argv表(命令行参数表),利用此表则可以实现命令的选项的功能。认识环境变量首先我们要执行一个程序,必须先找到它而环境变量可以帮助我们找到其位置使用env指令我们可以查看系统中所有的环境变量,也可以使用echo $名称,打印对应的环境变量环境变量的格式为:名称=内容 PATH 我们先来认识一下PATH环境变量。 PATH环境变量表示查找指令位置的默认路径,比如当我们执行指令:ls、cd等等指令时,bash就会使用PATH环境变量去找到指令对应的程序并执行。 我们知道系统指令的实现也是C语言,但为什么系统自己的指令可以直接执行,而我们自己写的代码却必须要 “./” 作为前缀才可以执行,PATH环境变量就是关键。理解两个问题:1.环境变量是如何存储的呢? bash下会形成一个环境变量表,存放各种各样的环境变量。(上面我们也讲到了bash中有一个命令行参数表,也是就是bash下有两张表)2.环境变量一开始是从哪里来的? bash下的环境变量表是重系统配置文件中来的(以下便是配置文件)。 认识更多环境变量HOME HOME环境变量,记录的是当前用户的家目录路径root@hcss-ecs-4ce7:~# echo $HOME/rootAI生成项目SHELL 表示当前使用的shell版本root@hcss-ecs-4ce7:~# echo $SHELL/bin/bashAI生成项目 USER 表示当前用户是谁root@hcss-ecs-4ce7:~# echo $USERrootAI生成项目 HISTSIZE 表示Linux中记录历史命令的最大数量(既Linux的历史命令最多记录HISTSIZE条)root@hcss-ecs-4ce7:~# echo $HISTSIZE1000AI生成项目HOSTNAME 表示当前主机的主机名root@hcss-ecs-4ce7:~# echo $HOSTNAMEhcss-ecs-4ce7AI生成项目PWD 记录当前所在的工作路径hyc@hcss-ecs-4ce7:~/linux/progress$ echo $PWD/home/hyc/linux/progressAI生成项目OLDPWD 记录上一次的工作路径hyc@hcss-ecs-4ce7:~/linux/progress$ echo $OLDPWD/home/hyc/linuxAI生成项目环境变量的获取方式指令env:查看全部的环境变量echo $xxx:查看某个环境变量expot:添加环境变量unset:删除环境变量 代码调用系统调用:getenv获取环境变量头文件为:#include<stdlib.h>返回值:获取成功返回指针,失败返回NULL#include<stdio.h>#include <stdlib.h> int main(){ char* str=getenv("PATH"); if(!str) printf("获取失败"); else printf("%s\n",str);}AI生成项目 环境变量特性1.环境变量具有全局性,可以被子进程继承,同一个bash下的进程都可以访问 。2.存在本地变量,只能在bash内部使用。如下:i 就是本地变量。 3.export添加环境变量,是添加在bash中的,再子进程继承达到全局可用。 export命令与其他命令不同:export命令是内键命令:既不需要创建子进程,bash自己调用函数执行(或系统调用完成)———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/huangyuchi/article/details/149715773
-
links:https://lm-studio.cn/docs/apiLM Studio 可以像 Ollama 一样通过暴露本地端口实现局域网内客户端调用。以下是具体实现方式和技术细节:1. 核心功能支持LM Studio 的 开发者选项卡 提供了完整的本地 API 服务器功能,支持以下配置:端口暴露:默认使用 1234 端口,可自定义修改(需注意端口冲突问题)3局域网访问:勾选 “在局域网内提供服务” 选项后,服务器会监听所有网络接口(0.0.0.0),允许其他设备通过 IP 地址访问34跨域支持:启用 CORS 后,可对接网页应用或其他客户端工具32. 配置步骤实现局域网调用的具体操作流程:启动服务端:在 LM Studio 的 “开发者” 选项卡中选择模型勾选 “在局域网内提供服务” 并设置端口(如 1234)3点击 “启动服务” 按钮,控制台会显示运行日志和访问地址(如 http://<本机IP>:1234)3客户端调用:其他设备可通过以下方式访问:# 示例:使用 curl 调用聊天接口curl http://<本机IP>:1234/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"messages": [{"role": "user", "content": "你好"}], "model": "<模型名称>"}'AI生成项目bash1234支持 OpenAI 兼容的 API 格式(包括 /v1/chat/completions、/v1/completions 等端点)13防火墙配置(Windows 系统):需在防火墙入站/出站规则中开放对应端口(如 1234)53. 与 Ollama 的对比特性 LM Studio Ollama协议兼容性 OpenAI API 兼容 自有 API 格式界面操作 图形界面一键配置 需命令行操作多模型支持 需手动加载不同模型 支持 Modelfile 动态切换日志管理 内置日志记录(可记录提示与响应)3 需自行配置日志工具4. 典型应用场景多人协作开发:团队成员通过局域网共享同一模型服务2跨设备调用:手机/平板通过浏览器或 Open WebUI 访问本地模型2自动化流程:对接 VS Code 插件或其他 AI 工具链1注意:如果遇到连接问题,需检查以下三点:LM Studio 服务端是否已勾选局域网选项防火墙是否开放对应端口客户端是否使用正确的 IP 地址(可通过 ipconfig/ifconfig 查看本机局域网 IP)35———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/a772304419/article/details/145800588
推荐直播
-
华为云码道 × 仓颉编程:工程化AI编码探索2026/05/27 周三 19:00-21:00
刘俊杰-华为云仓颉语言专家/李炎-华为云码道技术专家/王智鹏-OpenCangjie开源社区发起人
本场直播围绕华为云仓颉语言与华为云码道的深度结合,展示华为云智能编程从零基础到高效落地的完整生态能力。以华为云码道为引擎,仓颉语言为载体,带给大家日常提效、趣味创新到极速量产的开发体验。
回顾中
热门标签