内存 (随机访问存储器,RAM) 是计算机中一种需要认真管理的重要资源。不管存储器有多大,程序都能把它填满。经过多年的探索,我们有了 分层存储器体系 (memory hierarchy) 的概念,即计算机拥有若干 MiB 快速、昂贵且易失性的 Cache,数 GiB 速度与价格适中的易失性内存,以及数 TiB 快速、廉价但非易失性的磁盘存储。计算机中管理分层存储器体系的部分被称为 存储管理器 (memory manager)。它的任务是有效地管理内存,记录哪些内存正在使用,哪些内存是空闲的,在进程需要时为其分配内存,在进程使用完后释放内存。
无存储器抽象最简单的存储器抽象是不使用抽象。早期的大型机 (60 年代以前)、小型机 (70 年代以前) 以及个人计算机 (80 年代以前) 都是没有存储器抽象的,每一个程序都直接访问物理内存,这种模型中系统每次仅运行一个进程。
虽然直接使用物理内存,但还是有不同的模型,下图展示了三种模型。a 模型中操作系统位于 RAM 底部,这种模型曾被用于大型机与小型机;b 模型中操作系统位于内存顶端的 ROM (只读存储器) 中,这种模型被用于掌上电脑或嵌入式系统中;c 模型中设备驱动程序位于顶部的 ROM 中,而操作系统的其他部分位于 RAM 的底部,该方案被用于早期的个人计算机中 (如运行 MS-DOS 的计算机),在 ROM 中的系统部分被称为 BIOS (基本输入输出系统, Basic Input Output System)。a 和 c 模型当用户程序出错时,可能会摧毁操作系统,引发灾难性后果。
在无存储器抽象的系统中实现并行的方法是采用多线程编程。由于引入线程时假设一个进程中的所有线程对同一内存映像都可见,如此实现并行也就不是问题。虽然方法行得通,但没有被广泛使用,因为人们通常希望能够在同一时间运行没有关联的程序,而这正是线程抽象所不能提供的。因此一个无存储器抽象的系统也不大可能提供线程抽象的功能。
由于使用无存储器抽象时并发进程,可以在一个进程运行一段时间后,从磁盘中加载其他进程到 RAM 中。但由于两个进程都引用的绝对地址,因此可能会引用到第一个进程的私有地址,导致进程崩溃。IBM 360 对上述问题的补救方案就是在第二个进程装载到内存的时候,使用静态重定位的技术修改它。
一种存储器抽象:地址空间当物理地址暴露给进程会带来下面一些严重问题:
如果用户程序可以寻址内存的每个字节,它们就可以很容易地 (故意地或偶然地) 破坏操作系统,从而使整个系统慢慢地停止运行,除非有特殊的硬件保护 (IBM 360 的锁键模式) 使用这种模型,想要同时运行多个程序是很困难的 地址空间的概念要使多个应用程序同时处于内存中并且不互相影响,需要解决两个问题:保护 和 重定位 。
进程进程 (Process) 是操作系统中的核心概念,是对正在运行的程序的抽象。即使只有一个可用的 CPU,也可以启动多个进程,让操作系统具有并发能力。
进程模型一个进程就是一个正在执行的程序实例,每个进程都拥有一个自己的虚拟 CPU、程序计数器、寄存器、内存地址空间等,这些是一个进程私有的,不可被其他进程所访问、修改,真正的 CPU 在各个进程之间来回切换。
假设现在有 4 个程序,它们在运行时装入自己虚拟的程序计数器、寄存器,并有物理 CPU 运行程序。当程序被切换时,物理程序计数器等数据被保存到内存中。在观察足够长的时间时,所有的进程都运行了,但每个瞬间仅有一个程序在执行。需要注意的是,如果一个程序运行了两遍,那将被算作两个进程。
进程的核心思想即:一个进程是某种类型的一个活动,它有程序、输入、输出以及状态。单个处理器可以被若干进程共享,它使用调度算法决定何时停止一个进程的工作,并转而为另一个进程提供服务。
当进程创建了一个新进程后,其被称为父进程,新进程被称为子进程,这些进程组成了层次结构。在 UNIX 中一个称为 init 的特殊进程出现在启动映像中,init 运行时读入终端数量的文件,并为每一个终端创建一个新进程。这些终端等待用户登陆,登陆成功时便会执行 shell 进程来接收用户命令。整个系统中,所有进程都属于以 init 为根的进程树。 Windows 中没有层次结构概念,所有进程地位相同。创建进程时父进程得到 句柄 用于控制子进程,但也可以将句柄传送给其他进程。
进程的创建与终止操作系统往往需要一种方式来创建进程,一般由 4 种主要事件来创建进程:
系统初始化 正在运行的程序执行了创建进程的系统调用 (syscall) 用户请求创建新进程 批处理作业的初始化 新进程都是由于一个已存在的进程执行了一个用于创建进程的 syscall 用而创建的。这个系统调用通知操作系统创建一个新进程,并且直接或间接地指定在该进程中运行的程序。
在 UNIX 系统,往往采用 syscall fork 来创建一个与调用进程相同的副本。调用成功后,两个进程拥有相同的内存映像、相同的环境字符串与打开文件。通常子进程执行 exec 系列 syscall 来修改其内存映像,并运行一个新程序。在执行 exec 之前,允许子进程处理其文件描述符等操作。
UNIX 中子进程的初始地址空间是父进程的一个副本,但是两个不同的地址空间,不可写的部分则是共享的。而某些 UNIX 实现中,子进程共享父进程的所有内存,内存通过 写时复制 (Copy-On-Write, COW) 技术实现共享,即当两者之一修改内存时,这块内存被明确的复制,以保证修改发生在进程的私有区域。
进程在创建之后开始运行,完成其工作,进程可能在未来的某个时刻完成任务并被终止。通常终止进程由以下条件引起:
正常退出 (自愿) 错误退出 (自愿) 严重错误 (非自愿) 被其他进程杀死 (非自愿) 前两种即进程自己所处理的终止,完成工作或者在运行时遇到了一些可处理的错误,这时进程通过 exit 调用终止,并向父进程返回状态值。
微软的软件主要可以通过以下三个渠道获取:
零售 原始设备制造商 (OEM) 批量许可协议 OEM 在工厂执行激活,比如说新买的笔记本电脑自带的系统就是这种方式。零售主要通过联机、电话或 VAMT 代理激活。批量激活产品主要选择 MAK (多次激活密钥) 、 KMS (密钥管理服务) 以及 AD (Active Directory) 进行激活。
KMS 可以在本地网络完成激活,而无需将个别计算机连接到 Microsoft 进行产品激活。KMS 不需要专用系统的轻型服务,可以轻易地将其联合托管在提供其他服务的系统上。
KMS 服务器可以为局域网内所有连接的产品进行周期性激活,激活周期一般为 180 天,产品激活后会定期连接 KMS 服务器进行验证、续期,如果不能连接到服务器在激活周期过后,产品将过期而需要重新激活。
KMS 服务激活的是 VL 版,而我们常用的 RTL (零售版) 是无法激活的。自己搭建 KMS 服务器激活产品,虽然可以正常使用,但是不能算正版软件,请支持正版!
部署 KMS 服务器常用的 Microsoft KMS 服务器是开源的 Vlmcsd,它可以部署到不同平台上提供服务。 Vlmcsd 的使用很简单,下载下来启动即可提供服务,默认端口号是 1688
Windows对于 Windows 的下载,可以选择 官方渠道 或通过 MSDN, I tell you 进行下载,安装的专业版均可以 KMS 激活
激活 Windows相对来说激活 Windows 也很简单,以管理员身份打开 Powershell 或命令提示符,并输入命令即可激活
设置 GVLK,这里我们以 Windows Server 2016 标准版为例 1 slmgr /ipk WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY 设置 KMS 服务器 1 slmgr /skms example.
Phoenix Framework 是一个 MVC web 框架,与 Ruby 的 Rails 和 Python 的 Django 类似,是整个 Elixir 社区的核心项目之一,推荐阅读 Phoenix 文档
安装我们使用 Phoenix (v1.5.7) 前,需要安装相关依赖与 mix
Elixir (>= v1.6) Erlang (>= 20) node.js [optional] (>= 5.0.0) Database [default=PostgreSQL] inotify-tools [linux] erlang 与 elixir 是运行时环境,数据库方面使用同为社区维护的 Ecto 来操作,Phoenix 使用 node.js 的原因是使用 webpack 编译静态资源,当然你可以只开发 API 不使用静态资源
Phoenix 提供了非常有用的实时重新加载功能,不过 Linux 用户需要安装 inotify-tools 才能使用
创建新项目我们使用 mix 来创建一个 Phoenix 项目
1 mix phx.new awesome 如果你没有 phx.new 这个命令,你需要先使用 mix 安装一下
和好友联机的时候本地服务器实在是不爽,一个人起飞,其他人都是高PING战士,最开始主要是 L4D2 时各种 RPG 服务器有些不爽,为了纯净的服务器只好自己建了
事先声明,我们所有的操作在 Debian / Ubuntu 下操作,有些操作系统可能会不一样,不过大同小异,我们还是定义一些等等可能用到的变量 (主要是路径和密码之类的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 steam=/home/steam group_id=123456789 l4d2=${steam}/l4d2 l4d2_id=222860 l4d2_server_name="L4D2 Server" l4d2_port=1024 valheim=${steam}/valheim valheim_id=896660 valheim_server_name="Valheim Server" valheim_world="World" valheim_port=1024 valheim_passwd=valheim_password dst=${steam}/dst dst_id=343050 SteamCMD顾名思义,steamcmd 是一个命令行工具,同时支持 linux,是我们搭建服务器的好帮手,然而我不会用,不过这不重要,安装跑起来就好
1 2 3 4 add-apt-repository multiverse dpkg --add-architecture i386 apt update && apt upgrade apt install -y lib32gcc1 steamcmd 我们不仅要安装一个 steamcmd,还要将所有游戏服务器,存放在 ~steam 下,使用 steam 这个用户来运行游戏
1 2 adduser --disabled-login --gecos 'Steam' steam sudo -u steam -H ln -s /usr/games/steamcmd ${steam}/steamcmd 语法这里说的语法并不是 SteamCMD 的语法,而是 Steam 中所使用的文本标记语法,这些标记标签允许您为您的留言及发帖文字添加格式,类似于 HTML,官方展示在这里
MixMix 是 Elixir 社区开发的集包管理、依赖管理、构建工具于一身的开发工具,扩展性极好,功能强大,自带对 Erlang 的支持,可以类比 Golang 自带的 go,详细的使用方式请参考 mix help 以及 mix
我们如果需要创建一个新项目,使用 mix new 命令即可,详细使用方法可以使用 mix help new 查看,对于新建项目,mix 会很友好的创建一系列文件 (其中还包含 .gitignore)
1 mix new example 我们目前只需要关注其中的 mix.exs 就行了,它包含了配置应用、依赖、环境信息、版本等功能,project 函数设置项目相关信息, application 函数在生产应用文件的时候会用到,deps 函数则是定义项目的依赖项
管理依赖、环境我们需要把所需的依赖全部列入 deps 中,deps 返回一个列表,每一项依赖都写在元组中,格式如下
1 2 3 {app, requirement} {app, opts} {app, requirement, opts} app 是一个原子,是依赖项的名称 requirement 是一个字符串或正则表达式,用以设定版本 opts 是一个 keyword list,设置依赖相关操作 下面列出常用的添加依赖方式
1 2 3 4 5 6 {:plug, ">= 0.4.0"}, # 从 hex.
模块之前函数的时候也简单的见过模块了,Elixir 允许嵌套模块,这样可以轻松定义多层命名空间
1 2 3 4 5 defmodule Greeter.Greeting do def morning(name), do: "Good morning, #{name}" def evening(name), do: "Good evening, #{name}" end Greeter.Greeting.morning("iris") # "Good morning, iris" 模块通常还会有一些属性,这些属性通常被用作常量
1 2 3 4 5 6 7 defmodule Example do @greeting "Hello" def greeting(name) do ~s(#{@greeting}, #{name}.) end end Example.greeting("iris") # "Hello, iris." 当然还有一些的属性,用于保留功能,比如 moduledoc 和 doc 作为文档,文档可以用 ExDoc 生成 HTML,而 ExMark 是一个 Markdown 分析器,最终我们可以使用 mix 来生成文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 defmodule Example do @moduledoc """ This is the Hello module.
好久没学习,随便写点东西,一直想学FP来着,不过之前 Haskell 整的有点难受,好难啊不太会,下次静下心来好好学一学吧,不过先试试 Erlang / Elixir,听说也很难?
至于原因,莫名喜欢 Erlang,不知道为什么哈哈哈哈,得知有 Elixir 这个披着 Ruby 皮、用着 Beam 的 Lisp 觉得还不错?毕竟 Lisp 大法好!! (虽然我不会 lisp) 不过 Elixir 名字好听 Logo 也好看
好了,前置吐槽就这么多吧,希望可以静下心好好学学 Elixir,呃,我也不知道可不可以啦,但是如果对 Elixir 感兴趣的话可以在 Elixir School 尝试学习一下,我也才开始从这里开始学习
基本类型 整数类型:在 Erlang 和 Elixir 中,整数类型都是高精度类型,不区分类型所占的字节,有点类似 Python 中的整数 Elixir 支持 二(0b)、八(0o)、十、十六(0x)进制的整数字面量,使用起来十分方便
1 2 3 4 255 # 十进制整数 255 0b10001000 # 二进制整数 136 0o7654321 # 八进制整数 2054353 0xFFFF # 十六进制整数 65535 浮点类型:嗯,它是 IEEE 754,好了就这样吧,介绍完了
布尔类型:true 和 false,不过有一点需要注意,在 Elixir 中除了 false 和 nil 之外的所有值都为 true
原子操作原子操作是一个不可分割的操作,系统的所有线程不会观察到原子操作完成了一半。如果读取对象的加载操作是原子的,那么这个对象的所有修改操作也是原子的。
标准原子类型全部定义于头文件 atomic 中,这些类型的操作都是原子的,但是其内部实现可能使用原子操作或互斥量模拟,所以原子操作可以替代互斥量完成同步操作,但是如果内部使用互斥量实现那么不会有性能提升。
通常标准原子类型不能进行拷贝和赋值,但是可以隐式转化成对应的内置类型,使用 load()、exchange()、compare_exchange_weak() 和 compare_exchange_strong(),另外还有 store() 用以原子地赋值。每种函数类型的操作都有一个内存序参数,这个参数可以用来指定存储的顺序。
::std::atomic_flag::std::atomic_flag 是最简单的原子类型,标准保证其实现是 lock-free 的,这个类型的对象可以在 设置 和 清除 间切换,对象必须被 ATOMIC_FLAG_INIT,初始化标志位为清除状态。初始化后,对象进可以执行销毁、清除、设置
1 ::std::atomic_flag f = ATOMIC_FLAG_INIT; // 设置为清除状态 (false) 由于 clear() 清除操作原子地设置标志为 false,test_and_set() 设置操作原子地设置标志为 true 并获得其先前值,所以可以简单地实现一个自旋锁
1 2 3 4 5 6 7 8 9 10 11 12 class spinlock_mutex { private: ::std::atomic_flag flag; public: spinlock_mutex() : flag(ATOMIC_FLAG_INIT) {} void lock() { while (flag.test_and_set(::std::memory_order_acquire)); } void unlock() { flag.clear(::std::memory_order_release); } }; ::std::atomic::std::atomic 不再保证 lock-free,但相比 ::std::atomic_flag 有了更通用的操作, store() 是一个存储操作,load() 是一个加载操作,exchange() 是一个读-改-写操作。
线程管理 创建线程新的线程会在 ::std::thread (头文件 thread 中) 对象创建的时候被启动,在函数执行完毕后,该线程也就结束了,提供的函数对象会复制到新线程的存储空间中,函数对象的执行与操作都在线程的内存空间中执行。在创建新线程时你可以指定一个函数作为任务,或者是 仿函数,当然也可以是 lambda 表达式
1 2 3 4 5 6 7 8 9 10 ::std::thread my_thread0{do_something}; struct Task { void operator()() const { do_something(); } }; ::std::thread my_thread1{Task()}; ::std::thread my_thread2{[]() { do_something(); }}; 线程启动后,需要指定是等待线程结束还是让其自主运行,如果 ::std::thread 对象销毁之前没有做出决定,程序就会终止,因此必须确保线程能够正确 汇入 (joined) 或 分离 (detached)。调用 join() 可以等待线程完成,并在线程结束时清理相关的内存,使 ::std::thread 对象不再与已完成线程有任何关联,所以一个线程一旦被汇入将不能再次汇入。调用 detach() 会使线程在后台运行,不再与主线程进行直接交互, ::std::thread 对象不再引用这个线程,分离的线程也不可被再次汇入,不过C++运行时库保证线程退出时可以正确回收相关资源。
在C++中 ::std::thread 对象是一种 可移动但不可复制 的资源,它可以交出它的所有权,但不能与其他对象共享线程的所有权。如果你希望对一个已持有线程的对象更改其行为,那你必须先汇入或分离已关联的线程,或者将已关联的线程的所有权交出。
1 2 3 4 5 6 ::std::thread t1{do_something}; ::std::thread t2 = std::move(t1); t1 = std::thread{some_other_function}; std::thread t3; t3 = std::move(t2); // t1 = std::move(t3); // 错误 传递参数向线程中传递参数十分简单,为 ::std::thread 构造函数附加参数即可,所有参数 将会拷贝到新线程的内存空间中,即使函数中的参数是引用
搭建邮局服务器的想法之前一直都有,不过一直没有尝试,国庆的时候从阿里云换到了腾讯云的时候尝试直接使用 postfix 和 dovecot 搭建,尝试了大概3天被劝退了,重新使用现成的解决方案也算终于搭建好了,可以愉快的使用自建邮箱了 (可以愉快的装逼了
信息 更新了 mailu 的搭建,虽然 mailu 相比 mailcow 可以使用宿主机的数据库,不过 mailu 配置 SMTPS / IMAPS / POP3S 不如 mailcow 简单方便,也没怎么研究,目前没有切换到 mailu 的打算 警告 打算在更换服务器之后不再维护邮箱服务,装逼不存在的 部署开始搭建服务器,以下采用域名 (example.com) 和 IP (1.1.1.1),安装在 /mailcow,使用主机的nginx反向代理,部署之前我们首先定义一些Shell变量,以便之后使用,请根据自己的需求更改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 path_to="/path/to" mailcow_path="${path_to}/mailcow" # mailcow 所在目录 mailu_path="${path_to}/mailu" mail_host="mail.example.com" mail_ip="1.1.1.1" db_user="example_user" # 数据库用户 (Mailu使用宿主机PostgreSQL时使用) db_passwd="example_password" # 数据库密码 (Mailu使用宿主机PostgreSQL时使用) db_name="example_db" # 数据库名称 (Mailu使用宿主机PostgreSQL时使用) http_port="8080" https_port="8443" cert_path="/ssl/path/to/cert/" # 证书存放目录 cert_file="${cert_path}/cert.
Pretty Good Privacy (PGP),是一套用于讯息加密、验证的应用程序,由 Phil Zimmermann 于1991年发布,由一系列散列、数据压缩、对称密钥加密以及公钥加密的算法组合而成。GNU Privacy Guard (GPG),是一个用于加密、签名通信内容以及管理非对称密钥的自由软件,遵循IETF订定的 OpenPGP技术标准 设计,并与PGP保持兼容。
GPG的基于现代密码学,主要是对非对称加密的应用,由于自己本身是菜鸡,又没有学过密码学,所以对于以下加密方式进行简单的介绍,如有不准确请指正。
对称加密 又称私钥加密,这类算法在加密与解密时使用 相同的 的密钥,通信双方在通信之前需要协商一个密钥。对称加密简单、高效,加密强度随密钥长度的增加而增加,常见加密算法 DES、ChaCha20、AES 等 非对称加密 又称公开密钥加密,这类算法采用公钥加密私钥解密,公钥可以随意发布,私钥必须由用户严格保管,通信双方在通信时使用对方的公钥加密自己的信息。非对称加密的数学基础是超大整数的因数分解、整数有限域离散对数、椭圆曲线离散对数等问题的复杂性。数字签名也是基于非对称加密实现,简单地说即将文件散列后使用私钥加密生成签名,验证时散列文件并与公钥解密签名的值做对比进行验证,数字签名可以验证文件完整性,也有防止伪造的作用。常见的加密算法有 DSA、RSA、ECDSA 等 初体验 生成使用 --generate-key 参数可以创建一个使用默认值的密钥对,如果想设置更多的值可以使用 --full-generate-key 参数,如果再加上 --expert 开启专家模式,专家模式允许你自己选择 不同的加密算法 与 不同的密钥种类,在此仅介绍 --full-generate-key 参数。
选择你希望的密钥种类 我们选择默认的 RSA and RSA,会生成采用RSA算法且拥有加密、签名、验证功能的密钥 密钥长度 NIST建议 2030年之前推荐的最小密钥长度,对称加密 128bit ,非对称加密 2048bit ,椭圆曲线密码学 224bit 使用期限 默认为永久(0),在这里我们选择1天 (1) 我们生成了一个密钥对,可以看到一些关于新生成的密钥的信息,包括了密钥长度、uid、指纹,我们一般使用指纹来分别不同的密钥,指纹是用40位16进制数字表示的串,我们一般使用邮箱、整串或串的最后16位区分密钥。
备份我们采用最朴素的方式保存密钥 —— 本地存储,但是请记住一点,私钥一定不能丢失或外泄。为了以防万一,我们生成一份吊销证书,用以在特殊情况时吊销该密钥,当然吊销证书也应该妥善保管。
1 2 3 gpg -a --export EFC4B50FE8F8B2B3 > test.pub # 导出公钥 gpg -a --export-secret-key EFC4B50FE8F8B2B3 > test.