linux——线程(1)

news/2024/7/27 7:29:43/文章来源:https://blog.csdn.net/weixin_74074953/article/details/136402365
我们在使用各种面向对象语言的时候或多或少会使用到语言的线程库,
它可以让我们的一个程序分成多个执行流进行并发执行,这样的模式会
大大提高我们代码的运行效率,而Linux操作系统中也有着属于自己的线
程库。从今天开始就由我来开始介绍Linux中的线程。

线程的概念

1. 操作系统中的线程

在操作系统中关于线程的概念大致如下:

1. 线程是比进程更加轻量化的一种执行流
2. 线程是在进程内部的一种执行流

而我今天要说对于线程和进程的描述可以是这样:

1. 线程是CPU调度的基本单位
2. 进程是承担系统资源的基本实体

而我接下来都会围绕着这几句话来阐述说什么是线程

2. 自己设计一个线程

a. 设计线程

我们知道Linux中启动一个进程的大致过程如下:
在这里插入图片描述
然后就会将pcb被加入运行队列各种队列中进行调度阻塞等等各种操作,将进程跑起来。
对于进程而言除了文件需要独立的结构体来管理之外,基本上我们所使用的资源栈中的数据、堆中的数据、自己的函数,动态库、系统调用,都是凭借地址空间得到的,我们以前认为地址空间帮我们制定好一种规则来将内存中杂乱的数据整理成规则的,现在我们也应该认为地址空间是进程看到资源“窗口”。
但是我们也看到当创建一个进程的时候页表、进程pcb、地址空间等等一系列内核数据结构都要被申请好,我们的进程才能够创建好,这样说实话效率是比较低的。
那我们现在就有一种想法,我们再创建一个 “进程” ,但是这个进程只创建一个task_struct,然后让这个进程控制块也指向该地址空间,让它来执行其中的一段代码(比如一个函数),让这个pcb也能够进行各种调度阻塞挂起等状态,假设我们的函数中有四个函数,我们创建四个这样的pcb,将这四个函数的代码和数据给分配给这四个pcb,然后让主进程执行主要的代码,地址空间的中的各区域资源能分开管理的分开管理,该共享的共享。而代码段的分配其实也可以通过页表的分配来实现,这样我们就有了五个"进程"来执行我们的代码:
在这里插入图片描述
以前我们的进程是以串行的方式执行代码的,而现在我们实现了这样的方式之后,我们的代码可以分成多个执行流来运行了,我们在上面一直说这种方式创建出来的额外的pcb是叫"进程",而这其实也算是Linux中对于线程的实现方案了。
经过上面的描述之后我们发现这个线程在创建的时候并没有像进程一样创建页表,地址空间加载代码和数据,然后再初始化各种内核数据解雇并与加载的代码而和数据建立连接,而是只是简单的创建了一个pcb,这么来看线程是比进程更加轻量化的理解首先就体现在:线程的创建相比进程更简单,既然线程的创建简单的话,那么势必它的释放也很简单
而关于线程是进程中的一个执行流,我们可以发现线程只创建了一个pcb然后将它相当于是链入到一个进程中执行一个进程中的代码,而这也就是线程在地址空间中运行体现了线程是进程中的一个执行流。

b. 另一个角度设计线程

我们现在先思考一个问题:假如操作系统中支持线程的话,计算机中会不会同时运行多个线程?答案是肯定会的。既然会有多个线程,那这些线程就需要被管理。线程本质也还是为了执行代码嘛,先描述后组织,那么此时我们就可以描述出关于线程的结构体(有些操作系统书籍中叫做TCB thread control block)了。这个结构体中肯定会有自己的id,线程的状态,线程的信号,线程的一些其他信息。
建立好线程的结构体后,我们就需要将它放入各种队列如运行队列、阻塞队列挂起队列中,将线程管理起来。
然后再实现出线程的各种调度方法,而对于线程的调度也不过是对线程的数据结构的增删查改罢了。这样我们的计算机中就已经能存在多线程了。
单单实现了线程的体系还不够,操作系统说线程是进程内部执行的执行流,那么说,在进程中起码还要有一个像队列一样的数据结构来记录自己的线程,这种方式也一定会让线程和进程的代码产生了一定的耦合。
这样我们又创建了一个线程方案。这个时候我们发现,这上面的实现有点麻烦还要实现两个部分的代码耦合。而且我们发现线程的结构体中的字段与task_struct很相似,而且调度起来也无非就是依据线程中的状态描述将线程切换到不同的调度队列中。这整个过程中线程的行为都和进程很相似。那么在Linux中正是有了这样的思路,从而复用了进程的代码结构,实现出了线程的概念。
我们提出上面这么一大坨的时候,我们是在说线程吗?其实我们是在说Linux中的线程。操作系统书籍中说线程是更加轻量化的一种执行流,是进程内部的一种执行流,反过来说只要我们的操作系统中实现了符合这些特点的一个方案,那么它就叫做线程。所以操作系统不仅仅是一门课程,也是一个指导书籍。操作系统只会说明一个系统中应该有什么,运用什么样的调度方式,但是它不会说你应该使用什么样的方案来实现,使用什么调度方法。
我们上面说线程概念的提出应该伴随着一套新的结构体和管理结构体的数据结构以及合适的调度方法出现,在Linux中并没有这么做,它复用了进程的代码,但是我们经常使用的Windows操作系统中是真正的实现出了这样一套关于线程的体系。这么一对比明显是Linux的关于线程的设计更加的优秀,因为它并没有产生新的东西。而且也更加的简单,这也是为什么企业更愿意使用Linux来作为自己的服务器的原因之一。
而且这样创建出来的线程也是符合操作系统的定义的:
它比线程更加轻量化,线程是在进程内部的一种执行流。
现在我们大致认识了Linux中的线程是什么样的,但是我们可能仍然不清楚,Linux中的线程它到底是什么?还有CPU在调度pcb执行代码的时候这个pcb到底是线程还是进程。以前我们一说进程就会想到pcb,现在有这么多的pcb叫线程,我们又应该如何看待现在这个进程呢?

3. 重新认识Linux中的进程

我们在之前使用进程的时候,认为进程是最小的执行流,而现在当提出线程的概念之后,我们应该有这么一种意识:执行流的概念的范围是要小于等于进程的。对于cpu来说,当一个pcb被调度之后,你就只管将你的各种数据放到我的寄存器中,然后我给你跑了就行,时间片到了我在执行下一个执行流就可以了。所以对我来说你是线程还是进程cpu根本不关心,只要你是个执行流就行了。而Linux中实现线程又是复用了进程的代码,所以在Linux系统中根本就没有真正意义上的线程,有的只是各个pcb,我们也将这种线程叫做轻量级进程。至此线程才应该是cpu调度的基本单位,而Linux系统从现在开始就没有了线程和进程的说法,有的只是轻量级进程。就算进程中只有一个pcb,当cpu调度的时候,这个pcb也一定是被cpu认为成轻量级进程的。
那么我们应该怎么看待今天的进程呢?
我们发现进程中可以允许存在很多的执行流,但是这些线程所使用的大部分资源也就是地址空间只有一份,我们也说进程 = 内核数据结构 + 代码 + 代码数据,而代码和代码数据也是只有一份,我们创建线程时不会产生新的较多的资源,但是我们创建线程时就会伴随着各种内核结构以及代码和代码数据资源的加载,每创建一个进程,系统就要给这个进程许多资源。所以我们现在不应该再认为进程是一个执行流了,而是:进程应该是承担系统资源的基本实体。下图中的红色框标注的才应该是我们现在的进程。
在这里插入图片描述

我们怎么认识之前的进程呢?
这个其实也很简单理解,无非就是以前的进程就是内部只有一个执行流的进程。
我们来看这样一段代码:
在这里插入图片描述
这是一个Linux中创建一个线程的代码,这个线程会执行上面的task_thread函数,而主进程会继续执行main函数的代码:
在这里插入图片描述
我们可以看到,我们的代码中有两个死循环,但是这两个死循环能够同时执行,而右侧我们也只能看到一个进程,左侧两个执行流的pid是一样的。这也说明了Linux中确实又线程的存在,也说明了线程是进程内部的执行流。其实我们要更加详细的观察两个线程我们还需要ps的另一个选项:
在这里插入图片描述
我们会发向test中的两个执行流,它们两个pid是一样的,但是我们会发现一个新的参数LWP(light weight process)也就是轻量化进程的意思,他能够区分出线程。我们现在应该也明白了,CPU调度的时候看的是pid吗?其实看的是lwp。

4. 线程更加轻量化的体现

我们上面说了线程更加轻量化体现在线程的创建和释放。而线程的轻量化还体现在调度上。
我们都知道执行流在被调度时,需要加载硬件上下文到寄存器中,如果此时执行流切换的时候这两个执行流是一个进程中呢?那么就意味着寄存器中有些内容是不需要更换的比如页表和地址空间的地址。意味着线程间切换很可能会有更少的寄存器数据切换。当然这是一个影响原因,但是寄存器的存取速度是很快的,这么点数据不算什么。但是在我们的CPU中还有一个硬件那就是cache,当前要调度的执行流的代码和代码数据会被加载到cache缓存中,而这个代码和代码数据是我们当前执行代码附近的代码,cache中的数据也叫做热数据。cache的信息可以通过cat /proc/cpuinfo来查看。
在这里插入图片描述
这样的做法是符合局部性原理的。我们的大文件加载内存中也会用到局部性原理。而当我们进行线程间切换的时候,这两个线程在一个进程中,那就意味着这两个线程中的代码和数据有可能是临近的。这样的话cache缓存就不需要在此重新加载热数据了,这才是线程更加轻量化的主要原因之一。
而我们也需要注意一点。就是当一个进程中创建好一个线程的时候,该进程的时间片是不会增加的,而是进程中的所有线程瓜分该进程的时间片,因为时间片也是进程的资源。如果上面的说法成立的话,那么在理论上我们可以让一个进程一直被调度只要在时间片快耗尽的时候,我们再创建一个线程就可以了,所以这种现象是不允许发生的。

5. 线程的其他补充

a. 线程的优缺点

我们先来说优点:

1. 创建一个新线程的代价要比创建一个新进程小得多
2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3. 线程占用的资源要比进程少很多
4. 能充分利用多处理器的可并行数量
5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

对于计算密集型应用,我们可以理解为一个非常复杂的运算过程,我们可以把这个过程拆开然后分给多个线程并行处理,然后再把各线程的结果汇总得到最后的结果。但是其实计算密集型应用其实是不太建议使用多线程的,因为它涉及到了线程的切换。我们一般建议对于计算密集型应用建立的最多的线程数目应该是CPU的数目 * CPU的核数。
对于I/O密集型应用,我们可以理解为,我们的进程需要在网络上加载一个大文件,我们可以使用多个线程,将这个文件分开来加载,然后再归并为一个文件。而对于I/O密集型应用我们可以使用较多一点的线程,因为I/O密集型应用有相当一部分时间是处于阻塞的,对于多执行流来说,阻塞的时间可以叠加,也变相提高了效率。
接下来是缺点:

性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高:编写与调试一个多线程程序比单线程程序困难得多

我们知道在一个进程中的线程能够看到都能够地址空间任何一个地方,这就说明各个线程之间的数据是共享的,这导致了线程间通信是很简单与方便的,但是这种数据的共享是不安全的,所以在使用线程时要注意线程的保护。
在这里插入图片描述
在这里插入图片描述
这个代码可以证明线程之间数据是共享的。

b. 线程的异常

对于线程而言,线程出现了异常,接收到信号被终止,此时线程就会被终止,所有线程都退出。

c. 线程的独立的一些数据

进程我们知道它是具有独立性的,一个进程收到信号被终止后,其它进程不会受影响,而且进程之间的资源是相互独立的。但是也有例外那就是进程间通信,可以实现进程间的资源交互。
无独有偶,虽然线程间会共享大部分资源,但是有一些资源也是私有的,也是独立的。
比如就是线程ID,除了线程ID外还有硬件上下文因为我们知道线程是要被调度的,每个线程执行的代码,所拥有的数据大概率是不相同的。还有就是线程有着自己独立的栈结构,这个也很好理解。我们在使用C/C++语言的时候肯定不可避免地会使用到函数,既然使用函数就一定会产生栈结构,也一定会产生临时数据,而这些对于线程来说,也肯定是独立拥有的。
除此之外还有错误码errno,信号屏蔽字,和调度优先级(线程是被独立调度的)。

d. 线程间共享的一些数据

我们上面说线程会共享大部分的资源,这其中的资源指的是地址空间能够看到的资源。还有一些资源不是在地址空间中,但是他也是被线程间共享的,比如:文件描述符表,每种信号的处理方式(这里也说明信号为什么叫做进程信号),当前工作目录,用户id和组id。

6. 对页表的重新认识

a. 更正错误

这部分虽然线程没什么太大关系,但是这个知识也是很重要的,我在之前介绍地址空间的时候,我们说虚拟地址空间和物理内存是由页表建立映射关系,然后进程通过地址空间,地址空间再通过页表映射到物理内存中寻找资源的。并且我说页表中页表的一部分是虚拟地址,另一部分是物理内存地址,还有一部分是存储该地址的数据是否存在不存在要触发缺页中断,还要有一个位置要表示该地址是否可读写:
在这里插入图片描述
我们在仔细看一下这个关于页表的介绍,假如在32位系统下内存的大小是4GB,也就是2^32GB,我们的页表中存储着两个地址和两个字段,就把它们算成十字节,首先页表是有可能建立 2 ^ 32个映射的,那么这个页表的大小就是10 * 2 ^ 32,需要40GB。一个进程就需要40GB的页表,显然是不合理的,所以我之前讲述的页表的结构是不合理的。而接下来我要讲述页表的真正的结构。

b. 简单认识内存管理

在讲述页表结构之前我们需要一点的内存管理的大致认识作为前置准备。
我们知道操作系统中文件IO的基本单位一般是4KB也叫做page size,这是由文件系统决定的。这表明我们将磁盘中的数据读到内存中是以4KB的大小进行读,将内存中的文件缓冲区中的内容更新到磁盘的时候也是以4KB为基本单位写入的(这样的意思是就算你只需要在内存中的一个字节的大小来加载磁盘盘中数据,内存也会直接给你4KB,你就算只改变内存中一个4KB其中的一个字节,操作系统也会将这个4KB全部刷新到磁盘中。当然也会存在特殊情况(比如语言中用来申请内存的malloc和new),但这是一般现象),这样的规则将我们的磁盘进行了规则的分区(虽然磁盘中会有更小的基本单元)。
既然磁盘被规则化处理,为了更好的IO处理,其实内存中的基本单位也同样遵循4KB的基本单位,那么现在我们就能把物理内存分成许多个4KB,而这样的内存中的一个4KB的一个块叫做页框,我们在磁盘中的文件,它本身也遵循着按4KB为一部分共同组成的,文件中的4KB叫做页帧
这样一种现象造成的结果是,我们知道磁盘文件中文件的属性放在一个大小为128字节的inode结构体中,当我们加载某一个文件的文件属性时,大概率也会把其它文件的inode结构体也一并加载进来,因为内存和磁盘交互的基本大小是4KB。:
在这里插入图片描述
在32位下,物理内存中会有1048576个这样的4KB。那么理所当然,这么多的4KB同样需要被管理。先描述后组织。在操作系统中有一个结构体sruct page,这个结构体中是描述一个页框的信息,如可用大小,是否刷新内容到磁盘,是否有数据,是否可读写。这些大部分是可以使用一个比特位就可以说明的,所以这个结构体是不是那么的大。你可以理解为操作系统中有一个数组存储着这样的结构体struct page pages[]。这样就能将内存进行很好的管理了。这只是对内存管理进行大致的介绍,具体的内存管理是很复杂的,有兴趣可以自己去了解一下。

c. 重新认识页表

有了上面的认识之后我们就可以重新认识页表了,在32位系统下,我们的虚拟地址实际上是被逻辑上这么划分的:
在这里插入图片描述
虚拟地址被划分成了三个区域:
第一个区域的大小是2 ^ 10 也就是1024,这里你可以理解为它有一个大小为1024的数组,这个数组也叫做页表项:
在这里插入图片描述

这十个位正好可以通过数组下标的方式,访问到这个数组的所有内容
这个数组是一个指针数组,其中的指针指向又一个数组,这个数组的大小也是1024,通过第二个区域的十个位可以全部访问到:
在这里插入图片描述
这样通过这样的多级映射我们会发现这两张表可以实现1024 * 1024个映射,而这正好是物理内存页框的个数,而最后12位,我们发现它的大小正好可以完全映射4KB的内容,在这样的结构下我们的页表结构就可以映射到物理内存中的任意一个字节中,而这样的结构我们最多会使用1024 * 1024 * 32个字节,是要比原来的结构小的多的。
当我们发现页表可以读到物理内存中的任意一个字节之后,我们就发现了语言中类型的本质,它的本质不就是相当于一个偏移量吗?
这就是页表的结构。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_999151.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UE4 Niagara 关卡4.1官方案例解析

we now directly supporting playing audio from arbitrary locations in particle systems.users have control over volume and pitch,and the system can directly play sound waves,or sound cues which have multiple waves in them.(我们现在直接支持在粒子系统中从任意…

小白跟做江科大51单片机之AD/DA

1.看原理图找接口 2.看时序图编写读取数据代码 XPT2046.c代码 #include <REGX52.H> //引脚定义 sbit XPY2046_DINP3^4; sbit XPY2046_CSP3^5; sbit XPY2046_DCLKP3^6; sbit XPY2046_DOUTP3^7; unsigned int XPT2046_ReadAD(unsigned char Command) { unsigned char …

【重温设计模式】中介者模式及其Java示例

中介者模式的基本概念 在我们的日常生活中&#xff0c;有许多事情是需要通过一个“中介”来完成的&#xff0c;比如租房、买房、找工作等。在软件设计中&#xff0c;也有一种名为“中介者模式”的设计模式&#xff0c;它的作用和我们生活中的“中介”有着异曲同工之妙。 中介者…

鸿蒙Harmony应用开发—ArkTS声明式开发(基础手势:Divider)

提供分隔器组件&#xff0c;分隔不同内容块/内容元素。 说明&#xff1a; 该组件从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 子组件 无 接口 Divider() 从API version 9开始&#xff0c;该接口支持在ArkTS卡片中使…

微服务超大Excel文件导出方案优化

1、在导出Excel时经常会碰到文件过大&#xff0c;导出特别慢 2、微服务限制了请求超时时间&#xff0c;文件过大情况必然超时 优化思路&#xff1a; 1、文件过大时通过文件拆分、打包压缩zip&#xff0c;然后上传到oss,并设置有效期&#xff08;30天过期&#xff09; 2、把…

selenium爬取房价收入比可视化

数据来源&#xff1a;聚合数据 from selenium import webdriver from bs4 import BeautifulSoup import csv from selenium import webdriver from fake_useragent import UserAgent import random import subprocess from selenium import webdriver from selenium.webdrive…

Java高频面试之消息队列与分布式篇

有需要互关的小伙伴,关注一下,有关必回关,争取今年认证早日拿到博客专家 消息队列的基本作用&#xff1f; 异步通信&#xff1a;消息队列提供了异步通信的能力&#xff0c;发送方可以将消息发送到队列中&#xff0c;而无需等待接收方立即处理。发送方和接收方可以解耦&#x…

window vscode安装node.js

window vscode安装node.js 官网下好vscode 和nodejs 选.msi的安装 点这个安装 下载完 继续安装 完毕后倒杯水喝个茶等2分钟 重启VScode 或者在cmd 运行 npm -v node -v 显示版本号则成功

波卡 Alpha 计划启动,呼唤先锋创新者重新定义 Web3 开发

原文&#xff1a;https://polkadot.network/blog/the-polkadot-alpha-program-a-new-era-for-decentralized-building-collaboration/ 编译&#xff1a;OneBlock 区块链领域不断发展&#xff0c;随之而来的是发展和创新机会的增加。而最新里程碑是令人振奋的 Polkadot Alpha …

鸿蒙Harmony应用开发—ArkTS声明式开发(触摸交互控制:触摸热区设置)

适用于支持通用点击事件、通用触摸事件、通用手势处理的组件。 说明&#xff1a; 从API Version 8开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 responseRegion responseRegion(value: Array<Rectangle> | Rectangle) 设置一个或…

UI自动化测试使用场景及脚本录制

经常有人会问&#xff0c;什么样的项目才适合进行UI自动化测试呢&#xff1f;UI自动化测试相当于模拟手工测试&#xff0c;通过程序去操作页面上的控件。而在实际测试过程中&#xff0c;经常会遇到无法找到控件&#xff0c;或者因控件定义变更而带来的维护成本等问题。 哪些场…

《UE5_C++多人TPS完整教程》学习笔记26 ——《P27 在线会话测试(Testing An Online Session)》

本文为B站系列教学视频 《UE5_C多人TPS完整教程》 —— 《P27 在线会话测试&#xff08;Testing An Online Session&#xff09;》 的学习笔记&#xff0c;该系列教学视频为 Udemy 课程 《Unreal Engine 5 C Multiplayer Shooter》 的中文字幕翻译版&#xff0c;UP主&#xff0…

K8s-MySQL主从集群

K8s-MySQL主从集群 引言 该案例代码均可从https://github.com/WeiXiao-Hyy/k8s_example 获取&#xff0c;欢迎Star&#xff01; 需求 一个“主从复制”的MySQL集群有一个主节点Master有多个从节点Slave从节点需要能水平扩展所以写操作只能在主节点上执行读操作可以在所有节点…

ACM32系列 MCU安全特性概述

随着物联网的发展&#xff0c;智能化产品的不断涌现&#xff0c;信息安全问题也日渐受到关注。因此&#xff0c;通用安全MCU产品也应运而生&#xff0c;能够更好地帮助客户在其产品设计中加强安全性&#xff0c;助力IoT的应用创新。 本文将详细介绍ACM32系列 MCU的安全特性。 …

【Linux进阶之路】网络 —— “?“ (下)

文章目录 前言一、概念铺垫1.TCP2.全双工 二、网络版本计算器1. 原理简要2. 实现框架&&代码2.1 封装socket2.2 客户端与服务端2.3 封装与解包2.4 请求与响应2.5 对数据进行处理2.6 主程序逻辑 3.Json的简单使用 总结尾序 前言 在上文我们学习使用套接字的相关接口进行了…

面试经典150题【61-70】

文章目录 面试经典150题【61-70】61.旋转链表86.分隔链表104. 二叉树的最大深度100.相同的树226.翻转二叉树101.对称二叉树105.从前序与中序遍历序列构造二叉树106.从后序和中序遍历序列构造二叉树117.填充每个节点的下一个右侧节点指针II114.二叉树展开为链表 面试经典150题【…

怎么把视频变成gif动图?一招在线生成gif动画

MP4是一种常见的视频文件格式&#xff0c;它是一种数字多媒体容器格式&#xff0c;可以用于存储视频、音频和字幕等多种媒体数据。MP4格式通常用于在计算机、移动设备和互联网上播放和共享视频内容。要将MP4视频转换为GIF格式&#xff0c;您可以使用专门的视频转gif工具。这个工…

【Linux】第一个小程序--进度条

这篇博客要综合利用以前的知识&#xff0c;来实现一个进度条程序~ 目录 换行&回车 缓冲区 实现简单的倒计时 实现进度条 version1 version2 在开始写这个小程序之前&#xff0c;我们先学习一些预备知识&#xff1a; 换行&回车 缓冲区 在我们运行这个程序时&…

生活的色彩--爱摸鱼的美工(17)

题记 生活不如意事十之八九&#xff0c; 恶人成佛只需放下屠刀&#xff0c;善人想要成佛却要经理九九八十一难。而且历经磨难成佛的几率也很小&#xff0c;因为名额有限。 天地不仁以万物为刍狗&#xff01; 小美工记录生活&#xff0c;记录绘画演变过程的一天。 厨房 食…

Linux系统安装Dashy服务结合内网穿透实现公网访问本地导航页

文章目录 简介1. 安装Dashy2. 安装cpolar3.配置公网访问地址4. 固定域名访问 简介 Dashy 是一个开源的自托管的导航页配置服务&#xff0c;具有易于使用的可视化编辑器、状态检查、小工具和主题等功能。你可以将自己常用的一些网站聚合起来放在一起&#xff0c;形成自己的导航…