• 对比ARM架构和X86架构的指令集
    指令集设计理念X86 架构基于复杂指令集(CISC)理念设计,力求以单条指令完成复杂操作,通过丰富指令种类来减少程序中的指令总数,提升整体执行效率。例如,一条 X86 指令可直接实现内存到寄存器的复杂数据传输与运算,这在传统桌面和服务器应用场景中,可有效减少指令调用次数,优化多任务处理和大型数据库操作的性能。然而,这也导致指令集庞大复杂,指令长度从 1 字节到 15 字节不等,使得指令译码难度大增,硬件设计复杂度显著提升。ARM 架构遵循精简指令集(RISC)理念,指令功能简单且单一,仅执行基本操作,如简单的数据传输、算术运算等。这种设计使得指令能够快速执行,并且在硬件设计上更易于实现高度并行处理。在移动设备和嵌入式系统中,ARM 架构利用并行设计,可同时处理多个简单指令,实现高效数据处理。尽管单条指令功能有限,但凭借快速执行和并行机制,在特定领域表现出卓越的整体性能。指令格式X86 架构的指令格式极为复杂,由于指令长度可变,其操作码、操作数等字段的位置和长度不固定。这就要求指令译码依靠复杂的逻辑电路进行解析,增加了硬件设计的难度与成本。不同的指令格式适用于不同操作场景,如某些格式用于内存访问,某些用于寄存器操作,虽为编程提供了灵活性,但也要求开发者深入了解指令集,才能精准运用。ARM 架构的指令格式相对规整,指令长度固定为 32 位(ARMv8 及后续版本引入了 64 位指令集)。这种固定长度的指令格式使指令译码简单高效,在硬件设计时可简化译码电路,降低硬件成本。同时,ARM 指令格式的操作码、操作数等字段位置和长度相对固定,便于编译器优化,提高代码生成效率。例如,ARM 指令的操作码通常位于指令高位,操作数处于特定位置,这种清晰的结构有助于快速解析和执行指令。执行效率在执行效率方面,X86 架构在处理复杂任务时,因单条指令可完成较多工作,减少了指令间的切换开销,在多任务处理、大型数据库复杂查询等场景中具有优势。不过,由于指令复杂,译码和执行过程相对缓慢,在对实时性要求极高、需快速响应的场景下,可能无法满足需求。ARM 架构指令执行速度快,因为指令简单,译码和执行时间短。在移动设备和嵌入式系统中,数据处理通常有实时性要求,ARM 架构凭借快速的指令执行,能够高效完成任务。此外,ARM 架构采用高度并行设计,如超标量流水线技术,可同时执行多条指令,进一步提升整体执行效率。例如在图像识别的嵌入式应用中,ARM 处理器能快速处理大量图像数据,满足实时性需求。对硬件的要求X86 架构的复杂指令集致使硬件设计极为复杂。为实现复杂指令的译码和执行,需要大量晶体管和复杂的控制逻辑电路,这不仅增大了芯片面积,还提高了功耗。同时,复杂的指令集增加了处理器研发的难度和成本,因为要确保各种复杂指令的正确执行和兼容性。ARM 架构的精简指令集使硬件设计相对简单。由于指令格式固定、功能单一,所需的译码电路和控制逻辑相对简单,能够减少芯片上的晶体管数量,降低芯片面积和功耗。这使得 ARM 架构在移动设备、物联网设备等对功耗和成本敏感的领域具有显著优势。此外,简单的硬件设计还降低了研发成本,让更多厂商能够基于 ARM 架构进行芯片设计和开发。
  • Linux 主机:ARM 架构与 X86 架构的深度剖析
    在 Linux 主机的世界里,ARM 架构和 X86 架构犹如两颗璀璨的明星,各自闪耀着独特的光芒。它们在性能、功耗、成本以及应用场景等方面存在着显著的差异,这些差异也决定了它们在不同领域的广泛应用。性能表现指令集的力量X86 架构采用复杂指令集(CISC),指令长度可变,一条指令可以完成较为复杂的操作。这使得 X86 处理器在处理传统桌面和服务器应用时具有优势,例如多任务处理、大型数据库操作等。然而,复杂的指令集也带来了硬件设计的复杂性和较高的功耗。ARM 架构则基于精简指令集(RISC),指令长度固定,指令功能相对简单。这使得 ARM 处理器能够以较低的功耗运行,并且在一些特定的应用场景下,如移动设备和嵌入式系统,能够通过高度并行的设计实现高效的数据处理。虽然单条指令的功能不如 X86 指令强大,但通过指令的快速执行和并行处理,ARM 处理器在特定领域展现出了卓越的性能。核心设计与性能X86 架构的处理器通常具有较高的时钟频率,能够在单位时间内执行更多的指令。这使得它们在处理需要大量计算资源的任务时表现出色,例如图形渲染、科学计算等。此外,X86 处理器在多核设计方面也取得了显著进展,能够同时处理多个复杂的任务流。ARM 架构的处理器则更注重能效比,通过优化核心设计和采用先进的制程工艺,在较低的功耗下实现高效的运算。ARM 处理器的核心数量可以根据不同的应用需求进行灵活配置,从单核到多核甚至大规模的集群,能够满足从简单的嵌入式设备到高性能服务器的多样化需求。功耗管理ARM:低功耗的王者ARM 架构天生就是为低功耗而生的。其精简的指令集和高效的硬件设计使得 ARM 处理器在运行时能够以较低的电压和频率工作,从而大大降低了功耗。这一特性使得 ARM 架构在移动设备、物联网设备以及对功耗敏感的嵌入式系统中得到了广泛应用。例如,我们日常使用的智能手机和平板电脑,大多数都采用了 ARM 架构的处理器,以确保长时间的续航能力。X86:功耗挑战与应对相比之下,X86 架构的处理器由于复杂的指令集和较高的时钟频率,通常功耗较高。在传统的桌面和服务器应用中,这一问题可能并不突出,因为设备通常连接到电源插座,无需担心电池续航问题。然而,随着数据中心规模的不断扩大,功耗成为了一个重要的成本因素。为了应对这一挑战,X86 处理器厂商也在不断努力改进功耗管理技术,例如采用动态电压频率调整(DVFS)技术,根据处理器的负载情况动态调整电压和频率,以降低功耗。硬件成本ARM:低成本的优势ARM 架构的处理器通常采用授权模式,厂商可以根据自己的需求购买 ARM 的核心授权,并在此基础上进行定制化设计。这种模式降低了硬件开发的门槛和成本,使得更多的厂商能够进入市场,推出各种不同价位的产品。此外,ARM 架构的处理器在制程工艺上也具有一定的优势,能够在较低的成本下实现较高的集成度,进一步降低了硬件成本。这使得 ARM 架构在嵌入式设备、物联网设备以及一些低成本的服务器应用中具有明显的价格优势。X86:成本与性能的平衡X86 架构的处理器由于研发成本高、硬件设计复杂,通常价格较高。尤其是在高端服务器市场,X86 处理器的价格相对昂贵。然而,X86 架构在性能方面的优势使得它在一些对性能要求极高的应用场景中仍然具有不可替代的地位。在这些场景下,用户愿意为高性能的计算能力支付较高的成本。同时,随着技术的不断进步,X86 处理器厂商也在努力降低成本,通过优化生产工艺和提高产品的集成度,使得 X86 处理器在价格上逐渐具有竞争力。兼容性与生态系统X86:成熟的生态系统X86 架构在桌面和服务器领域拥有悠久的历史,经过多年的发展,已经形成了非常成熟的生态系统。几乎所有的主流操作系统,如 Windows、Linux 等,都对 X86 架构提供了全面的支持。此外,大量的软件应用程序也都是基于 X86 架构进行开发的,这使得 X86 架构在兼容性方面具有无可比拟的优势。无论是办公软件、游戏还是专业的设计和开发工具,都能够在 X86 架构的 Linux 主机上稳定运行。ARM:快速发展的生态虽然 ARM 架构在生态系统方面相对 X86 架构起步较晚,但近年来取得了飞速的发展。随着 ARM 架构在移动设备和嵌入式系统中的广泛应用,越来越多的软件开发者开始关注 ARM 平台。Linux 作为一款开源的操作系统,对 ARM 架构的支持也越来越完善。许多主流的 Linux 发行版,如 Ubuntu、Debian 等,都已经能够在 ARM 架构的设备上稳定运行。此外,一些云服务提供商也开始推出基于 ARM 架构的云服务器,为开发者提供了更多的选择。然而,与 X86 架构相比,ARM 架构的生态系统仍然相对薄弱,尤其是在一些专业软件和企业级应用方面,还需要进一步的完善和发展。应用场景ARM 架构的应用领域ARM 架构凭借其低功耗、低成本和灵活的设计,在移动设备、物联网、嵌入式系统以及边缘计算等领域占据了主导地位。在移动设备市场,几乎所有的智能手机和平板电脑都采用了 ARM 架构的处理器。在物联网领域,ARM 架构的芯片被广泛应用于各种智能设备,如智能家居设备、智能穿戴设备等。在嵌入式系统中,ARM 架构的处理器由于其体积小、功耗低、性能稳定等特点,成为了许多工业控制、医疗设备、汽车电子等领域的首选。此外,随着边缘计算的兴起,ARM 架构的设备因其能够在靠近数据源的地方进行实时数据处理,也得到了越来越广泛的应用。X86 架构的应用场景X86 架构则在传统的桌面电脑、服务器以及高性能计算等领域发挥着重要作用。在桌面电脑市场,X86 架构凭借其强大的性能和良好的兼容性,满足了用户对于办公、娱乐、游戏等各种应用的需求。在服务器领域,X86 架构的服务器占据了主导地位,为企业提供了强大的计算、存储和网络服务。在高性能计算领域,X86 架构的超级计算机能够处理复杂的科学计算、天气预报、金融分析等任务,推动了科学研究和技术创新的发展。总结ARM 架构和 X86 架构在 Linux 主机领域各有千秋。ARM 架构以其低功耗、低成本和灵活的设计,在移动设备、物联网和嵌入式系统等领域展现出了强大的竞争力;而 X86 架构则凭借其高性能、成熟的生态系统和广泛的兼容性,在桌面电脑、服务器和高性能计算等领域占据着重要地位。随着技术的不断发展,两者之间的界限也在逐渐模糊,ARM 架构在性能方面不断提升,开始向服务器和高性能计算领域进军;而 X86 架构也在努力降低功耗和成本,以适应更多的应用场景。在未来的发展中,我们有理由相信,ARM 架构和 X86 架构将继续相互竞争、相互促进,为 Linux 主机的发展带来更多的创新和机遇。希望通过本文的介绍,能够帮助你更好地理解 Linux 主机在 ARM 架构和 X86 架构之间的区别,从而在选择和使用 Linux 主机时做出更加明智的决策。如果你对这两种架构还有其他的疑问或者想法,欢迎在评论区留言讨论。
  • [互动交流] 鲲鹏916和鲲鹏920有AArch32支持吗?
    我目前手上有一个棘手的需求,需要在服务器上利用KVM运行32位的Linux操作系统。通过搜索网络我发现许多64位的ARM处理器已经不具备32位的支持,或者仅仅在个别EL下支持32位。我正在寻找具备EL1和EL0下的32位支持、且具备虚拟化扩展的64位ARM处理器。请问有使用过搭载鲲鹏916和鲲鹏920处理器的服务器的同行吗?鲲鹏916和鲲鹏920是否具备EL0和EL1下的32位支持?
  • [技术干货] ExaGear for Server on Ubuntu
    安装安装前,检查host系统的/opt/exagear目录是否存在。如存在,用户可根据实际情况,选择在当前版本上进行升级,参见“升级ExaGear for Server on Ubuntu”,也可选择卸载当前已安装版本,参见“卸载ExaGear for Server on Ubuntu”,然后重新进行新版本的安装,新版本安装参见如下。ExaGear for Server on Ubuntu发布件包含如下六个安装包:exagear-core-x32a64_<package_version>_arm64.debexagear-core-x64a64_<package_version>_arm64.debexagear-integration_<package_version>_all.debexagear-utils_<package_version>_all.debexagear-guest-for-ubuntu-<os_version>-x86_64_<package_version>_all.debexagear-cfg-builder-a64_<package_version>_arm.deb其中exagear-guest-for-ubuntu-<os_version>-x86_64-<package_version>_all.deb 需要通过以下链接单独获取:- Ubuntu 18.04 guest系统包- Ubuntu 20.04 guest系统包执行以下命令安装exagear:   sudo dpkg -i exagear-utils_<package_version>_all.deb exagear-core-x64a64_<package_version>_arm64.deb exagear-core-x32a64_<package_version>_arm64.deb exagear-guest-for-ubuntu-<os_version>-x86_64-<package_version>_all.deb exagear-integration_<package_version>_all.deb exagear-cfg-builder-a64_<package_version>_arm.debsudo apt-get install -f 注意:上述命令中的<package_version>表示ExaGear包的版本号,例如3213。安装时需根据实际获取的安装包版本号执行操作,否则会提示失败。上述命令中的<os_version>表示guest系统的版本号,安装时需根据实际获取的安装包版本号执行操作,否则会提示失败。此处<os_version>以for Ubuntu 20.04为例,则<os_version>为2004。运行guest系统运行exagear命令,即可启动一个x86 shell,也称为guest shell,进入到虚拟的x86 OS环境。exagear显示:Starting the shell in the guest image /opt/exagear/images/ubuntu-<os_version>-x86_64arch显示:x86_64至此,你已置身于x86运行环境,根目录在host系统上的绝对路径为:/opt/exagear/images/ubuntu-<os_version>-x86_64。在这里,guest shell的运行情况和在x86机器上一样。须知用户账号在host系统和guest系统之间是共享的。在guest系统中增加或删除用户时,host上也会自动进行相应的修改。/home目录也在host系统和guest系统之间共享(更多host和guest系统之间共享目录的详细信息,请参考配置ExaGear for Server on Ubuntu)。运行exit命令即可退出guest会话。exit至此,你又回到了host系统。执行如下命令:arch显示:aarch64。安装x86应用程序x86或x86_64应用程序以及依赖库的安装等,需要在guest环境中进行。推荐使用apt工具安装x86应用,首先要配置apt源,需要进入guest环境后进行配置,相关操作和在x86机器上的操作一样。如果你需要在本地网络中通过/etc/environment使用代理配置,确保你已经通过如下命令将配置复制到了guest系统(更多请参考“配置ExaGear for Server on Ubuntu”)。sudo cp /etc/environment /opt/exagear/images/ubuntu-<os_version>-x86_64/etc/environment运行exagear命令进入guest环境,以安装x86的nginx为例:xagear示:Starting the shell in the guest image /opt/exagear/images/ubuntu-<os_version>-x86_64通过apt-get进行安装:sudo apt-get updatesudo apt-get install nginx在guest环境中安装x86应用程序,guest 环境中查看到的路径如果是/path/to/binary,则实际路径是:/opt/exagear/images/ubuntu-<os_version>-x86_64/path/to/binary。比如,在guest环境中 nignx的路径为 /usr/sbin/nginx,在host 环境上的实际路径为/opt/exagear/images/ubuntu-<os_version>-x86_64/usr/sbin/nginx。如果x86应用是以deb安装包方式提供的,需要将该安装包拷贝至guest系统可见的目录,推荐个人home目录“~/”,然后进入guest环境,通过apt-get进行安装,以安装x86 xxx.deb包为例:exagear显示:Starting the shell in the guest image /opt/exagear/images/ubuntu-<os_version>-x86_64通过apt-get进行安装:sudo apt-get install xxx运行x86应用程序x86应用程序安装完成后,在guest和host中均可运行。你可以运行exagear命令启动guest shell,并运行任何x86应用程序,运行方式和在x86系统中一样。例如:exagear显示:Starting the shell in the guest image /opt/exagear/images/ubuntu-<os_version>-x86_64 which nginx显示:/usr/sbin/nginx/usr/sbin/nginx -h在host会话中,你有如下两种选择:在同一行中输入exagear -- 命令和guest系统内部的x86应用程序路径。例如:exagear -- /usr/sbin/nginx -h输入x86应用程序二进制文件的完整路径。这个文件位于一个特定的目录下:/opt/exagear/images/ubuntu-<os_version>-x86_64,即x86运行环境。例如: /opt/exagear/images/ubuntu-<os_version>-x86_64/usr/sbin/nginx -h默认配置下,x86应用程序只能访问x86运行环境中的文件,如果需要访问host系统上的某些文件,可以通过设置host与guest共享目录文件方式。详细配置方法请参考“配置ExaGear for Server on Ubuntu”中的host系统和guest系统共享的共享章节。注:对于脚本程序,推荐进入guest环境后运行,或者在host上运行该命令:exagear -- /path/to/script以test.sh脚本为例,脚本内容:#!/bin/bashif [ `arch` == 'x86_64' ]then echo "ok"else echo "fail"fi运行结果对比:./test.sh显示:failexagear -- ./test.sh显示:ok配置ExaGear for Server on Ubuntuhost系统和guest系统共享的目录ExaGear for Server整个文件系统对host系统应用程序可见,只有guest系统文件对Linux on x86应用程序可见。图1 host系统和guest系统的文件系统(以Ubuntu 18为例)放大配置文件/opt/exagear/images/ubuntu-<os_version>-x86_64/.exagear/vpaths-list包含了host系统和guest系统共享的一系列目录和文件。cat /opt/exagear/images/ubuntu-<os_version>-x86_64/.exagear/vpaths-list显示:/home//etc/adduser.conf/etc/deluser.conf...以下文件及配置会在host系统和guest系统中共享使用:User accountsuser groupsuser privileges/home directorieshost configurationssystem information provided by Linux kerneldevices and diskssocketspidfilesmount pointslogstemporary files如需创建一个在host系统和guest系统之间共享的目录或文件,可以选择如下两种方法:方法1:修改vpaths-list配置文件。修改vpaths-list配置文件的操作步骤如下:确保host系统中存在所需的目录(或文件)。在guest系统中创建同名的假目录(或文件)。将目录(或文件)的完整路径另取一行添加到配置文件opt/exagear/images/ubuntu-<os_version>-x86_64/.exagear/vpaths-list中。请注意,配置文件的目录应以“/”结尾方法2:将host系统要共享的目录挂载到guest系统以/newdir为例,使用coreutils中的mount实用程序将host系统要共享的目录挂载到guest系统:在x86运行环境中创建挂载点:sudo mkdir /opt/exagear/images/ubuntu-<os_version>-x86_64/shareddir将newdir挂载到shareddir挂载点下:sudo mount --bind /newdir /opt/exagear/images/ubuntu-<os_version>-x86_64/shareddir说明在卸载ExaGear for Server前,需要卸载挂载的目录:sudo umount /opt/exagear/images/ubuntu-<os_version>-x86_64/shareddir代理配置/etc/environment在host系统和guest系统之间不共享。因此,如果你需要在本地网络中通过/etc/environment使用代理配置,确保你已经使用如下命令将配置复制到guest系统:sudo cp /etc/environment /opt/exagear/images/ubuntu-<os_version>-x86_64/etc/environmentsysctl和网络配置guest系统可以访问/proc/sys/中的sysctl设置,但不能修改/etc/sysctl.conf或/etc/sysctl.d/。sysctl参数的配置应在host系统上进行。guest系统可以访问描述网络配置的文件(例如/etc/resolv.conf)但guest系统不可修改网络设置。网络设置的配置应在host系统上进行。guest SysV风格的init脚本和systemd在host中的集成Ubuntu采用systemd作为init系统,并支持SysV风格的init脚本作为legacy。许多Ubuntu包中会提供这些服务的配置。默认情况下,系统采用全自动集成机制将guest服务表示为host服务。guest服务被注册为host服务,服务名称相同。因此,你可以从guest和host shell来管理guest服务(在这种情况下,host服务会将所有start/stop/testconfig等请求委托给各自的guest服务)。以Ubuntu为例:    exagear显示:Starting the shell in the guest image /opt/exagear/images/ubuntu-<os_version>-x86_64    sudo apt-get updatesudo apt-get install nginxexitsudo service nginx startservice nginx status显示:● nginx.service - nginx - high performance web serverLoaded: loaded (/run/systemd/generator.late/nginx.service; generated)Active: active (running) since Fri 2019-05-17 11:56:35 EDT; 2h 54min ago ...你可以通过列出guest系统的/opt/exagear/images/ubuntu-<os_version>-x86_64/etc/init.d/和/opt/exagear/images/ubuntu-<os_version>-x86_64/lib/systemd/system/目录中的文件来查看guest系统中注册了哪些服务。内存序模型ExaGear for Server 默认为Linux on x86应用程序提供弱内存序模型。如果软件依赖于x86强内存序模型,则可能需要强制ExaGear严格遵循x86强内存序模型。可以通过修改ExaGear for Server配置文件来完成: sudo sed -i -e '/EXAGEAR_SMO_MODE="/s/fbase"/all"/' /etc/exagear-x86_32.confsudo sed -i -e '/EXAGEAR_SMO_MODE="/s/fbase"/all"/' /etc/exagear-x86_64.conf使用以下命令恢复弱内存序模型: sudo sed -i -e '/EXAGEAR_SMO_MODE="/s/all"/fbase"/' /etc/exagear-x86_32.confsudo sed -i -e '/EXAGEAR_SMO_MODE="/s/all"/fbase"/' /etc/exagear-x86_64.conf动态二次优化ExaGear 动态二次优化可以在程序翻译运行时动态识别优化的机会,进一步提升翻译效果。动态二次优化依赖硬件SPE功能(SPE是否可用的检测手段查看6-FAQ),以系统服务进程的形式存在。启用动态二次优化需要设置ExaGear配置选项EXAGEAR_USE_OPT="y"。该选项默认为"n"关闭。例如:分别在32位和64位应用程序的配置文件/etc/exagear-x86_32.conf 和 /etc/exagear-x86_64.conf中,设置EXAGEAR_USE_OPT="y",以开启动态二次优化。启动动态二次优化服务: sudo systemctl start exagear-x86_64-opt.servicesudo systemctl start exagear-x86_32-opt.service查看动态二次优化服务状态: sudo systemctl status exagear-x86_64-opt.servicesudo systemctl status exagear-x86_32-opt.service此时可以看到opt.x64a64.socket、opt.x32a64.socket 的进程服务启动。设置/关闭动态二次优化服务自启动:    sudo systemctl enable exagear-x86_64-opt.servicesudo systemctl disable exagear-x86_64-opt.servicesudo systemctl enable exagear-x86_32-opt.servicesudo systemctl disable exagear-x86_32-opt.service须知设置EXAGEAR_USE_OPT="y"之前,请确认exagear-x86_*-opt.service相关进程处于开启状态。返回地址优化ExaGear for Server默认关闭了返回地址优化,相关配置及其值设置为EXAGEAR_DISABLE_NATIVE_RETURN_ADDR_OPT="y",如需开启返回地址优化,参考以下操作实现。sudo sed -i -e '/EXAGEAR_DISABLE_NATIVE_RETURN_ADDR_OPT="/s/y"/n"/' /etc/exagear-x86_32.confsudo sed -i -e '/EXAGEAR_DISABLE_NATIVE_RETURN_ADDR_OPT="/s/y"/n"/' /etc/exagear-x86_64.confx86位相同精度在ExaGear中,如果需要以与x86 bit位相同精度进行32/64位的浮点计算(例如,对于使用 SIMD 寄存器的计算),应该启用额外的 ExaGear 选项EXAGEAR_X86_PRECISE_FP。此选项默认为禁用,但可以通过将 EXAGEAR_X86_PRECISE_FP 参数设置为 "y"进行开启,例如:在分别针对32位和64位应用程序进行符号链接配置的文件/etc/exagear- x86_32.conf 和 /etc/exagear-x86_64.conf中添加EXAGEAR_X86_PRECISE_FP="y"。说明启用EXAGEAR_X86_PRECISE_FP选项可能会显著降低ExaGear的整体性能。80位 x86精度在 ExaGear 中,如果需要具有 80 位 x86 精度,则应启用额外的 ExaGear 选项 EXAGEAR_X86_PRECISE_80BIT_FP。此选项默认为禁用,但可以通过将EXAGEAR_X86_PRECISE_80BIT_FP参数设置为“y”进行开启,例如:在分别针对32位和64位应用程序进行符号链接配置的文件/etc/exagear- x86_32.conf 和 /etc/exagear-x86_64.conf中添加EXAGEAR_X86_PRECISE_80BIT_FP="y"。
  • [技术干货] 【Linux】gdb_进程概念-转载
    📢前言在 Linux 系统的广袤世界里,高效的程序调试与对进程概念的深刻理解,宛如鸟之双翼、车两轮,是开发者不可或缺的重要技能。GDB(GNU Debugger)作为一款强大的调试工具,犹如开发者手中的精密仪器,能够深入程序内部,精准定位问题所在。而进程概念则是理解 Linux 操作系统运行机制的核心,它关乎资源分配、任务调度等诸多关键环节。本文将引领大家深入探索 GDB 调试工具与进程概念,通过丰富的实例和详细的解析,助力大家掌握这两项重要技能。 🏳️‍🌈预备程序的发布⽅式有两种, debug 模式和 release 模式, Linux gcc/g++ 出来的⼆进制程序,默认是 release 模式。 要使⽤gdb调试,必须在源代码⽣成⼆进制程序的时候, 加上 -g 选项,如果没有添加,程序⽆法被编译 $ gcc mycmd.c -o mycmd # 默认模式,不⽀持调试$ file mycmdmycmd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamicallylinked, interpreter /lib64/ld-linux-x86-64.so.2,BuildID[sha1]=82f5cbaada10a9987d9f325384861a88d278b160, for GNU/Linux3.2.0, not stripped$ gcc mycmd.c -o mycmd -g # debug模式$ file mycmdmycmd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamicallylinked, interpreter /lib64/ld-linux-x86-64.so.2,BuildID[sha1]=3d5a2317809ef86c7827e9199cfefa622e3c187f, for GNU/Linux3.2.0, with debug_info, not stripped 🏳️‍🌈常见使用开始: gdb File退出: ctrl + d 或 quit 调试命令  这是要用到的两个文件,执行生成 myexe 文件,然后用 gdb 调试 [wzy@VM-20-5-centos lesson11]$ cat code.c#include <stdio.h> int Sum(int s, int e){    int sum = 0;    int i = s;    for(; i <= e; ++i){        sum += i;    }    return sum;} int main(){    printf("process is running\n");        int start = 1;    int end = 100;    int result = Sum(start, end);     printf("process has done, result: %d\n", result);}[wzy@VM-20-5-centos lesson11]$ cat Makefilemyexe:code.cgcc -o $@ $^ -g .PHONY:cleanclean:rm -f myexe[wzy@VM-20-5-centos lesson11]$ lltotal 20-rw-rw-r-- 1 wzy wzy  324 Jan 17 15:54 code.c-rw-rw-r-- 1 wzy wzy   64 Jan 17 16:02 Makefile-rwxrwxr-x 1 wzy wzy 9688 Jan 17 16:02 myexe l + 数字:查看当前文件中以想要查找的行为中间行的上下10行 l + 字符:效果同上 回车:gdb会记录最近的一条命令,直接回车会继续执行 b + 数字:在指定行添加断点,若当前行没有数据,会提示info b:显示当前有哪些断点 d + 数字:删除指定序号的断点 🏳️‍🌈常见技巧 - cgdb先需要安装一下 cgdb - sudo tum install -y cgdb cgdb的用法和gdb如出一辙,但是多了一个可视化界面,能够在调试的同时观察代码 cgdb myexe1  👥总结本篇博文对 gdb_进程概念 做了一个较为详细的介绍,不知道对你有没有帮助呢 觉得博主写得还不错的三连支持下吧!会继续努力的~————————————————                             版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/2301_77954967/article/details/145208356
  • [互动交流] 安装固件报错,找不到PCIE设备
    安装固件报错,找不到PCIE设备
  • [互动交流] HiLens支持从SD卡启动吗?
    SD卡烧录好欧拉系统,HiLens能够设置SD卡为启动首选项,使用SD卡烧录的系统吗?备注:HiLens根目录只有2GB,空间太少了,安装几个库就没有空间了,总计32GB的空间,分成了很多个区,不敢贸然合并,所以想搞个大容量SD卡,烧录系统使用
  • [技术干货] Linux下shell基本命令之grep用法及示例小结【转】
    grep 是 Unix 和 Linux 系统中广泛使用的文本搜索工具,它允许用户搜索包含指定模式的文本行。以下是 grep 命令的基本用法及示例:一、基本用法1grep [选项] 模式 [文件...]二、常用选项-i:忽略大小写。-v:反向选择,显示不包含模式的行。-n:显示匹配行的行号。-l:显示包含匹配模式的文件名。-L:显示不包含匹配模式的文件名。-c:只输出匹配到的行数。-w:只匹配整个单词。-x:只匹配整行。-r 或 -R:递归搜索目录中的文件。-A NUM:匹配行及后面 NUM 行一起显示。-B NUM:匹配行及前面 NUM 行一起显示。-C NUM:匹配行及前后各 NUM 行一起显示。三、正则表达式grep 支持正则表达式,这使得它可以进行复杂的文本搜索。例如:.:匹配任意单个字符。*:匹配前一个字符零次或多次。^:匹配行的开始。$:匹配行的结束。[]:匹配括号内的任意一个字符。|:表示或的关系,如 a|b 匹配 a 或 b。():分组,用于后向引用。四、示例‌搜索文本‌假设有一个文件 example.txt,内容如下:Hello World hello unix GREP is powerful要在文件中搜索包含 "hello" 的行(忽略大小写),可以使用以下命令:1grep -i "hello" example.txt输出:Hello World hello unix使用正则表达式‌要在文件中搜索以 "G" 开头并以 "p" 结尾的单词,可以使用以下命令:1grep -w "\bG\w*p\b" example.txt输出:GREP is powerful‌显示行号‌要在文件中搜索包含 "World" 的行并显示行号,可以使用以下命令:1grep -n "World" example.txt输出:1:Hello World‌反向选择‌要在文件中搜索不包含 "unix" 的行,可以使用以下命令:1grep -v "unix" example.txt输出:Hello World GREP is powerful‌递归搜索‌要在目录 mydir 及其子目录中的所有文件中搜索包含 "error" 的行,可以使用以下命令:1grep -r "error" mydir/
  • [技术干货] Linux内核验证套件(LKVS)
     LKVS 介绍 Linux内核验证套件(LKVS)是英特尔内核组开发的一款面向Linux内核测试的综合测试工具集。目前已在openEuler社区开源。它汇聚了英特尔内核开发和验证团队多年积累的专业知识,具有专业性轻量级、低耦合、高覆盖三大特点,可广泛应用于Linux系统开发和验证的多个场景。 高覆盖测试内容 1. 全面测试范围 LKVS集成了600余项测试用例,覆盖Linux内核的20余项关键特性,测试领域涉及CPU特性、电源管理和安全特性等方面。具体来说包括这些英特尔平台的功能:  CET(Control flow Enhancement Technology),cstate,Intel_TH(Trace Hub),Intel_PT,UMIP(User-Mode Instruction Prevention),xsave,IFS(In Field Scan),FRED (in progress),guest-test,IFS,ISST,PMU,RAPL,SDSI,splitlock,tdx-compliance,thermal,CPU topology,UFS,AMX。  2. 深入测试覆盖 测试内容高度专业,紧跟英特尔当下开发中的新平台、新功能,能够有效覆盖到关键内核路径和硬件集成细节,挖掘异常并提高稳定性。  3. 持续扩展测试 LKVS套件从上线开始,就不断在迭代更新中。通过涵盖广泛的专业测试内容,LKVS能够有效保证Linux环境在英特尔平台上的健壮性,发现新的或者留存的隐患和异常。  灵活解耦的测试框架 1. 解耦组件设计 LKVS是从大量内部功能测试、回滚测试框架中解耦并独立出来的测试用例。最大程度降低了测试用例的耦合度,使得测试用例更易于移植和扩展。可以方便地集成至各类基础设施比如openEuler社区的EulerPipeLine(CI/CD)系统。  2. 多语言测试脚本 测试套件支持C语言、bash、python等语言编写测试用例。因为各个功能测试之间相互独立,使得各个不同的功能测试之间零耦合,使其对开发者更友好,易于贡献测试用例。  通过这种可扩展可定制的低耦合架构,有利于LKVS的长期演进。 丰富的场景应用 LKVS可广泛服务于以下关键场景:  1. CI/CD集成检验 自动化测试减少集成风险,提高质量管控水平;  2.  基线功能验证 充分覆盖基线功能,成熟度评估;  3. 硬件兼容性 验证最新硬件特性,避免兼容性问题;  4. 安全审计 主动发掘和减轻安全隐患;  5. 回归测试 定位修补程序和版本升级可能引入的问题。  后续规划 本项目已开源在openEuler社区,并且已经集成到EulerPipeLine系统中,计划中的开发:  ● 支持将要发布的新平台的新功能。  ● 支持新的内核特性。  ● 进一步完善框架本身,提高测试用例的可扩展性和可移植性。  ● 完善虚拟机测试场景,多虚拟机测试场景。  LKVS是结合Linux Kernel专业知识而产出的工程化测试集,可广泛应用于Linux系统开发和验证的多个场景。其专业性、低耦合和高覆盖等特点为生态中的众多参与方创造了价值。 
  • [技术干货] 【Linux】多用户协作-转载
    前言 前一个系列【Linux之权限】我们详细地了解了Linux中权限的问题,那么,我们理解了用户间的“隔离”,那么又该怎么进行多用户协作呢? 本文就来回答这个问题。  现象 在正式回答这个问题之前,我们先来看一下这个现象。 我们在用户whb的所属的一个目录里以root身份创造了一个文件root.txt,其拥有者和所属组都是root,然后我们通过sudo chmod a-rwx root.txt把所有人的所有权限去掉。 可以看到,所以现在我们的whb没法cat和echo这个root.txt,没有读写权限。但是我们居然可以把这个文件删除: 这个问题的原因是什么?这正常吗?这其实是正常的。因为当前我们是在lesson5目录下: whb是这个目录的拥有者,对该目录有写权限,而对目录有写权限就意味着可以在这个目录里创建和删除文件。因为一个文件能否被删除和文件自己无关!而是和所处的目录权限有关。  一个用户对文件所处的目录有写权限,那么就算这个文件没有给这个用户开放任何权限,这个用户却可以把这个文件删除。  不过除了root,一般情况下没有人可以进入“我家”,在“我家”新建文件。 多用户协作 但若两人想协作呢?想共同修改或者查看一个配置文件。  它俩账号之间隔离,进入不了彼此的目录。如果两个用户之间要进行文件级别的协作呢?那么这个文件就不能放在任何一个用户的私人账号里。  应该放在哪?  Linux里不只有home目录。  Linux的根目录下有一个和home平级的路径:tmp ll / 我们可以看到根目录里: bin或者user/bin里面一般放的就是系统的指令;  boot里面一般是与启动相关的操作系统或配置文件;  dev是启动时操作系统所识别到的各种设备,如键盘、显示器、网卡;  etc都是系统启动后的一些配置文件;  home我们已经知道了;  lib或者user/lib代表的一般是一些动静态库;  lib64指的是64位的库;  lost+found这个不管;  media是一些媒体设备(u盘);  mnt和opt不管;  proc是查看进程相关的信息;  root就是root账号;  var是日志信息;  ……  我们将目光放到tmp路径,这里面放的是系统产生的各种临时文件。  根目录可以用d选项进行查看其权限:  ls -ld / 根目录的拥有者和所属组都是root,想在根目录下新建文件,也必须使用root权限。  我们现在在根目录下新建一个temp-backup目录,并把权限放开。  (共享文件夹一般都属于root) (其实以后用tmp就可以)  但是,这个目录里面的文件现在,谁都能删除。  共享类目录下,别人为什么能删自己的文件呢?因为删一个文件和文件本身无关,而是和所处目录的写权限相关。  但是other的w我们不能去掉,因为这样,其他人就不能在这个目录下创建文件,共享的作用就不复存在了。  这就形成了一个悖论。  所以 Linux中引入了一个新的权限标志位,粘滞位:t 可以看到,x的位置变成了t。  这种给目录添加的t,叫做粘滞位,任何一个人可以在共享目录下新建,但是不能让非拥有者删除。只给需要共享的目录添加粘滞位。  但这粘滞位,对root没办法,它还是可以删除里面的文件。  我们可以看回根目录下的tmp,已经有粘滞位了。所以用它就可以 ————————————————                              版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                          原文链接:https://blog.csdn.net/2301_82135086/article/details/143807906 
  • [技术干货] 【Linux】线程同步与互斥 (生产者消费者模型-转载
     一:🔥 线程互斥 🦋 1-1 进程线程间的互斥相关背景概念 临界资源:多线程执⾏流共享的资源就叫做临界资源。 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起保护作⽤。 原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。 🦋 1-2 互斥量mutex ⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程⽆法获得这种变量。 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。 多个线程并发的操作共享变量,会带来⼀些问题。 // 操作共享变量会有问题的售票系统代码 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h>  int ticket = 100;  void *route(void *arg) {     char *id = (char*)arg;     while ( 1 ) {         if ( ticket > 0 ) {             usleep(1000);             printf("%s sells ticket:%d\n", id, ticket);             ticket--;             } else {             break;         }     } }   int main( void ) {     pthread_t t1, t2, t3, t4;          pthread_create(&t1, NULL, route, "thread 1");     pthread_create(&t2, NULL, route, "thread 2");     pthread_create(&t3, NULL, route, "thread 3");     pthread_create(&t4, NULL, route, "thread 4");          pthread_join(t1, NULL);     pthread_join(t2, NULL);     pthread_join(t3, NULL);     pthread_join(t4, NULL); }  🥗 ⼀次执⾏结果:  thread 4 sells ticket:100 ... thread 4 sells ticket:1 thread 2 sells ticket:0 thread 1 sells ticket:-1 thread 3 sells ticket:-2 为什么可能⽆法获得争取结果?  if 语句判断条件为真以后,代码可以并发的切换到其他线程 usleep 这个模拟漫⻓业务的过程,在这个漫⻓的业务过程中,可能有很多个线程会进⼊该代码段 –ticket 操作本⾝就不是⼀个原⼦操作 操作并不是原⼦操作,⽽是对应三条汇编指令:  load :将共享变量 ticket 从内存加载到寄存器中 update : 更新寄存器⾥⾯的值,执⾏ -1 操作 store :将新值,从寄存器写回共享变量 ticket 的内存地址 要解决以上问题,需要做到三点:  代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程进⼊该临界区。 如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区。要做到这三点,本质上就是需要⼀把锁。Linux上提供的这把锁叫互斥量。  🦋 互斥量的接⼝ 🍡 初始化互斥量 初始化互斥量有两种⽅法: ⽅法1,静态分配:  pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ⽅法2,动态分配:  int pthread_mutex_init(pthread_mutex_t *restrict mutex, const                         pthread_mutexattr_t *restrict attr);  参数:     mutex:要初始化的互斥量     attr:NULL 🍡 销毁互斥量 销毁互斥量需要注意:  使⽤ PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁 不要销毁⼀个已经加锁的互斥量 已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁 int pthread_mutex_destroy(pthread_mutex_t *mutex); 1 🍡 互斥量加锁和解锁  int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); 调⽤ pthread_ lock 时,可能会遇到以下情况:  互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功 发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么 pthread_ lock 调⽤会陷⼊阻塞(执⾏流被挂起),等待互斥量解锁。 🍧 改进上⾯的售票系统:  #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <sched.h>  int ticket = 100; pthread_mutex_t mutex;  void *route(void *arg) {     char *id = (char *)arg;     while (1)     {         pthread_mutex_lock(&mutex);         if (ticket > 0)         {             usleep(1000);             printf("%s sells ticket:%d\n", id, ticket);             ticket--;             pthread_mutex_unlock(&mutex);             // sched_yield(); 放弃CPU         }         else         {             pthread_mutex_unlock(&mutex);             break;         }     } }  int main() {     pthread_t t1, t2, t3, t4;     pthread_mutex_init(&mutex, NULL);     pthread_create(&t1, NULL, route, (void*)"thread 1");     pthread_create(&t2, NULL, route, (void*)"thread 2");     pthread_create(&t3, NULL, route, (void*)"thread 3");     pthread_create(&t4, NULL, route, (void*)"thread 4");      pthread_join(t1, NULL);     pthread_join(t2, NULL);     pthread_join(t3, NULL);     pthread_join(t4, NULL);      pthread_mutex_destroy(&mutex);     return 0; } 🦋 1-3 互斥量实现原理探究 经过上⾯的例⼦,⼤家已经意识到单纯的 i++ 或者 ++i 都不是原⼦的,有可能会有数据⼀致性问题 为了实现互斥锁操作, ⼤多数体系结构都提供了 swap 或 exchange 指令, 该指令的作⽤是把寄存器和内存单元的数据相交换, 由于只有⼀条指令, 保证了原⼦性, 即使是多处理器平台, 访问内存的总线周期也有先后, ⼀个处理器上的交换指令执⾏时另⼀个处理器的交换指令只能等待总线周期。 现在我们把 lock 和 unlock 的伪代码改⼀下 。  🦋 1-4 互斥量的封装 Mutex.hpp  #pragma once  #include <iostream> #include <pthread.h>  namespace MutexModule {     class Mutex     {     public:         Mutex(const Mutex&) = delete;         const Mutex& operator = (const Mutex&) = delete;         Mutex()         {             int n = ::pthread_mutex_init(&_lock, nullptr);             (void)n;         }          void Lock()         {             int n = ::pthread_mutex_lock(&_lock);             (void)n;         }          void Unlock()         {             int n = ::pthread_mutex_unlock(&_lock);             (void)n;         }          ~Mutex()         {             int n = ::pthread_mutex_destroy(&_lock);             (void)n;         }     private:         pthread_mutex_t _lock;     };      class LockGuard     {     public:         LockGuard(Mutex &mtx)             :_mtx(mtx)         {             _mtx.Lock();         }         ~LockGuard()         {             _mtx.Unlock();         }     private:         Mutex &_mtx;     }; } 🍧 抢票的代码就可以更新成为  #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include "Mutex.hpp"  using namespace MutexModule; int ticket = 1000;  Mutex mutex; void *route(void *arg) {     char *id = (char *)arg;     while (1)     {         LockGuard lockguard(mutex); // 使⽤RAII⻛格的锁         if (ticket > 0)         {             usleep(1000);             printf("%s sells ticket:%d\n", id, ticket);             ticket--;         }         else         {             break;         }     }     return nullptr; }  int main() {     pthread_t t1, t2, t3, t4;      pthread_create(&t1, NULL, route, (void *)"thread 1");     pthread_create(&t2, NULL, route, (void *)"thread 2");     pthread_create(&t3, NULL, route, (void *)"thread 3");     pthread_create(&t4, NULL, route, (void *)"thread 4");      pthread_join(t1, NULL);     pthread_join(t2, NULL);     pthread_join(t3, NULL);     pthread_join(t4, NULL);      return 0; } RAII⻛格的互斥锁, C++11也有,⽐如:   std::mutex mtx; std::lock_guard<std::mutex> guard(mtx);  此处我们仅做封装,⽅便后续使⽤,详情⻅C++博客  二:🔥 线程同步 🦋 2-1 条件变量 🍥 当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。 🍥 例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列中。这种情况就需要⽤到条件变量。 🦋 2-2 同步概念与竞态条件 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从⽽有效避免饥饿问题,叫做同步。  竞态条件:因为时序问题,⽽导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解  🦋 2-3 条件变量函数 🌯 初始化  int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t                         *restrict attr);  参数:     cond:要初始化的条件变量     attr:NULL 🌯 销毁  int pthread_cond_destroy(pthread_cond_t *cond) 1 🌯 等待条件满⾜  int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict                         mutex);  参数:     cond:要在这个条件变量上等待     mutex:互斥量,后⾯详细解释 🌯 唤醒等待  int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond); 1 2 简单案例:  我们先使⽤ PTHREAD_COND / MUTEX_INITIALIZER 进⾏测试,对其他细节暂不追究 然后将接⼝更改成为使⽤ pthread_cond_init / pthread_cond_destroy 的⽅式,⽅便后续进⾏封装 #include <iostream> #include <string> #include <pthread.h> #include <unistd.h>  pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  void *active(void *args) {     std::string name = static_cast<const char*>(args);      while(true)     {         pthread_mutex_lock(&mutex);          // 没有对资源是否就绪的判定         pthread_cond_wait(&cond, &mutex);         printf("%s is active!\n", name.c_str());          pthread_mutex_unlock(&mutex);     }      return nullptr; }  int main() {     pthread_t tid1, tid2, tid3;     pthread_create(&tid1, nullptr, active, (void*)"thread-1");     pthread_create(&tid1, nullptr, active, (void*)"thread-2");     pthread_create(&tid1, nullptr, active, (void*)"thread-3");      sleep(1);     printf("Main thread ctrl begin...\n");      while(true)     {         printf("main wakeup thread...\n");         pthread_cond_signal(&cond);         sleep(1);     }       pthread_join(tid1, nullptr);     pthread_join(tid2, nullptr);     pthread_join(tid3, nullptr);      return 0; } 🧋运行结果:  Main thread ctrl begin... main wakeup thread... thread-1 is active! main wakeup thread... thread-2 is active! main wakeup thread... thread-3 is active! 三:🔥 ⽣产者消费者模型 321原则(便于记忆) 三种关系 两个角色 一个消费场所(某种数据结构组织的连续的内存空间) 🥙 生产者-消费者模型(Producer-Consumer Model)是一种经典的多线程同步问题,它描述了两个线程(或进程)之间的协作:一个或多个生产者线程生成数据项,并将它们放入缓冲区中;一个或多个消费者线程从缓冲区中取出数据项,并进行处理。这个模型通常用于解决生产者和消费者在不同速度下工作时的同步和数据传输问题。  🦋 3-1 为何要使⽤⽣产者消费者模型 💜 ⽣产者消费者模式就是通过⼀个容器来解决⽣产者和消费者的强耦合问题。⽣产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取,阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒。这个阻塞队列就是⽤来给⽣产者和消费者解耦的。  🦋 3-2 ⽣产者消费者模型优点 🌶️ 解耦 🌶️ 支持并发 🌶️ 支持忙闲不均  🦋 3-3 基于BlockingQueue的⽣产者消费者模型 🍱 3-3-1 BlockingQueue 在多线程编程中阻塞队列 (Blocking Queue) 是⼀种常⽤于实现⽣产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放⼊了元素;当队列满时,往队列⾥存放元素的操作也会被阻塞,直到有元素被从队列中取出 (以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)   🍱 3-3-2 C++ queue模拟阻塞队列的⽣产消费模型 代码:  为了便于理解,我们以单⽣产者,单消费者,来进⾏讲解。 刚开始写,我们采⽤原始接⼝。 我们先写单⽣产,单消费。然后改成多⽣产,多消费(这⾥代码其实不变,这里用到了更后面的cond的封装头文件)。 BlockQueue.hpp  #pragma  #include <iostream> #include <queue> #include <pthread.h> #include "Mutex.hpp" #include "Cond.hpp"  namespace BlockQueueModule {     using namespace LockModule;     using namespace CondModule;      // version2     static const int gcap = 10;      template<typename T>     class BlockQueue     {     public:         BlockQueue(int cap = gcap)             :_cap(cap)             ,_cwait_num(0)             ,_pwait_num(0)         {}          bool IsFull() { return _q.size() == _cap; }          bool IsEmpty() { return _q.empty(); }          void Equeue(const T &in)   // 生产者         {             LockGuard lockguard(_mutex);              // 生产数据有条件             // 结论1:在临界区中等待是必然的(当前)             while(IsFull())  // 为了防止伪唤醒 使用while判断             {                 std::cout << "生产者进入等待..." << std::endl;                  // 2. 等待 释放锁                 _pwait_num++;                 _productor_cond.Wait(_mutex);     // wait的时候,必定是持有锁的                 _pwait_num--;                 // 3. 返回,线程被唤醒 重新申请并持有锁                 std::cout << "生产者被唤醒..." << std::endl;             }             // 4. isfull不满足 || 线程被唤醒              _q.push(in);    // 生产              // 肯定有数据             if(_cwait_num)             {                 std::cout << "叫醒消费者" << std::endl;                 _consumer_cond.Notify();             }         }          void Pop(T* out)  // 消费者         {             LockGuard lockguard(_mutex);              while(IsEmpty())             {                 std::cout << "消费者进入等待..." << std::endl;                 _cwait_num++;                 _consumer_cond.Wait(_mutex);   // 伪唤醒                 _cwait_num--;                 std::cout << "消费者被唤醒..." << std::endl;             }             // 4. 线程被唤醒             *out = _q.front();             _q.pop();              // 一定不为满             if(_pwait_num)             {                 std::cout << "叫醒生产者" << std::endl;                 _productor_cond.Notify();             }         }          ~BlockQueue()         {}     private:         std::queue<T> _q;                   // 临界资源         Mutex _mutex;             // 互斥         Cond _productor_cond;     // 生产者条件变量         Cond _consumer_cond;      // 消费者条件变量         int _cap;                           // bq最大容量          int _cwait_num;         int _pwait_num;     }; }  main.cc  #include <functional> #include "BlockQueue.hpp" #include "Task.hpp" #include <unistd.h>  using namespace BlockQueueModule; using namespace TaskModule;  using task_t = std::function<void()>;  void *Comsumer(void *args) {     BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);      int data = 10;     while(true)     {         // 1. 从bq中拿到数据         bq->Pop(&data);          // 2. 做处理         printf("Comsumer 消费了一个数据:%d\n", data);         data++;     } }  void *Productor(void *args) {     BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);      // 1. 从外部获取数据     int data = 10;     while(true)     {         sleep(2);         // 2. 生产到队列中         printf("Productor 生产了一个数据:%d\n", data);         bq->Equeue(data);          data++;     } }  int main() {     BlockQueue<int> *bq = new BlockQueue<int>(5);     // 共享资源 -> 临界资源     // 单生产 单消费     pthread_t c1, c2, p1, p2, p3;     pthread_create(&c1, nullptr, Comsumer, (void*)bq);     pthread_create(&p3, nullptr, Productor, (void*)bq);      pthread_join(c1, nullptr);     pthread_join(p3, nullptr);      delete bq;      return 0; } main.cc  #include "BlockQueue.hpp" #include <unistd.h>  using namespace BlockQueueModule;  void *Comsumer(void *args) {     BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);      int data = 10;     while(true)     {         // 1. 从bq中拿到数据         bq->Pop(&data);          // 2. 做处理         printf("Comsumer 消费了一个数据:%d\n", data);         data++;     } }  void *Productor(void *args) {     BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);      // 1. 从外部获取数据     int data = 10;     while(true)     {         sleep(2);         // 2. 生产到队列中         printf("Productor 生产了一个数据:%d\n", data);         bq->Equeue(data);          data++;     } }  int main() {     BlockQueue<int> *bq = new BlockQueue<int>(5);     // 共享资源 -> 临界资源     // 单生产 单消费     pthread_t c1, c2, p1, p2, p3;     pthread_create(&c1, nullptr, Comsumer, (void*)bq);     pthread_create(&p3, nullptr, Productor, (void*)bq);      pthread_join(c1, nullptr);     pthread_join(p3, nullptr);      delete bq;      return 0; } 输出结果:  root@hcss-ecs-a9ee:~/code/linux/112/lesson31/2.BlockQueue# ./bq  消费者进入等待... Productor 生产了一个数据:10 叫醒消费者 消费者被唤醒... Comsumer 消费了一个数据:10 消费者进入等待... Productor 生产了一个数据:11 叫醒消费者 消费者被唤醒... Comsumer 消费了一个数据:11 四:🔥 为什么 pthread_cond_wait 需要互斥量? 条件等待是线程间同步的⼀种⼿段,如果只有⼀个线程,条件不满⾜,⼀直等下去都不会满⾜,所以必须要有⼀个线程通过某些操作,改变共享变量,使原先不满⾜的条件变得满⾜,并且友好的通知等待在条件变量上的线程。 条件不会⽆缘⽆故的突然变得满⾜了,必然会牵扯到共享数据的变化。所以⼀定要⽤互斥锁来保护。没有互斥锁就⽆法安全的获取和修改共享数据。  按照上⾯的说法,我们设计出如下的代码:先上锁,发现条件不满⾜,解锁,然后等待在条件变量上不就⾏了,如下代码: 错误的设计 pthread_mutex_lock(&mutex); while (condition_is_false) {     pthread_mutex_unlock(&mutex);     //解锁之后,等待之前,条件可能已经满⾜,信号已经发出,但是该信号可能被错过     pthread_cond_wait(&cond);     pthread_mutex_lock(&mutex); }  pthread_mutex_unlock(&mutex); 由于解锁和等待不是原⼦操作。调⽤解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满⾜,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是⼀个原⼦操作。 (这就是为什么wait的时候需要把条件变量和锁一起传进去) int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t *mutex); 进⼊该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到 cond_ wait 返回,把条件量改成1,把互斥量恢复成原样。 🦋 4-1 条件变量使⽤规范 等待条件代码 pthread_mutex_lock(&mutex); while (条件为假)     pthread_cond_wait(cond, mutex);  修改条件 pthread_mutex_unlock(&mutex); 给条件发送信号代码 pthread_mutex_lock(&mutex); 设置条件为真 pthread_cond_signal(cond); pthread_mutex_unlock(&mutex); 🦋 4-2 条件变量的封装 基于上⾯的基本认识,我们已经知道条件变量如何使⽤,虽然细节需要后⾯再来进⾏解释,但这⾥可以做⼀下基本的封装,以备后⽤.  Cond.hpp  #pragma once  #include <iostream> #include <pthread.h> #include "Mutex.hpp"  namespace CondModule {     using namespace LockModule;      class Cond     {     public:         Cond()         {             int n = ::pthread_cond_init(&_cond, nullptr);             (void)n;         }                  void Wait(Mutex &mutex) // 让我们的线程释放曾经持有的锁!         {             int n = ::pthread_cond_wait(&_cond, mutex.LockPtr());         }          void Notify()         {             int n = ::pthread_cond_signal(&_cond);             (void)n;         }          void NotifyAll()         {             int n = ::pthread_cond_broadcast(&_cond);             (void)n;         }          ~Cond()         {             int n = ::pthread_cond_destroy(&_cond);         }     private:         pthread_cond_t _cond;     }; } 五:🔥 POSIX信号量 POSIX 信号量和 SystemV 信号量作⽤相同,都是⽤于同步操作,达到⽆冲突的访问共享资源⽬的。但 POSIX 可以⽤于线程间同步。  🍲 初始化信号量  #include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value);  参数:     pshared:0表⽰线程间共享,⾮零表⽰进程间共享     value:信号量初始值 🍲 销毁信号量  int sem_destroy(sem_t *sem); 1 🍲 等待信号量  功能:等待信号量,会将信号量的值减1   为0将进行阻塞等待 int sem_wait(sem_t *sem); //P() 1 2 🍲 发布信号量  功能:发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1。 int sem_post(sem_t *sem);//V() 1 2 上⼀节⽣产者-消费者的例⼦是基于queue的,其空间可以动态分配,现在基于固定⼤⼩的环形队列重写这个程序(POSIX信号量): ————————————————                              版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                          原文链接:https://blog.csdn.net/weixin_50776420/article/details/144121359 
  • [技术干货] 【Linux】:线程安全 + 死锁问题-转载
     1. 线程安全和重入问题🚀 1.1 基本概念  线程安全:就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。 1.2 线程安全和重入情况  🍥 学到现在,其实我们已经能理解重入其实可以分为两种情况  多线程重入函数 信号导致一个执行流重复进入函数 ① 常见的线程不安全的情况  不保护共享变量的函数 函数状态随着被调用,状态发生变化的函数 返回指向静态变量指针的函数 调用线程不安全函数的函数 ② 常见的线程安全的情况  每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的 类或者接口对于线程来说都是原子操作 多个线程之间的切换不会导致该接口的执行结果存在二义性  ③ 常见不可重入的情况  调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构 可重入函数体内使用了静态的数据结构  ④ 常见可重入的情况  不使用全局变量或静态变量 不使用用malloc或者new开辟出的空间 不调用不可重入函数 不返回静态或全局数据,所有数据都有函数的调用者提供 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据  提示 :不要被上面的一系列所弄晕,其实对应概念说的都是一回事  1.3 线程安全和重入的联系区别 📌 可重入与线程安全联系  函数是可重入的,那就是线程安全的 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。  📌 可重入与线程安全区别  可重入函数是线程安全函数的一种 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。  📌 注意:  如果不考虑 信号导致一个执行流重复进入函数 这种重入情况,线程安全和重入在安全角度不做区分 但是线程安全侧重说明线程访问公共资源的安全情况,表现的是 并发线程 的特点 可重入描述的是一个函数是否能被重复进入,表示的是 函数 的特点 2. 死锁 🖊 2.1 死锁基本概念 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。  为了方便表述,假设现在线程A,线程B必须同时持有 锁1和 锁2 ,才能进行后续资源的访问  🥑 申请一把锁是原子的,但是申请两把锁就不一定了​​​​​​  🥑 造成的结果如下:  2.2 死锁的四个必要条件 互斥条件:一个资源每次只能被一个执行流使用 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放  不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺  循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系   2.3 避免死锁 破坏死锁的四个必要条件 破坏循环条件等待问题:资源一次性分配,使用超时机制,加锁顺序一致 #include <iostream> #include <mutex> #include <thread> #include <vector> #include <unistd.h>   // 定义两个共享资源(整数变量)和两个互斥锁 int shared_resource1 = 0; int shared_resource2 = 0; std::mutex mtx1, mtx2;   // ⼀个函数,同时访问两个共享资源 void access_shared_resources() {     std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);     std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);     // 使⽤ std::lock 同时锁定两个互斥锁     std::lock(lock1, lock2);       // 现在两个互斥锁都已锁定,可以安全地访问共享资源     int cnt = 10000;     while (cnt--)     {         ++shared_resource1;         ++shared_resource2;     }     // 当离开 access_shared_resources 的作⽤域时,lock1 和 lock2 的析构函数会被自动调⽤     // 这会导致它们各⾃的互斥量被⾃动解锁 }   // 模拟多线程同时访问共享资源的场景 void simulate_concurrent_access() {     std::vector<std::thread> threads;     // 创建多个线程来模拟并发访问     for (int i = 0; i < 10; ++i)     {         threads.emplace_back(access_shared_resources);     }     // 等待所有线程完成     for (auto &thread : threads)     {         thread.join();     }     // 输出共享资源的最终状态     std::cout << "Shared Resource 1: " << shared_resource1 << std::endl;     std::cout << "Shared Resource 2: " << shared_resource2 << std::endl; }   int main() {     simulate_concurrent_access();     return 0; }  一次申请 不一次申请 避免锁未释放场景 2.4 死锁的预防 🔥 死锁的预防是通过破坏产生死锁的必要条件之一,是系统不会产生死锁。  简单方法是在系统运行之前就采取措施,即在系统设计时确定资源分配算法,消除发生死锁的任何可能性。该方法虽然比较保守、资源利用率低,但因简单明了并且安全可靠,仍被广泛采用。这是一种预先的静态策略。 🥝 破坏互斥条件 🎐 互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁。 如果把只能互斥使用的资源改造为允许共享使用,则系统不会进入死锁状态。比如:SPOOLing 技术。操作系统可以采用 SPOOLing 技术  把独占设备在逻辑上改造成共享设备。比如,用SPOOLing 技术 将打印机改造为共享设备.. 该策略的缺点:并不是所有的资源都可以改造成可共享使用的资源。并且为了系统安全,很多地方还必须保护这种互斥性。因此,很多时候都无法破坏互斥条件。  🥝 破坏不可剥夺条件  💢 不剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。  破坏不剥夺条件  当某个进程请求新的资源得不到满足时,它必须立即释放保持的所有资源,待以后需要时再重新申请。也就是说,即使某些资源尚未使用完,也需要主动释放,从而破坏了不可剥夺条件。 当某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级(比如:剥夺调度方式,就是将处理机资源强行剥夺给优先级更高的进程使用) 该策略的缺点  实现起来比较复杂。 释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源,如CPU。 反复地申请和释放资源会增加系统开销,降低系统吞量。 若采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源就都需要放弃,以后再重新申请。如果一直发生这样的情况,就会导致进程饥饿。 🥝 破坏请求并保持条件 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己己有的资源保持不放。  可以采用静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源了。 该策略实现起来简单,但也有明显的缺点:有些资源可能只需要用很短的时间,因此如果进程的整个运行期间都一直保持着所有资源,就会造成严重的资源浪费,资源利用率极低。另外,该策略也有可能导致某些进程饥饿。   🥝 破坏循环等待条件 循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。 可采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源同类资源(即编号相同的资源)一次申请完。  原理分析:一个进程只有已占有小编号的资源时,才有资格申请更大编号的资源。按此规则,已持有大编号资源的进程不可能逆向地回来申请小编号的资源,从而就不会产生循环等待的现象。 该策略的缺点:  不方便增加新的设备,因为可能需要重新分配所有的编号 进程实际使用资源的顺序可能和编号递增顺序不一致,会导致资源浪费 必须按规定次序申请资源,用户编程麻烦。 2.5 死锁的避免 避免死锁同样属于事先预防策略,并不是采取某种限制措施破坏死锁的必要条件,而是在资源动态分配过程中,防止系统进入不完全状态。  🍉 安全序列 进程可以动态的申请资源,但是系统在进行资源分配之前,必须先计算此次分配的安全性。如果计算所得是安全的,则允许分配,但如果是不安全的,则让进程等待。而所谓的安全状态就是,系统可以按照某种进程的推进顺序  这里举了一个银行给BAT三家公司借钱的例子用来引出银行家算法  这时候如果将 30亿 借给了B公司,那么手里还有 10亿元,这 10亿 已经小于3家公司最小的最多还会借的钱数,没有公司能够达到提出的最大要求,这样银行的钱就会打水漂了!!!  如果是这种情况呢?   这样按照T->B->A的顺序借钱是没有问题的,是安全的。   按照A->T->B的顺序借钱也是没有问题的。  这样我们就会得到安全序列、不安全序列和死锁的关系了  注意:  (1)系统在某一时刻的安全状态可能不唯一,但这不影响对系统安全性的判断。 (2)安全状态是非死锁状态,而不安全状态并不一定是死锁状态。即系统处于安全状态一定可以避免死锁,而系统处于不安全状态则仅仅可能进入死锁状态。  原因是如果进入了不安全状态,但是没有进程去请求资源,并且有进程提前归还了一些资源,这样就不会死锁。 ————————————————                              版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                          原文链接:https://blog.csdn.net/island1314/article/details/144571097 
  • [技术干货] 【Linux】开启你的Linux之旅:初学者指令指南-转载
    知识铺垫 1.1 如何登入账号 首先输出ssh 用户名@公网IP ,跳出SSH用户身份验证输入密码即可。SSH(Secure Shell,安全外壳)是一种网络协议,通过加密和认证机制实现安全访问和文件传输等业务。 1.2 关于创建和删除普通账号 windows,Linux属于多用户操作系统  添加普通账号的步骤(如果创建成功就会出现@)  adduser 用户名 passwd(密码) 用户名(输入密码时,是不显示密码的) passwd命令用于更改设置用户密码 删除普通账号的步骤:userdel -r 用户名@  1.3 操作系统概念(OS) 操作系统是一款进行软硬件资源管理的软件,对于Linux是一款具体的操作系统的一个品类,而centos7 是一款具体的Linux操作系统,计算机是一个工具,被人使用的工具。   【操作系统的作用】:   提供了人机交互接口,在早期计算机使用者使用计算机的时候,面对一大堆的硬件进行操作,通过硬开关进行控制,操作系统可以避免直接操作硬件带来的麻烦,这也是操作系统最大的功能 提供了计算机软硬件资源管理 对下操作系统提供了基本的管理工作,让多种硬件处于一个稳定、高效、安全的工作环境 对上(运用软件)提供了一个稳定、高效、安全的运行环境(用户的目的) 1.4 Xshell相关快捷键 【Alt + 回车】:全屏  【Ctrl + D】:退出  【ctrl+insert】:复制(如果不行,打开Fn配合使用)  【shift+insert】:粘贴  这里不支持ctrl c和ctrl v快捷键  1.5 文件 当在电脑上进行操作时,实际上是通过操作系统来控制文件和文件夹。操作系统提供了图形用户界面(GUI)和命令行界面(CLI),让你可以方便地浏览、创建、删除、移动和修改文件和文件夹。比如我们的桌面实际上是一个文件夹,存储了在桌面上看到的所有文件和快捷方式。  【桌面属于文件夹】  当你登入windows进行如下操作:  确定你是谁 根据用户名,找到改用户名目录下的“桌面文件夹” 将桌面文件夹显示成为图形化界面  1.5.1 文件占用内存(内存≠大小)  文件虽然显示大小是0KB,但是同样会占用内存。由于【文件=文件属性+文件内容】,这里显示的时间、类型和文件名等都属于文件属性(对应的数据是字符串之类,同样占用内存),对此文件属性是数据,并且也是需要保存。  对此未来对文件的任何操作,无外乎就是对文件的属性和内容进行操作。这里操作可以通过指令控制文件,编程访问文件的内容。  1.6 路径 文件路径是用来指定文件或文件夹在计算机文件系统中的位置的,路径分为绝对路径和相对路径。  以下这些都称为路径,不同在于它们的路径分隔符  //Linux下: [root@iZ7xv21eg69v0bihv6nnufZ 111]# pwd root/111      //Windows下: D:\C—language\C++\string模拟实现    两个路径分隔符之间,一定是一个文件夹,而路径最末端,一定是一个普通文件或者文件夹 (这里的文件夹通常叫做目录,但是一个目录中可以有文件,也可以有目录)  【/】:Linux下路径分隔符  【\】:windows下路径分隔符  【问题与答案】 1.【为什么要有路径?】 路径是系统层标识一个特定的文件,路径分为绝对路径和相对路径,在Linux的整个文件目录结构是一个多叉树,属于树状结构  根据树状结构,从中可以知道每个孩子(子文件)都只有一个父目录,这也导致了路径必须具有唯一性 ,最开始的目录称为根目录  2.【为什么要找到目标文件?】  访问任何文件之前,都必须先找到这个文件,为了找到这个目标文件,所以需要使用到目录  1.7 .与…用法 1.7.1 隐藏文件 无论是在Linux下还是在Windows下,都有隐藏文件存在。  【Windows下隐藏文件】   【Linux下隐藏文件】   关于查看Linux目录中隐藏文件,我们需要使用ls -l或ll指令进行查看,该指令作用是更详细罗列目录下所有子目录和文件信息,而ls -la是列出目录下的所有文件,包括以 . 开头的隐含文件。任何目录下,都会默认具有两个隐藏目录.和…  1.7.2 .当前用法  【.两种用法】:  可以表示当前路径 指定执行当前目录下的一个可执行文件,表示明确该文件在该目录下,可以直接执行该可执行文件 1.7.3 …当前用法   【…用法】:  表示上级路径,可以方便我们进行路径的回退。毕竟不光要进去,也要可以出来 【价值体现】  在Linux的整个文件目录结构是一个多叉树,是属于树状结构的,那么可以灵活地使用…返回上一级路径配合相对路径和绝对路径进行在Linux的整个文件目录下就行游走。  二、常见指令介绍 前言Linux的指令和与之对应的常用选项很多,那么下面是一些常见的使用,下列大约有二十个指令,一开始记不住,之后忘不掉,不用死背指令,见多就记住了。这里介绍途中会混合一些小指令,知道如何使用和作用即可,附加一些周边知识,接下来将正式开始。  2.1 pwd指令 【语法】:pwd  【功能】:显式用户当前所在的目录   2.2 cd指令 Linux系统中,磁盘上的文件和目录被组成一颗目录树,每个节点都是目录或文件。   【语法】:cd 目录名(不是文件名)  【功能】:改变工作目录,将当前工作目录改变到指定目录下  【返回上级目录】:cd … 【以绝对路径跳转目录】:cd /home/litao/linux/ 【以相对路径跳转目录】:cd …/day02/ 【进入用户家目】:cd ~ 【返回最近访问目录】:cd - 【cd ~ 使用介绍】  【作用】:  跳转到我们最近一次所处的路径下,这有助于我们是实现处理两个路径的快速切换  【cd - 使用介绍】  【作用】:  进入用户家目录 2.3 家目录 家目录(Home Directory)是操作系统为每个用户分配的一个专用目录,用于存储该用户的个人文件、配置文件和数据  【windows中的用户默认的家目录】  C:\Users\用户名 【Linux下指定用户的家目录】  对于root账号:默认的家目录/root—>超级管理员账号  对于普通用户:默认的家目录/home/新建的用户名  关于以上两点,任何一个用户,首次登录所处的路径都是自己的家目录,关于这点可以使用whoami指令查看当前正在使用Linux系统的用户名  2.4 whoami指令  whoami指令查看当前正在使用Linux系统的用户名,在# 表达的时候,经常说我们在XXX路径下【“我们” -whoami】。  2.5 重新认识指令 目前阶段来说指令的本质都是程序。指令、程序、可执行程序都是一回事,并且也是文件。  2.5.1 安装和卸载行为含义 安装和卸载就是把可执行程序拷贝/删除到系统路径下  2.6 which指令 which指令要求系统打印出我所制定的指令名称在系统中所在路径位置  那么我们可以根据which指令,得到ll和ls -l指令之间存在某种关系。  2.7 alias指令 alias也是一个Linux指令,给其他命令起一个别名。  目前不建议大家使用该指令为其他指令取别名,由于目前指令接触不多,很容易导致混乱。  关于--color == auto(auto可省略),这里就是是否带上颜色。  2.8 ls指令  【语法】:ls [选项] [目录或文件] 【功能】:对于目录,该命令列出该目录下的所有子目录于文件。对于文件,将列出文件名以及其他信息 【常用选择】:主要掌握-d -l -a选项,剩下有需要记  [-a ]:列出目录下的所有文件,包括以 . 开头的隐含文件。 [-d ]:将目录象文件一样显示,而不是显示其下的文件。 如: ls –d 指定目录 [-i ]:输出文件的 i 节点的索引信息。 如 ls –ai 指定文件 [-k ]:以 k 字节的形式表示文件的大小。 ls –alk 指定文件 [-l] :列出文件的详细信息。 [-n] :用数字的 UID,GID 代替名称。 (介绍 UID, GID) [-F ]:在每个文件名后附上一个字符以说明该文件的类型, “*”表示可执行的普通文件; “/”表示目录; “@”表示符号链接; “|”表示FIFOs; “=”表示套接字(sockets)。(目录类型识别) [-r ]:对目录反向排序。 [-t ]:以时间排序。 [-s] :在l文件名后输出该文件的大小。(大小排序,如何找到目录下最大的文件) [-R ]:列出所有子目录下的文件。 (递归) [-1 ] :一行只输出一个文件。 2.9 touch指令  【语法】:touch [选项]… 文件… 【功能】:touch命令参数可改变文档或目录的时间,包括存储时间和更改时间,或者新建一个不存在的文件 【常用选项】[有需要记]:  -a 或–time=atime或–time=access或–time=use只更改存取时间。 -c 或–no-create 不建立任何文档。 -d 使用指定的日期时间,而非现在的时间。 -f 此参数将忽略不予处理,仅负责解决BSD版本touch指令的兼容性问题。 -m 或–time=mtime或–time=modify 只更改变动时间。 -r 把指定文档或目录的日期时间,统统设成和参考文档或目录的日期时间相同。 -t 使用指定的日期时间,而非现在的时间 2.10 stat指令与文件ACM时间 可以使用stat指令进行查看,文件或目录的不同时间戳   【Access Time (atime)】:文件最后一次被访问(读取)的时间。 【Modify Time (mtime)】:文件内容最后一次被修改的时间。 【Change Time (ctime)】:文件元数据(如权限)最后一次被修改的时间。 2.11 mkdir指令(重要)  【语法】:mkdir[选项] dirname… 【功能】:在当前目录下创建一个名为"dirname"的目录 【常用选项】:  [-p (parent )] :可以是一个路径名称。此时若路径中的某些目录尚不存在,加上此选项后系统将自动建立好那些尚不存在的目录,**既一次性建立多个目录。**如果没有添加-p选项,就不能一次性建立多个目录。  ————————————————                              版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                          原文链接:https://blog.csdn.net/2302_79177254/article/details/142755397 
  • [技术干货] 【Linux】:日志策略 + 线程池(单例模式)-转载
     1. 前言 🚀 🔥 下面开始,我们结合我们之前所做的所有封装,进行一个线程池的设计。在写之前,我们要做如下准备  准备 线程 的封装 准备 锁 和 条件变量的封装 引入日志,对线程进行封装 这里用到了我们之前博客用到的头文件及代码  【Linux】:多线程(互斥 && 同步)  2. 日志和策略 💞 🍧什么是设计模式  T行业这么火,涌入的人很多,俗话说林子大了啥鸟都有,大佬和菜鸡们两极分化的越来越严重。为了让菜鸡们不太拖大佬的后腿,于是大佬们针对一些经典的常见的场景,给定了一些对应的解决方案,这个解决方案就是 设计模式 🍧 认识日志  计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具 🍧日志格式的指标  必须有的:时间戳、日志等级、日志内容 可选的:文件名行号、进程线程相关信息id 等等 日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx  等等,但是我们依旧采用自定义日志的方式。比如这里我们采用 设计模式-策略模式 来进行日志的设计  我们想要的日志格式如下:  [可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持可变参数 [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world [2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world 🎈 日志模式 详解 见代码注释 Log.hpp   注意:我们这里用到的 "Mutex.hpp" 是我们之前在 【Linux】:多线程(互斥 && 同步) 实现的互斥量封装 #pragma once   #include <iostream> #include <string> #include <unistd.h> #include <sstream> #include <fstream> #include <memory> #include <filesystem> // C++ 17 后的标准 #include <unistd.h> #include <time.h> #include "Mutex.hpp"   namespace LogModule {     // 获取当前系统时间     std::string CurrentTime()     {         time_t time_stamp = ::time(nullptr);         struct tm curr;         localtime_r(&time_stamp, &curr); // 时间戳,获取可读性更高的时间信息           char buffer[1024];            // bug ->  ?         snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",                  curr.tm_year + 1900, // 这里需要 + 1900                  curr.tm_mon + 1,                  curr.tm_mday,                  curr.tm_hour,                  curr.tm_min,                  curr.tm_sec);           return buffer;       }         using namespace LockModule;       // 构成: 1. 构建日志字符串 2. 刷新落盘(①screen ②file)     //  1. 日志文件的默认路径 和 文件名     const std::string defaultlogpath = "./log/";     const std::string defaultlogname = "log.txt";     // 2. 日志等级     enum class LogLevel     {         DEBUG = 1,         INFO,         WARNING,         ERROR,         FATAL     };       std::string Level2String(LogLevel level)     {         switch(level)         {         case LogLevel::DEBUG:             return "DEBUG";         case LogLevel::INFO:             return "INFO";         case LogLevel::WARNING:             return "WARNING";         case LogLevel::ERROR:             return "ERROR";         case LogLevel::FATAL:             return "FATAL";         default:             return "None";         }     }       // 3. 刷新策略,只进行刷新,不提供方法     class LogStrategy     {     public: // 基类 需要 析构设成 虚方法         virtual ~LogStrategy() = default;         virtual void SyncLog(const std::string &message) = 0;     };       // 3.1 控制台策略(screen)     class  ConsoleLogStrategy : public LogStrategy     {     public:         ConsoleLogStrategy()         {}         ~ConsoleLogStrategy()         {}           void SyncLog(const std::string &message)         {             LockGuard lockguard(_lock);             std::cout << message << std::endl;           }     private:         Mutex _lock;     };       // 3.2 文件级(磁盘)策略     class FileLogStrategy : public LogStrategy     {     public:         FileLogStrategy(const std::string &logpath = defaultlogpath, const std::string &logname = defaultlogname)           : _logpath(logpath),             _logname(logname)         {             // 确认 _logpath 是存在的             LockGuard lockguard(_lock);             if(std::filesystem::exists(_logpath))             {                 return;             }             try{                 std::filesystem::create_directories(_logpath); // 新建             }             catch(std::filesystem::filesystem_error &e)             {                 std::cerr << e.what() << "\n";             }         }         ~FileLogStrategy()         {}           // 下面用的是 c++ 的文件操作         void SyncLog(const std::string &message)         {             LockGuard lockguard(_lock);             std::string log = _logpath + _logname; // ./log/log.txt             std::ofstream out(log, std::ios::app);  // 日志写入,一定是追加, app -> append             if(!out.is_open()) return ;             out << message << "\n";             out.close();         }       private:         std::string _logpath;         std::string _logname;           // 锁         Mutex _lock;     };       // 日志类:构建日志字符串,根据策略 进行刷新     class Logger     {     public:         Logger()         {             // 默认采用 ConsoleLogStrategy 策略             _strategy = std::make_shared<ConsoleLogStrategy>();           }                  void EnableConsoleLog()         {             _strategy = std::make_shared<ConsoleLogStrategy>();         }         void EnableFileLog()         {             _strategy = std::make_shared<FileLogStrategy>();         }                  ~Logger() {}         // 一条完整的信息: [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] + 日志的可变部分(<< "hello world" << 3.14 << a << b;)                  class LogMessage         {         public:             LogMessage(LogLevel level, const std::string &filename, int line, Logger &logger)                 : _currtime(CurrentTime()),                   _level(level),                   _pid(::getpid()),                   _filename(filename),                   _line(line),                   _logger(logger)             {                 std::stringstream ssbuffer;                 ssbuffer << "[" << _currtime << "] "                          << "[" << Level2String(_level) << "] " // 对日志等级进行转换显示                          << "[" << _pid << "] "                          << "[" << _filename << "] "                          << "[" << _line << "] - ";                 _loginfo = ssbuffer.str();             }               template<typename T>             LogMessage &operator<<(const T&info)             {                 std::stringstream ss;                 ss << info;                 _loginfo += ss.str();                 return *this;             }               ~LogMessage()             {                 if (_logger._strategy)                 {                     _logger._strategy->SyncLog(_loginfo);                 }             }         private:             std::string _currtime; // 当前日志的时间             LogLevel _level;       // 日志等级             pid_t _pid;            // 进程pid             std::string _filename; // 源文件名称             int _line;             // 日志所在的行号             Logger &_logger;       // 负责根据不同的策略进行刷新             std::string _loginfo;  // 一条完整的日志记录         };           // 就是要拷贝(临时的 logmessage), 故意的拷贝         LogMessage operator()(LogLevel level, const std::string &filename, int line)         {             return LogMessage(level, filename, line, *this);         }     private:         std::shared_ptr<LogStrategy> _strategy; // 日志刷新的策略方案     };       Logger logger;   #define LOG(Level) logger(Level, __FILE__, __LINE__) #define ENABLE_CONSOLE_LOG() logger.EnableConsoleLog() #define ENABLE_FILE_LOG() logger.EnableFileLog() }  代码剖析:  🔥 我们这里实现了一个日志模块(LogModule),通过不同的日志策略(如控制台输出或文件输出)来记录日志。具体来说,它分为以下几个部分:  1. 获取当前时间 (CurrentTime 函数)   该函数通过 C++ 标准库的 time 和 localtime_r 获取当前系统时间并格式化为 YYYY-MM-DD HH:MM:SS 的字符串格式。这个时间戳用于日志记录。 2. 日志等级 (LogLevel 枚举)  定义了五个日志等级: DEBUG、INFO、WARNING、ERROR、FATAL ,表示日志的严重性。 Level2String 函数根据 LogLevel 转换为对应的字符串形式,用于日志输出。 3. 日志策略(LogStrategy 类及其派生类)  LogStrategy 基类:定义了一个纯虚函数 SyncLog,用于实际的日志刷新操作(即将日志信息输出到目标介质)。 ConsoleLogStrategy 类:实现了 SyncLog 方法,将日志信息输出到控制台。 FileLogStrategy 类:实现了 SyncLog 方法,将日志信息输出到文件中。文件路径和文件名默认设置为 ./log/log.txt 。如果目录不存在,则会尝试创建目录。 4. 日志类 (Logger 类)  Logger 类负责管理日志的策略,可以切换控制台输出或文件输出。 Logger 提供了两个方法: EnableConsoleLog:切换为控制台输出策略。 EnableFileLog:切换为文件输出策略。 内部有一个嵌套类 LogMessage,它用来生成具体的日志条目。每次创建一个 LogMessage 对象时,会自动格式化日志信息并最终将其传递给策略进行输出。 5. 日志信息格式化  LogMessage 类的构造函数会根据当前时间、日志等级、进程 ID、文件名和行号等信息来生成一条完整的日志记录。 operator<< 被重载,以支持日志信息的追加,可以向日志信息中添加不同类型的内容(如字符串、数字等)。 6. 宏定义  LOG(Level):简化日志记录的调用方式,自动记录当前文件名和行号。 ENABLE_CONSOLE_LOG():设置日志策略为控制台输出。 ENABLE_FILE_LOG():设置日志策略为文件输出。 🥗 测试代码 Main.cc 如下:  #include "Log.hpp"   using namespace LogModule;   int main() {     ENABLE_FILE_LOG(); // 开启日志文件的文件输出       LOG(LogLevel::DEBUG) << "Hello File";     LOG(LogLevel::DEBUG) << "Hello File";     LOG(LogLevel::DEBUG) << "Hello File";     LOG(LogLevel::DEBUG) << "Hello File";       ENABLE_CONSOLE_LOG(); // 往显示器输出     LOG(LogLevel::DEBUG) << "Hello IsLand";     LOG(LogLevel::DEBUG) << "Hello IsLand";     LOG(LogLevel::DEBUG) << "Hello IsLand";     LOG(LogLevel::DEBUG) << "Hello IsLand";       return 0; }  输出如下:  ​  3. 线程池设计 🖊 🐇 3.1 线程池的基本概念 💢 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。  线程池 通过一个线程安全的阻塞任务队列加上一个或一个以上的线程实现,线程池中的线程可以从阻塞队列中获取任务进行任务处理,当线程都处于繁忙状态时可以将任务加入阻塞队列中,等到其它的线程空闲后进行处理。 这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 可以避免大量线程频繁创建或销毁所带来的时间成本,也可以避免在峰值压力下,系统资源耗尽的风险;并且可以统一对线程池中的线程进行管理,调度监控。 🐇 3.2 线程池应用场景 需要大量的线程来完成任务,且完成任务的时间比较短。  比如 WEB 服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,比如我们可以想象一个热门网站的点击次数。 但对于一些长时间的任务,比如一个 Telnet 连接请求,线程池的优点就不明显了。因为 Telnet 会话时间比线程的创建时间大多了。 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。  突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误 🐇 3.3 线程池种类 创建 固定数量线程池 ,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口 浮动线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口 这里我是选择固定线程个数的线程池来做样例进行实现     🐇 3.4 线程池实现 (ThreadPool) Task.hpp  #pragma once   #include <iostream> #include <string> #include <string> #include <functional> #include "Log.hpp"   using namespace LogMudule;     using task_t = std::function<void(std::string name)>;   void Push(std::string name) {     LOG(LogLevel::DEBUG) << "我是一个推送数据到服务器的一个任务, 我正在被执行" << "[" << name << "]"; }  Log.hpp 是我们上面日志那里实现的,"Mutex.hpp" 和 "Cond.hpp" 是我们之前博客【Linux】:多线程(互斥 && 同步) 实现的互斥量和条件变量封装,而 "Thread.hpp" 也是我们之前博客 【Linux】:线程库简单封装 实现的 1 号 版本 ThreadPool.hpp   #pragma once   #include <iostream> #include <string> #include "Log.hpp" #include "Mutex.hpp" #include <queue> #include <vector> #include <memory> #include "Cond.hpp" #include "Thread.hpp"   namespace ThreadPoolModule {     using namespace LogModule;     using namespace ThreadModule;     using namespace CondModule;     using namespace LockModule;       // 用来做测试的线程方法     void DefaultTest()     {         while(true)         {             LOG(LogLevel::DEBUG) << "我是一个测试线程";             sleep(1);         }     }       using thread_t = std::shared_ptr<Thread>;       const static int defaultnum = 5;          template<typename T>     class ThreadPool     {     private:         bool IsEmpty() { return _taskq.empty();}         void HandlerTask(std::string name)         {             LOG(LogLevel::INFO) << "线程" << name << ", 进入HandlerTask 的逻辑";             while(true)             {                 // 1. 拿任务                 T t;                 {                     LockGuard lockguard(_lock);                     while(IsEmpty() && _isrunning)                     {                         _wait_num++; // 线程等待数量加 1                         _cond.Wait(_lock);                         _wait_num--;                     }                     // 2. 任务队列不为空 && 线程池退出                     if(IsEmpty() && !_isrunning){                         break;                     }                     t = _taskq.front();                     _taskq.pop();                 }                   // 2. 处理任务                 t(name); // 规定,未来所有的任务处理,全部都是必须提供 () 方法             }             LOG(LogLevel::INFO) << "线程" << name << "退出";         }     public:         ThreadPool(int num = defaultnum): _num(num), _wait_num(0), _isrunning(false)         {             for(int i = 0; i < _num; i++)             {                 // _threads.push_back(std::make_shared<Thread>(DefaultTest)); // 构建 make_shared 对象                 _threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1))); // 构建 make_shared 对象,当我们传递名字的时候,需要带上 placeholder                                  // _threads.push_back(std::make_shared<Thread>([this]{ThreadPool::HandlerTask})); // 构建 make_shared 对象                                  LOG(LogLevel::INFO) << "构建线程" << _threads.back()->Name() << "对象...成功";             }         }           void Equeue(T &&in)         {             LockGuard lockguard(_lock); // 保护临界资源,使其安全             if(!_isrunning) return ;             _taskq.push(std::move(in));             if(_wait_num > 0){                 _cond.Notify();             }         }           void Start()         {             if(_isrunning) return ;             _isrunning = true; // 注意这里             for(auto &thread_ptr : _threads)             {                 LOG(LogLevel::INFO) << "启动线程" << thread_ptr->Name() << "...成功";                 thread_ptr->Start();             }         }           void Wait()         {             for(auto &thread_ptr: _threads)             {                 thread_ptr->Join();                 LOG(LogLevel::INFO) << "回收线程" << thread_ptr->Name() << "...成功";             }         }           void Stop()         {             LockGuard lockguard(_lock);             if(_isrunning)             {                 // 3. 不能再入任务了                 _isrunning = false;                 // 1. 让线程自己退出 && 2. 历史的任务被处理完了                 if(_wait_num > 0)                     _cond.NotifyAll();                              }         }           ~ThreadPool()         {         }       private:         std::vector<thread_t> _threads;          int _num;         int _wait_num;          std::queue<T> _taskq; // 任务队列 -> 临界资源            Mutex _lock;         Cond _cond;           bool _isrunning;     }; }  1. 命名空间  namespace ThreadPoolModule {     using namespace LogMudule;     using namespace ThreadModule;     using namespace CondModule;     using namespace LockModule; } 使用了四个外部命名空间:LogMudule(日志模块),ThreadModule(线程模块),CondModule(条件变量模块),LockModule(锁模块)。这些模块分别提供日志记录、线程管理、条件变量和互斥锁功能。 2. 测试线程方法  void DefaultTest() {     while(true)     {         LOG(LogLevel::DEBUG) << "我是一个测试线程";         sleep(1);     } } DefaultTest 是一个模拟的线程任务函数,线程会每秒打印一次日志。sleep(1) 用于让线程每秒钟执行一次。 3. thread_t 类型定义  using thread_t = std::shared_ptr<Thread>; 定义了 thread_t 类型,它是 std::shared_ptr<Thread> 的别名。这样可以方便管理线程对象的生命周期,避免手动管理内存。 4. ThreadPool 类  ThreadPool 类是一个模板类,用来管理和分配线程池中的任务。其主要功能包括任务队列管理、线程创建、启动、停止和等待等。  成员变量  _threads: 存储线程的容器,类型为 std::vector<thread_t>,存放线程的共享指针。 _num: 线程池中的线程数量,默认值为 defaultnum(5)。 _wait_num: 记录当前等待任务的线程数量。 _taskq: 任务队列,存储待执行的任务。类型为 std::queue<T>。 _lock: 互斥锁,保护任务队列和其他临界资源。 _cond: 条件变量,用于线程之间的同步,帮助线程等待任务或通知线程执行任务。 _isrunning: 布尔标志,表示线程池是否正在运行。 成员函数  ① IsEmpty  bool IsEmpty() { return _taskq.empty(); } 判断任务队列是否为空。 ② HandlerTask  void HandlerTask(std::string name) {     LOG(LogLevel::INFO) << "线程" << name << ", 进入HandlerTask 的逻辑";     while(true)     {         // 拿任务         T t;         {             LockGuard lockguard(_lock);             while(IsEmpty() && _isrunning)             {                 _wait_num++; // 线程等待数量加 1                 _cond.Wait(_lock);                 _wait_num--;             }             if (IsEmpty() && !_isrunning) { break; }             t = _taskq.front();             _taskq.pop();         }           // 处理任务         t(name);  // 假设任务对象可以直接调用     }     LOG(LogLevel::INFO) << "线程" << name << "退出"; }  HandlerTask 是每个线程执行的函数:  线程不断从任务队列中取任务执行。 如果任务队列为空,线程会等待,直到有新任务被加入队列。 线程通过条件变量 Wait() 等待任务,避免空转浪费 CPU 资源。 t(name) 任务执行函数,通过传递线程名称进行日志记录。 ③ Equeue  void Equeue(T &&in) {     LockGuard lockguard(_lock);     if (!_isrunning) return;     _taskq.push(std::move(in));  // 把任务添加到队列中     if (_wait_num > 0)     {         _cond.Notify();  // 通知等待线程有新任务     } } Equeue 将任务加入到任务队列 _taskq 中。通过 std::move 移动任务对象来避免不必要的拷贝。 如果有线程在等待任务,调用 Notify() 通知这些线程继续工作。 ④ Start  void Start() {     if (_isrunning) return;     _isrunning = true;     for (auto &thread_ptr : _threads)     {         LOG(LogLevel::INFO) << "启动线程" << thread_ptr->Name() << "...成功";         thread_ptr->Start();     } } Start 启动线程池中的所有线程。线程会调用 HandlerTask 开始处理任务。 ⑤ Wait  void Wait() {     for (auto &thread_ptr : _threads)     {         thread_ptr->Join();         LOG(LogLevel::INFO) << "回收线程" << thread_ptr->Name() << "...成功";     } } Wait 等待所有线程执行完毕。Join 会阻塞当前线程直到每个线程完成任务。 ⑥ Stop  void Stop() {     LockGuard lockguard(_lock);     if (_isrunning)     {         _isrunning = false;         if (_wait_num > 0)             _cond.NotifyAll();  // 通知所有等待的线程退出     } } Stop 用于停止线程池的运行,标记 _isrunning = false,使线程池不再接收新任务。  如果有线程正在等待任务,则通过 _cond.NotifyAll() 唤醒这些线程,让它们退出。 5. 线程池的工作流程  线程池在构造时会创建指定数量的线程。 任务通过 Equeue 方法提交到任务队列中。 线程池通过 Start 启动所有线程,每个线程执行 HandlerTask,从任务队列中取任务并处理。 通过 Wait 等待所有线程执行完毕。 通过 Stop 停止线程池并通知所有线程退出。 6. 线程管理  线程池通过 std::shared_ptr<Thread> 来管理线程,避免了手动内存管理的问题。 使用条件变量来实现线程的等待和通知机制。 使用互斥锁 Mutex 确保任务队列的线程安全。 7. 日志记录  通过 LOG 宏记录线程池的各种操作,如线程的启动、任务的处理等。这些日志有助于调试和监控线程池的运行状态。 测试代码 ThreadPool.cc:  #include "ThreadPool.hpp" #include "Task.hpp" #include <memory>   using namespace ThreadPoolModule;   int main() {     ENABLE_CONSOLE_LOG(); // 默认开启 -- 日志显示策略     // ENABLE_FILE_LOG(); // 文件显示     std::unique_ptr<ThreadPool<task_t>> tp = std::make_unique<ThreadPool<task_t>>();     tp->Start();       int cnt = 10;     while(cnt)     {         tp->Equeue(Push);         cnt--;         sleep(1);     }       tp->Stop();     sleep(3);     tp->Wait();       return 0; }  ————————————————                              版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                          原文链接:https://blog.csdn.net/island1314/article/details/144567703 
  • [技术干货] Linux 简单命令总结-转载
     1. 简单命令 1.1. ls 列出该目录下的所有子目录与文件,后面还可以跟上一些选项  常用选项: ・-a 列出目录下的所有文件,包括以。开头的隐含文件。 ・-d 将目录象文件一样显示,而不是显示其下的文件。如:ls -d 指定目录 ・-k 以 k 字节的形式表示文件的大小。ls -alk 指定文件 ・-l 列出文件的详细信息。 ・-r 对目录反向排序。 ・-t 以时间排序。 ・-R 列出所有子目录下的文件。  1.2. cd cd :切换目录 2的n次方_-CSDN博客 1.3. pwd pwd:显示当前目录 2. 创建文件和文件夹 2.1. 创建文件 touch + 文件名 2.2. 创建文件夹 2.2.1. 创建单个文件夹 mkdir + 文件名称 2.2.2. 创建多级文件夹 mkdir -p 表示创建多级目录,./ 表示在当前目录下创建文件夹,  mkdir -p ./parent/child/grandchild 3. 删除 3.1. 删除文件 rm + 要删除的文件名 3.2. 删除文件夹 rm -r + 要删除的文件夹名称 也可以加上 / 表示删除此文件夹下的文件夹 4. 复制和移动 4.1. cp cp 命令可以复制文件或者目录:cp 源文件或目录 目标文件或目录 4.2. mv mv 的功能和剪切一样,就是把一个文件或目录移动到另一个位置 mv cat.jpg test/ 也可以实现文件重命名的效果 这样就相当于把 test1.txt 重命名为了 test2.txt  5. 上传和下载 上传和下载是通过 rz 和 sz 命令来完成的,如果输入之后显示没下载的话可以通过命令先下载一下 apt-get install lrzsz 5.1. rz 输入 rz 后就会弹出一个框,用来选择要将哪个 windows 中的文件上传到 Linux 中 5.2. sz sz 要下载的文件名 输入之后也会弹出就直接下载到 windows 中了 除了上面通过命令的方式进行上传之外,还可以直接将 Windows 中的文件拖到 finalshell 的文件区中 也可以直接右键来选择上传和下载 6. 查看文件内容 6.1. cat 输入 cat + 要查看的文件名,就会显示文件内容在命令行中 6.2. more 使用 cat 命令是把该文件所有的内容都展示出来,这样就会把命令行铺满,影响操作 通过 more 命令来读取文件就会显示文件的一部分: more + 要查看的文件名    按下回车可以继续阅读剩下的部分,b 键可以查看上一页,如果直接想退出的话 ctrl + c 就能退出了,可以输入 / 后面跟要查找的内容,然后按下回车就会跳转到目标位置  6.3. less less + 文件名也可以查看文件   此时是通过 PgUp 和 PgDn 键来进行翻页的 使用 less 的话文件的内容是不会留在命令行中的,就像打开了一个记事本进行查看一样,还支持搜索功能,也可以输入 / 查找的内容,按下回车就会把所有搜索到的都标记出来  :q 可以退出查看  6.4. head head 默认查看的是文件的前 10 行,也可以指定查看的行数  6.5. tail tail 就是从末尾开始查看  7. clear 清屏 在查看了文件内容之后,命令行被很多其他东西占满了,就可以使用 clear 命令来清理一下屏幕,之前输入的内容还是在上面,通过滚轮或者上键可以查看之前的内容 8. 编辑文件 8.1. vi / vim 使用 vi + 要编辑的文件,进来之后是处于阅读模式,并不能进行编辑,需要按下 i 键才能进入编辑模式 下面显示 INSERT 就可以编辑了  编辑完成之后需要按 Esc 键退出编辑模式,输入:w 是保存,:q 是退出,:q! 是强制退出,:wq 就是退出并保存  也可以使用 vim 来编辑,vim 是 vi 的增强版,如果系统中没有的话还需要手动下载一下  8.2. nano nano 后面加要编辑的文件,进来之后可以直接编辑,下面也展示了一些写入,查找,退出等快捷键 9. grep 查找 grep 用于查找文件中是否包含指定字符串,并显示出来 还可以加上其他内容来配合使用 -n <行数> 显示的行数 w 全字匹配。要求整个单词都完全相同的结果才能匹配出来,而不仅仅是一个单词的一部分。 r 递归查找。可以搜索多级目录下的所有文件。 --color 高亮查找到的结果 --include 指定查找某些文件 --exclude 指定排除某些文件 10. ps 查询进程 ps 查询进程时一般结合下面这些选项来使用  a 显示一个终端的所有进程 u 以用户为主的格式来显示程序状况 x 显示所有程序,不止是会话中的进程 e 显示所有进程,包括系统守护进程 f 显示完整格式输出 一般情况下使用 ps aux 或者 ps -ef   也可以结合 grep 来使用,来查询目标进程   也可以来查看端口号   上面的 | 表示管道,意思是将前一个指令标准输出的内容,作为第二个指令的标准输入内容。  11. netstat  可以跟下面这些选项  -a 显示所有正在或不在侦听的套接字 -n 显示数字形式地址而不是去解析主机、端口或用户名 -p 显示套接字所属进程的 PID 和名称  ————————————————                              版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                          原文链接:https://blog.csdn.net/2202_76097976/article/details/144419918