0%

在mac系统的“设置”-“通知”中有很多应用标签,这些标签中有些是我们所需要的,有些是某某某流氓软件,强行装上的(流氓软件卸载后,该应用标签一直存在)。那么这个应用标签该如何清理呢?

上网找了好多方法,其中以删除~/Library/Application Support/NotificationCenter/<id>然后重启,这种方法最火。不知道这种方法实在osx(or macos)的哪个版本上的,本人mbp是osx 10.11.6,在我的mbp上没有NotificationCenter这个目录。对于没有这个目录的可以查看一下getconf DARWIN_USER_DIR这个目录。该目录下有个com.apple.notificationcenter目录,这个目录才是你要找到目录。

mac上NotificationCenter中的应用标签是存储在SQLite3数据库中的。可使用sqlite3 \getconf DARWIN_USER_DIR`com.apple.notificationcenter/db/db`打开sqlite数据库

1
2
3
4
$ sqlite3 `getconf DARWIN_USER_DIR`com.apple.notificationcenter/db/db                                                                                     1 ↵
SQLite version 3.8.10.2 2015-05-20 18:17:19
Enter ".help" for usage hints.
sqlite>

然后可用.tables查看库中的表

1
2
3
4
5
6
7
sqlite> .tables
app_info notifications
app_loc presented_alerts
app_push presented_notifications
app_source scheduled_notifications
dbinfo today_summary_notifications
notification_source tomorrow_summary_notifications

由于sqlite mode默认是list模式,需要将其改为line模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
sqlite> .show
echo: off
eqp: off
explain: off
headers: off
mode: list
nullvalue: ""
output: stdout
colseparator: "|"
rowseparator: "\n"
stats: off
width:
sqlite> .mode line
sqlite> .show
echo: off
eqp: off
explain: off
headers: off
mode: line
nullvalue: ""
output: stdout
colseparator: "|"
rowseparator: "\n"
stats: off
width:

可用select查询app_info表,并删除想要删除的记录,然后重启系统,通知中的应用标签消失。

参考&鸣谢

介绍

IP地址对于人来说很难记忆区分,但域名却很方便记忆,所以要将域名与IP地址对应起来,就催生了DNS。DNS不仅提供域名到IP的映射服务,还能提供主机别名、邮件服务器识别、负载均衡服务。

协议

DNS属于应用层协议,通常由HTTP、SMTP、FTP等协议使用,占用53端口。

交互流程

dns_protocol

  • 客户端发送一个包含域名的请求给DNS服务器(DNS查询报文)
  • DNS服务器查询到域名对应的IP地址后,给客户端一个应答回复(DNS应答报文),回复中包含客户端所请求域名对应的IP地址
  • 客户端收到回复后,取出IP地址,与该地址服务器建立链接

协议格式

DNS协议分为查询协议和应答协议,这两种协议的格式是一样的。

dns-protocol-format

DNS协议包括两部分,协议头和协议体

协议头

DNS协议头由固定的12个字节组成

  • ID
    由程序分配的16位标识符,该标识在查询时产生,应答报文中该ID与对应的查询请求ID相同。
  • QR
    表示该报文类型,“0”表示查询,“1”表示应答
  • OPcode
    表示查询种类,只在查询协议中作用。“0”为标准查询(QUERY),“1”为反相查询(IQUERY),“2”为服务器状态请求(STATUS),“3”~“15”为保留
  • AA
    授权应答的标志位。该位在应答报文中有效,“1”表示名字服务器是权限服务器
  • TC
    截断标志位。1表示响应已超过512字节并已被截断。(截断和UDP协议有关)
  • RD
    期望递归标志,作用在查询报文中,该位为“1”表示客户端希望得到递归应答
  • RA
    递归可用标志,作用在应答报文中,该位为“1”表示可以得到递归应答
  • zero
    用“0”占位,保留字段
  • Rcode
    返回码,在应答报文中出现,“0”表示无差错,“1”表示格式差错,“2”表示问题在域名服务器上,“3”表示域参照问题,“4”表示查询类型不支持,“5”表示在管理上被禁止,“6”~“15”预留
  • QD Count
    查询信息的数量
  • AN Count
    应答信息的数量
  • NS Count
    授权信息的数量
  • AR Count
    附加信息的数量

协议体

查询段

描述查询信息

  • QNAME
    表示需要查询的域名,该字段为变长字段,用标签序列表示域名(如:www.baidu.com 显示为 03 77 77 77 05 62 61 69 64 75 03 63 6f 6d 00)
  • QTYPE
    表示查询资源的类型,详细请见下文资源类型列表
  • QCLASS
    表示查询网络类别,“1”表示Internet互联网系统(助记“IN”),“CH”表示Chaos

应答段、授权段、附加段

对应答信息、授权信息、附加信息的描述

  • NAME
    资源记录对应的域名,该字段为变长字段,格式同QNAME
  • TYPE
    同QTYPE
  • CLASS
    同QCLASS
  • TTL
    表示资源记录的生命周期(以秒为单位),一般用于当地址杰西程序读取资源记录后决定保存及使用缓存数据的时间
  • RDLENGTH
    表示资源数据的长度
  • RDATA
    资源数据,按查询段要求返回的相关资源记录数据。
    若其TYPE为A,则返回4字节的IP地址;
    若其TYPE为NS,则返回授权域名服务器的域名;
    若其TYPE为CNAME,则返回规范名或与别名对应的真实名称。

资源类型列表

助记符 说明
A 指定主机名(或域名)对应的IPv4地址记录
AAAA 指定主机名(或域名)对应的IPv6地址记录
CNAME 别名 如:dig www.baidu.com, www.baidu.com.的cname就是www.a.shifen.com.
PTR 指针记录,用于将一个IP地址映射到对应的主机名,也可以看成是A记录的反向,通过IP访问域名
MX 邮件路由记录,用户可以将该域名下的邮件服务器指向到自己的mail server上,然后即可自行操控所有的邮箱设置
TXT 一般指为某个主机名或域名设置的说明,没啥用,可忽略
SRV 记录了哪台计算机提供了哪个服务
NS 域名解析服务器记录,如果要将子域名指定某个域名服务器来解析,需要设置NS记录
  • SRV
    格式:优先级 权重 端口 服务的名字.协议的类型.域名
    eg:
    1
    2
    3
    4
    5
    6
    _http._tcp.example.com. SRV 10 5 80. www.example.com
    _http - 服务名
    _tcp - 协议
    10 - 优先级
    5 - 权重
    80 - 端口

内部流程

dns-work-flow

参考&鸣谢

背景

analyse-paxos-byzantine

拜占庭位于如今的土耳其的伊斯坦布尔,是东罗马帝国的首都。由于当时拜占庭罗马帝国国土辽阔,为了防御目的,因此每个军队都分隔很远,将军与将军之间只能靠信差传消息。 在战争的时候,拜占庭军队内所有将军和副官必需达成一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,在军队内有可能存有叛徒和敌军的间谍,左右将军们的决定又扰乱整体军队的秩序。在进行共识时,结果并不代表大多数人的意见。这时候,在已知有成员谋反的情况下,其余忠诚的将军在不受叛徒的影响下如何达成一致的协议,拜占庭问题就此形成。

拜占庭将军问题是一个协议问题,拜占庭帝国军队的将军们必须全体一致的决定是否攻击某一支敌军。问题是这些将军在地理上是分隔开来的,并且将军中存在叛徒。叛徒可以任意行动以达到以下目标:欺骗某些将军采取进攻行动;促成一个不是所有将军都同意的决定,如当将军们不希望进攻时促成进攻行动;或者迷惑某些将军,使他们无法做出决定。如果叛徒达到了这些目的之一,则任何攻击行动的结果都是注定要失败的,只有完全达成一致的努力才能获得胜利。

拜占庭假设是对现实世界的模型化,由于硬件错误、网络拥塞或断开以及遭到恶意攻击,计算机和网络可能出现不可预料的行为。拜占庭容错协议必须处理这些失效,并且这些协议还要满足所要解决的问题要求的规范。这些算法通常以其弹性t作为特征,t表示算法可以应付的错误进程数。

很多经典算法问题只有在t<n/3时才有解,如拜占庭将军问题,其中n是系统中进程的总数。

为了解决拜占庭将军问题,图灵奖大神Leslie Lamport提出了Paxos算法,该算法可以帮助解决分布式系统中的一致性问题。

原理

在分布式系统中,为了保证数据的高可用,我们会将数据保留多个副本,这些副本会放置在不同的物理机上。为了对用户提供正确的读写,我们需要保证这些放置在不同物理机上的副本是一致的。

其中Proposer与Acceptor之间的交互主要有两个阶段、4类消息构成。

  • Phase1
    本阶段由2类消息构成prepare和promise,Proposer向网络内超过半数的Acceptor发送prepare消息
  • Phase2
    prepare、promise、accept、accepted。

角色

Paxos中有三类角色Proposer、Acceptor、Learner

analyse-paxos-members

选举流程

整个Paxos算法流程分为3个阶段

  • 准备阶段
  • 决议阶段
  • 学习阶段

analyse-paxos-flow

准备阶段

  • Proposer向大多数Acceptor发起自己要发起Proposal(epochNo, value)的Prepare请求
  • Acceptor收到Prepare请求,如果epochNo比已经接受的小的,直接拒绝; 如果epochNo比已经接受的大,保证不再接受比该epochNo小的请求,且将已经接受的epochNo最大的Proposal返回给Proposer

决议阶段

  • Proposer收到大多数Acceptor的Prepare应答后,如果已经有被接受的Proposal,就从中选出epochNo最大的Proposal, 发起对该Proposal的Accept请求。如果没有已经接受的Proposal, 就自己提出一个Proposal, 发起Accept请求。
  • Acceptor收到Accept请求后,如果该Proposal的epochNo比它最后一次应答的Prepare请求的epochNo要小,那么要拒绝该请求;否则接受该请求。

学习阶段

  • 当各个Acceptor达到一致之后,需要将达到一致的结果通知给所有的Learner

analyse-paxos-flow-detail

Proposer角色

(Phase1.a) 向所有的acceptors发送Prepare(i, b)请求;

(Phase2.a) 如果收到Reject(i,b)消息,那么重新发送Prepare(i,b+n),n为一个整型值,不同的proposer具有不同的n值,使得proposer之间保持一个偏序关系,保证不同的proposer不会使用相同的b值,即提案编号;

(Phase2.a) 如果收到acceptors集合的任意一个majority的Promise(i, b, V, VB)回复,那么如果所有的V均为空,proposer可以自由选取一个v(value),一般为用户提出的请求,回发Accept(i, b, v);否则回发Accept(i,b,V);

(Phase2.b) 如果收到Nack(b),回到(Phase1.a)发送Prepare(i,b+n);

(Phase2.b) 如果收到任意一个majority所有成员的Accepted(i,b,v)消息(表明投票已经完成)。这个过程learner也能收到Accepted消息,learner查看i是否为当前需要确认的iid,如果是则立即执行这个被批准的决议v;否则将该Accepted保存下来。

Phase2.b阶段完成后,各个角色上对应该实例的状态都将变为closed状态,即该实例已经选出决议,proposer不能再提出新的提案。这样保证一个实例只能选出一个决议。在实际应用过程中,为了简化实现,常常在proposers中选举出一个leader,来充当协调者。当leader选举出来后,系统中只能由leader向acceptors发出Prepare请求,也就是说这能由leader发起提案,而其它的proposers则只干一件事,即定时检测系统中的leader是否还在工作,如果在一定时间内收不到leader的心跳消息,则剩下的proposers发起新一轮leader竞选,选取新的leader。

Acceptor

acceptor会维护一个状态记录表,表的每一行维护这样四个数据<iid, B, V, VB>, iid表示实例id。B是一个整数,用来表示同意或接受过的该提案的最高编号。V表示该提案对应的决议,里面保存着客户端发送过来的数据。VB表示已经接受过的提案的编号。

(Phase 1.b) 接收Prepare(i,b)消息,i为实例id号,b为提案编号。对于同一个i,如果b>B,那么回复Promise(i, b, V, VB),并设B=b;否则,回复Reject(i,b),其中b=B。

(Phase 2.b) 接收Accept(i, b, v),如果b<B,那么回复Nack(b)信息,其中b=B(暗示该proposer提出提案后至少有一个其它的proposer广播了具有更高编号的提案);否则设置V=v,VB=b,并且回复Accepted(i,b,v)消息。

其中:Promise(i, b, V, VB)表示向proposer保证对于该实例不再接受编号不大于b的相同iid的提案;Accepted表示向learner和proposer发送该提案被通过的消息。

Learner

learner的主要任务就是监听来自acceptors的消息,用以最终确认并学习决议(value),即被批准的提案。当learner收到来自大多数(majority)acceptors的接受消息后,就可以确定该实例(instance)的value已经被最终无歧义的确认。这个时候便可以执行决议里的操作。决议序列在所有learner上顺序都是一致的,每一个提案的发起将会触发一次Paxos过程,每个这样的过程是一个Paxos的实例。而在实际应用中常使用单增的整数来标识每一个实例,即iid(instance id)。iid从1开始,而所有从1开始到当前iid的实例都必须是已经被确认过的,即这些决议都已经被执行过。比如:learner A已经确认了前10个实例,这时iid为11的决议还没有被通过,而iid为12和13的提案已经得到大多数acceptors的接受。此时就会产生一个决议序列缺口(gap),在这种情况下,A不能跳过11直接确认12和13,而是去询问acceptors是否已经通过11的决议。只有当iid为11的决议被确认后,iid为12和13的决议才能被确认学习。

活锁问题

Todo…

应用

Paxos在Ceph Monitor应用。Monitor要做的事情很明确了,就是管理、维护和发布集群的状态信息,但是为了避免单点故障或者性能热点问题,一般使用多个Monitor来做这一件事情,也就是管理层有多个成员。集群的正常运行,首先需要管理层达成一致,达成一致就需要有一个能拍板的monitor(leader),大家都听它的就行了。所以要达成一致核心问题就是在众多monitor中选出那个能拍板的monitor。Ceph解决这个问题的方法很简单,有点类似于领导人的选举,即有资格的monitor先形成一个quorum(委员会),然后委员会的成员在quorum这个范围内选出一个leader,集群状态信息的更新以及quorum成员的维护就有这个leader负责。Leader的选取规则也比较简单,每个monitor在初始化的时候都会根据它的IP地址被赋予一个rank值,当选举leader时,rank值最小的monitor胜出当选leader。当quorum成员发生变化时(增加或者减少),都会触发重新选举流程,再选出一个leader。

monitor的代码目录结构:
analyse-paxos-ceph-monitor-src.png

架构设计

paxos-ceph-monitor-frame.png

  • DBStore层
    数据的最终存储组件,以leveldb为例
  • Paxos层
    在集群上对上层提供一致的数据访问逻辑,在这一层看来所有的数据都是kv;上层的多中PaxosService将不同的组件的map数据序列化为单条value,公用同一个paxos实例
  • PaxosService层
    每个PaxosService代表集群的一种状态信息。对应的,Ceph Moinitor中包含分别负责OSD Map,Monitor Map, PG Map, CRUSH Map的几种PaxosService。PaxosService负责将自己对应的数据序列化为kv写入Paxos层。Ceph集群所有与Monitor的交互最终都是在调用对应的PaxosSevice功能

关键流程及结构

初始化流程

analyse-paxos-ceph-monitor-init-flow.png

  • 自下而上依次初始化上述的三大组成部分:DBStroe,Paxos,PaxoService
  • 初始化Messager,并向其中注册命令执行回调函数。Messager是Ceph中的网络线程模块,Messager会在收到网络请求后,回调Moniotor在初始化阶段注册命令处理函数
  • Bootstrap过程在整个Monitor的生命周期中被反复调用

Boostrap

  1. 执行Boostrap的Monitor节点会首先进入PROBING状态,并开始向所有monmap中其他节点发送Probing消息
  2. 收到Probing消息的节点执行Boostrap并回复Probing_ack,并给出自己的last_commit以及first_commit,其中first_commit指示当前机器的commit记录中最早的一条,其存在使得单个节点上可以仅保存最近的几条记录
  3. 收到Probing_ack的节点发现commit数据的差距早于对方first_commit,则主动发起全同步,并在之后重新Boostrap
  4. 收到超过半数的ack并不需要全同步时,则进入选主过程

analyse-paxos-ceph-monitor-boostrap-1.png

经过boostrap过程,保证可以与半数以上的节点通讯,并且节点间commit数据历史差距不大了。

select & victory

analyse-paxos-ceph-monitor-victory.png

select
  1. 将election_epoch加1,向Monmap中的所有其他节点发送Propose消息
  2. 收到Propose消息的节点进入election状态并仅对有更新的election_epoch且rank值大于自己的消息答复Ack。这里的rank简单的由ip大小决定
  3. 发送Propose的节点统计收到的Ack数,超时时间内收到Monmap中大多数的ack后可进入victory过程,这些发送ack的节点形成quorum
victory
  1. election_epoch加1,可以看出election_epoch的奇偶可以表示是否在选举轮次
  2. 向quorum中的所有节点发送VICTORY消息,并告知自己的epoch及quorum
  3. 当前节点完成Election,进入Leader状态
  4. 收到VICTORY消息的节点完成Election,进入Peon状态

recovery

经过了Boostrap、select、victory,能确定leader和peon角色,以及quorum成员。在recovery阶段将leader和quorum节点间的数据更新到一致。整个集群进入可用状态。

analyse-paxos-ceph-monitor-recovery.png

一致性读写流程

经过了上面的初始化流程,整个集群进入到一个正常状态,可以用Paxos进行一致性读写了。其中读流程比较简单,lease内的所有quorum均可以提供读服务。而所有写都会转发给leader。

一致性写流程

  1. leader在本地记录要提交的value,并向quroum中的所有节点发送begin消息,其中携带了要提交的value, accept_pn及last_commit
  2. peon收到begin消息,如果accept过更高的pn则忽略,否则将value写入db并返回accept消息。同时peon会将当前的lease过期掉,在下一次收到lease前不再提供服务
  3. leader收到 全部 quorum的accept后进行commit。本地commit后向所有quorum节点发送commit消息
  4. peon收到commit消息,本地commit数据
  5. leader通过lease消息将整个集群带入到active状态

analyse-paxos-ceph-monitor-rw.png

状态转换

初始化阶段状态转换

analyse-paxos-ceph-monitor-status-1.png

  • STATE_PROBING
    boostrap过程中节点间相互探测,发现数据差距
  • STATE_SYNCHRONIZING
    当数据差距较大无法通过后续机制补齐时,进行全同步
  • STATE_ELECTING
    Monitor在进行选主
  • STATE_LEADER
    当前Monitor成为leader
  • STATE_PEON
    非leader节点

一致性读写阶段状态转换

analyse-paxos-ceph-monitor-status-2.png

  • STATE_RECOVERING
    对应上述RECOVERING过程
  • STATE_ACTIVE
    leader可以读写或peon拥有lease
  • STATE_UPDATING
    向quroum发送begin,等待accept
  • STATE_WRITING
    收到accept
  • STATE_REFERSH
    本地提交并向quorum发送commit

参考&鸣谢

Docker历史

版本

Docker 1.2

Docker架构

Docker对使用者来说是一个C/S模式的架构,S端采用松耦合架构,各模块有机组合并支撑Docker运行。

docker-frame-main

用户是使用Docker Client与Docker Daemon建立通信,并发送请求给后者。而Docker Daemon作为Docker架构中的主体部分,首先提供Server的功能使其可以接受Docker Client的请求;而后Engine执行Docker内部的一系列工作,每一项工作都是以一个Job的形式的存在。
Job的运行过程中,当需要容器镜像时,则从Docker Registry中下载镜像,并通过镜像管理驱动graphdriver将下载镜像以Graph的形式存储;当需要为Docker创建网络环境时,通过网络管理驱动networkdriver创建并配置Docker容器网络环境;当需要限制Docker容器运行资源或执行用户指令等操作时,则通过execdriver来完成。
libcontainer是一项独立的容器管理包,networkdriver以及execdriver都是通过libcontainer来实现具体对容器进行的操作。
当执行完运行容器的命令后,一个实际的Docker容器就处于运行状态,该容器拥有独立的文件系统,独立并且安全的运行环境等。

功能模块

Docker Client

Docker Client可以通过以下三种方式和Docker Daemon建立通信:tcp://host:port,unix://path_to_socket和fd://socketfd。Docker Client可以通过设置命令行flag参数的形式设置安全传输层协议(TLS)的有关参数,保证传输的安全性。
Docker Client发送容器管理请求后,由Docker Daemon接受并处理请求,当Docker Client接收到返回的请求相应并简单处理后,Docker Client一次完整的生命周期就结束了。当需要继续发送容器管理请求时,用户必须再次通过docker可执行文件创建Docker Client。

Docker Daemon

接受并处理Docker Client发送的请求。该守护进程在后台启动了一个Server,Server负责接受Docker Client发送的请求;接受请求后,Server通过路由与分发调度,找到相应的Handler来执行请求。

docker-frame-daemon-main
Docker Daemon的大致可以分为三部分:Docker Server、Engine和Job。

  • Docker Server
    专门服务于Docker Client的server。接受并调度分发Docker Client发送的请求。
    docker-frame-daemon-server
    通过包gorilla/mux,创建了一个mux.Router,提供请求的路由功能。在Golang中,gorilla/mux是一个强大的URL路由器以及调度分发器。该mux.Router中添加了众多的路由项,每一个路由项由HTTP请求方法(PUT、POST、GET或DELETE)、URL、Handler三部分组成。

  • Docker Engine
    Engine是Docker架构中的运行引擎,同时也Docker运行的核心模块。

  • Docker Job
    一个Job可以认为是Docker架构中Engine内部最基本的工作执行单元。Docker可以做的每一项工作,都可以抽象为一个job。

Docker Registry

是一个存储容器镜像的仓库。而容器镜像是在容器被创建时,被加载用来初始化容器的文件架构与目录。
在Docker的运行过程中,Docker Daemon会与Docker Registry通信,并实现搜索镜像、下载镜像、上传镜像三个功能,这三个功能对应的job名称分别为”search”,”pull” 与 “push”。

Graph

已下载容器镜像的保管者,以及已下载容器镜像之间关系的记录者。一方面,Graph存储着本地具有版本信息的文件系统镜像,另一方面也通过GraphDB记录着所有文件系统镜像彼此之间的关系。

docker-frame-graph-main

  • GraphDB
    一个构建在SQLite之上的小型图数据库,实现了节点的命名以及节点之间关联关系的记录。它仅仅实现了大多数图数据库所拥有的一个小的子集,但是提供了简单的接口表示节点之间的关系。
  • Repository
    关于每一个的容器镜像,具体存储的信息有:该容器镜像的元数据,容器镜像的大小信息,以及该容器镜像所代表的具体rootfs。

Driver

通过Driver驱动,Docker可以实现对Docker容器执行环境的定制。由于Docker运行的生命周期中,并非用户所有的操作都是针对Docker容器的管理,另外还有关于Docker运行信息的获取,Graph的存储与记录等。因此,为了将Docker容器的管理从Docker Daemon内部业务逻辑中区分开来,设计了Driver层驱动来接管所有这部分请求。
在Docker Driver的实现中,可以分为以下三类驱动:graphdriver、networkdriver和execdriver。

graphdriver

graphdriver主要用于完成容器镜像的管理,包括存储与获取。即当用户需要下载指定的容器镜像时,graphdriver将容器镜像存储在本地的指定目录;同时当用户需要使用指定的容器镜像来创建容器的rootfs时,graphdriver从本地镜像存储目录中获取指定的容器镜像。

docker-frame-graphdriver-main

在graphdriver的初始化过程之前,有4种文件系统或类文件系统在其内部注册,它们分别是aufs、btrfs、vfs和devmapper。而Docker在初始化之时,通过获取系统环境变量”DOCKER_DRIVER”来提取所使用driver的指定类型。而之后所有的graph操作,都使用该driver来执行。

networkdriver

networkdriver的用途是完成Docker容器网络环境的配置,其中包括Docker启动时为Docker环境创建网桥;Docker容器创建时为其创建专属虚拟网卡设备;以及为Docker容器分配IP、端口并与宿主机做端口映射,设置容器防火墙策略等。

docker-frame-networkdriver-main

execdriver

execdriver作为Docker容器的执行驱动,负责创建容器运行命名空间,负责容器资源使用的统计与限制,负责容器内部进程的真正运行等。在execdriver的实现过程中,原先可以使用LXC驱动调用LXC的接口,来操纵容器的配置以及生命周期,而现在execdriver默认使用native驱动,不依赖于LXC。具体体现在Daemon启动过程中加载的ExecDriverflag参数,该参数在配置文件已经被设为”native”。

docker-frame-execdriver-main

libcontainer

bcontainer是Docker架构中一个使用Go语言设计实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问内核中与容器相关的API。正是由于libcontainer的存在,Docker可以直接调用libcontainer,而最终操纵容器的namespace、cgroups、apparmor、网络设备以及防火墙规则等。这一系列操作的完成都不需要依赖LXC或者其他包。

docker-frame-libcontainer-main

Docker container

Todo…

流程

docker pull

docker-flow-pull

docker run

docker-flow-run

参考&鸣谢

介绍

Kubernetes是为生产环境而设计的容器调度管理系统,对于负载均衡、服务发现、高可用、滚动升级、自动伸缩等容器云平台的功能要求有原生支持。由于Kubernetes在K和s间有8个字母,因此常简称K8s。事实上,随着对K8s系统架构与设计理念的了解深入,我们会发现K8s系统正是处处为运行云原生应用而设计考虑;同时,随着对K8s系统使用的加深和加广,也会有越来越多有关云原生应用的设计模式产生出来,使得基于K8s系统设计和开发生产级的复杂云原生应用变得像启动一个单机版容器服务那样简单易用。

系统架构

一个K8s集群是由etcd(分布式存储)、Minion/node(服务节点)、Master(控制节点)构成的。集群状态都保存在etcd中,Master节点上则运行集群的管理控制模块,多个Master节点以Active/Standby方式运行。Minion节点是真正运行应用容器的主机节点,在每个Minion节点上都会运行一个Kubelet代理,控制该节点上的容器、镜像和存储卷等。

k8s_frame

Master中模块

API Server

Kubernetes系统的入口,其封装了核心对象的增删改查操作,以RESTful API接口方式提供给外部客户和内部组件调用。维护的REST对象持久化到Etcd中存储。

Scheduler

为新建立的Pod进行节点选择(即分配机器),负责集群的资源调度。组件抽离,可以方便替换成其他调度器。

Controller

  • Replication Controller(RC)
    保证定义的副本数量与实际运行Pod数量一致。
  • Node Controller
    管理维护Minion/node,定期检查Node的健康状态,标识出(失效|未失效)的节点。
  • Namespace Controller
    管理维护Namespace,定期清理无效的Namespace。
  • Service Controller
    Todo…
  • EndPoints Controller
    Todo…
  • Service Account Controller
    Todo…
  • Persistent Volume Controller
    Todo…
  • Daemon Set Controller
    管理维护Daemon Set,负责创建Daemon Pod,保证指定的Node上正常的运行Daemon Pod。
  • Deployment Controller
    Todo…
  • Job Controller
    管理维护Job,为Jod创建一次性任务Pod,保证完成Job指定完成的任务数目
  • Pod Autoscaler Controller
    实现Pod的自动伸缩,定时获取监控数据,进行策略匹配,当满足条件时执行Pod的伸缩动作。

Minion中模块

Kubelet

负责管控容器,Kubelet会从Kubernetes API Server接收Pod的创建请求,启动和停止容器,监控容器运行状态并汇报给Kubernetes API Server。

Kube proxy

Kube-proxy是K8s集群内部的负载均衡器。它是一个分布式代理服务器,在K8s的每个节点上都有一个;这一设计体现了它的伸缩性优势,需要访问服务的节点越多,提供负载均衡能力的Kube-proxy就越多,高可用节点也随之增多。

Pod

Pod是在K8s集群中运行部署应用或服务的最小单元,它是可以支持多容器的。Pod的设计理念是支持多个容器在一个Pod中共享网络地址和文件系统,可以通过进程间通信和文件共享这种简单高效的方式组合完成服务。Pod对多容器的支持是K8最基础的设计理念。Pod是K8s集群中所有业务类型的基础,目前K8s中的业务主要可以分为长期伺服型(long-running)、批处理型(batch)、节点后台支撑型(node-daemon)和有状态应用型(stateful application),分别对应控制器为Deployment Controller、Job Controller、Daemon Set Controller、Pet Set Controller

参考&鸣谢

介绍

docker不是一项新技术,docker是老旧技术的组合,为了更方便使用容器技术,docker提供了简单方便的UI。docker隔离资源主要用到了两种技术namespace、cgroup。

Namespace

namespace是linux内核提供的隔离技术,它包括六种资源隔离,UTS(主机名与域名)、IPC(信号量、消息队列和共享内存)、PID(进程编号)、NET(网络设备、网络栈、端口…)、Mount(文件系统、挂载点)、User(用户和用户组)。

Type Sys Params ker ver
UTS CLONE_NEWUTS 2.6.19
IPC CLONE_NEWIPC 2.6.19
PID CLONE_NEWPID 2.6.24
NET CLONE_NEWNET 2.6.29
Mount CLONE_NEWNS 2.4.19
User CLONE_NEWUSER 3.8

操作系统调用接口:

  • clone()
    创建一个独立的进程独立的namespace
  • setns()
    使用已有的一个namespace
  • unshare()
    不启动新进程,在原进程上进行namespace隔离

docker run中提供了使用namespace的接口:

1
2
3
4
5
# docker run --help | grep -i namespace
--ipc string IPC namespace to use
--pid string PID namespace to use
--userns string User namespace to use
--uts string UTS namespace to use

UTS

提供了主机名和域名的隔离,这样每个容器就可以拥有了独立的主机名和域名,在网络上可以被视作一个独立的节点而非宿主机上的一个进程。
docker在run或create时,使用-h--hostname指定hostname

IPC

IPC是Unix/linux下进程间通讯的一种方式,包括信号量、消息队列、共享内存。容器内部进程间通信对宿主机来说,实际上是具有相同PID namespace中的进程间通信,因此需要一个唯一的标识符来进行区别。申请IPC资源就申请了这样一个全局唯一的32位ID,所以IPC namespace中实际上包含了系统IPC标识符以及实现POSIX消息队列的文件系统。在同一个IPC namespace下的进程彼此可见,而与其他的IPC namespace下的进程则互相不可见。

在宿主机上创建IPC(以消息队列为例):

1
2
# ipcmk -Q
消息队列 id:32768

在宿主机上查询IPC:

1
2
3
4
5
# ipcs

--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
0xabba8164 32768 root 644 0 0

在容器中查询IPC:

1
2
3
4
5
6
7
8
9
10
11
# docker exec -it net_5 ipcs

------ Message Queues --------
key msqid owner perms used-bytes messages

------ Shared Memory Segments --------
key shmid owner perms bytes nattch status

------ Semaphore Arrays --------
key semid owner perms nsems

PID

它对进程PID重新标号,即两个不同namespace下的进程可以有同一个PID。每个PID namespace都有自己的计数程序。内核为所有的PID namespace维护了一个树状结构,最顶层的是系统初始时创建的,我们称之为root namespace。他创建的新PID namespace就称之为child namespace(树的子节点),而原先的PID namespace就是新创建的PID namespace的parent namespace(树的父节点)。通过这种方式,不同的PID namespaces会形成一个等级体系。所属的父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响。反过来,子节点不能看到父节点PID namespace中的任何内容。
* 每个PID namespace中的第一个进程“PID 1“,都会像传统Linux中的init进程一样拥有特权,起特殊作用。
* 一个namespace中的进程,不可能通过kill或ptrace影响父节点或者兄弟节点中的进程,因为其他节点的PID在这个namespace中没有任何意义。
* 如果你在新的PID namespace中重新挂载/proc文件系统,会发现其下只显示同属一个PID namespace中的其他进程。(挂载/proc 文件系统尤为重要)
* 在root namespace中可以看到所有的进程,并且递归包含所有子节点中的进程。

docker-resource-separation-pid

以容器的ceph-mon节点为例:

1
2
3
4
5
6
7
8
9
10
11
# 宿主机上查看 ceph-mon 进程
# ps -ef| grep 9184
root 9184 3122 0 1月20 ? 00:00:04 docker-containerd-shim be0583fade06df3f6510dd629bde4a636e68c755aa8b0733db6702493b1d0c38 /var/run/docker/libcontainerd/be0583fade06df3f6510dd629bde4a636e68c755aa8b0733db6702493b1d0c38 docker-runc
64045 9200 9184 0 1月20 ? 00:01:03 /usr/bin/ceph-mon --cluster ceph -d -i rhel82 --public-addr 192.168.1.82:6789 --setuser ceph --setgroup ceph
root 21893 9855 0 16:15 pts/5 00:00:00 grep --color=auto 9184

# 容器中查看 ceph-mon 进程
# docker exec -it ceph-mon ps -ef
UID PID PPID C STIME TTY TIME CMD
ceph 1 0 0 Jan20 ? 00:01:03 /usr/bin/ceph-mon --cluster ceph
root 26 0 0 08:15 ? 00:00:00 ps -ef

NET

Pv4和IPv6协议栈、IP路由表、防火墙、/proc/net目录、/sys/class/net目录、端口(socket)等等。一个物理的网络设备最多存在在一个network namespace中,你可以通过创建veth pair(虚拟网络设备对:有两端,类似管道,如果数据从一端传入另一端也能接收到,反之亦然)在不同的network namespace间创建通道,以此达到通信的目的。

一般情况下,物理网络设备都分配在最初的root namespace(表示系统默认的namespace,在PID namespace中已经提及)中。但是如果你有多块物理网卡,也可以把其中一块或多块分配给新创建的network namespace。需要注意的是,当新创建的network namespace被释放时(所有内部的进程都终止并且namespace文件没有被挂载或打开),在这个namespace中的物理网卡会返回到root namespace而非创建该进程的父进程所在的network namespace。

当我们说到network namespace时,其实我们指的未必是真正的网络隔离,而是把网络独立出来,给外部用户一种透明的感觉,仿佛跟另外一个网络实体在进行通信。为了达到这个目的,容器的经典做法就是创建一个veth pair,一端放置在新的namespace中,通常命名为eth0,一端放在原先的namespace中连接物理网络设备,再通过网桥把别的设备连接进来或者进行路由转发,以此网络实现通信的目的。

docker-resource-separation-network

可通过ip netnsbrctl管理Network Namespace,docker创建的netns路径为/proc/{进程ID}/ns/netip netns访问的默认路径为/var/run/netns/
若需要访问其Network Namespace内部,先创建软连接链至ip netns访问路径,然后使用ip netns exec访问该网络内部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 在ip netns访问路径下创建network namespace的软链接
# ln -s /proc/7013/ns/net /var/run/netns/net_5

# 使用 ip netns exec 访问指定network namespace的网络
# ip netns exec net_5 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
15: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:3/64 scope link
valid_lft forever preferred_lft forever

Mount

隔离后,不同mount namespace中的文件结构发生变化也互不影响。你可以通过/proc/[pid]/mounts查看到所有挂载在当前namespace中的文件系统,还可以通过/proc/[pid]/mountstats看到mount namespace中文件设备的统计信息,包括挂载文件的名字、文件系统类型、挂载位置等等。(此处用到了mount propagation 技术)

docker-resource-separation-mount.png
上图mount挂载方式有待确认。

USER

主要隔离了安全相关的标识符(identifiers)和属性(attributes),包括用户ID、用户组ID、root目录、key(指密钥)以及特殊权限。说得通俗一点,一个普通用户的进程通过clone()创建的新进程在新user namespace中可以拥有不同的用户和用户组。这意味着一个进程在容器外属于一个没有特权的普通用户,但是他创建的容器进程却属于拥有所有权限的超级用户,这个技术为容器提供了极大的自由。

docker通过/proc/{进程ID}/uid_map/proc/{进程ID}/gid_map把容器中的uid、gid和真实系统的uid、gid给映射在一起,格式为:ID-inside-ns ID-outside-ns length

  • ID-inside-ns 表示在容器内显示的uid或gid
  • ID-outside-ns 表示在容器外映射的真实的uid或gid
  • length 表示映射范围,一般为1,表示一一对应(把ID-outside-ns ~(ID-outside-ns+length) 映射到 ID-inside-ns ~(ID-inside-ns+length)上)

cgroups

Control Groups(cgroups),是Linux 内核提供的一种可以限制、记录、隔离进程组所使用的物理资源(如 cpu、memory、磁盘IO等等) 的机制,被LXC、docker等很多项目用于实现进程资源控制。

cgroups子系统:

  • cpu
    使用调度程序提供对 CPU 的 cgroup 任务访问
  • cpuset
    为cgroup中的任务分配独立CPU(在多核系统)和内存节点
  • devices
    可允许或者拒绝 cgroup 中的任务访问设备
  • blkio
    为块设备设定输入/输出限制,比如物理设备(磁盘,固态硬盘,USB 等等)
  • freezer
    挂起或者恢复 cgroup 中的任务
  • memory
    设定 cgroup 中任务使用的内存限制,并自动生成由那些任务使用的内存资源报告
  • net_cls
    使用等级识别符(classid)标记网络数据包,可允许 Linux 流量控制程序(tc)识别从具体 cgroup 中生成的数据包

CPU

cpu-shares

docker create/run容器时可以通过--cpu-shares参数来指定容器所使用的CPU加权值。默认情况下,每个docker容器的cpu-shares值都是1024。单独一个容器的cpu-shares是没有意义的,只在多个容器分配的资源紧缺时,也就是说在需要对容器使用的资源进行限制时,才会生效。配之后,可通过./cgroup/cpu/docker/<容器ID>/cpu.shares查看。

cpu-period & cpu-quota

  • cpu-period 用来指定容器对CPU的使用要在多长时间内做一次重新分配
  • cpu-quota 用来指定在这个周期内,最多可以有多少时间用来跑这个容器
    docker create/run时使用,参数为--cpu-period--cpu-quota单位为微秒,cpu-period的默认值为0.1秒(100000 微秒),cpu-quota的默认值为-1(表示不控制)。配置后,可通过./cgroup/cpu/docker/<容器ID>/cpu.cfs_period_us./cgroup/cpu/docker/<容器ID>/cpu.cfs_quota_us查看。

cpuset

docker可使用--cpuset-cpus--cpuset-mems参数控制容器运行限定使用哪些cpu和内存节点。配之后,可通过./cgroup/cpuset/docker/<容器ID>/cpuset.cpus./cgroup/cpuset/docker/<容器ID>/cpuset.mems查看。

*** 注:对于具有NUMA的服务器很重要 ***

Memory

docker create/run时,可以对内存资源加以限制。

  • kernel-memory
    使用参数--kernel-memory,限制内核内存,该内存不会被交换到swap上。
  • memory
    使用参数--memory,设置容器使用的最大内存上限。默认单位为byte,可以使用K、G、M等带单位的字符串。
  • memory-reservation
    使用参数--memory-reservation,启用弹性的内存共享,当宿主机资源充足时,允许容器尽量多地使用内存,当检测到内存竞争或者低内存时,强制将容器的内存降低到memory-reservation所指定的内存大小。不设置此选项时,有可能出现某些容器长时间占用大量内存,导致性能上的损失。
  • memory-swap
    使用参数--memory-swap,设置总内存大小,相当于内存和swap大小的总和,设置-1时,表示swap分区大小是无限的。默认单位为byte,可以使用K、G、M等带单位的字符串。
  • memory-swappiness
    使用参数--memory-swappiness,设置控制进程将物理内存交换到swap分区的倾向,系数越小,就越倾向于使用物理内存。值范围为0-100。当值为100时,表示尽量使用swap分区;当值为0时,表示禁用容器 swap 功能(这点不同于宿主机,宿主机 swappiness 设置为 0 也不保证 swap 不会被使用)

Block Device

I/O

  • device-read-bps
    限制此设备上的读速度(bytes per second),单位可以是kb、mb或者gb
  • device-read-iops
    通过每秒读IO次数来限制指定设备的读速度
  • device-write-bps
    限制此设备上的写速度(bytes per second),单位可以是kb、mb或者gb
  • device-write-iops
    通过每秒写IO次数来限制指定设备的写速度
  • blkio-weight
    容器默认磁盘IO的加权值,有效值范围为10-100。要使-–blkio-weight生效,需要保证IO的调度算法为CFQ
    echo "cfq" > /sys/block/<设备名>/queue/scheduler
  • blkio-weight-device
    针对特定设备的IO加权控制。其格式为DEVICE_NAME:WEIGHT

Volume

使用参数--storage-opt,传入dm.basesize=<容量大小>可以设置rootfs大小。如果不设置dm.basesize,默认值为10G,若要使dm.basesize生效,storage driver 必须是 device mapper。
设置rootfs大小后,需要重启docker服务,并且--storage-opts参数需要在启动docker服务时使用。
以RHEL7.2为例,需要修改/etc/systemd/system/multi-user.target.wants/docker.service/usr/bin/dockerd的参数。

参考&鸣谢

介绍

每一个使用linux或类unix系统的人都会用到的命令mount,它用来挂载/卸载文件系统。这是该命令基本功能,除了基本功能该命令还有其它功能。

更多多多多多多多多多多多多多多多用法

绑定挂载(bind mount)

windows的快捷方式、linux的软硬链接,相信你已很熟悉了,下面我来说说mount的绑定挂载--bind

linux的软硬链接原理:
原理图

  • 软连接受应用程序影响,不是所有应用程序都能通过软连接访问资源的
  • 硬链接只能作用在文件上,不能为文件夹创建硬链接

bind mount:
能成功克服软硬链接的缺点,将文件或文件夹挂载到指定的挂载点上。使对挂载点的操作转移到文件或文件夹上。

1
2
3
4
5
6
7
 # ls -li uts*
583819234 -rw-r--r--. 1 root root 4 1月 23 16:55 uts
606248334 -rw-r--r--. 1 root root 2 1月 23 10:44 uts2
# mount --bind ./uts2 ./uts
# ls -li uts*
606248334 -rw-r--r--. 1 root root 2 1月 23 10:44 uts
606248334 -rw-r--r--. 1 root root 2 1月 23 10:44 uts2

uts的inode变成了,uts2的inode id,所有访问uts的io都会转移到uts2上;并且在mount中能查看到bind mount信息。

1
2
3
 # mount
...
/dev/mapper/rhel_rhel82-root on /home/zhoub/uts type xfs (rw,relatime,seclabel,attr2,inode64,noquota)

bind mount是用挂载文件或目录信息遮盖了挂载的文件或目录,当不需要时,可以通过umount卸载,恢复原文件或目录的信息。

挂载传播(mount propagation)

执行clone()时,进程拷贝当前文件系统树,此后,新进程就拥有与原进程相同的文件系统树拷贝,两个文件系统树中的任何挂载操作都不会影响另一个拷贝。尽管每个进程使用单独的文件系统名称空间在理论上非常有意义,但完全隔离也会造成较大限制性。进程克隆了系统的文件系统名称空间之后,已经运行的系统守护进程无法为这个用户自动挂载 CD-ROM,因为在原文件系统名称空间中执行的挂载无法影响用户的拷贝。2006 年引入的挂载传播(mount propagation)解决了这个问题,挂载传播定义了挂载对象之间的关系。

  • 共享挂载(share mount)
    如果两个挂载对象具有共享关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,反之亦然
    mount --make-shared <mount-object>
    mount --make-rshared <mount-object>
  • 从属挂载(slave mount)
    如果两个挂载对象形成从属(slave)关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,但是反过来不行;在这种关系中,从属对象是事件的接收者
    mount --make-slave <mount-object>
  • 私有挂载(private mount)
    既不传播也不接收挂载事件
    mount --make-private <mount-object>
  • 不可绑定挂载(unbindable mount)
    与私有挂载相似,但是不允许执行绑定挂载
    mount --make-unbindable <mount-object>

参考&鸣谢

介绍

ceph为统一存储,包括块存储(rbd)、文件存储(cephfs)、对象存储(radosgw)。正常我们所使用rbd cli使用的是库librbd来操作ceph的。

版本

ceph-jewel

架构

librbd组织结构:
librbd

librbd架构:
librbd_frame

接口

*** todo ***

参考&鸣谢

*** None ***

介绍

新买了一个TF卡,Class10的,应该能挺快。原来的卡是class4的,如果用新卡做系统,然后所有软件都重装重配,太麻烦!所以将现有系统,做成镜像(img),然后直接dd到新卡上,然后再执行raspi-conf进行容量扩展就可以正常使用了。

版本

  • 硬件,RaspberryPi3
  • OS,raspbian jessie

步骤

安装需要的软件

安装dosfstoolspartedkpartx软件及依赖包。

1
sudo apt-get install dosfstools parted kpartx

计算镜像大小

获取/dev/root/dev/mmcblk0p1占用的空间,然后计算镜像大小,计算公式:1.2 * (size(/dev/root) + size(/dev/mmcblk0p1))

1
2
3
4
$ df -P
Filesystem 1024-blocks Used Available Capacity Mounted on
/dev/root 30690780 1390740 28024812 5% /
/dev/mmcblk0p1 64366 20698 43668 33% /boot

df返回的容量单位为KB。

生成空白镜像文件

使用dd/dev/zero生成空白镜像文件sudo dd if=/dev/zero of={镜像文件} bs=1K count={镜像大小(单位为K)}

分区镜像文件并格式化

分区

使用parted为img文件分区

1
2
3
4
5
6
7
8
# 标记label,选择MBR or GPT主引导格式,MBR:msdos, GPT:gpt
sudo parted {镜像文件} -s -- mklabel msdos

# 创建分区
sudo parted {镜像文件} -s -- mkpart primary fat32 {开始扇区}s {结束扇区}s

# 创建分区
sudo parted {镜像文件} -s -- mkpart primary ext4 {开始扇区}s -1

开始扇区和结束扇区可是通过sudo fdisk -l /dev/mmcblk0来查看。

格式化

在格式化前,需要先将img文件映射给系统的loop设备,然后在将loop设备映射给device mapper。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 映射loop设备
$ sudo losetup -f --show {镜像文件}
/dev/loop0

# device mapper
$ sudo kpartx -va /dev/loop0
add map loop0p1 (254:0): 0 257 linear /dev/loop0 256
add map loop0p2 (254:1): 0 18015 linear /dev/loop0 513

# 格式化
$ sudo mkfs.vfat /dev/mapper/loop0p1
......
$ sudo mkfs.ext4 /dev/mapper/loop0p2
......

挂载并备份系统

备份/boot

1
2
3
$ sudo mount -f vfat /dev/mapper/loop0p1 /media
$ sudo cp -rfp /boot/* /media/
$ sudo umount /media

备份/

1
2
3
4
$ sudo mount -t ext4 /dev/mapper/loop0p2 /media
$ sudo rsync -aP --exclude={镜像文件} --exclude=/media/* --exclude=/sys/* --exclude=/proc/* --exclude=/tmp/* / /media/
...
$ sudo umount /media

卸载打包镜像文件

1
2
3
4
5
# 关闭device mapper
sudo kpartx -d /dev/loop0

#关闭 loop 设备
sudo losetup -d /dev/loop0

参考&鸣谢

背景

文件系统,是任何OS都不可却少的。想要编写一个属于自己的文件系统很容易,但调试十分不方便。为了方便调试,提高开发效率,可以使用FUSE(Filesystem in userspace)框架进行开发。这是一个内核模块,能够让用户在用户空间实现文件系统并且挂载到某个目录,就像在内核实现的文件系统一样。使用 FUSE 有几个好处:一是因为在用户空间实现,开发和调试都比较方便;二是可以把一些常用的服务以文件系统的形式展现,方便操作,如 ftpfs,sshfs,mailfs 等;另外可以避免一些版权问题,如 Linux 上对 ntfs,zfs 的操作都是通过 FUSE 实现的。当然用户空间的实现也有缺点,最明显的就是由多次在用户态/内核态切换带来的性能下降。

FUSE架构

FUSE Frame
将文件系统需要处理的读、写、创建、删除等函数,以回调的方式注册到FUSE模块中,当用户访问挂载目录时,FUSE模块回调相应的注册接口。

FUSE安装

ubuntu 16.04 安装

需要安装libfuse2libfuse-dev

1
sudo apt-get install libfuse2 libfuse-dev pkg-config

版本:

1
2
3
FUSE library version: 2.9.4
fusermount version: 2.9.4
using FUSE kernel interface version 7.19

OSX 10.11.6 安装

需要安装Caskroom/cask/osxfuse

1
brew cask install osxfuse

版本:

1
2
3
OSXFUSE 3.5.4
FUSE library version: 2.9.7
fuse: no mount point

FUSE使用

FUSE有两种接口,一种是fuse_operations另一种是fuse_lowlevel_ops

  • fuse_operations是较为上层的接口,我们可以使用fuse_main函数将其传入FUSE中
    eg:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    static struct fuse_operations cryptfs_ops = {
    .init = cfs_init,
    .destroy = cfs_destroy,
    .open = cfs_open,
    .read = cfs_read,
    .write = cfs_write,
    .release = cfs_release,
    .readdir = cfs_readdir,
    .getattr = cfs_getattr,
    };
  • fuse_lowlevel_ops是较底层的接口,我们可以使用fuse_session_loop函数实现
    eg:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    static struct fuse_lowlevel_ops  lowlevel_handler =   
    {
    .lookup = lowlevel_lookup,
    .getattr = lowlevel_getattr,
    .readdir = lowlevel_readdir,
    .mkdir = lowlevel_mkdir,
    .rmdir = lowlevel_rmdir,
    .open = lowlevel_open,
    .read = lowlevel_read,
    .write = lowlevel_write,
    .unlink = lowlevel_unlink,
    .rename = lowlevel_rename,
    };
    fuse_operations使用简单,容易上手。fuse_lowlevel_ops灵活性大,需要有FS开发经验。

Helloworld示例

实现ls ./mp系统提示Hello-world(./mp为挂载目录)
ls会用到的方法代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string.h>
#include <fuse.h>

static int cfs_readdir(const char* path, void* buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info* fi)
{
fprintf(prd->logfile, "cfs_readdir\t path : %s\n", path);
fflush(prd->logfile);

return filler(buf, "Hello-world", NULL, 0);
}

static int cfs_getattr(const char* path, struct stat *stbuf)
{
fprintf(prd->logfile, "cfs_getattr\t path : %s\n", path);
fflush(prd->logfile);
if(strcmp(path, "/") == 0)
stbuf->st_mode = 0755 | S_IFDIR;
else
stbuf->st_mode = 0644 | S_IFREG;
return 0;
}

fuse_main将接口注册到FUSE中:

1
2
3
4
5
6
7
8
9
10
11
static struct fuse_operations cryptfs_ops = {
.readdir = cfs_readdir,
.getattr = cfs_getattr,
};

int main(int argc, char *argv[])
{
int ret = 0;
ret = fuse_main(argc, argv, &cryptfs_ops);
return ret;
}

编译

ubuntu 16.04

编译命令:

1
gcc myfuse.c -o myfuse -DFUSE_USE_VERSION=22 `pkg-config fuse --cflags --libs` -g

or

1
gcc myfuse.c -o myfuse -DFUSE_USE_VERSION=22 -D_FILE_OFFSET_BITS=64 -I/usr/include/fuse -lfuse -pthread -g 

使用后者编译时不需要安装包pkg-config,由于不同操作系统fuse安装位置不一样,所以还是推荐使用pkg-config的方式

调试

可使用GDB调试,myfuse会以守护进程方式启动,调试有三种方法。

  • myfuse进程启动后,使用GDB中的attach方法进行调试
  • 使用GDB调试myfuse,参数中增加-d参数,该参数可以使myfuse不已守护进程方式启动 ( ** Version: 2.9.9上无法实现 ** )
  • myfuse中增加调试打印信息进行调试

挂载FUSE

先创建一个目录(mkdir ./mp),用于挂载myfuse文件系统,然后调用myfuse进行挂载。
eg:

1
sudo ./myfuse ./mp

对于mac系统挂载文件系统需要使用参数-o allow_other,否则挂在后,挂载目录将无权访问。
eg:

1
sudo ./myfuse ./mp -o allow_other

使用mount查看挂载的文件系统。
eg:

1
2
3
$ mount
...
/home/xxxx/xxxx/myfuse on /home/xxxx/xxxx/mp type fuse.myfuse (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000)

使用ls ./mp查看挂载点内容,根据实现应提示Hello-world
eg:

1
2
$ ls ./mp/
Hello-world

卸载FUSE

使用umount卸载myfuse文件系统,需要输入挂载点的全路径
eg:

1
sudo umount /home/xxxx/xxxx/mp

FUSE Options说明

fuse_main函数自带usage信息,只需要将--help参数传入,便能将usage信息打出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
$ ./myfuse --help
usage: ./myfuse mountpoint [options]

general options:
-o opt,[opt...] mount options
-h --help print help
-V --version print version

FUSE options:
-d -o debug enable debug output (implies -f)
-f foreground operation
-s disable multi-threaded operation

-o allow_other allow access to other users
-o allow_root allow access to root
-o auto_unmount auto unmount on process termination
-o nonempty allow mounts over non-empty file/dir
-o default_permissions enable permission checking by kernel
-o fsname=NAME set filesystem name
-o subtype=NAME set filesystem type
-o large_read issue large read requests (2.4 only)
-o max_read=N set maximum size of read requests

-o hard_remove immediate removal (don't hide files)
-o use_ino let filesystem set inode numbers
-o readdir_ino try to fill in d_ino in readdir
-o direct_io use direct I/O
-o kernel_cache cache files in kernel
-o [no]auto_cache enable caching based on modification times (off)
-o umask=M set file permissions (octal)
-o uid=N set file owner
-o gid=N set file group
-o entry_timeout=T cache timeout for names (1.0s)
-o negative_timeout=T cache timeout for deleted names (0.0s)
-o attr_timeout=T cache timeout for attributes (1.0s)
-o ac_attr_timeout=T auto cache timeout for attributes (attr_timeout)
-o noforget never forget cached inodes
-o remember=T remember cached inodes for T seconds (0s)
-o nopath don't supply path if not necessary
-o intr allow requests to be interrupted
-o intr_signal=NUM signal to send on interrupt (10)
-o modules=M1[:M2...] names of modules to push onto filesystem stack
-o max_write=N set maximum size of write requests
-o max_readahead=N set maximum readahead
-o max_background=N set number of maximum background requests
-o congestion_threshold=N set kernel's congestion threshold
-o async_read perform reads asynchronously (default)
-o sync_read perform reads synchronously
-o atomic_o_trunc enable atomic open+truncate support
-o big_writes enable larger than 4kB writes
-o no_remote_lock disable remote file locking
-o no_remote_flock disable remote file locking (BSD)
-o no_remote_posix_lock disable remove file locking (POSIX)
-o [no_]splice_write use splice to write to the fuse device
-o [no_]splice_move move data while splicing to the fuse device
-o [no_]splice_read use splice to read from the fuse device

FUSE Operations说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
struct fuse_operations {
int (*getattr) (const char *, struct stat *);
/* 这个函数与 stat() 类似。st_dev 和 st_blksize 域都可以忽略。st_ino 域也会被忽略,除非在执行 mount 时指定了 use_ino 选项 */

int (*readlink) (const char *, char *, size_t);
/* 这个函数会读取一个符号链接的目标。缓冲区应该是一个以 null 结束的字符串。缓冲区的大小参数包括这个 null 结束字符的空间。如果链接名太长,不能保存到缓冲区中,就应该被截断。成功时的返回值应该是 “0” */

int (*getdir) (const char *, fuse_dirh_t, fuse_dirfil_t);
/* 这个函数会读取一个目录中的内容。这个操作实际上是在一次调用中执行 opendir()、readdir()、...、closedir() 序列。对于每个目录项来说,都应该调用 filldir() 函数 */

int (*mknod) (const char *, mode_t, dev_t);
/* 这个函数会创建一个文件节点。此处没有 create() 操作;mknod() 会在创建非目录、非符号链接的节点时调用 */

int (*mkdir) (const char *, mode_t);
int (*rmdir) (const char *);
/* 这两个函数分别用来创建和删除一个目录 */

int (*unlink) (const char *);
int (*rename) (const char *, const char *);
/* 这两个函数分别用来删除和重命名一个文件 */

int (*symlink) (const char *, const char *);
/* 这个函数用来创建一个符号链接 */

int (*link) (const char *, const char *);
/* 这个函数创建一个到文件的硬链接 */

int (*chmod) (const char *, mode_t);
int (*chown) (const char *, uid_t, gid_t);
int (*truncate) (const char *, off_t);
int (*utime) (const char *, struct utimbuf *);
/* 这 4 个函数分别用来修改文件的权限位、属主和用户、大小以及文件的访问/修改时间 */

int (*open) (const char *, struct fuse_file_info *);
/* 这是文件的打开操作。对 open() 函数不能传递创建或截断标记(O_CREAT、O_EXCL、O_TRUNC)。这个函数应该检查是否允许执行给定的标记的操作。另外,open() 也可能在 fuse_file_info 结构中返回任意的文件句柄,这会传递给所有的文件操作 */
int (*read) (const char *, char *, size_t, off_t, struct fuse_file_info *);
/* 这个函数从一个打开文件中读取数据。除非碰到 EOF 或出现错误,否则 read() 应该返回所请求的字节数的数据;否则,其余数据都会被替换成 0。一个例外是在执行 mount 命令时指定了 direct_io 选项,在这种情况中 read() 系统调用的返回值会影响这个操作的返回值 */

int (*write) (const char *, const char *, size_t, off_t,struct fuse_file_info *);
/* 这个函数将数据写入一个打开的文件中。除非碰到 EOF 或出现错误,否则 write() 应该返回所请求的字节数的数据。一个例外是在执行 mount 命令时指定了 direct_io 选项(这于 read() 操作的情况类似) */

int (*statfs) (const char *, struct statfs *);
/* 这个函数获取文件系统的统计信息。f_type 和 f_fsid 域都会被忽略 */

int (*flush) (const char *, struct fuse_file_info *);
/* 这表示要刷新缓存数据。它并不等于 fsync() 函数 —— 也不是请求同步脏数据。每次对一个文件描述符执行 close() 函数时,都会调用 flush();因此如果文件系统希望在 close() 中返回写错误,并且这个文件已经缓存了脏数据,那么此处就是回写数据并返回错误的好地方。由于很多应用程序都会忽略 close() 错误,因此这通常用处不大 */

int (*release) (const char *, struct fuse_file_info *);
/* 这个函数释放一个打开文件。release() 是在对一个打开文件没有其他引用时调用的 —— 此时所有的文件描述符都会被关闭,所有的内存映射都会被取消。对于每个 open() 调用来说,都必须有一个使用完全相同标记和文件描述符的 release() 调用。对一个文件打开多次是可能的,在这种情况中只会考虑最后一次 release,然后就不能再对这个文件执行更多的读/写操作了。release 的返回值会被忽略 */

int (*fsync) (const char *, int, struct fuse_file_info *);
/* 这个函数用来同步文件内容。如果 datasync 参数为非 0,那么就只会刷新用户数据,而不会刷新元数据 */

int (*setxattr) (const char *, const char *, const char *, size_t, int);
int (*getxattr) (const char *, const char *, char *, size_t);
int (*listxattr) (const char *, char *, size_t);
int (*removexattr) (const char *, const char *);
/* 这些函数分别用来设置、获取、列出和删除扩展属性 */
......
};

更多详细说明,请见fuse.h文件。

参考&鸣谢