-
SD卡烧录好欧拉系统,HiLens能够设置SD卡为启动首选项,使用SD卡烧录的系统吗?备注:HiLens根目录只有2GB,空间太少了,安装几个库就没有空间了,总计32GB的空间,分成了很多个区,不敢贸然合并,所以想搞个大容量SD卡,烧录系统使用
-
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/
-
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中权限的问题,那么,我们理解了用户间的“隔离”,那么又该怎么进行多用户协作呢? 本文就来回答这个问题。 现象 在正式回答这个问题之前,我们先来看一下这个现象。 我们在用户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
-
一:🔥 线程互斥 🦋 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
-
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
-
知识铺垫 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
-
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
-
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
-
【Linux 篇】Docker 容器星河与镜像灯塔:Linux 系统下解锁应用部署奇幻征程 💬欢迎交流:在学习过程中如果你有任何疑问或想法,欢迎在评论区留言,我们可以共同探讨学习的内容。你的支持是我持续创作的动力! 👍点赞、收藏与推荐:如果你觉得这篇文章对你有所帮助,请不要忘记点赞、收藏,并分享给更多的小伙伴!你们的鼓励是我不断进步的源泉! 🚀推广给更多人:如果你认为这篇文章对你有帮助,欢迎分享给更多对Linux感兴趣的朋友,让我们一起进步,共同提升! 前言 docker是一个开源的应用容器引擎,基于go语言开发。docker可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的linux机器上,也可以是实现虚拟化。相互之间不会有任何接口,容器的开销性能极低。 本文我们来介绍docker的使用方法 一 、docker上部署mysql 我们如果把 Linux 看作浩瀚宇宙里的一艘巨型星际飞船,肩负探索数据 “星际” 重任。Docker 如同飞船内部超能 “舱室定制大师”,拥有高科技模块拼接手段。MySQL 便是飞船至关重要的 “数据中枢舱”,源源不断为整艘飞船提供精准导航、协同各舱室(和其他关联应用)顺畅运作的数据能量。在 Linux 系统的 “航行轨道” 上,通过 Docker 部署 MySQL,类似在飞船里组装激活中枢舱,支撑飞船协同运作、精准导航。 1. 拉取mysql镜像 [root@CentOS02 dockerTar]# docker load -i mysql5.7.tar 2. 创建容器 创建mysql5.7容器 docker run -di --name=容器名字 -p 宿主机端口:容器端口 -e MYSQL_ROOT_PASSWORD=salmon(密码) 容器名称 -p :表示端口映射,格式 宿主机映射端口:容器运行端口 -e :代表添加环境变量MYSQL_ROOT_PASSWORD 是root用户远程登录密码 [root@CentOS02 dockerTar]# docker run -di --name=mysql5.7 -p 33306:3306 -e MYSQL_ROOT_PASSWORD=123456 centos/mysql-57-centos7 创建守护式容器,并且通过docker ps 查看是否映射成功 3. 远程登录mysql 连接宿主机的IP ,指定端口号为33306 使用Windows上面的小海豚进行连接 二 、docker上部署nginx 把 Linux 系统看作是浩瀚的星际空间,有无数的 “数据飞船”(网络请求和数据传输)在其中穿梭,寻找正确的目的地。Docker 就像一个星际工程师,可以在这个星际空间中安置一个个功能强大的 “灯塔建筑”(容器),每个灯塔都有自己独立的能源供应(资源管理)和信号发射系统(网络配置)。Nginx 就像是灯塔里的 “信号导航员”,它通过明亮而精准的 “信号灯”(服务器配置和路由规则),为经过的 “数据飞船” 指引方向。当飞船靠近灯塔时,Nginx 这个导航员会根据飞船的 “标识”(请求头信息)和 “目的地坐标”(请求的目标服务),发送精确的信号,引导飞船顺利驶向正确的 “星球港口”(后端服务器或者服务端点),在 Docker 构建的灯塔环境下,稳定、高效地完成星际数据传输的导航工作。 1. 拉取nginx镜像 2. 在dockerTar目录下 上传nginx.tar rz命令 [root@CentOS02 dockerTar]# docker load -i nginx.tar 3. 创建nginx容器 [root@CentOS02 dockerTar]# docker run -id --name=mynginx -p 88:80 mynginx 1 4. 通过docker ps 查看是否映射成功 [root@CentOS02 dockerTar]# docker ps 1 5. 请求nginx页面 安装完成之后,请求nginx页面 直接访问页面:http://192.168.174.140:88 三 、docker上部署redis 1. 拉取镜像 先上传redis 2. 创建容器 [root@CentOS02 dockerTar]# docker run -id --name=myredis -p 6379:6379 redis 可以通过客户端工具连接测试.或者通过java代码redis客户端进行测试 结语 Docker安装MySQL、Tomcat、Redis的用处: MySQL 数据持久存储:作为经典的关系数据库管理系统,在Docker容器内运行可安全、高效存储应用结构化数据,像电商系统的订单、用户信息,内容管理平台的文章、用户资料等,保障数据完整性与持久性,便于随时读写调用。 Tomcat 环境隔离适配:利用Docker隔离特性,在同一台主机上为不同Java项目创建独立Tomcat容器,各自适配JDK版本、依赖库等,互不干扰,解决环境冲突问题,适配复杂项目部署场景。 Redis 数据缓存加速:把频繁读写的数据(热门商品信息、高频查询结果)存于内存型的Redis,应用先从Redis获取数据,极大加快响应速度,减轻后端数据库压力,像社交平台动态缓存、电商商品详情缓存。 ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/2302_79751907/article/details/144120404
-
Linux 用户模式管理:深入解析多用户环境的基石在 Linux 操作系统的广袤世界里,用户模式管理犹如一座精心构建的大厦,其不同的层次和规则确保了系统的安全性、稳定性以及多用户协作的高效性。理解 Linux 的用户模式管理对于系统管理员、开发者以及任何深度使用 Linux 系统的用户来说,都是至关重要的知识储备。一、Linux 用户与用户组的基本概念Linux 是一个多用户操作系统,每个用户都拥有独立的身份标识和权限设置。用户通过用户名和密码登录系统,系统根据用户的身份来决定其对文件、目录和系统资源的访问权限。用户组则是一种将多个用户组织在一起的机制。通过将用户划分到不同的组中,可以方便地对一组用户统一设置权限。例如,一个项目开发团队可能有多个成员,他们都需要对项目相关的文件和目录具有相同的读写权限,这时就可以创建一个专门的用户组,并将团队成员加入该组。二、用户模式的分类1. 根用户(root)模式根用户,也称为超级用户,是 Linux 系统中权限最高的用户。根用户拥有对系统的完全控制权,可以执行任何操作,包括创建、修改和删除系统文件,启动和停止系统服务,以及更改系统配置等。然而,正是由于根用户的权限过于强大,使用根用户进行日常操作存在很大的风险。一个不小心的误操作可能会导致系统崩溃或数据丢失。因此,在一般情况下,建议仅在必要时切换到根用户模式,例如进行系统安装、软件升级或系统维护等关键操作。在大多数 Linux 发行版中,可以使用 su 命令切换到根用户模式。$ su - Password: # 此时已切换到根用户模式,命令提示符变为2. 普通用户模式普通用户是在系统中创建的具有受限权限的用户。普通用户只能访问和操作自己拥有权限的文件和目录,无法执行一些系统级别的关键操作,如修改系统核心配置文件等。这有效地保护了系统的安全性,防止普通用户因误操作或恶意行为对系统造成损害。普通用户可以执行日常的应用程序、创建和管理自己的文件和目录、进行文本编辑等常规任务。当普通用户需要执行一些需要更高权限的操作时,可以使用 sudo 命令以根用户的权限临时执行特定命令。例如,普通用户想要安装一个软件包,而该软件包的安装需要写入系统目录的权限,就可以使用 sudo 命令:$ sudo apt-get install some_package Password: # 输入当前普通用户的密码后,将以根用户权限执行软件包安装命令三、用户权限管理1. 文件和目录权限在 Linux 中,每个文件和目录都有一组权限设置,用于控制不同用户和用户组对其的访问权限。权限分为读(r)、写(w)和执行(x)三种类型,分别对应数字 4、2 和 1。对于文件来说,读权限允许用户查看文件内容,写权限允许用户修改文件内容,执行权限则允许用户将文件作为程序来运行(例如脚本文件或可执行二进制文件)。对于目录而言,读权限允许用户列出目录中的内容,写权限允许用户在目录中创建、删除和重命名文件和目录,执行权限则允许用户进入该目录并访问其中的文件和子目录。文件和目录的权限可以使用 ls -l 命令查看,例如:$ ls -l total 4 -rw-r--r-- 1 user user 0 Nov 24 10:00 test.txt drwxr-xr-x 2 user user 4096 Nov 24 10:05 test_dir在上述输出中,第一列显示的就是文件或目录的权限信息。以 test.txt 文件为例,-rw-r--r-- 表示该文件的所有者具有读写权限,所属用户组的成员具有读权限,其他用户也具有读权限。可以使用 chmod 命令来更改文件和目录的权限。例如,要为 test.txt 文件的所有者添加执行权限,可以使用以下命令:$ chmod u+x test.txt其中,u 表示所有者(user),+x 表示添加执行权限。2. 用户和用户组管理命令Linux 提供了一系列命令用于用户和用户组的管理。useradd 命令用于创建新用户。例如:$ useradd newuser这将创建一个名为 newuser 的新用户。passwd 命令用于设置或更改用户密码。例如:$ passwd newuser系统会提示输入新密码并进行确认。groupadd 命令用于创建新的用户组。例如:$ groupadd newgroupusermod 命令用于修改用户的属性,如将用户添加到某个用户组中:$ usermod -a -G newgroup newuser这里的 -a 选项表示追加,-G 选项指定要添加的用户组。userdel 命令用于删除用户,但需要注意在删除用户之前,最好先备份该用户的相关数据。例如:$ userdel newuser四、用户模式管理的最佳实践1. 谨慎使用根用户如前所述,根用户权限过高,应尽量避免在日常操作中使用。只有在进行系统关键操作且明确知晓操作风险的情况下,才切换到根用户模式。2. 合理规划用户和用户组根据系统的使用场景和需求,合理创建用户和用户组,并为它们设置恰当的权限。例如,对于一个 Web 服务器,可以创建一个专门的用户组来管理网站文件,将负责网站维护的用户添加到该组中,并给予该组对网站文件目录的读写权限。3. 定期审查用户权限随着系统的使用和用户的变更,应定期审查用户和用户组的权限设置,确保权限的合理性和安全性。及时删除不再需要的用户或修改用户的权限,以防止潜在的安全漏洞。4. 使用 sudo 进行权限提升鼓励普通用户在需要时使用 sudo 命令来执行需要更高权限的操作,而不是直接切换到根用户模式。这样可以更好地跟踪和记录用户的操作行为,便于事后审计。Linux 的用户模式管理是构建安全、稳定和高效多用户操作系统环境的核心要素。通过深入理解不同用户模式的特点、合理管理用户和用户组权限以及遵循最佳实践,我们能够充分发挥 Linux 系统在多用户协作、服务器管理等方面的优势,保障系统的正常运行和数据的安全。无论是初涉 Linux 领域的新手,还是经验丰富的系统管理员,不断学习和优化用户模式管理知识都是提升 Linux 系统使用技能的重要环节。
-
一、概述 Linux 中新建用户的命令是 useradd ,一般系统中这个命令对应的路径都在 PATH 环境变量里,如果直接输入 useradd 不管用的话,就用绝对路径名的方式:/usr/sbin/useradd 。 useradd 新建用户命令只有 root 用户才能执行,我们新建用户kangll ,并设置密码。 二、su 命令介绍及主要用法 首先需要解释下 su 代表什么意思。su 表示 switch user,它提供的功能就是切换用户。 官方释义: su允许运行带有替代用户和组ID的命令。当不带参数调用时,su默认以root身份运行交互式shell。为了向后兼容,su默认不更改当前目录,并且只设置环境变量HOME和SHELL(如果目标用户不是根用户,则加上USER和LOGNAME)。建议始终使用 --login选项(而不是它的快捷方式-),以避免混合环境造成的副作用。 这个版本的su使用PAM进行身份验证、帐户和会话管理。一些其他su实现中的配置选项(例如对wheel组的支持)必须通过PAM进行配置。 su主要是为非特权用户设计的,这是特权用户的推荐解决方案。 2.1 参数- su 的一般使用方法是: su <user_name> 或者 su - <user_name> 两种方法只差了一个字符 -,会有比较大的差异: 如果加入了 - 参数,那么是一种 login-shell 的方式,意思是说切换到另一个用户 <user_name> 之后,当前的 shell 会加载 <user_name> 对应的环境变量和各种设置; 如果没有加入 - 参数,那么是一种 non-login-shell 的方式,意思是说我现在切换到了 <user_name>,但是当前的 shell 还是加载切换之前的那个用户的环境变量以及各种设置。 光解释会比较抽象,我们看一个例子就比较容易理解了。 我们从kangll 用户以 non-login-shell 的方式切换到 root 用户,比较两种用户状态下环境变量中 PWD 的值(su 命令不跟任何 <user_name> ,默认切换到 root 用户): 我们的确是切换到 root用户了,但是 shell 环境中的变量并没有改变,还是用之前 kangll 用户的环境变量。 接着我们从 kangll 用户以 login-shell 的方式切换到 root 用户,同样比较两种用户转台下环境变量中 PWD 的值: 可以看到用 login-shell 的方式切换用户的话,shell 中的环境变量也跟着改变了。 总结:具体使用哪种方式切换用户看个人需求: 如果不想因为切换到另一个用户导致自己在当前用户下的设置不可用,那么用 non-login-shell 的方式; 如果切换用户后,需要用到该用户的各种环境变量(不同用户的环境变量设置一般是不同的),那么使用 login-shell 的方式。 2.2 切换到指定用户 前面已经介绍了,如果 su 命令后面不跟任何 <user_name>,那么默认是切换到 root 用户: 如下我们可以从 mysql 用户切换到 kangll 用户: 2.3 参数-c 前面的方法中,我们都是先切换到另一个用户(root 或者 kangll),在哪个用户的状态下执行命令,最后输入 exit 返回到当前用户。 还有一种方式是:不需要先切换用户再执行命令,可以直接在当前用户下,以另一个用户的方式执行命令,执行结束后就返回当前用户。这就得用到 -c 参数。 具体使用方法是: su - -c "指令串" # 以 root 的方式执行 "指令串" 看个例子: 这种执行方式一般是我们想以某个用户启动一个特定的进程,比如下面后台启动hive进程: su - hive -c 'env HADOOP_HOME=//usr/hdp/2.6.4.0-91/hadoop nohup hive --service metastore > /var/log/hive/hive.out 2> /var/log/hive/hive.log &' 三、sudo 命令介绍及主要用法 sudo 的英文全称是 super user do,即以超级用户(root 用户)的方式执行命令。这里的 sudo 和之前 su 表示的 switch user 是不同的,这点需要注意,很容易搞混。 我们先介绍 sudo 命令能做什么事情。 3.1 主要用法 我们在 Linux 中经常会碰到 Permission denied 这种情况,比如以 kangll 用户的身份查看 /etc/profile 的内容。因为这个文件的内容是只有 root 用户能查看的。 那如果我们想要查看怎么办呢?这时候就可以使用 sudo : 需要输入当前这个用户的密码,本例中需要输入 knagll 用户的登录密码。 两次相邻的 sudo 操作,如果间隔在 5min 之内,第二次输入 sudo 不需要重新输入密码;如果超过 5min,那么再输入 sudo 时,又需要输入密码。所以一个比较省事的方法是设置 sudo 操作不需要密码,后面我们将介绍如何设置。 sudo 除了以 root 用户的权限执行命令外,还有其它几个用法,这里做简单介绍。 切换到 root 用户: sudo su - 这种方式也能以 login-shell 的方式切换到 root 用户,但是它和 su - 方法是由区别的: 前者输入 sudo su - 后,需要提供当前用户的登录密码,也就是 kangll 用户的密码; 后者输入 su - 后,是切换到root用户,需要提供 root 用户的登录密码。 还有一个命令: sudo -i 这个命令和 sudo su - 效果一致,也是切换到 root 用户,也是需要提供当前用户(ubuntu 用户)的登录密码。 我们现在切换到 winner 用户,尝试显示 /etc/shadow 文件的内容: 我们会看到错误提示信息,并无法查看 /etc/shadow 的内容,这就需要为普通用户设置sudo权限。 3.2 sudo 工作原理 一个用户能否使用 sudo 命令,取决于 /etc/sudoers 文件的设置。 从上节我们已经看到,kangll 用户可以正常使用 sudo ,这是因为 /etc/sudoers 文件里做了设置。 /etc/sudoers 是一个文本文件,但是因其有特定的语法,我们一般不直接用 vim 或者 vi 来编辑它,需要用 visudo 这个命令。输入这个命令之后就能直接编辑文件 /etc/sudoers 。需要注意的是,只有 root 用户有权限使用 visudo 命令。 我们先来看下输入 visudo 命令后显示的内容。 [root@windp-aio ~]# visudo 输出: ## Allow root to run any commands anywhere root ALL=(ALL) ALL ## Allows members of the 'sys' group to run networking, software, ## service management apps and more. # %sys ALL = NETWORKING, SOFTWARE, SERVICES, STORAGE, DELEGATING, PROCESSES, LOCATE, DRIVERS ## Allows people in group wheel to run all commands %wheel ALL=(ALL) ALL ## Same thing without a password # %wheel ALL=(ALL) NOPASSWD: ALL kangll ALL=(ALL) ALL ## Allows members of the users group to mount and unmount the ## cdrom as root # %users ALL=/sbin/mount /mnt/cdrom, /sbin/umount /mnt/cdrom ## Allows members of the users group to shutdown this system # %users localhost=/sbin/shutdown -h now ## Read drop-in files from /etc/sudoers.d (the # here does not mean a comment) #includedir /etc/sudoers.d 解释下每一行的格式: 第一个表示用户名,如 root 、kangll 等; 接下来等号左边的 ALL 表示允许从任何主机登录当前的用户账户; 等号右边的 ALL 表示:这一行行首用户可以切换到系统中任何一个其它用户; 行尾的 ALL 表示:当前行首的用户,能以 root 用户的身份下达什么命令,ALL 表示可以下达任何命令。 我们还注意到 kangll 对应的那一行有个 NOPASSWD 关键字,这就是表明 kangll 这个用户在请求 sudo 时不需要输入密码,到这里就解释了前面的问题。 同时这个文件里并没有 winner 对应的行,这也就解释了为什么 winner 无法使用 sudo 命令。 接下来,我们尝试将 winner 添加到 /etc/sudoers 文件中,使 winner 也能使用 sudo 命令。我们在最后一行添加: # winner 使用 sudo 不需要提供密码 ,且后面对操作目录做了限制 winner ALL=(ALL) NOPASSWD:ALL,!/usr/bin/su,!/usr/bin/chattr,!/usr/sbin/init,!/usr/sbin/reboot,!/sbin/reboot,!/usr/sbin/shutdown,!/usr/bin/passwd,!/bin/bash,!/bin/sh winner表示的是开发用户 NOPASSWD:ALL表示sudo命令可以免密使用 !/usr/bin/su表示禁止使用sudo su - !/usr/sbin/iptables表示禁止使用reboot !/bin/chattr表示禁止使用chattr 如上特别注意命令需要加上绝对路径。 接下来我们再在 winner 账户下执行 sudo : 可以看到,现在已经可以使用 sudo 了,限制的命令也起作用了。 四、二者的差异对比 我们可以看到: 使用 su - ,提供 root 账户的密码,可以切换到 root 用户; 使用 sudo su - ,提供当前用户的密码,也可以切换到 root 用户 两种方式的差异也显而易见:如果我们的 Linux 系统有很多用户需要使用的话,前者要求所有用户都知道 root 用户的密码,这显然是非常危险的;后者是不需要暴露 root 账户密码的,用户只需要输入自己的账户密码就可以,而且哪些用户可以切换到 root,这完全是受 root 控制的(root 通过设置 /etc/sudoers 实现的),这样系统就安全很多了。 ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/qq_35995514/article/details/143199093
-
1 epoll的作用和定位 之前提过的多路转接方案select和poll 都有致命缺点:底层都是暴力的遍历,效率不高! 对此,诞生出了epoll这个更好的方案! 按照 man 手册的说法: 是为处理大批量句柄而作了改进的 poll。它是在 2.5.44 内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)。它几乎具备了之前所说的一切优点, 被公认为 Linux2.6 下性能最好的多路 I/O 就绪通知方法. 2 epoll 的接口 epoll的相关接口有三个: epoll_create EPOLL_CREATE(2) Linux Programmer's Manual EPOLL_CREATE(2) NAME epoll_create, epoll_create1 - open an epoll file descriptor SYNOPSIS #include <sys/epoll.h> int epoll_create(int size); int epoll_create1(int flags); epoll_create接口只有一个参数,其功能是在内核创建一个epoll模型!这个模型我们后面详细谈。这个size我们只有设置为一个大于零的数即可。创建成功之后会给我们返回一个文件描述符,现在我们还理解不了,后续讲解。 epoll_ctl EPOLL_CTL(2) Linux Programmer's Manual EPOLL_CTL(2) NAME epoll_ctl - control interface for an epoll file descriptor SYNOPSIS #include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); DESCRIPTION This system call is used to add, modify, or remove entries in the interest list of the epoll(7) instance referred to by the file descriptor epfd. It requests that the operation op be performed for the target file descriptor, fd. epoll_ctl有四个参数: int epfd:这个就是通过epoll_create获得的文件描述符 int op:这个是操作选项,我们这个函数共用三种选项:EPOLL_CTL_ADD增加 EPOLL_CTL_MOD 修改EPOLL_CTL_DEL删除。 int fd:对这个文件描述符进行操作。 struct epoll_event * event:这时一个结构体,类似struct pollfd,但内部更加复杂: typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; 其中的events位图就可以设置读事件,写事件…注意这里没有返回事件! epoll_wait EPOLL_WAIT(2) Linux Programmer's Manual EPOLL_WAIT(2) NAME epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor SYNOPSIS #include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask); DESCRIPTION The epoll_wait() system call waits for events on the epoll(7) instance referred to by the file descriptor epfd. The buffer pointed to by events is used to return in‐ formation from the ready list about file descriptors in the interest list that have some events available. Up to maxevents are returned by epoll_wait(). The max‐ events argument must be greater than zero. epoll_wait有四个参数: int epfd:这个就是通过epoll_create获得的文件描述符。 *struct epoll_event events :这是一个数组,向内核输入一个缓冲区,想让内核提供这个数组将就绪事件返回来! ** int maxevents**:数组的元素个数。 int timeout:等价于poll接口的timeout,以毫秒为单位! 返回值等价于poll! 总而言之:epoll将传入与传出分成了两个接口来进行! 3 epoll工作原理 对于epoll更深入的理解我们需要从底层进行讲解: 数据到达主机时,数据首先会到达物理层,那么操作系统如何知道网卡里有数据呢?通过硬件中断!通过针脚中断,就可以通知操作系统!从而数据链路层从网络层读取数据! 当我们使用epoll时,系统内部会建立一个红黑树,这个红黑树创建时是空树。红黑树的节点字段主要存在:文件描述符fd , 事件位图 events ,左右指针,节点颜色...,这个树标识了用户想让OS关心的文件操作符fd以及其对应事件!epoll_ctl接口中的op就是对应的增添修改删除红黑树节点!注意:这个红黑树的键值是fd! 其中还有一个就绪队列,这是一个双向链表,每个节点与红黑树中的节点类似。当网卡中有数据了,网卡通过硬件中断把数据交给网络协议栈。OS可以知道每个文件描述符对应的输入输出缓冲区状态,当回红黑树节点对应fd的EPOLLIN事件等就绪,那么OS就把这个fd的事件放入就绪队列。这个就绪队列就是储存就绪事件的数据结构,当用户调用epoll_wait时,就通过就绪队列进行检测哪个fd对应事件就绪了!将事件依次严格按照顺序放入struct epoll_event *events数组中! 这个检测就绪事件的算法的时间复杂度就是O(1)!只需要判断就绪队列是否为空就可以!而将就绪事件获取的时间复杂度是O(n)! 这就是epoll模型!!! 而这个epoll模型是可以打开多个的,就和打开多个文件一样。当我们打开多个epoll模型时,那么操作系统如何管理这些epoll模型呢? 在内核中有一个eventpoll,这个是描述epoll模型的结构体,其中就有rbr红黑树与rdllist就绪队列。那为什么创建epoll模型之后会返回一个文件描述符呢? 在内核中有无数个task_struct进程结构体,每个进程都有一张文件描述符表struct files_struct,这个表的元素就指向文件结构体struct file。文件结构体中就有一个指针指向epoll模型。那么在进程中想要找到epoll模型就可以通过文件描述符表找到epoll模型! 我们来谈一个十分巧妙的设计。在epoll模型中,存在红黑树和就绪队列。每个节点都有对应的文件描述符。在之前所学的数据结构中,我们每个数据结构的节点都是独属于自身的,比如二叉树的节点不可能是链表的节点。 但是在epoll模型中,一个节点是可以属于多个数据结构的!我们来看是如何实现的: 首先,有这样一个链表节点listnode,其中只包含左右指针。 然后在task_struct中,就可以存在listnode link,那么每一个task_struct就可以通过这个link进行连接起来的。 但是,这个指向的只是下一个task_struct结构体中的link,那么怎么才能访问task_struct全部的数据呢? 可以先计算这个link在task_struct的偏移量,通过将0地址强制类型转换,得到里面link的地址,就知道了偏移量!然后通过task_struct中link里的指针减去偏移量,我们就得到了task_struct的起始地址,再进行类型转换我们就得到了task_struct! 同样的,task_struct还可以存在二叉树节点link2 , 队列节点link3,就都可以通过这种方式进行链接,并且是一个节点属于了多个数据结构中!!! 这是十分巧妙的设计!!!而epoll模型中的epitem结构体就是这样设计的!一个节点既属于红黑树,也属于就绪队列! 其中epitem还有一个status变量,表示其是否被激活。可以判断是否在红黑树或者就绪队列中! 下面我们开始编写v1版本的epollserver 4 实现epollserverV1 下面我们来实现epollserver: 成员变量需要以下: 端口号_port :用于创建listen套接字 套接字socket :_listensock监听套接字,使用TCP进行通信。 文件描述符_epfd :epoll模型的文件操作符,是使用epoll系列接口的必要参数。 epoll_event revs[] 数组:从epoll模型中获取就绪事件的结构体数组。 根据成员变量,进行构造,创建套接字,创建epoll模型。 初始化函数中,建立struct epoll_event ev设置其中的 fd 与events位图;先将_listensock套接字fd添加到epoll中 通过epoll_ctl进行ADD操作。 #pragma once #include <string> #include <iostream> #include <memory> #include <sys/epoll.h> #include "Log.hpp" #include "Socket.hpp" using namespace log_ns; using namespace socket_ns; class EpollServer { private: const static int gnum = 1024; const static int size = 128; public: EpollServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>()) { // 建立监听套接字 _listensock->BuildListenSocket(port); // 建立epoll模型 _epollfd = ::epoll_create(size); if (_epollfd < 0) { // 创建失败 LOG(FATAL, "epoll_create failed!\n"); exit(1); } } void InitServer() { // 将监听套接字放入epoll模型 struct epoll_event ev; ev.data.fd = _listensock->GetSockfd(); ev.events = EPOLLIN; // 放入 int n = ::epoll_ctl(_epollfd, EPOLL_CTL_ADD, _listensock->GetSockfd(), &ev); // 根据返回值判断 if (n < 0) { // 发生错误 LOG(FATAL, "epoll_ctl failed ,errno :%d", errno); exit(1); } } void Accepter() { } void HandlerIO(int fd) { // 普通fdIO 就绪 } void HandlerEvent(int n) { } void Loop() { } ~EpollServer() { // 关闭epoll模型 if (_epollfd > 0) close(_epollfd); // 关闭监听套接字 _listensock->Close(); } private: // 端口号 uint16_t _port; // 套接字 std::unique_ptr<Socket> _listensock; // epoll模型描述符 int _epollfd; // 文件描述符 struct epoll_event revs[gnum]; }; Loop 循环函数,设置timeout 调用epoll_wait接口进行等待事件就绪 ,将就绪的事件放入到revs数组中。根据返回值进行判断结果: void Loop() { int timeout = 2000; while (true) { // 进行等待 int n = ::epoll_wait(_epollfd, revs, gnum, timeout); // 判断结果 switch (n) { case 0: LOG(INFO, "epoll timeout...\n"); break; case -1: LOG(ERROR, "epoll error\n"); break; default: LOG(INFO, "haved event happened! , n :%d\n", n); // 处理事件 HandlerEvent(n); break; } } } HandlerEvent处理事件,将数组中的n个事件全部处理遍历一遍, 根据就绪的文件描述符种类进行区分判断 (设计一个简单的接口可以通过事件级返回事件种类);读事件就绪 我们进行处理 _listensock套接字事件获取连接 Accepter 将新的fd加入到epoll模型 打印客户端信息 普通fd 事件HandlerIO 进行读取recv ;读取失败的话要从epoll删除后再close ,处理后Send回去。 std::string PrintEvent(uint32_t revents) { std::string ret; if (revents & EPOLLIN) ret += "EPOLLIN"; if (revents & EPOLLOUT) ret += "| EPOLLOUT"; return ret; } void Accepter() { // 获取_listensock的新fd InetAddr addr; int sockfd = _listensock->Accepter(&addr); if (sockfd < 0) { LOG(ERROR, "Accepter error\n"); exit(1); } // 成功获取连接 LOG(INFO, "成功获取连接 ,客户端: %s\n", addr.AddrStr().c_str()); // 将连接添加到epoll模型中 struct epoll_event ev; ev.data.fd = sockfd; ev.events = EPOLLIN; int n = ::epoll_ctl(_epollfd, EPOLL_CTL_ADD, sockfd, &ev); // 根据返回值判断 if (n < 0) { // 发生错误 LOG(FATAL, "epoll_ctl failed ,errno :%d", errno); exit(1); } } void HandlerIO(int fd) { // 普通fdIO 就绪 char buffer[4096]; int n = ::recv(fd, buffer, sizeof(buffer), 0); if (n > 0) { // 读取到了数据 buffer[n] = 0; std::string echo_str = "[client say]#"; echo_str += buffer; std::cout << echo_str << std::endl; // 返回一个报文 std::string content = "<html><body><h1>hello bite</h1></body></html>
-
1. ls 指令 语法: ls [选项][目录或文件] 功能:对于目录,该命令列出该目录下的所有子目录与文件。对于文件,将列出文件名以及其他信息 ls -l 命令与 ll 命令所发出的指令作用相同,因为 ll 指令是系统用 ls -l 指令的默认使用 'alias' 指令取的一个别名 常用选项: -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. pwd指令 语法: pwd 功能:显示用户当前所在的目录 注意:在Windows操作系统下是以 '\'作为路径分隔符,在Linux操作系统下是以'/'作为路径分隔符 3. cd 指令 Linux系统中,磁盘上的文件和目录被组成一棵目录树,每个节点都是目录或文件 语法:cd 目录名 功能:改变工作目录。将当前工作目录改变到指定的目录下 举例: cd . : 返回当前目录 cd .. : 返回上级目录 cd /home/litao/linux/ : 绝对路径 cd ../day02/ : 相对路径 cd ~:进入用户家目 cd -:返回最近访问目录 root@hcss-ecs-78b3:/# cd /root root@hcss-ecs-78b3:~# pwd /root root@hcss-ecs-78b3:~# cd . root@hcss-ecs-78b3:~# pwd /root root@hcss-ecs-78b3:~# cd .. root@hcss-ecs-78b3:/# pwd / root@hcss-ecs-78b3:/# 切换文件路径,格式为 cd 空格 目标路径 任何一个目录包括根目录,系统都会默认自带 . 和 .. 目录,第一个'/'代表Linux中的根目录,没有上级目录返回,这里我们要注意Linux的文件结构是一个以 '/' 为根目录的多叉树 4. touch指令 语法:touch [选项]... 文件... 功能:touch命令参数可更改文档或目录的日期时间,包括存取时间和更改时间,或者新建一个不存在的文件 常用选项: -a 或--time=atime或--time=access或--time=use只更改存取时间 -c 或--no-create 不建立任何文档 -d 使用指定的日期时间,而非现在的时间 -f 此参数将忽略不予处理,仅负责解决BSD版本touch指令的兼容性问题 -m 或--time=mtime或--time=modify 只更改变动时间 -r 把指定文档或目录的日期时间,统统设成和参考文档或目录的日期时间相同 -t 使用指定的日期时间,而非现在的时间 root@hcss-ecs-78b3:~# touch hello.txt root@hcss-ecs-78b3:~# pwd /root root@hcss-ecs-78b3:~# ls hello.txt 5. mkdir指令 语法:mkdir [选项] kiana 功能:在当前目录下创建一个名为 “kiana”的目录 常用选项: -p, kiana 可以是一个路径名称 此时若路径中的某些目录尚不存在,加上此选项后,系统将自动建立好那些尚不存在的目录,即一次可以建立多个目录 root@hcss-ecs-78b3:~# mkdir kiana root@hcss-ecs-78b3:~# ll total 116 drwx------ 12 root root 4096 Oct 15 11:03 ./ drwxr-xr-x 24 root root 4096 Oct 15 09:32 ../ drwx------ 3 root root 4096 Jul 22 2023 .ansible/ drwxr-xr-x 2 root root 4096 Jul 22 2023 .ansible_async/ -rw-r--r-- 1 root root 298 Oct 15 10:15 .bash_history -rw-r--r-- 1 root root 3144 Jul 22 2023 .bashrc drwx------ 2 root root 4096 Feb 10 2023 .cache/ drwxr-xr-x 7 root root 4096 Jul 22 2023 docker-library/ -rw-r--r-- 1 root root 21926 Jul 22 2023 get-docker.sh -rw-r--r-- 1 root root 1326 Jul 21 2023 githubclone.sh -rw------- 1 root root 0 Feb 10 2023 .history -rwxr-xr-x 1 root root 15353 Jul 22 2023 HSSInstall* drwxr-xr-x 2 root root 4096 Oct 15 11:03 kiana/ -rw------- 1 root root 20 Oct 15 10:11 .lesshst drwxr-xr-x 8 root root 4096 Jul 22 2023 library/ drwxr-xr-x 3 root root 4096 Oct 15 09:29 .local/ -rw-r--r-- 1 root root 161 Jul 9 2019 .profile drwx------ 4 root root 4096 Jul 22 2023 snap/ drwx------ 2 root root 4096 Oct 11 19:41 .ssh/ drwxr-xr-x 9 root root 4096 Jul 22 2023 stackhub/ -rw-r--r-- 1 root root 173 Jul 22 2023 .wget-hsts -rw------- 1 root root 177 Oct 15 11:03 .Xauthority root@hcss-ecs-78b3:~# mkdir命令创建连续的文件 root@hcss-ecs-78b3:~# mkdir -p a/b/c root@hcss-ecs-78b3:~# ll total 120 drwx------ 13 root root 4096 Oct 15 11:05 ./ drwxr-xr-x 24 root root 4096 Oct 15 09:32 ../ drwxr-xr-x 3 root root 4096 Oct 15 11:05 a/ drwx------ 3 root root 4096 Jul 22 2023 .ansible/ drwxr-xr-x 2 root root 4096 Jul 22 2023 .ansible_async/ -rw-r--r-- 1 root root 298 Oct 15 10:15 .bash_history -rw-r--r-- 1 root root 3144 Jul 22 2023 .bashrc drwx------ 2 root root 4096 Feb 10 2023 .cache/ drwxr-xr-x 7 root root 4096 Jul 22 2023 docker-library/ -rw-r--r-- 1 root root 21926 Jul 22 2023 get-docker.sh -rw-r--r-- 1 root root 1326 Jul 21 2023 githubclone.sh -rw------- 1 root root 0 Feb 10 2023 .history -rwxr-xr-x 1 root root 15353 Jul 22 2023 HSSInstall* drwxr-xr-x 2 root root 4096 Oct 15 11:03 kiana/ -rw------- 1 root root 20 Oct 15 10:11 .lesshst drwxr-xr-x 8 root root 4096 Jul 22 2023 library/ drwxr-xr-x 3 root root 4096 Oct 15 09:29 .local/ -rw-r--r-- 1 root root 161 Jul 9 2019 .profile drwx------ 4 root root 4096 Jul 22 2023 snap/ drwx------ 2 root root 4096 Oct 11 19:41 .ssh/ drwxr-xr-x 9 root root 4096 Jul 22 2023 stackhub/ -rw-r--r-- 1 root root 173 Jul 22 2023 .wget-hsts -rw------- 1 root root 177 Oct 15 11:03 .Xauthority root@hcss-ecs-78b3:~# 使用tree命令展示目录下树形结构的文件 root@hcss-ecs-78b3:~# tree kiana kiana 0 directories, 0 files root@hcss-ecs-78b3:~# tree a a └── b └── c 2 directories, 0 files root@hcss-ecs-78b3:~# 6. which指令 查看命令所在的路径,命令其实就是可执行文件,也就相当于我们平常写的C/C++程序 root@hcss-ecs-78b3:~# root@hcss-ecs-78b3:~# which tree /usr/bin/tree root@hcss-ecs-78b3:~# which pwd /usr/bin/pwd root@hcss-ecs-78b3:~# which ls /usr/bin/ls root@hcss-ecs-78b3:~# 7. alias指令 给命令起别名,比如 ll 就是 ls -l 的别名,二者执行后结果是一样的,当然这里alias起的别名在重启程序后就会自动失效 如: 给pwd命令取别名叫做kiana root@hcss-ecs-78b3:alias kiana=pwd 8. rm指令 rm命令可以同时删除文件或目录 语法:rm [-f-i-r-v][dirName/dir] 适用对象:所有使用者 功能:删除文件或目录 常用选项: -f 即使文件属性为只读(即写保护),亦直接删除 -i 删除前逐一询问确认 -r 删除目录及其下所有文件 root@hcss-ecs-78b3:~# ll total 120 drwx------ 13 root root 4096 Oct 15 11:05 ./ drwxr-xr-x 24 root root 4096 Oct 15 09:32 ../ drwxr-xr-x 3 root root 4096 Oct 15 11:05 a/ drwx------ 3 root root 4096 Jul 22 2023 .ansible/ drwxr-xr-x 2 root root 4096 Jul 22 2023 .ansible_async/ -rw-r--r-- 1 root root 298 Oct 15 10:15 .bash_history -rw-r--r-- 1 root root 3144 Jul 22 2023 .bashrc drwx------ 2 root root 4096 Feb 10 2023 .cache/ drwxr-xr-x 7 root root 4096 Jul 22 2023 docker-library/ -rw-r--r-- 1 root root 21926 Jul 22 2023 get-docker.sh -rw-r--r-- 1 root root 1326 Jul 21 2023 githubclone.sh -rw------- 1 root root 0 Feb 10 2023 .history -rwxr-xr-x 1 root root 15353 Jul 22 2023 HSSInstall* drwxr-xr-x 2 root root 4096 Oct 15 11:03 kiana/ -rw------- 1 root root 20 Oct 15 10:11 .lesshst drwxr-xr-x 8 root root 4096 Jul 22 2023 library/ drwxr-xr-x 3 root root 4096 Oct 15 09:29 .local/ -rw-r--r-- 1 root root 161 Jul 9 2019 .profile drwx------ 4 root root 4096 Jul 22 2023 snap/ drwx------ 2 root root 4096 Oct 11 19:41 .ssh/ drwxr-xr-x 9 root root 4096 Jul 22 2023 stackhub/ -rw-r--r-- 1 root root 173 Jul 22 2023 .wget-hsts -rw------- 1 root root 177 Oct 15 11:03 .Xauthority root@hcss-ecs-78b3:~# rm -rf kiana root@hcss-ecs-78b3:~# ll total 116 drwx------ 12 root root 4096 Oct 15 11:22 ./ drwxr-xr-x 24 root root 4096 Oct 15 09:32 ../ drwxr-xr-x 3 root root 4096 Oct 15 11:05 a/ drwx------ 3 root root 4096 Jul 22 2023 .ansible/ drwxr-xr-x 2 root root 4096 Jul 22 2023 .ansible_async/ -rw-r--r-- 1 root root 298 Oct 15 10:15 .bash_history -rw-r--r-- 1 root root 3144 Jul 22 2023 .bashrc drwx------ 2 root root 4096 Feb 10 2023 .cache/ drwxr-xr-x 7 root root 4096 Jul 22 2023 docker-library/ -rw-r--r-- 1 root root 21926 Jul 22 2023 get-docker.sh -rw-r--r-- 1 root root 1326 Jul 21 2023 githubclone.sh -rw------- 1 root root 0 Feb 10 2023 .history -rwxr-xr-x 1 root root 15353 Jul 22 2023 HSSInstall* -rw------- 1 root root 20 Oct 15 10:11 .lesshst drwxr-xr-x 8 root root 4096 Jul 22 2023 library/ drwxr-xr-x 3 root root 4096 Oct 15 09:29 .local/ -rw-r--r-- 1 root root 161 Jul 9 2019 .profile drwx------ 4 root root 4096 Jul 22 2023 snap/ drwx------ 2 root root 4096 Oct 11 19:41 .ssh/ drwxr-xr-x 9 root root 4096 Jul 22 2023 stackhub/ -rw-r--r-- 1 root root 173 Jul 22 2023 .wget-hsts -rw------- 1 root root 177 Oct 15 11:03 .Xauthority root@hcss-ecs-78b3: 9. man指令 查找系统手册返回所查找命令的实质 语法: man [选项] 命令 常用选项: -k 根据关键字搜索联机帮助 num 只在第num章节找 -a 将所有章节的都显示出来,比如 man printf 它缺省从第一章开始搜索,知道就停止,用a选项,当按下q退出,他会继续往后面搜索,直到所有章节都搜索完毕 解释一下,面手册分为8章: 1 是普通的命令 2 是系统调用,如open,write之类的(通过这个,至少可以很方便的查到调用这个函数,需要加什么头文件) 3 是库函数,如printf,fread4是特殊文件,也就是/dev下的各种设备文件 5 是指文件的格式,比如passwd, 就会说明这个文件中各个字段的含义 6 是给游戏留的,由各个游戏自己定义 7 是附件还有一些变量,比如向environ这种全局变量在这里就有说明 8 是系统管理用的命令,这些命令只能由root使用,如ifconfig root@hcss-ecs-78b3:~# man man root@hcss-ecs-78b3:~# man pwd root@hcss-ecs-78b3:~# man ll 10. cp指令 语法:cp [选项] 源文件或目录 目标文件或目录 功能: 复制文件或目录 说明: cp指令用于复制文件或目录,如同时指定两个以上的文件或目录,且最后的目的地是一个已经存在的目录,则它会把前面指定的所有文件或目录复制到此目录中。若同时指定多个文件或目录,而最后的目的地并非一个已存在的目录,则会出现错误信息 常用选项: -f 或 --force 强行复制文件或目录, 不论目的文件或目录是否已经存在 -i 或 --interactive 覆盖文件之前先询问用户 -r递归处理,将指定目录下的文件与子目录一并处理。若源文件或目录的形态,不属于目录或符号链接,则一律视为普通文件处理 -R 或 --recursive递归处理,将指定目录下的文件及子目录一并处理 drwx------ 12 root root 4096 Oct 15 11:22 ./ drwxr-xr-x 24 root root 4096 Oct 15 09:32 ../ drwxr-xr-x 3 root root 4096 Oct 15 11:05 a/ drwx------ 3 root root 4096 Jul 22 2023 .ansible/ drwxr-xr-x 2 root root 4096 Jul 22 2023 .ansible_async/ -rw-r--r-- 1 root root 298 Oct 15 10:15 .bash_history -rw-r--r-- 1 root root 3144 Jul 22 2023 .bashrc drwx------ 2 root root 4096 Feb 10 2023 .cache/ drwxr-xr-x 7 root root 4096 Jul 22 2023 docker-library/ -rw-r--r-- 1 root root 21926 Jul 22 2023 get-docker.sh -rw-r--r-- 1 root root 1326 Jul 21 2023 githubclone.sh -rw------- 1 root root 0 Feb 10 2023 .history -rwxr-xr-x 1 root root 15353 Jul 22 2023 HSSInstall* -rw------- 1 root root 20 Oct 15 10:11 .lesshst drwxr-xr-x 8 root root 4096 Jul 22 2023 library/ drwxr-xr-x 3 root root 4096 Oct 15 09:29 .local/ -rw-r--r-- 1 root root 161 Jul 9 2019 .profile drwx------ 4 root root 4096 Jul 22 2023 snap/ drwx------ 2 root root 4096 Oct 11 19:41 .ssh/ drwxr-xr-x 9 root root 4096 Jul 22 2023 stackhub/ -rw-r--r-- 1 root root 173 Jul 22 2023 .wget-hsts -rw------- 1 root root 177 Oct 15 11:03 .Xauthority root@hcss-ecs-78b3:~# root@hcss-ecs-78b3:~# man man root@hcss-ecs-78b3:~# root@hcss-ecs-78b3:~# cp .wget-hsts get-docker.sh root@hcss-ecs-78b3:~# ll total 96 drwx------ 12 root root 4096 Oct 15 11:27 ./ drwxr-xr-x 24 root root 4096 Oct 15 09:32 ../ drwxr-xr-x 3 root root 4096 Oct 15 11:05 a/ drwx------ 3 root root 4096 Jul 22 2023 .ansible/ drwxr-xr-x 2 root root 4096 Jul 22 2023 .ansible_async/ -rw-r--r-- 1 root root 298 Oct 15 10:15 .bash_history -rw-r--r-- 1 root root 3144 Jul 22 2023 .bashrc drwx------ 2 root root 4096 Feb 10 2023 .cache/ drwxr-xr-x 7 root root 4096 Jul 22 2023 docker-library/ -rw-r--r-- 1 root root 173 Oct 15 11:32 get-docker.sh -rw-r--r-- 1 root root 1326 Jul 21 2023 githubclone.sh -rw------- 1 root root 0 Feb 10 2023 .history -rwxr-xr-x 1 root root 15353 Jul 22 2023 HSSInstall* -rw------- 1 root root 20 Oct 15 11:27 .lesshst drwxr-xr-x 8 root root 4096 Jul 22 2023 library/ drwxr-xr-x 3 root root 4096 Oct 15 09:29 .local/ -rw-r--r-- 1 root root 161 Jul 9 2019 .profile drwx------ 4 root root 4096 Jul 22 2023 snap/ drwx------ 2 root root 4096 Oct 11 19:41 .ssh/ drwxr-xr-x 9 root root 4096 Jul 22 2023 stackhub/ -rw-r--r-- 1 root root 173 Jul 22 2023 .wget-hsts -rw------- 1 root root 177 Oct 15 11:03 .Xauthority root@hcss-ecs-78b3:~# 11. mv指令 mv命令是move的缩写,可以用来移动文件或者将文件改名(move (rename) files),是Linux系统下常用的命令,经常用来备份文件或者目录 语法: mv [选项] 源文件或目录 目标文件或目录 功能: 1. 视mv命令中第二个参数类型的不同(是目标文件还是目标目录),mv命令将文件重命名或将其移至一个新的目录中 2. 当第二个参数类型是文件时,mv命令完成文件重命名,此时,源文件只能有一个(也可以是源目录名),它将所给的源文件或目录重命名为给定的目标文件名 3. 当第二个参数是已存在的目录名称时,源文件或目录参数可以有多个,mv命令将各参数指定的源文件均移至目标目录中 常用选项: -f :force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖 -i :若目标文件 (destination) 已经存在时,就会询问是否覆盖! 理论杂谈 12. cat指令 语法:cat [选项][文件] 功能: 查看目标文件的内容 常用选项: -b 对非空输出行编号 -n 对输出的所有行编号 -s 不输出多行空行 使用nano记事本写入数据到1.c文件中 使用cat打印文件中的内容 cat正向打印,tac反向打印 echo只打印后面跟着的字符 重定向操作 1.输出重定向 > 语法: echo "指定字符串" > 目标写入文件 特点: 1.当目标写入文件不存在就直接自动创建一个 2.每一次写入操作都会替代原来文件中的内容,先清空后写入 所以在没有字符串时可以使用该输出重定向来只新建文件或者清空文件 2.追加重定向 >> 语法:echo "字符串" >> 目标文件 特点: 1.写入文件时不会清空原文件内容而是直接添加进目标写入文件中 3. 输入重定向 < cat 默认从键盘读取输入内容,而输入重定向就是改变输入路径,可以是一个文件直接被cat读取后显示在显示屏上 13. more指令 语法:more [选项][文件] 功能:more命令,功能类似 cat 常用选项: -n 对输出的所有行编号 q 退出more [atong@LiWenTong ~]$ ls -l / | more total 162 drwxr-xr-x 2 root root 4096 Apr 25 05:39 bin drwxr-xr-x 4 root root 1024 Apr 25 04:11boot drwxr-xr-x 9 root root 3820 May 4 23:20 dev drwxr-xr-x 84 root root 4096 May 5 00:37 etc 14. less指令 less 工具也是对文件或其它输出进行分页显示的工具,应该说是linux正统查看文件内容的工具,功能极其强大 less 的用法比起 more 更加的有弹性。在 more 的时候,我们并没有办法向前面翻, 只能往后面看 但若使用了 less 时,就可以使用 [pageup][pagedown] 等按键的功能来往前往后翻看文件,更容易用来查看一个文件的内容! 除此之外,在 less 里头可以拥有更多的搜索功能,不止可以向下搜,也可以向上搜 语法: less [参数] 文件 功能: less与more类似,但使用less可以随意浏览文件,而more仅能向前移动,却不能向后移动,而且less在查看之前不会加载整个文件 -i 忽略搜索时的大小写 -N 显示每行的行号 /字符串:向下搜索“字符串”的功能 ?字符串:向上搜索“字符串”的功能 n:重复前一个搜索(与 / 或 ? 有关) N:反向重复前一个搜索(与 / 或 ? 有关) q:quit 15. head指令 head 与 tail 就像它的名字一样的浅显易懂,它是用来显示开头或结尾某个数量的文字区块,head 用来显示档案的开头至标准输出中,而 tail 想当然尔就是看档案的结尾 语法: head [参数]... [文件]... 功能: head 用来显示档案的开头至标准输出中,默认head命令打印其相应文件的开头10行 选项: -n<行数> 显示的行数 16. tail指令 tail 命令从指定点开始将文件写到标准输出.使用tail命令的-f选项可以方便的查阅正在改变的日志文件,tail -f filename会把filename里最尾部的内容显示在屏幕上,并且不但刷新,使你看到最新的文件内容 语法: tail[必要参数][选择参数][文件] 功能: 用于显示指定文件末尾内容,不指定文件时,作为输入信息进行处理。常用查看日志文件 选项: -f 循环读取 -n<行数> 显示行数 扩展:管道 | 可以类比条件概率中的"|",这里指在"|"前面的操作指令完成后再进行"|"之后的操作指令 17. 时间相关的指令 17.1. date显示 date 指定格式显示时间: date +%Y:%m:%d date 用法:date [OPTION]... [+FORMAT] 1.在显示方面,使用者可以设定欲显示的格式,格式设定为一个加号后接数个标记,其中常用的标记列表如下 %H : 小时(00..23) %M : 分钟(00..59) %S : 秒(00..61) %X : 相当于 %H:%M:%S %d : 日 (01..31) %m : 月份 (01..12) %Y : 完整年份 (0000..9999) %F : 相当于 %Y-%m-%d 2.在设定时间方面 date -s //设置当前时间,只有root权限才能设置,其他只能查看 date -s 20080523 //设置成20080523,这样会把具体时间设置成空00:00:00 date -s 01:01:01 //设置具体时间,不会对日期做更改 date -s “01:01:01 2008-05-23″ //这样可以设置全部时间 date -s “01:01:01 20080523″ //这样可以设置全部时间 date -s “2008-05-23 01:01:01″ //这样可以设置全部时间 date -s “20080523 01:01:01″ //这样可以设置全部时间 3.时间戳 时间->时间戳:date +%s 时间戳->时间:date -d@1508749502 Unix时间戳(英文为Unix epoch, Unix time, POSIX time 或 Unix timestamp)是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒 18. Cal指令 cal命令可以用来显示公历(阳历)日历。公历是现在国际通用的历法,又称格列历,通称阳历。“阳历”又名“太阳历”,系以地球绕行太阳一周为一年,为西方各国所通用,故又名“西历” 命令格式: cal [参数][月份][年份] 功能: 用于查看日历等时间信息,如只有一个参数,则表示年份(1-9999),如有两个参数,则表示月份和年份 常用选项: -3 显示系统前一个月,当前月,下一个月的月历 -j 显示在当年中的第几天(一年日期按天算,从1月1号算起,默认显示当前月在一年中的天数) -y 显示当前年份的日历 19. find 查找指令 Linux下find命令在目录结构中搜索文件,并执行指定的操作 Linux下find命令提供了相当多的查找条件,功能很强大。由于find具有强大的功能,所以它的选项也很 多,其中大部分选项都值得我们花时间来了解一下 即使系统中含有网络文件系统( NFS),find命令在该文件系统中同样有效,只你具有相应的权限 在运行一个非常消耗资源的find命令时,很多人都倾向于把它放在后台执行,因为遍历一个大的文件系 统可能会花费很长的时间(这里是指30G字节以上的文件系统) 语法: find pathname -options 功能: 用于在文件树种查找文件,并作出相应的处理(可能访问磁盘) 常用选项: -name 按照文件名查找文件 20. grep指令 语法: grep [选项] 搜寻字符串 文件 功能: 在文件中搜索字符串,将找到的行打印出来 常用选项: -i :忽略大小写的不同,所以大小写视为相同 -n :顺便输出行号 -v :反向选择,亦即显示出没有 '搜寻字符串' 内容的那一行 21 .zip/unzip指令 语法: zip 压缩文件.zip 目录或文件 功能: 将目录或文件压缩成zip格式 常用选项: -r 递 归处理,将指定目录下的所有文件和子目录一并处理 举例: 将test2目录压缩:zip test2.zip test2/* 解压到指定目录tmp:unzip test2.zip -d /tmp zip --version :确认系统有没有装zip 如图,我们压缩了一个压缩包day4.zip,将这个压缩包传递给other,但是当我们打开之后发现day4.zip下的文件并没有一起传送过去,只传了一个空文件夹day4.zip,解决方式就是要加上一个 ‘-r’ zip -r day4.zip day4 解压到指定目录下需要添加 -d 将linux中的压缩包传给windows系统可以使用 sz 指令直接回车就会出现以下界面 选择桌面传完之后就能在桌面上看到文件了 将windows中的压缩包传给linux系统可以使用 rz 指令直接回车就会出现以下界面 22. tar指令:打包/解包,不打开它,直接看内容 tar [-cxtzjvf] 文件与目录 .... 参数: -c :建立一个压缩文件的参数指令(create 的意思) -x :解开一个压缩文件的参数指令! -t :查看 tarfile 里面的文件! -z :是否同时具有 gzip 的属性?亦即是否需要用 gzip 压缩? -j :是否同时具有 bzip2 的属性?亦即是否需要用 bzip2 压缩? -v :压缩的过程中显示文件!这个常用,但不建议用在背景执行过程! -f :使用档名,请留意,在 f 之后要立即接档名喔!不要再加参数! -C : 解压到指定目录 Linux和Linux之间的压缩包互相传输 23. uname指令 功能: uname用来获取电脑和操作系统的相关信息 语法:uname [选项] 补充说明:uname可显示linux主机所用的操作系统的版本、硬件的名称等基本信息 常用选项: -a或–all 详细输出所有信息,依次为内核名称,主机名,内核版本号,内核版本,硬件名,处理器类 型,硬件平台类型,操作系统名称 收尾杂谈 显示出100条“hello+数字”的信息命令 cnt=0; while [ $cnt -le 100 ]; do echo "hello $cnt"; let cnt++;done 在当前目录下建立100个原文件命令 cnt=0; while [ $cnt -le 100 ]; do touch code${cnt}.c; let cnt++;done echo加上 | bc 就可以直接算出答案 ctrl + r:搜索历史命令 ctrl + d:退出当前用户 ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/hedhjd/article/details/142934064
-
一、认识硬件——磁盘 1.1 物理构成 磁盘是唯一的一个机械设备,也是一个外设! 以前的老式电脑上装的就是机械磁盘,现在由于用户对使用计算机的速度要求越来越高,现在我们普通人使用的电脑基本上都是用的SSD固态硬盘,SSD固态硬盘并没有像机械磁盘那样的机械运动,读写速度更快,且具有体积小、低功耗、耐用性好、无噪音等特点!且未来还有很大的研究空间!所以在桌面领域几乎取代了机械磁盘! 但是企业级存储更倾向于使用机械硬盘,由于其成本低、容量大的特点更有利于进行大规模的数据存储,所以他其实很难被淘汰! 1、写入磁盘工作原理:二进制序列会通过磁头的充放电(任何硬件都只认识二进制序列,因为本质上是用线连接的),将数据写到盘片上。 2、 一些特点: (1)我们的计算机内部的信息流动是以电子或者光电信号的形式传递(非常快),而磁盘是机械运动相比之下速度很慢!! (2)盘片高速旋转,磁头左右转动,磁头是一面一个且和盘面不接触! (3)磁盘在设计的时候必须保证无尘环境且密封完好,因为有灰尘的话可能会导致盘面刮花造成数据丢失。 (4)内存是掉电易失存储介质,盘片是永久性存储介质。 3、注:磁盘是有寿命的,大公司磁盘快报废的时候并不敢直接把磁盘给丢掉,因为里面存储了大量的用户数据(磁盘密封性好且不易被销毁),所以相关的安全部门对大公司的磁盘销毁工作时有严格的要求的! 1.2 存储构成 磁头的左右摆动——>定位磁道(柱面) 磁头不动的时候,盘片的旋转——>定位扇区 所以磁盘被访问的最基本单位是扇区 ——> 512字节/4KB ——>因此我们可以把磁盘看做由无数个扇区构成的存储介质 ! ——>所以我们要把数据存到磁盘,首先就是定位一个扇区:(1)先找到哪一面(定位磁头)(2)哪一个磁道(3)哪一个扇区 1.3 逻辑抽象 我们把假设把磁带摊开,从逻辑上我们就可以把他看做是线性的!我们就可以用一个数组把扇区组织起来,每个下标对应着一个扇区(我们把逻辑扇区地址叫做LBA地址)! 问题1:那么我们如何通过下标找到我们要写入的扇区呢?? 问题2: 为什么扇区大小不均匀的但是LBA地址是均匀?? ——>因为磁道是从中间向外辐射的,所以里面的磁道有多少个扇区,外面的磁道就有多少个扇区,只不过里面磁道的扇区会小一点(扇区大小不均匀) 但其实可以通过调整密度来变得均匀(里面的01序列稠密一点,外面的稀疏一点) 现在的磁盘其实也是可以做到让外面扇区较大的多存点数据,但是这样的话相关的算法就得更改!! 1.4 回归硬件 所以我们究竟是如何和硬件交互的呢??? 不仅CPU有寄存器,其他外设也有寄存器,包括磁盘!! CPU向磁盘发送IO方向的请求时——> 1、控制寄存器(告诉磁盘是打算读还是打算写) 2、数据寄存器(告诉磁盘要写入哪些数据) 3、地址寄存器(告诉磁盘要写的LBA地址,磁盘自己通过CHS寻址) 4、结果寄存器(CPU获取IO请求是否成功的状态,比如可能空间不足写入失败) 二、文件系统 通过逻辑抽象,我们可以把对扇区的地址抽象成线性的LBA地址,但是具体磁盘有多大呢?已经用了多少扇区?哪些扇区还没被使用?哪些扇区存的是属性?哪些扇区存的是内容?要往哪个扇区去写入??——>诸如以上问题,注定了我们的操作系统必须想办法把磁盘空间组织起来!! 首先是磁盘分区,我们可以把一个磁盘分成多个分区。 而每个分区,都可以用以下的区域来表示 Boot Block: 是文件系统中的一个特殊块,位于文件系统的起始位置。 它包含了引导加载程序(Boot Loader)所需的信息,用于引导操作系统的启动过程。 Block group:每个分区被分成了一个个的block,该block的大小是由格式化确定的,不可被修改,每个block由可以细分为6个区域。 Linux中,文件的属性和内容是分开存储的!! 2.1 inode inode:存储的是单个文件的全部属性(128字节) ,一般而言,一个文件只有一个inode! Linux系统里标记文件的唯一标识用的是inode,且文件的属性中不包含文件的名称!! 通过ls -li 可以观察到文件的inode编号 其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息 2.2 Data Block Data Block:存文件内容的区域,以块的形式呈现,常见的是4KB大小,一般而言一个块只有自己的数据! 一个文件只有一个inode,但如果是个大文件可能会有很多个块。所以在inode结构体内部会有一个block数组,存储的是数据块的块号。 问题:那难道是文件内容需要多少个数据块,block数组就需要有多大么??? ——>并不是的,block数组内部除了一部分是直接索引(存储文件块号,可直接找到文件内容),还有一小部分是二级索引(该块号不会存储文件内容而是继续存储文件的块号) 2.3 Bitmap 我怎么知道,哪些块被使用过,哪些块没被使用过呢??? 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用 inode那么多,我怎么知道要给该文件分配哪个呢?? inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。 问题1: 为什么我们下载一个文件需要很久,但是删除的时候却很快呢?? ——>删除一个文件的时候,并不会把块(文件内容)清空,而仅仅只是把对应比特标志位给修改了,表明该块是空闲可用的。 问题2:如果我们想恢复一个被删除的文件,要怎么办呢?? ——> 如果我们不小心误删的一个文件,如果这个文件很重要,最好的办法就是什么都不做然后找专业的人去恢复(一般来说恢复其实就是得找到该文件的inode编号,比如通过Linux的日志信息找到被删文件的inode,但是具体怎么恢复得依靠一些专业的东西!),因为虽然内容还在,但是他的位图信息表明是可以被使用的,所以你如果乱操作可能会导致文件内容被覆盖。 2.4 GDT和超级块 GDT(Group Descriptor Table):块组描述符,描述块组属性信息(该block组的分配信息和使用情况)。 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个 文件系统结构就被破坏了 (整个分区的相关信息) 因为超级快是在组里面存在的,但是记录了整个分区的信息,所以并不需要每个组都有, 其实理论上来说有一个就够了(太多会影响速度),但是由于他非常重要,一旦损坏就会造成文件系统崩溃,所以一般来说一个分区里面会有个别组有超级快,这样即使一个超级块崩了,也可以通过其他超级块区对文件系统做修正!! 问题:超级块决定了文件系统能否正常运行,块又那么多,操作系统是如何定位超级块的呢?? ——> 魔数,一个随机值,在超级块中的位置是确定的,只要我们操作系统去读该块的规定偏移量位置看看是否存在魔数,就可以确定该块是不是超级块!! 2.5 格式化 每个分区在使用之前,就必须提前将部分文件系统的属性信息提前设置到对应的分区中,方便我们后续使用这个分区或者分组!! 对磁盘做格式化可以帮助我们让磁盘恢复到完全没有被使用过的状态。 三、对目录的理解 3.1 新建和删除文件,系统做了什么? 1、新建文件:系统会在某个路径下去创建,路径帮助我们确定在哪一个分区,读取了超级块后确定了一个block,然后通过查该block的GDT知道该分区的inode还有很多没有被使用,于是到inodebitmap位图结构里找到了没有用过的inode,然后分配给该文件,分配好之后把自己的属性往里面填,这样就算新建成功了!! 如果还打算写入的话,先确认一下要写入内容的大小,确定一下需要几个块,然后到blockbitmap里面找到没被使用的块,把块号填到inode结构体里面的block数组里,然后再把文件内容写进这些块里面 2、删除文件:先找到分区,再找到inode,然后把对应inodebitmap和blockbitmap的位置置0。其实文件还在,只不过对应的块可以被覆盖!! 我们会发现,无论是什么操作,最重要的就是如何去找到文件的inode,可以我们如何知道一个文件的inode呢??其实使用者压根没关心过inode,我们只关心文件名!! 3.2 目录文件 目录也是文件,也有自己的inode ,也是有内容+属性构成! 重点:目录文件的文件内容放的是文件的文件名+对应文件的inode映射关系!! 问题1:为什么一个目录下不能有同名文件?? ——>因为文件名和inode是key和value的关系,key值必须唯一。 问题2:为什么没有w无法创建文件?? ——>w意味着无法写,所以我们就无法将文件名和inode的映射关系写进去,因此无法创建文件。 问题3:为什么没r无法查看文件?? ——>r意味着无法读,所以我们也看不到文件名和inode的映射关系,找不到inode就找不到文件。 问题4:为什么没有x无法进入目录?? ——>x意味着没有操作权限,要进入一个目录必须cd目录名,并且将当前目录名更新到环境变量PWD中,所以无法进行该操作的话我们就无法进入。 问题5:通过读取目录文件内容可以找到文件的inode,那么目录的inode如何查找呢?? ——>你当前的目录其实也是别的目录的子目录,所以就得递归往上找,一直找到根目录,然后再从根目录往下找回来,读取每个目录的数据块,直到找到该目录的inode.所以访问文件必须带路径!! 3.3 dentry缓存(扩展) 找目录的inode要递归向上找到根目录,然后再找回来,难度不会很慢么??确实会的,所以Linux提供了dentry缓存,将常用文件的inode信息缓存起来!! dentry缓存,简称dcache,是Linux为了提高目录项对象的处理效率而设计的。它是一个slab cache,保存在全局变量dentry_cache中,用于保存目录项的缓存。dentry结构是一种含有指向父节点和子节点指针的双向结构,多个这样的双向结构构成一个内存里面的树状结构,也就是文件系统的目录结构在内存中的缓存了。 linux 内核 目录项高速缓存 dentry cache 简介-CSDN博客 四、软硬链接 建立方式: 4.1 如何理解硬链接? 硬链接不是一个独立的文件,因为他没有独立的inode!! ——>所谓的建立硬链接,本质上就是在特定目录的数据块中新增文件名和指向的文件inode编号的映射关系(有点像起别名的感觉,可以让多个文件名指向一个inode)。 问题1:为什么dir的引用计数是2?? ——>因为 . 是dir的一个硬链接 问题2:为什么dir的上级目录引用计数是3? ——>因为 dir中的 ..是上级目录的一个硬链接 总结: (1)无论他的引用计数是多少,-2就是他当前子目录的数目(比如21,那么他的子目录就是19) (2)所以删除文件并不是直接把该文件的位图清空,本质上是先把目录里该文件的inode映射关系去掉,然后再到inode里面把引用计数-- 引用计数为0时才是把文件的位图清空。 问题3:为什么Linux不允许对目录建立硬链接??(重点!!) ——>为了防止出现环的题,b比方说我们要找一个目录的inode,会向上索引找到根目录,再向下找回来,如果恰好这个过程中出现了 上级目录的硬链接,那么就会回退回去,造成死循环!! 问题4:可是目录内部不是有. 和 .. 的硬链接吗??(重点!!) ——> (1) . 和 .. 是操作系统创建的,他不让你创建是为了你好,担心你创建之后出现环的问题,其实. 和..按照道理也会有环的问题,但是操作系统提前规定好了 .和..不会被做搜索,这是强制规定的!所以不会有环的问题! (2)其实操作系统干嘛要多此一举搞个. 和 ..呢??不就是为了方便让用户使用相对路径么?? 由于目录文件的inode需要递归向上索引才能找到,所以我们总是需要给想要找的文件加上绝对路径,现在操作系统给我们. 和 .. ,我们就可以用相对路径了! 硬链接应用场景:通常用来做路径定位!!可以通过硬链接进行目录切换!(不常用) 4.2 如何理解软链接? 软连接是一个独立的文件,有独立的inode,也有独立的数据块 ,他的内容里面保存的是指向的文件的路径。(相当于windows的快捷方式) 应用场景:当一个可执行文件在路径深处时,我们要找到他比较麻烦,如果我们在当前路径使用,就可以在当前路径建立一个该文件的软连接。(可执行程序随便放都行,只要有软链接,较常用) 五、文件系统和内存系统的关联 5.1 硬件和内存交互的基本单位 物理内存是以4KB为基本单位的(由操作系统决定的) 物理内存交互的单位叫做页框,而磁盘交互的单位叫做页帧 问题:为什么是4KB而不是要多少给多少呢??难道我访问100字节不比访问4KB快吗?? ——>理论上是这样的,但是100字节可能在4KB的不同位置,需要访问的话还需要对磁盘做更精细的计算(磁盘速度太慢了),甚至是文件系统也需要更精细的方法,操作系统文:你能保证你后几秒不用上下文数据吗??反正4kB和100字节效率差不是很多,我都给你拿过来,说不定你用得上呢!(根据局部性原理:程序倾向于访问近期或者近邻的数据,这是计算机性能优化的重要原则,合理运用可以整体提速!) 总结:(1)硬件:减少IO的次数——减少访问外设的次数 (2)软件:基于局部性原理而产生的预加载——整体提速 5.2 操作系统如何管理内存 虚拟地址是操作系统提供的,我们用户只能看到纯的虚拟地址,具体这个虚拟地址具体映射到物理内存的哪个位置,我们并不关心也并不知道,这是操作系统帮我们决定好的,所以操作系统必然可以看得到物理地址!! 那么操作系统要如何去管理我们的内存呢?? Page的配置不能太大,因为这样的话会占据大量的空间。 flag:检查当前page的状态(比如说当前检测到我们要访问的内容并没有被加载到内存中,所以这个时候就会发生缺页中断) count:引用计数(可以通过引用计数知道当前有多少个进程在共享我这个page,当有进程需要去修改内部的数据的时候,就会发生写时拷贝) 所有申请内存的工作,本质上都是访问Page数组!!(根据一个随机地址判断这个地址属于哪个Page的方法:1KB=2^10字节 所以4KB=2^12字节 恰好在16进制地址中就是后三位数字,所以我们任何一个地址只要 & 0xFFFF F000 ,就可以找到该地址对应的Page) 5.3 文件页缓冲区 在Linux中,我们每一个进程打开的每一个文件都具有自己的inod和文件页缓冲区!! 在我们的file结构体狸猫有一个address_spqce结构体,里面又有一个page_tree,里面又有一个 radix_tree_node节点 page_tree这个结构有点类似于B树。他不断延伸最后会找到我们想要寻找的Page结构体! 内存管理,并不是直接让那个我们去访问page数组,而是通过一些配套的算法。 5.4 字典树的理解 以上是一颗相对简单的字典树(组合式的KV模型),比方说我们想要找cba,我们就可以按照这颗树索引下去,找到我们想要找到的内容(某个对象数据) 为什么操作系统要用字典树这个结构来管理我们的page呢?? ——>因为文件的内容按照4KB是有偏移量的。(虽然我们的块是4KB大小,但是我们不一定是在开头去读取或写入,而操作系统通过字典树这种组合式的KV结构来帮助我们定位具体的位置) 5.5 串联进程管理、文件管理、内存管理 首先我们创建了一个进程,创建了一个PCB结构体,然后还会顺便创建一张文件描述符表,我们调用C接口去打开写入文件的时候,该文件会在该文件描述符表中分配一个位置,然后创建一个file结构体,里面分配了inode,但是文件的内容会先写入在C层的缓冲区, 当满足刷新策略的时候,再通过一些方式将其刷新到我们的page中(内核缓冲区)。然后最后再刷新到磁盘中。所以这个过程数据被拷贝了3次 当我们的进程将数据交给内存后,其实他就不管了,所以我们的操作系统必须关心内存要如何刷新到磁盘上,且可能同一时间有大量的IO请求,因此我们的操作系统也要关心先执行哪个请求。所以这就涉及到IO子系统(操作系统和驱动做交互的系统) 我们的IO请求会将相关的一些需求和配置写到一个request结构体里,然后再由操作系统做管理 ——>因为我们的不同的IO请求可能是向不同的扇区写入,所以我们肯定希望比较接近的扇区在一起被写入,这样减少磁头定位能够提高整体效率,所以我们会用一个队列把request管理起来,然后设计一些IO排序和IO合并算法,来给IO请求整体提速!! 在我们开机的时候,因为物理内存经常需要跟磁盘做交互,所以会提前把一些访问物理内存所需要的区域会被预先加载进去,尤其是文件系统的相关功能。 ——>说明操作系统真的帮助我们做了很多事情!! 5.6 slab分配器和伙伴系统(扩展) 从我们创建进程开始,或者是申请文件,我们就会有各种各样的结构体不断地被创建和释放 所以当我们的内核数据结构想要释放的时候,操作系统并不会直接释放,而是会把一些高频使用的内核数据结构体通过某些机制暂时缓存起来,当你下次需要的时候你就不需要跟内存模块做申请,而是直接把之前缓存里面的内核数据结构拿出来初始化即可! slap分配器:Linux 内核 | 内存管理——slab 分配器 - 知乎 (zhihu.com) 伙伴系统:一篇看懂!伙伴系统之伙伴系统概述--Linux内存管理 - 知乎 (zhihu.com) ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/weixin_51142926/article/details/142334013
推荐直播
-
OpenHarmony应用开发之网络数据请求与数据解析
2025/01/16 周四 19:00-20:30
华为开发者布道师、南京师范大学泰州学院副教授,硕士研究生导师,开放原子教育银牌认证讲师
科技浪潮中,鸿蒙生态强势崛起,OpenHarmony开启智能终端无限可能。当下,其原生应用开发适配潜力巨大,终端设备已广泛融入生活各场景,从家居到办公、穿戴至车载。 现在,机会敲门!我们的直播聚焦OpenHarmony关键的网络数据请求与解析,抛开晦涩理论,用真实案例带你掌握数据访问接口,轻松应对复杂网络请求、精准解析Json与Xml数据。参与直播,为开发鸿蒙App夯实基础,抢占科技新高地,别错过!
回顾中 -
Ascend C高层API设计原理与实现系列
2025/01/17 周五 15:30-17:00
Ascend C 技术专家
以LayerNorm算子开发为例,讲解开箱即用的Ascend C高层API
回顾中
热门标签