ThinkNotes

Simple is not easy | 化繁为简,知易行难

0%

搭建可移植的Markdown blog环境:Obsidian/Typora+PicGo+Hexo+Github

背景

写技术笔记并发布博客,通常有以下方式:

  • 第三方平台CSDN/cnblogs,最简单但是最不可控,例如写过如何使用shadow-sock直接被删掉,各种广告也不能忍。
  • 使用云服务器自建Leanote博客系统,最大缺点是服务器续费太贵,且文章数据存在数据库很难导出,。
  • 使用github+picgo+hexo, github作为图床和博客的云服务,picgo用于建立图床通道,hexo用于发布博客。缺点是github访问慢,用开源CDN可以很好解决;优点是全部免费,数据可移植(图片链接都在github图床),可长期使用(只要github不倒闭不锁区)

最终我选择github+picgo+hexo方案。
为什么不用gitee: gitee上传图片限制1M, github有25M。有了CDN, github的速度也不是问题

比较几个Markdown编辑器:

VSCode加Markdown插件:体验一般
Typora体验完美,但windows收费(Linux版不收费,Windows旧版不收费)
Obsidian免费,特性:

  • 支持动态渲染,即写出的Markdown语句自动显示预览
  • 支持各种快捷键,无需手动输入Markdown语法格式(Markdown 基本语法)。比如标题,链接,列表,引用,可以设置标准的Markdown快捷键。
  • 支持关联PicGo实现粘贴图片即上传到云端图床,这一点对于写作体验和文章的可迁移性很重要
  • 特色的Zettelkasten笔记管理方法,本文不描述这部分,参考玩转 Obsidian 01:打造知识循环利器

搭建可移植的Markdown写作环境

Github+PicGo搭建图床

阅读全文 »

1.MD5简介

MD5的全称是Message-Digest Algorithm 5(信息摘要算法),经MD2、MD3和MD4发展而来。
所谓信息摘要,就是包含数据关键特性,能(唯一)识别原数据的关键信息。

MD5也称为单向散列算法,这是从其实现方式命名,因为:

  • MD5能对大量数据,进行哈希映射,输出固定长度(128bit)的数据,输出数据也称为原数据的信息摘要。
  • 不能由摘要推测出原数据,即MD5算法是单向的,当加密来用的话,只能加密不能解密。

MD5的特点:

  • 固定长度:输入任意长度的信息,经过MD5处理,输出总是128位的信息。
  • 唯一性:不同的输入得到的不同的结果;同样的输入一定得到相同的结果。
  • 不可逆:根据128位的输出结果不可能反推出输入的信息。

2.MD5的应用

1、防止被篡改:
1)比如A和B发送一个电子文档,发送前,A先自己计算出数据的MD5输出结果a。
然后在B收到电子文档后,B计算得到一个MD5的输出结果b。
如果a与b一样就代表传输中途数据未被篡改。
2)比如A提供文件下载,为了防止不法分子在文件中添加木马,伪装成A的文件。A可以在网站上公布由安装文件得到的MD5输出结果。
要下载文件的人只需要下载后,验证MD5是否和A一致,如果不一致,就是被其他人修改过。

2、防止暴露明文:
基本上存储用户密码的场景,都用到MD5加密明文。
1)例如网站服务器在其数据库存储用户的密码,都是存储用户密码的MD5值。就算不法分子得用户密码的MD5值,也无法知道用户的密码。
2)在UNIX、Linux系统中,用户密码就是以MD5(或其它类似的算法)经加密后存储在文件系统中。当用户登录的时候,系统把用户输入的密码计算成MD5值,然后再去和保存在文件系统中的MD5值进行比较,进而确定输入的密码是否正确。通过这样的步骤,系统在并不知道用户密码的明码的情况下就可以确定用户登录系统的合法性。这不但可以避免用户的密码被具有系统管理员权限的用户知道,而且还在一定程度上增加了密码被破解的难度。

3、防止抵赖(数字签名):
这需要一个存储MD5值的第三方认证机构。例如A写了一个文件,认证机构对此文件用MD5算法产生摘要信息并做好记录。若以后A说这文件不是他写的,权威机构只需对此文件重新产生摘要信息,然后跟记录在册的摘要信息进行比对,相同的话,就证明是A写的了。这就是所谓的“数字签名”。

阅读全文 »

1.什么是编码解码

编码:利用特定的算法,对原始内容进行处理,生成运算后的内容,形成另一种数据的表现形式,可以根据算法,再还原回来,这种操作称之为编码。
解码:利用编码使用的算法的逆运算,对经过编码的数据进行处理,还原出原始数据,这种操作称之为解码。

2.什么是Base64编码算法

将任意的字节数组数据,通过Base64算法,生成只有(大小写英文、数字、+、/)(一共64个字符)内容表示的字符串数据。即将任意的内容转换为可见的字符串形式。

3.为什么需要Base64编码

Base64算法最开始是被用于解决电子邮件数据传输问题。以前发送邮件只支持可见字符的传送,但ASCII码中,有一部分不支持直接显示。由此,需要有一个方法将不可见的字符转换为可见的字符,便产生了Base64编码算法。

4.Base64算法的实现

特点:

  • 将数据按照 3个字节一组的形式进行处理,每三个字节在编码之后被转换为4个字节。即:如果一个数据有6个字节,可编码后将包含6/3*4=8个字节
  • 当数据的长度无法满足3的倍数的情况下,最后的数据需要进行填充操作,即补“=” ,这里“=”是填充字符,不要理解为第65个字符。因此我们经常看见base64编码的字符串结尾有几个”=”号

示例:

阅读全文 »

背景

树莓派4B自带蓝牙和Wifi, 无需外接 USB dongle;
蓝牙最常见的应用是近距离传输数据,比如蓝牙传文件,蓝牙音箱等。正好家里有个普通的usb供电的便携音箱;

本文用树莓派蓝牙+普通音箱,实现简单的蓝牙音箱。

首先需要了解Linux音频系统的整体框架:
image-20221208194352559

大致分为三个部分:

  • kernel/driver层的ALSA驱动框架
  • 蓝牙音频协议栈:A2DP, 这是使蓝牙具有传输音频流能力的基石; Linux官方的bluez包实现了A2DP
  • 音频应用层, Linux最常用的音频服务器是Pulse Audio

怎样理解这三层:可以类比Linux网络层:
ALSA 类似网络驱动框架
A2DP 类似TCP/UDP层
PulseAudio 类似HTTP层的服务器,类比Apache

而蓝牙连接类似http连接和会话;
声卡(输入、输出)类似网卡(Ethernet和wifi),音频设备(音箱,麦克风)类似具体的网口设备

深入了解 ALSA 音频驱动和 A2DP 蓝牙音频协议,参考:
Advanced Linux Sound Architecture (ALSA) project homepage
A2DP Spec

本文的环境
树莓派4B, 系统: ubuntu-server raspberry pi版本
音箱:usb供电,音频线
安卓手机:用于配对树莓派的蓝牙音频服务

阅读全文 »

选型

  • 为什么用树莓派4:

资料多遇到容易解决问题;
性能较强适合作为终端服务器;
自带WIFI, BT5.0,GPIO 方便拓展开发IOT相关项目;
适配系统丰富,基本PC上linux版本树莓派都有对应版本

  • 为什么用USB摄像头:

为了快速实现,Linux对USB设备支持非常好,USB设备基本都是免驱;
USB摄像头支持高分辨率,带麦克风,满足其他项目拓展应用;
当然CSI接口摄像头也有优势,同等条件下其CPU占用率比USB低;不过本地测试中CPU并不是USB摄像头性能瓶颈
关于CSI和USB 摄像头区别:CSI摄像头 vs USB摄像头

  • 树莓派用什么系统:

看个人喜好,我用的ubuntu server的树莓派版本,软件源基本最新;

  • 用什么云服务器:

看个人喜好和价格;云服务器最大价值在于公网IP
我目前用的Aliyun + CentOS7 系统

系统实拍:
image-20221208194427802

阅读全文 »

1.GDB简介

官网文档:
GDB: The GNU Project Debugger

关于GDB的原理:
GDB实现原理和使用范例
GDB工作原理和内核实现
GDB的基本工作原理

其他教程:GDB调试教程

几个重点:

  • 多种运行方式:gdb启动程序再调试(独立功能程序),gdb attach进程再调试(服务端程序),gdb加载core dump调试(离线调试)
  • GDB的本质是“截获”被调试程序,attach用ptrace截获了OS和应用程序之间的通信, 端点本质是trap中断,截获了CPU正常取指执行流程

本文源码:cursorhu/SimpleMultiThread/4.gdb_thread/

2.多线程程序的GDB调试

待调试代码:

#include <thread>
#include <chrono>
#include <mutex>
#include <iostream>

int g_mydata = 0;
std::mutex g_mutex;

void thread_func1()
{
	while (true)
	{
		g_mutex.lock();
		++g_mydata;
		if(g_mydata == 1024)
			g_mydata = 0;
		g_mutex.unlock();
		std::this_thread::sleep_for(std::chrono::seconds(1));
	}
}

void thread_func2()
{
	while (true)
	{
		g_mutex.lock();
		std::cout << "g_mydata = " << g_mydata << ", ThreadID = " << std::this_thread::get_id() << std::endl;
		g_mutex.unlock();
		std::this_thread::sleep_for(std::chrono::seconds(1));
	}
}

int main()
{
	std::thread t1(thread_func1);
	std::thread t2(thread_func2);
	t1.join();
	t2.join();
	return 0;
}
阅读全文 »

相关资料

线程池的概念和相关示例可以参考:
C++实现线程池
基于C++11实现线程池的工作原理

本代码相关的C++基础,参考:
c++拷贝构造函数详解
智能指针shared_ptr的用法
深入解析条件变量

其他相关文章
jorion/c++11 多线程(X)
jorionwen/threadtest

线程池示例

调用线程池

#include "TaskPool.h"
#include <chrono>

int main()
{
    TaskPool threadPool;
    threadPool.init(); //初始化线程对象队列

    Task* task = NULL;
    for (int i = 0; i < 10; ++i)
    {
        task = new Task();
        threadPool.addTask(task); //初始化任务对象队列,调度线程时会取出执行
    }
    
    std::this_thread::sleep_for(std::chrono::seconds(2));

    threadPool.stop(); //等待所有工作线程结束

    return 0; //析构
}

线程池的方法

#include "TaskPool.h"

TaskPool::TaskPool() : m_bRunning(false)
{

}

TaskPool::~TaskPool()
{
    removeAllTasks();
}

void TaskPool::init(int threadNum/* = 5*/)
{
    if (threadNum <= 0)
        threadNum = 5;

    m_bRunning = true;

    for (int i = 0; i < threadNum; ++i)
    {
        std::shared_ptr<std::thread> spThread;
        //shared_ptr.reset带参数是初始化,指向new出的thread对象
        //bind绑定了thread对象和其执行函数threadFunc
        spThread.reset(new std::thread(std::bind(&TaskPool::threadFunc, this))); 
        m_threads.push_back(spThread); //thread对象入队
    }
}

void TaskPool::threadFunc() //thread对象唤醒时执行
{
    std::shared_ptr<Task> spTask;
    while (true)
    {
        std::unique_lock<std::mutex> guard(m_mutexList); //RAII实现,作用域结束自动解锁
        while (m_taskList.empty())
        {                 
            if (!m_bRunning)
                break;
            
            //如果获得了互斥锁,但是条件不合适的话,pthread_cond_wait会释放锁,不往下执行。
            //当发生变化后,条件合适,pthread_cond_wait将直接获得锁。
            m_cv.wait(guard);
        }

        if (!m_bRunning)
            break;

        spTask = m_taskList.front(); //取m_taskList的task对象
        m_taskList.pop_front(); //更新m_taskList

        if (spTask == NULL)
            continue;

        spTask->doIt(); //执行task
        spTask.reset(); //shared_ptr.reset不带参数,指向对象的计数-1
    }

    std::unique_lock<std::mutex> guard(m_mutexList); //为了打印的原子性,再加锁
    {
        std::cout << "Exit thread, threadID: " << std::this_thread::get_id() << std::endl;
    }
    
}

void TaskPool::stop()
{
    m_bRunning = false;
    m_cv.notify_all(); //唤醒所有等待条件变量的线程

    //等待所有线程退出
    for (auto& iter : m_threads)
    {
        if (iter->joinable())   //该线程是否可join
            iter->join();       //主线程等待该线程
    }
}

void TaskPool::addTask(Task* task)
{
    std::shared_ptr<Task> spTask;
    spTask.reset(task); //shared_ptr初始化,指向task

    {
        std::lock_guard<std::mutex> guard(m_mutexList);       
        //m_taskList.push_back(std::make_shared<Task>(task));
        m_taskList.push_back(spTask); //Task对象入队
        std::cout << "Add a Task." << std::endl;
    }
    
    m_cv.notify_one(); //唤醒随机一个等待条件变量的线程
}

void TaskPool::removeAllTasks()   //析构时调用
{
    {
        std::lock_guard<std::mutex> guard(m_mutexList);
        for (auto& iter : m_taskList)
        {
            iter.reset();
        }
        m_taskList.clear();
    }
}

类定义

阅读全文 »

本文讲解并发环境中的几个线程同步示例
线程同步,即多个线程如何协调,谁先谁后
本文基于Linux/POSIX API
本系列源码:cursorhu/SimpleMultiThread

生产者消费者模式

生产者/消费者模式是并发环境常见的模式,简单地讲,通过中介缓冲,支持多组任务并发执行,避免任务间发生通信阻塞。
参考:生产者/消费者模式的理解及实现

常用的实现方式

信号量实现

关于LInux信号量:Linux信号量

示例:

#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <list>
#include <semaphore.h>
#include <iostream>

class Task
{
public:
	Task(int taskID)
	{
		this->taskID = taskID;
	}
	
	void doTask()
	{
		std::cout << "handle a task, taskID: " << taskID << ", threadID: " << pthread_self() << std::endl; 
	}
	
private:
	int taskID;
};

pthread_mutex_t  mymutex;
std::list<Task*> tasks;
sem_t            mysemaphore;

void* consumer_thread(void* param)
{	
	Task* pTask = NULL;
	while (true)
	{
		struct timespec ts;
		ts.tv_sec = 3;
		ts.tv_nsec = 0;
		
		if (sem_timewait(&mysemaphore, &ts) != 0)
		{
			if (errno == ETIMEOUT)
			{
				std::cout << "ETIMEOUT" << std::endl;
			}
			continue;
		}
		
		if (tasks.empty())
			continue;
		
		pthread_mutex_lock(&mymutex);	
		pTask = tasks.front();
		tasks.pop_front();
		pthread_mutex_unlock(&mymutex);
		
		pTask->doTask();
		delete pTask;
	}
	
	return NULL;
}

void* producer_thread(void* param)
{
	int taskID = 0;
	Task* pTask = NULL;
	
	while (true)
	{
		pTask = new Task(taskID);
			
		pthread_mutex_lock(&mymutex);
		tasks.push_back(pTask);
		std::cout << "produce a task, taskID: " << taskID << ", threadID: " << pthread_self() << std::endl; 
		
		pthread_mutex_unlock(&mymutex);
		
		//释放信号量,通知消费者线程
		sem_post(&mysemaphore);
		
		taskID ++;

		//休眠1秒
		sleep(1);
	}
	
	return NULL;
}

int main()
{
	pthread_mutex_init(&mymutex, NULL);
	//初始信号量资源计数为0
	sem_init(&mysemaphore, 0, 0);

	//创建5个消费者线程
	pthread_t consumerThreadID[5];
	for (int i = 0; i < 5; ++i)
	{
		pthread_create(&consumerThreadID[i], NULL, consumer_thread, NULL);
	}
	
	//创建一个生产者线程
	pthread_t producerThreadID;
	pthread_create(&producerThreadID, NULL, producer_thread, NULL);

	pthread_join(producerThreadID, NULL);
	
	for (int i = 0; i < 5; ++i)
	{
		pthread_join(consumerThreadID[i], NULL);
	}
	
	sem_destroy(&mysemaphore);
	pthread_mutex_destroy(&mymutex);

	return 0;
}

说明几点:

阅读全文 »

背景

多线程概述:应用层的多线程的目的就是让每一个任务(例如一系列函数调用)都认为自己独占CPU资源,即宏观上,多个任务可以同时执行(实际可能是轮转的串行执行)。
代码实现:线程库可以由编程语言的标准库或者操作系统的库实现,具体包含的头文件如下:

  • C/C++ : < thread >
  • POSIX(Portable Operating System Interface of UNIX, Linux环境使用较多) :< pthread.h >
  • Windows OS : < windows.h >

具体环境使用哪个库,有不同的观点,参考
c++多线程编程主要用pthread还是c++11中的thread类?
即使是同一环境,也有不同封装层次的API
CreateThread()与_beginthread()的区别详细解析

主线程与工作线程:
一般应用程序都有主要的执行流程,例如C/C++的main入口函数,主要执行流程是在进程中执行的,也可以认为main是线程,独占了进程的全部资源,称为主线程。如果在该进程执行时,创建多个线程,用于并行处理其他任务,称为工作线程。

本文讲不同风格的线程创建\销毁,和访问共享数据的锁操作
本系列源码:cursorhu/SimpleMultiThread

Windows风格多线程

(1)双线程打印

#include <iostream>   
#include <windows.h>   
using namespace std;
 
DWORD WINAPI Print(LPVOID lpParamter)
{
    std::string s = (char*)lpParamter;
    for (int i = 0; i < 10; i++)
        cout << s << endl;
    return 0;
}
 
int main()
{
    std::string s1 = "Work thread";
    std::string s2 = "Main thread";
    HANDLE hThread = CreateThread(NULL, 0, Print, (LPVOID)s1.c_str(), 0, NULL);
    Print((LPVOID)s2.c_str());
	CloseHandle(hThread);
	
    return 0;
}

主线程和工作线程都运行Print(),各线程的栈空间保存自己的局部数据。
windows API使用CreateThread和CloseHandle创建线程、释放线程句柄,说明如下

阅读全文 »

最近在ChromeOS上做一些shell script测试用例开发,ChromeOS基于Debian9,但没有Ubuntu那种GNOME的gedit编辑器,更不谈安装Linux版VSCode,正好借此机会练习一下之前一直不熟悉的vim编辑器。

ChromeOS不方便截图,所以本文以ubuntu上的linux0.11代码为例,整理vim最常用的操作。

关于Linux上的文本编辑器基础概念,可以参考<Linux命令行与shell脚本编程大全.第3版>

1. 三种编辑模式

我将vim归为三种编辑模式:

  • 文本编辑模式
    文本编辑模式是默认模式,vim编辑器会将按键解释成命令。在任意模式按esc进入此默认模式。

  • 文本插入模式
    文本插入模式, vim会将你在当前光标位置输入的每个键都插入到缓冲区,即文本输入字符。在普通模式下按下”i 键” 进入(含义:insert)

  • 命令行模式
    命令行模式和shell命令行类似,在普通模式下按下”: 键”进入(形似shell terminal的冒号)

怎么知道当前处于哪种模式?
vim左下角是状态行,以下是三种模式的状态示例:

  • vim init/main.c默认进入文本编辑模式,下面显示文件名和行号

输入i, 进入文本插入模式,下面显示insert状态

按esc退出文本编辑,再输入: 进入命令行模式,例如输入:wq保存文件

阅读全文 »