分布式系统设计模式

本文是对 Nishant 文章 Distributed System Design Patterns 的翻译,主要介绍分布式系统相关设计问题中涉及到的关键模式。

Keys

1.布隆过滤器

布隆过滤器是一个空间友好的概率数据结构,通常用来判断数据是否是集合中的一个元素,其通常用于我们需要知道某个元素是否属于某类对象的场景。具体应用见下图:

xxywRK.png

BigTable,一个分布式数据存储系统中,任何读操作都需要读取构成 Tablet 的所有 SSTables 结构。当 SSTables 结构不在内存的时候,需要进行大量的磁盘 io 才能完成一个读请求,为了减少磁盘读写次数,BigTable 使用了布隆过滤器。

2.一致性哈希

一致性哈希可以帮助开发者更加容易有效的扩展和复制数据,并且保证良好的可用性和容错能力。
每个被key标识的数据项最终都会分配到一个节点,具体计算逻辑是,通过对数据项的key值进行 hash 可以计算出该key值在环上的位置,然后沿着哈希环顺时针行走,直到发现第一个比key值的位置大的节点,发现的这个节点就是将要分配给key标识的数据项的节点。示例:

xxyDMD.png

一致性 hash 的主要优势是增量稳定性,集群中的节点删除和新增仅仅影响其相邻的节点,其他的节点不受影响。

3.Quorum

在分布式环境中,quorum指的是所有的分布式操作执行成功所需要的最小服务数目。

xxy6Zd.png

Cassanddra(一个开源分布式 NoSQL 数据库系统),为了确保数据一致性,可以配置对于每一个写请求只有当数据写入副本节点中至少一个quorum才算成功。

对于 Leader 选举,Chubby 使用Paxos,它使用quorum来保证强一致性。Raft 和 Paxos 共识算法学习参考资料:Raft user study

Dynamo复制写入数据到整个系统其他节点的一个 quorum 中,而不是像 Paxos 一样严格的大多数 quorum 中。系统的所有的读写操作都会被 preference list 上的第一个正常的节点执行,该节点不一定总是在遍历一致性哈希环是遇到的第一个节点。

4.Leader and Follower

为了确保数据管理系统的容错性,需要在集群内的多个服务器上复制数据。所以需要在该集群内选出一个服务作为leader, leader负责代表整个集群做出决策,并将决策传播到剩余的其他服务器,其他机器可以看作是follower

5.心跳

心跳机制用于检测现有的 leader 服务状态,如果不正常的话需要开始新的 leader 选举。

6.Fencing

在上面 leader-follower 模型中,当一个 leader 失败之后, 我们无法确定该 leader 是否已经停止工作。比如说,受网络环境或者网络分区的影响,可能会触发新的 leader 选举,然而此时先前的 leader 仍旧在作为一个活跃的 leader 正常工作。

Fencing是解决上面这种问题的一个解决方案。Fencing会在先前的 leader 周围设置一个屏障,这样该 leader 就无法正常访问集群资源,从而停止处理所有的读写请求。

具体实现时有以下两种实现思路:

  • 资源屏障:系统阻止以前活跃的 leader 访问执行基本任务所需的资源。
  • 节点屏障:系统阻止之前活跃的 leader 访问所有资源, 执行此操作的常用方法是关闭或重置节点。

7.预写日志

8.Segmented Log

9.High-Water mark

10.Lease

11.Gossip Protocol

12.Phi Accrual Failure Detection

13.Split-brain

14. Checksum

15.CAP Theorem

CAP 定理指出,分布式系统不可能同时具有以下三个属性:

  • 一致性,Consistency (C)
  • 可用性,Availability (A)
  • 分区容错性,Partition Tolerance(P)

根据 CAP 定理,任何分布式系统都需要满足三个属性中的两个。这三个选项是 CA、CP 和 AP。

Dynamo:该分布式系统属于 CAP 理论模型中的 AP 系统,旨在以牺牲强一致性为代价实现高可用性。

BigTable:该分布式系统属于 CAP 理论模型中的 CP 系统,它具有严格一致的读取和写入。

16.PACELEC Theorem

PACELEC 定理指出,在复制数据的分布式系统中,

  • 如果分布式系统存在网络分区(P)的情况下,必须在可用性(A)和一致性(C)之间进行选择(和 CAP 定理相同)
  • 否则(E),即使系统在没有分区的情况下正常运行,也必须在延迟(L)和一致性(C)之间进行选择

xxygII.png

定理(PAC)的第一部分与 CAP 定理相同,ELC 是扩展。整个论点假设我们通过复制来保持高可用性。因此,当系统出现故障时,选择 CAP 定理。但如果没有,我们仍然需要在复制系统的一致性和延迟之间做出选择。

17.Hinted Handoff

18.Read Repair

19.Merkle Trees

Git后台架构

在传统的 git 后端服务中,首要面对的挑战就是存储容量上限问题和用户并发访问高负载问题。本文简要概述 git 后端服务的架构,以及一些有用的文章。

后台存储方案

1. 单机+冷备模式

gitlab 社区版提供了单机版,可以通过配置 git hooks 触发同步任务,将数据保存备份到备机。
存在的问题是同步任务经常出现卡住,同步失败,对象缺失等问题,备机无法保证实时最新,也无法保证数据完整性,备机不能开启读服务。

2. 单机+分布式存储

该场景希望使用分布式存储解决存储容量上限问题的问题,这样存储可以无限扩展,具体如下所示:

xxrylD.png

通过上图可以看见只使用了一个应用集群操作分布式文件系统,试想一下若新建多个应用集群,然后使用类似 dns 轮询分发请求的方式是否可以降低用户高并发访问时的负载?
答案是否定的,Gitee 曾在该架构下发生过事故,主要原因是分布式存储系统并不适合用在 Git 这种海量小文件的场景下,因为 Git 每一次的操作都需要遍历大量的引用和对象,导致每一次操作整体耗时非常多。

3. 数据分片 + 主备模式

3.1 只分片

xxsSpT.png

上图为数据分片模式下的架构模型,其中集群内的 slave 由于仅仅是在主机写入完成后才会发起 hook 消息到从机(slave),然后 slave 复制 master 数据到 slave。显然 slave 的同步可能会失败或缺失、不完整,所以该模式下所有的读写请求都是 master 来处理。
在这种模式下一个集群只有一台主机提供服务,由于网络带宽和机器资源的限制,当集群的访问超出 master 最大 qps 时, 服务器就会出现不稳定的状况。当前 coding-git 就是运行在这种模式之下的,所以运行效率差强人意。

3.2 分片+读写分离

该点其实就是在 2.2.1 的基础上考虑,如何判定 master 和 slave 的仓库对等?也就是读到备机的数据是最新的,且完整的?最简单的一个步骤如下所示:

  1. 由 master 节点,进行写操作,结束后计算并更新当前的 depot 的 hash 值;
  2. 通过 git hook 向 slave 发起同步任务;
  3. slave 同步完 master 最新数据之后,计算并更新当前 depot 的 hash 值;
  4. 当新的读请求过来时,先查看是否有跟 master 的 depot hash 值相同的 slave,如果有,再根据负载均衡算法选择一台 slave,提供读服务。

3.3 存在的问题

当某个分片的 master 节点发生异常(服务 crash 或服务器宕机等),会导致整个分片无法正常提供服务,下面是几点可能会产生的问题,摘自阿里巴巴代码平台架构的演进之路

  1. 可用性: 由于读写操作是分离的,所以在写服务器故障期间,服务的写功能是无法使用的; 对于单片而言,写操作是单点的,一台服务波动则整个分片都波动。
  2. 性能:主备机器在同步上需要额外的时间开销。对于松散文件、文件压缩的 Git 仓库,这个耗时比单文件拷贝耗时更久。
  3. 安全: 用户侧的短时间内的瞬时操作,对于节点同步来说可能是并发的,无法保证同步中的事务顺序。
  4. 成本:同分片写,主备机器要求规格完全一致,但由于接收的请求不同,存在严重的资源消耗不均;当同步的小文件多时,对延时敏感。

4. 多读多写

该节是阿里代码平台的一个实现方案,具体特征有:多写(写时复制)、无状态服务、快速校验仓库一致性。

xxrL0s.png

参考文章

container/heap包使用指南

本文基于官方文档介绍 golang 标准库中提供的堆/优先队列的使用方法.

1. 概述

“container/heap” 包提供了实现堆操作的接口,用户只需要定义满足 “heap.Interface” 接口的类型,就可以通过包提供的函数,像操作大根堆或小根堆一样,对实例数组变量进行 PushPop 操作。
堆通常是一个可以被看做一棵树的数组对象,堆总是满足下列性质:

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树。

堆的定义如下:n 个元素的序列 {k1,k2,ki,…,kn} 当且仅当满足下关系时,称之为堆:

xxrlF0.png

下面将基于具体实例介绍"container/heap" 包的使用。

2. 整数堆

下面的代码实现了一个整数类型的最小堆:

package main

import (
    "container/heap"
    "log"
)

type IntHeap []int

// 下面几个函数必须实现,heap包会进行回调
func (h IntHeap) Len() int { return len(h) }

// Less函数的实现决定最终实现是最大堆还是最小堆
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any)        { *h = append((*h), x.(int)) }
func (h *IntHeap) Pop() any {
    old := *h
    n := len(old)
    x := old[n-1]
    *h = old[0 : n-1]
    return x
}

func main() {
    h := &IntHeap{2, 1, 5}
    heap.Init(h)
    heap.Push(h, 3)
    log.Println(*h)
    for h.Len() > 0 {
        log.Println(heap.Pop(h))
    }
}

3. 标准库堆的实现

通过整数堆的使用方式,发现是通过定义新的整数数组类型,并为其实现 pointer receivers 的方法 Push、Pop,以及 value receivers 的方法 Len、Less、Swap 方法之后,借助 “container/heap” 包提供的方法对该类型定义的几个方法进行回调,从而实现堆的功能。下面是具体的实现代码,逻辑见注释:

package heap

import "sort"

// Interface 接口指明了想要使用这个包中的方法去实现堆,应该提供的接口方法
// 在 heap.Init 方法被调用、数据为空或者原始数据有序时,满足下列条件的情况下,小根堆会被建立
// !h.Less(j, i) for 0 <= i < h.Len() and 2*i+1 <= j <= 2*i+2 and j < h.Len()
// 注意:包中的方法会在添加和删除元素的时候调用Interface 接口实现的 Push 和 Pop方法,详见下面代码
type Interface interface {
    sort.Interface // 包括 Len()、Less(i, j int)、Swap(i, j int)
    Push(x any)    // add x as element Len()
    Pop() any      // remove and return element Len() - 1.
}

// 将传入的变量初始化为堆,时间复杂度为 O(n), 其中 n = h.Len().
func Init(h Interface) {
    // heapify
    n := h.Len()
    for i := n/2 - 1; i >= 0; i-- {
        down(h, i, n)
    }
}

// 向堆中加入新的元素,时间复杂度为 O(log n), 其中 n = h.Len().
func Push(h Interface, x any) {
    h.Push(x)
    up(h, h.Len()-1)
}

// Pop 移除并返回堆中的最小或最大元素,具体根据 h.Less 函数确定,时间复杂度为 O(log n).
// Pop 等价于 Remove(h, 0).
func Pop(h Interface) any {
    n := h.Len() - 1
    h.Swap(0, n)
    down(h, 0, n)
    // 由此看出实现 h.Pop 方法时只需要将数组最后元素取出并返回即可
    return h.Pop()
}

// Remove 移除并返回堆中索引为 i 的元素,时间复杂度为 O(log n).
func Remove(h Interface, i int) any {
    n := h.Len() - 1
    if n != i {
        h.Swap(i, n)
        if !down(h, i, n) {
            up(h, i)
        }
    }
    return h.Pop()
}

// 当堆数组中索引 i 处的元素的值或优先级发生变更的时候调用 Fix 调整元素 i 在堆中的位置
func Fix(h Interface, i int) {
    if !down(h, i, h.Len()) {
        up(h, i)
    }
}

// 向上进行堆调整,将新增元素上升到满足条件的位置
func up(h Interface, j int) {
    for {
        i := (j - 1) / 2 // parent
        if i == j || !h.Less(j, i) {
            break
        }
        h.Swap(i, j)
        j = i
    }
}

// 向下进行堆调整,确保i0节点是左右子树中的最小节点
func down(h Interface, i0, n int) bool {
    i := i0
    for {
        j1 := 2*i + 1
        if j1 >= n || j1 < 0 { // j1 < 0 after int overflow
            break
        }
        j := j1 // left child
        if j2 := j1 + 1; j2 < n && h.Less(j2, j1) {
            j = j2 // = 2*i + 2  // right child
        }
        if !h.Less(j, i) {
            break
        }
        h.Swap(i, j)
        i = j
    }
    return i > i0
}

4. 优先队列

举例实现针对结构实现的最小堆,也即一般意义上的优先队列:

package main

import (
    "container/heap"
    "log"
)

type Node struct {
    Val  int
    Next float32
}

type NodeHeap []Node

func (pq NodeHeap) Less(i, j int) bool { return pq[i].Val < pq[j].Val }
func (pq NodeHeap) Swap(i, j int)      { pq[i], pq[j] = pq[j], pq[i] }
func (pq NodeHeap) Len() int           { return len(pq) }
func (pq *NodeHeap) Push(x any)        { *pq = append(*pq, x.(Node)) }
func (pq *NodeHeap) Pop() any {
    old := *pq
    n := len(old)
    x := old[n-1]
    *pq = old[0 : n-1]
    return x
}

func main() {
    pq := &NodeHeap{}
    heap.Init(pq)
    heap.Push(pq, Node{Val: 10, Next: 1.0})
    heap.Push(pq, Node{Val: 11, Next: 2.0})
    heap.Push(pq, Node{Val: 1, Next: 3.0})
    for pq.Len() > 0 {
        log.Println(heap.Pop(pq).(Node))
    }
}

5. 优先队列的应用

力扣第 23 题「 合并 K 个升序链表」,要求合并 k 个有序链表为 1 个有序列表,如何快速得到 k 个节点中的最小节点,接到结果链表上?
此时就可以使用上述实现的优先队列了,不过需要稍微改动一下结构体,实现如下:

/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
type ItemHeap []*ListNode
func (pq ItemHeap) Less(i, j int) bool { return pq[i].Val < pq[j].Val}
func (pq ItemHeap) Swap(i, j int) {pq[i], pq[j] = pq[j], pq[i]}
func (pq ItemHeap) Len() int {return len(pq)}
func (pq *ItemHeap) Push(x interface{}) { *pq = append(*pq, x.(*ListNode)) }
func (pq *ItemHeap) Pop() interface{} {
    old := *pq
    n := len(old)
    x := old[n-1]
    *pq = old[0:n-1]
    return x
}


func mergeKLists(lists []*ListNode) *ListNode {
    pq := &ItemHeap{}
    heap.Init(pq)
    for _, v := range lists {
        if v != nil {
            heap.Push(pq, v)
        }
    }
    // dummy
    dummy := &ListNode{}
    p := dummy
    for pq.Len() > 0 {
        x := heap.Pop(pq).(*ListNode)
        p.Next = x
        p = p.Next
        if x.Next != nil {
            heap.Push(pq, x.Next)
        }
    }
    return dummy.Next
}

6. 参考资料

基于腾讯云cvm的云原生环境搭建

本文所有操作基于腾讯云 cvm 实例。

具体配置如下,操作系统为 TencentOS Server 3.1:

[root@VM-0-16-centos cms]$ cat /etc/motd
Welcome to TencentOS 3 64bit
Version 3.1 20210604
tlinux3.1-64bit-5.4.119-19.0006-20210623

1.安装 vim

主要是更新 vim 的版本,使其>8.0,这样某些插件才可以正常安装。

yum remove vim vi
rm -fr /usr/local/vim /usr/bin/vim
cd /tmp
wget https://github.com/vim/vim/archive/v8.2.0000.tar.gz
tar xzf  v8.2.0000.tar.gz
cd vim-8.2.0000/
./configure --prefix=/usr/local/vim  --with-features=huge --enable-multibyte --enable-gtk3-check  --enable-rubyinterp=yes --with-python3-command=python3 --enable-python3interp=yes --enable-perlinterp=yes --enable-luainterp=yes --enable-cscope
make && make install
ln -s /usr/local/vim/bin/vim  /usr/bin/vim
vim --version #验证是否安装成功

接下来快速安装基本插件:

cd ~
git clone https://gitcode.net/qq_41345173/tvim.git
cd tvim
cp .vimrc ~/.vimrc
mkdir -p ~/.vim/
cp -r colors/ ~/.vim/
cp -r autoload/ ~/.vim/
cp -r plugged/ ~/.vim/
cd ~/.vim/plugged
unzip plugged.zip
cd ~
rm -fr tvim

相关文章: https://blog.csdn.net/qq_41345173/article/details/120381818

2.tlinux3 安装 docker

Tlinux3 和其他原生操作系统不同,有团队维护 tlinux 源,安装 docker-ce 的方式如下:

yum install tencentos-release-docker-ce
yum update
yum install docker-ce docker-ce-cli containerd.io
# 测试
docker version

配置 github 相关域名的 ip 地址,以进行加速访问 github 资源:

vim /etc/hosts
# 尾部添加如下内容
# 在网站https://www.ipaddress.com/获取最新ip
140.82.113.3 github.com
185.199.108.133 raw.githubusercontent.com
185.199.109.133 raw.githubusercontent.com
185.199.110.133 raw.githubusercontent.com
185.199.111.133 raw.githubusercontent.com

Docker-compose 安装:

curl -LO "https://github.com/docker/compose/releases/download/1.29.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

3.安装 golang

安装 golang

wget https://dl.google.com/go/go1.16.linux-amd64.tar.gz
sha256sum go1.16.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.16.linux-amd64.tar.gz
# 设置环境变量,在~/.bashrc中添加如下配置
export PATH=$PATH:/usr/local/go/bin
export GO111MODULE=on
export GOPROXY=https://goproxy.cn,direct
export GOSUMDB=off
export GOPATH=/root/go
export PATH=$PATH:$GOPATH/bin

安装 proto 依赖,便于进行 RPC 开发

git clone https://github.com/protocolbuffers/protobuf
# v3.6.0+以上版本支持map解析,syntax=2、3消息序列化后是二进制兼容的,用root执行以下命令
cd protobuf
./autogen.sh
./configure
make
make install
ldconfig
protoc --version # 检测是否安装成功
go get github.com/golang/protobuf # 安装依赖包
# 安装gprc protocol插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# 安装grpc-gateway插件
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest

4.安装 nginx

通过以下命令安装和启动 nginx:

yum install nginx
systemctl start nginx
# nginx默认监听端口为80,安装后可以通过/etc/nginx/nginx.conf配置文件修改监听的端口
# systemctl reload nginx可以一键重启nginx服务

5.安装 nodejs

首先在官网下载页面获取下载链接,然后下载安装:

mkdir -p /usr/local/nodejs
cd /tmp
curl -fLO https://nodejs.org/dist/v16.16.0/node-v16.16.0-linux-x64.tar.xz
tar -xJvf node-v16.16.0-linux-x64.tar.xz -C /usr/local/nodejs
echo 'export PATH=/usr/local/nodejs/node-v16.16.0-linux-x64.tar.xz/bin:$PATH' >> ~/.bashrc
source ~/.bashrc

6.安装 commitizen 工具

commitizen 工具是规范化 git 提交信息的 node 插件,安装流程如下:

npm install -g commitizen
echo '{ "path": "cz-conventional-changelog" }' >> ~/.czrc

Linux入门基础

  • 能简单使用 cat,grep,cut 等命令进行一些操作;
  • 文件系统相关的原理,inode 和 block 等概念,数据恢复;
  • 硬链接与软链接;
  • 进程管理相关,僵尸进程与孤儿进程,SIGCHLD 。

一、常用操作以及概念

快捷键

  • Tab:命令和文件名补全;
  • Ctrl+C:中断正在运行的程序;
  • Ctrl+D:结束键盘输入(End Of File,EOF)

求助

1. –help

指令的基本用法与选项介绍。

2. man

man 是 manual 的缩写,将指令的具体信息显示出来。

当执行 man date 时,有 DATE(1) 出现,其中的数字代表指令的类型,常用的数字及其类型如下:

代号 类型
1 用户在 shell 环境中可以操作的指令或者可执行文件
5 配置文件
8 系统管理员可以使用的管理指令

3. info

info 与 man 类似,但是 info 将文档分成一个个页面,每个页面可以跳转。

4. doc

/usr/share/doc 存放着软件的一整套说明文件。

关机

1. who

在关机前需要先使用 who 命令查看有没有其它用户在线。

2. sync

为了加快对磁盘文件的读写速度,位于内存中的文件数据不会立即同步到磁盘,因此关机之前需要先进行 sync 同步操作。

3. shutdown

## shutdown [-krhc] 时间 [信息]
-k : 不会关机,只是发送警告信息,通知所有在线的用户
-r : 将系统的服务停掉后就重新启动
-h : 将系统的服务停掉后就立即关机
-c : 取消已经在进行的 shutdown

PATH

可以在环境变量 PATH 中声明可执行文件的路径,路径之间用 : 分隔。

/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin

sudo

sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。

包管理工具

RPM 和 DPKG 为最常见的两类软件包管理工具:

  • RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为许多 Linux 系统的既定软件标准。YUM 基于 RPM,具有依赖管理和软件升级功能。
  • 与 RPM 竞争的是基于 Debian 操作系统的 DEB 软件包管理工具 DPKG,全称为 Debian Package,功能方面与 RPM 相似。

发行版

Linux 发行版是 Linux 内核及各种应用软件的集成版本。

基于的包管理工具 商业发行版 社区发行版
RPM Red Hat Fedora / CentOS
DPKG Ubuntu Debian

VIM 三个模式


  • 一般指令模式(Command mode):VIM 的默认模式,可以用于移动游标查看内容;
  • 编辑模式(Insert mode):按下 “i” 等按键之后进入,可以对文本进行编辑;
  • 指令列模式(Bottom-line mode):按下 “:” 按键之后进入,用于保存退出等操作。

在指令列模式下,有以下命令用于离开或者保存文件。

命令 作用
:w 写入磁盘
:w! 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关
:q 离开
:q! 强制离开不保存
:wq 写入磁盘后离开
:wq! 强制写入磁盘后离开

GNU

GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU,其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议(GNU General Public License),包含了以下内容:

  • 以任何目的运行此程序的自由;
  • 再复制的自由;
  • 改进此程序,并公开发布改进的自由。

开源协议

二、磁盘

磁盘接口

1. IDE

IDE(ATA)全称 Advanced Technology Attachment,接口速度最大为 133MB/s,因为并口线的抗干扰性太差,且排线占用空间较大,不利电脑内部散热,已逐渐被 SATA 所取代。


2. SATA

SATA 全称 Serial ATA,也就是使用串口的 ATA 接口,抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能。SATA-II 的接口速度为 300MB/s,而 SATA-III 标准可达到 600MB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。


3. SCSI

SCSI 全称是 Small Computer System Interface(小型机系统接口),SCSI 硬盘广为工作站以及个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。


4. SAS

SAS(Serial Attached SCSI)是新一代的 SCSI 技术,和 SATA 硬盘相同,都是采取序列式技术以获得更高的传输速度,可达到 6Gb/s。此外也通过缩小连接线改善系统内部空间等。


磁盘的文件名

Linux 中每个硬件都被当做一个文件,包括磁盘。磁盘以磁盘接口类型进行命名,常见磁盘的文件名如下:

  • IDE 磁盘:/dev/hd[a-d]
  • SATA/SCSI/SAS 磁盘:/dev/sd[a-p]

其中文件名后面的序号的确定与系统检测到磁盘的顺序有关,而与磁盘所插入的插槽位置无关。

三、分区

分区表

磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。

1. MBR

MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中主要开机记录占 446 bytes,分区表占 64 bytes。

分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它使用其它扇区来记录额外的分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。

Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名 + 编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。

2. GPT

扇区是磁盘的最小存储单位,旧磁盘的扇区大小通常为 512 bytes,而最新的磁盘支持 4 k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA),LBA 默认大小为 512 bytes。

GPT 第 1 个区块记录了主要开机记录(MBR),紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录,这个部份纪录了分区表本身的位置与大小和备份分区的位置,同时放置了分区表的校验码 (CRC32),操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误,可以使用备份分区进行恢复。

GPT 没有扩展分区概念,都是主分区,每个 LBA 可以分 4 个分区,因此总共可以分 4 * 32 = 128 个分区。

MBR 不支持 2.2 TB 以上的硬盘,GPT 则最多支持到 233 TB = 8 ZB。


开机检测程序

1. BIOS

BIOS(Basic Input/Output System,基本输入输出系统),它是一个固件(嵌入在硬件中的软件),BIOS 程序存放在断电后内容不会丢失的只读内存中。


BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的主要开机记录(MBR),由主要开机记录(MBR)执行其中的开机管理程序,这个开机管理程序会加载操作系统的核心文件。

主要开机记录(MBR)中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。

下图中,第一扇区的主要开机记录(MBR)中的开机管理程序提供了两个选单:M1、M2,M1 指向了 Windows 操作系统,而 M2 指向其它分区的启动扇区,里面包含了另外一个开机管理程序,提供了一个指向 Linux 的选单。


安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉主要开机记录(MBR),而 Linux 可以选择将开机管理程序安装在主要开机记录(MBR)或者其它分区的启动扇区,并且可以设置开机管理程序的选单。

2. UEFI

BIOS 不可以读取 GPT 分区表,而 UEFI 可以。

四、文件系统

分区与文件系统

对分区进行格式化是为了在分区上建立文件系统。一个分区通常只能格式化为一个文件系统,但是磁盘阵列等技术可以将一个分区格式化为多个文件系统。

组成

最主要的几个组成部分如下:

  • inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号;
  • block:记录文件的内容,文件太大时,会占用多个 block。

除此之外还包括:

  • superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等;
  • block bitmap:记录 block 是否被使用的位图。

文件读取

对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中查找文件内容所在的所有 block,然后把所有 block 的内容读出来。


而对于 FAT 文件系统,它没有 inode,每个 block 中存储着下一个 block 的编号。


磁盘碎片

指一个文件内容所在的 block 过于分散,导致磁盘磁头移动距离过大,从而降低磁盘读写性能。

block

在 Ext2 文件系统中所支持的 block 大小有 1K,2K 及 4K 三种,不同的大小限制了单个文件和文件系统的最大大小。

大小 1KB 2KB 4KB
最大单一文件 16GB 256GB 2TB
最大文件系统 2TB 8TB 16TB

一个 block 只能被一个文件所使用,未使用的部分直接浪费了。因此如果需要存储大量的小文件,那么最好选用比较小的 block。

inode

inode 具体包含以下信息:

  • 权限 (read/write/excute);
  • 拥有者与群组 (owner/group);
  • 容量;
  • 建立或状态改变的时间 (ctime);
  • 最近读取时间 (atime);
  • 最近修改时间 (mtime);
  • 定义文件特性的旗标 (flag),如 SetUID…;
  • 该文件真正内容的指向 (pointer)。

inode 具有以下特点:

  • 每个 inode 大小均固定为 128 bytes (新的 ext4 与 xfs 可设定到 256 bytes);
  • 每个文件都仅会占用一个 inode。

inode 中记录了文件内容所在的 block 编号,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用让 inode 记录的引用 block 块记录引用信息。


目录

建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。

可以看到文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的写权限有关。

日志

如果突然断电,那么文件系统会发生错误,例如断电前只修改了 block bitmap,而还没有将数据真正写入 block 中。

ext3/ext4 文件系统引入了日志功能,可以利用日志来修复文件系统。

挂载

挂载利用目录作为文件系统的进入点,也就是说,进入目录之后就可以读取文件系统的数据。

目录配置

为了使不同 Linux 发行版本的目录结构保持一致性,Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下:

  • / (root, 根目录)
  • /usr (unix software resource):所有系统默认软件都会安装到这个目录;
  • /var (variable):存放系统或程序运行过程中的数据文件。

五、文件

文件属性

用户分为三种:文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。

使用 ls 查看一个文件时,会显示一个文件的信息,例如 drwxr-xr-x 3 root root 17 May 6 00:14 .config,对这个信息的解释如下:

  • drwxr-xr-x:文件类型以及权限,第 1 位为文件类型字段,后 9 位为文件权限字段
  • 3:链接数
  • root:文件拥有者
  • root:所属群组
  • 17:文件大小
  • May 6 00:14:文件最后被修改的时间
  • .config:文件名

常见的文件类型及其含义有:

  • d:目录
  • -:文件
  • l:链接文件

9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。

文件时间有以下三种:

  • modification time (mtime):文件的内容更新就会更新;
  • status time (ctime):文件的状态(权限、属性)更新就会更新;
  • access time (atime):读取文件时就会更新。

文件与目录的基本操作

1. ls

列出文件或者目录的信息,目录的信息就是其中包含的文件。

## ls [-aAdfFhilnrRSt] file|dir
-a :列出全部的文件
-d :仅列出目录本身
-l :以长数据串行列出,包含文件的属性与权限等等数据

2. cd

更换当前目录。

cd [相对路径或绝对路径]

3. mkdir

创建目录。

## mkdir [-mp] 目录名称
-m :配置目录权限
-p :递归创建目录

4. rmdir

删除目录,目录必须为空。

rmdir [-p] 目录名称
-p :递归删除目录

5. touch

更新文件时间或者建立新文件。

## touch [-acdmt] filename
-a : 更新 atime
-c : 更新 ctime,若该文件不存在则不建立新文件
-m : 更新 mtime
-d : 后面可以接更新日期而不使用当前日期,也可以使用 --date="日期或时间"
-t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm]

6. cp

复制文件。如果源文件有两个以上,则目的文件一定要是目录才行。

cp [-adfilprsu] source destination
-a :相当于 -dr --preserve=all
-d :若来源文件为链接文件,则复制链接文件属性而非文件本身
-i :若目标文件已经存在时,在覆盖前会先询问
-p :连同文件的属性一起复制过去
-r :递归复制
-u :destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制
--preserve=all :除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了

7. rm

删除文件。

## rm [-fir] 文件或目录
-r :递归删除

8. mv

移动文件。

## mv [-fiu] source destination
## mv [options] source1 source2 source3 .... directory
-f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖

修改权限

可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1,即每个权限对应的数字权值为 r : 4、w : 2、x : 1。

## chmod [-R] xyz dirname/filename

示例:将 .bashrc 文件的权限修改为 -rwxr-xr–。

## chmod 754 .bashrc

也可以使用符号来设定权限。

## chmod [ugoa]  [+-=] [rwx] dirname/filename
- u:拥有者
- g:所属群组
- o:其他人
- a:所有人
- +:添加权限
- -:移除权限
- =:设定权限

示例:为 .bashrc 文件的所有用户添加写权限。

## chmod a+w .bashrc

默认权限

  • 文件默认权限:文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw- 。
  • 目录默认权限:目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。

可以通过 umask 设置或者查看默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r–。

目录的权限

文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。

目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。

链接


## ln [-sf] source_filename dist_filename
-s :默认是实体链接,加 -s 为符号链接
-f :如果目标文件存在时,先删除目标文件

1. 实体链接

在目录下创建一个条目,记录着文件名与 inode 编号,这个 inode 就是源文件的 inode。

删除任意一个条目,文件还是存在,只要引用数量不为 0。

有以下限制:不能跨越文件系统、不能对目录进行链接。

## ln /etc/crontab .
## ll -i /etc/crontab crontab
34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab
34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab

2. 符号链接

符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 Windows 的快捷方式。

当源文件被删除了,链接文件就打不开了。

因为记录的是路径,所以可以为目录建立符号链接。

## ll -i /etc/crontab /root/crontab2
34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab
53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab

获取文件内容

1. cat

取得文件内容。

## cat [-AbEnTv] filename
-n :打印出行号,连同空白行也会有行号,-b 不会

2. tac

是 cat 的反向操作,从最后一行开始打印。

3. more

和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。

4. less

和 more 类似,但是多了一个向前翻页的功能。

5. head

取得文件前几行。

## head [-n number] filename
-n :后面接数字,代表显示几行的意思

6. tail

是 head 的反向操作,只是取得是后几行。

7. od

以字符或者十六进制的形式显示二进制文件。

指令与文件搜索

1. which

指令搜索。

## which [-a] command
-a :将所有指令列出,而不是只列第一个

2. whereis

文件搜索。速度比较快,因为它只搜索几个特定的目录。

## whereis [-bmsu] dirname/filename

3. locate

文件搜索。可以用关键字或者正则表达式进行搜索。

locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。

## locate [-ir] keyword
-r:正则表达式

4. find

文件搜索。可以使用文件的属性和权限进行搜索。

## find [basedir] [option]
example: find . -name "shadow*"
① 与时间有关的选项
-mtime  n :列出在 n 天前的那一天修改过内容的文件
-mtime +n :列出在 n 天之前 (不含 n 天本身) 修改过内容的文件
-mtime -n :列出在 n 天之内 (含 n 天本身) 修改过内容的文件
-newer file : 列出比 file 更新的文件

+4、4 和 -4 的指示的时间范围如下:


② 与文件拥有者和所属群组有关的选项
-uid n
-gid n
-user name
-group name
-nouser :搜索拥有者不存在 /etc/passwd 的文件
-nogroup:搜索所属群组不存在于 /etc/group 的文件
③ 与文件权限和名称有关的选项
-name filename
-size [+-]SIZE:搜寻比 SIZE 还要大 (+) 或小 (-) 的文件。这个 SIZE 的规格有:c: 代表 byte,k: 代表 1024bytes。所以,要找比 50KB 还要大的文件,就是 -size +50k
-type TYPE
-perm mode  :搜索权限等于 mode 的文件
-perm -mode :搜索权限包含 mode 的文件
-perm /mode :搜索权限包含任一 mode 的文件

六、压缩与打包

压缩文件名

Linux 底下有很多压缩文件名,常见的如下:

扩展名 压缩程序
*.Z compress
*.zip zip
*.gz gzip
*.bz2 bzip2
*.xz xz
*.tar tar 程序打包的数据,没有经过压缩
*.tar.gz tar 程序打包的文件,经过 gzip 的压缩
*.tar.bz2 tar 程序打包的文件,经过 bzip2 的压缩
*.tar.xz tar 程序打包的文件,经过 xz 的压缩

压缩指令

1. gzip

gzip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。

经过 gzip 压缩过,源文件就不存在了。

有 9 个不同的压缩等级可以使用。

可以使用 zcat、zmore、zless 来读取压缩文件的内容。

$ gzip [-cdtv#] filename
-c :将压缩的数据输出到屏幕上
-d :解压缩
-t :检验压缩文件是否出错
-v :显示压缩比等信息
-# : # 为数字的意思,代表压缩等级,数字越大压缩比越高,默认为 6

2. bzip2

提供比 gzip 更高的压缩比。

查看命令:bzcat、bzmore、bzless、bzgrep。

$ bzip2 [-cdkzv#] filename
-k :保留源文件

3. xz

提供比 bzip2 更佳的压缩比。

可以看到,gzip、bzip2、xz 的压缩比不断优化。不过要注意的是,压缩比越高,压缩的时间也越长。

查看命令:xzcat、xzmore、xzless、xzgrep。

xz [-dtlkc#] filename

打包

压缩指令只能对一个文件进行压缩,而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gzip、bzip2、xz 将打包文件进行压缩。

$ tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename...  ==打包压缩
$ tar [-z|-j|-J] [tv] [-f 已有的 tar 文件]              ==查看
$ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录]    ==解压缩
-z :使用 zip;
-j :使用 bzip2;
-J :使用 xz;
-c :新建打包文件;
-t :查看打包文件里面有哪些文件;
-x :解打包或解压缩的功能;
-v :在压缩/解压缩的过程中,显示正在处理的文件名;
-f : filename:要处理的文件;
-C 目录 : 在特定目录解压缩。
使用方式 命令
打包压缩 tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称
查 看 tar -jtv -f filename.tar.bz2
解压缩 tar -jxv -f filename.tar.bz2 -C 要解压缩的目录

七、Bash

可以通过 Shell 请求内核提供服务,Bash 正是 Shell 的一种。

特性

  • 命令历史:记录使用过的命令
  • 命令与文件补全:快捷键:tab
  • 命名别名:例如 ll 是 ls -al 的别名
  • shell scripts
  • 通配符:例如 ls -l /usr/bin/X* 列出 /usr/bin 下面所有以 X 开头的文件

变量操作

对一个变量赋值直接使用 =。

对变量取用需要在变量前加上 $ ,也可以用 ${} 的形式;

输出变量使用 echo 命令。

x=abc
echo $x
echo ${x}

变量内容如果有空格,必须使用双引号或者单引号。

  • 双引号内的特殊字符可以保留原本特性,例如 x=“lang is $LANG”,则 x 的值为 lang is zh_TW.UTF-8;
  • 单引号内的特殊字符就是特殊字符本身,例如 x=‘lang is $LANG’,则 x 的值为 lang is $LANG。

可以使用 `指令` 或者 $(指令) 的方式将指令的执行结果赋值给变量。例如 version=$(uname -r),则 version 的值为 4.15.0-22-generic。

可以使用 export 命令将自定义变量转成环境变量,环境变量可以在子程序中使用,所谓子程序就是由当前 Bash 而产生的子 Bash。

Bash 的变量可以声明为数组和整数数字。注意数字类型没有浮点数。如果不进行声明,默认是字符串类型。变量的声明使用 declare 命令:

$ declare [-aixr] variable
-a : 定义为数组类型
-i : 定义为整数类型
-x : 定义为环境变量
-r : 定义为 readonly 类型

使用 [ ] 来对数组进行索引操作:

array[1]=a
array[2]=b
echo ${array[1]}

指令搜索顺序

  • 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls ;
  • 由别名找到该指令来执行;
  • 由 Bash 内置的指令来执行;
  • 按 $PATH 变量指定的搜索路径的顺序找到第一个指令来执行。

数据流重定向

重定向指的是使用文件代替标准输入、标准输出和标准错误输出。

1 代码 运算符
标准输入 (stdin) 0 < 或 <<
标准输出 (stdout) 1 > 或 >>
标准错误输出 (stderr) 2 2> 或 2>>

其中,有一个箭头的表示以覆盖的方式重定向,而有两个箭头的表示以追加的方式重定向。

可以将不需要的标准输出以及标准错误输出重定向到 /dev/null,相当于扔进垃圾箱。

如果需要将标准输出以及标准错误输出同时重定向到一个文件,需要将某个输出转换为另一个输出,例如 2>&1 表示将标准错误输出转换为标准输出。

find /home -name .bashrc > list 2>&1

八、管道指令

管道是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管道。

在命令之间使用 | 分隔各个管道命令。

ls -al /etc | less

提取指令

cut 对数据进行切分,取出想要的部分。

切分过程一行一行地进行。

$ cut
-d :分隔符
-f :经过 -d 分隔后,使用 -f n 取出第 n 个区间
-c :以字符为单位取出区间

示例 1:last 显示登入者的信息,取出用户名。

$ last
root pts/1 192.168.201.101 Sat Feb 7 12:35 still logged in
root pts/1 192.168.201.101 Fri Feb 6 12:13 - 18:46 (06:33)
root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16)

$ last | cut -d ' ' -f 1

示例 2:将 export 输出的信息,取出第 12 字符以后的所有字符串。

$ export
declare -x HISTCONTROL="ignoredups"
declare -x HISTSIZE="1000"
declare -x HOME="/home/dmtsai"
declare -x HOSTNAME="study.centos.vbird"
.....(其他省略).....

$ export | cut -c 12-

排序指令

sort 用于排序。

$ sort [-fbMnrtuk] [file or stdin]
-f :忽略大小写
-b :忽略最前面的空格
-M :以月份的名字来排序,例如 JAN,DEC
-n :使用数字
-r :反向排序
-u :相当于 unique,重复的内容只出现一次
-t :分隔符,默认为 tab
-k :指定排序的区间

示例:/etc/passwd 文件内容以 : 来分隔,要求以第三列进行排序。

$ cat /etc/passwd | sort -t ':' -k 3
root❌0:0:root:/root:/bin/bash
dmtsai❌1000:1000:dmtsai:/home/dmtsai:/bin/bash
alex❌1001:1002::/home/alex:/bin/bash
arod❌1002:1003::/home/arod:/bin/bash

uniq 可以将重复的数据只取一个。

$ uniq [-ic]
-i :忽略大小写
-c :进行计数

示例:取得每个人的登录总次数

$ last | cut -d ' ' -f 1 | sort | uniq -c
1
6 (unknown
47 dmtsai
4 reboot
7 root
1 wtmp

双向输出重定向

输出重定向会将输出内容重定向到文件中,而 tee 不仅能够完成这个功能,还能保留屏幕上的输出。也就是说,使用 tee 指令,一个输出会同时传送到文件和屏幕上。

tee [-a] file

字符转换指令

tr 用来删除一行中的字符,或者对字符进行替换。

$ tr [-ds] SET1 ...
-d : 删除行中 SET1 这个字符串

示例,将 last 输出的信息所有小写转换为大写。

last | tr '[a-z]' '[A-Z]'

col 将 tab 字符转为空格字符。

$ col [-xb]
-x : 将 tab 键转换成对等的空格键

expand 将 tab 转换一定数量的空格,默认是 8 个。

$ expand [-t] file
-t :tab 转为空格的数量

join 将有相同数据的那一行合并在一起。

$ join [-ti12] file1 file2
-t :分隔符,默认为空格
-i :忽略大小写的差异
-1 :第一个文件所用的比较字段
-2 :第二个文件所用的比较字段

paste 直接将两行粘贴在一起。

$ paste [-d] file1 file2
-d :分隔符,默认为 tab

分区指令

split 将一个文件划分成多个文件。

$ split [-bl] file PREFIX
-b :以大小来进行分区,可加单位,例如 b, k, m 等
-l :以行数来进行分区。
- PREFIX :分区文件的前导名称

九、正则表达式

grep

g/re/p(globally search a regular expression and print),使用正则表示式进行全局查找并打印。

$ grep [-acinv] [--color=auto] 搜寻字符串 filename
-c : 统计匹配到行的个数
-i : 忽略大小写
-n : 输出行号
-v : 反向选择,也就是显示出没有 搜寻字符串 内容的那一行
--color=auto :找到的关键字加颜色显示

示例:把含有 the 字符串的行提取出来(注意默认会有 –color=auto 选项,因此以下内容在 Linux 中有颜色显示 the 字符串)

$ grep -n 'the' regular_express.txt
8:I can't finish the test.
12:the symbol '*' is represented as start.
15:You are the best is mean you are the no. 1.
16:The world Happy is the same with "glad".
18:google is the best tools for search keyword

示例:正则表达式 a{m,n} 用来匹配字符 a m~n 次,这里需要将 { 和 } 进行转义,因为它们在 shell 是有特殊意义的。

grep -n 'a\{2,5\}' regular_express.txt

printf

用于格式化输出。它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。

$ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt)
    DmTsai    80    60    92    77.33
     VBird    75    55    80    70.00
       Ken    60    90    70    73.33

awk

是由 Alfred Aho,Peter Weinberger 和 Brian Kernighan 创造,awk 这个名字就是这三个创始人名字的首字母。

awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:$n,n 为字段号,从 1 开始,$0 表示一整行。

示例:取出最近五个登录用户的用户名和 IP。首先用 last -n 5 取出用最近五个登录用户的所有信息,可以看到用户名和 IP 分别在第 1 列和第 3 列,我们用 $1 和 $3 就能取出这两个字段,然后用 print 进行打印。

$ last -n 5
dmtsai pts/0 192.168.1.100 Tue Jul 14 17:32 still logged in
dmtsai pts/0 192.168.1.100 Thu Jul 9 23:36 - 02:58 (03:22)
dmtsai pts/0 192.168.1.100 Thu Jul 9 17:23 - 23:36 (06:12)
dmtsai pts/0 192.168.1.100 Thu Jul 9 08:02 - 08:17 (00:14)
dmtsai tty1 Fri May 29 11:55 - 12:11 (00:15)
$ last -n 5 | awk '{print $1 "\t" $3}'
dmtsai   192.168.1.100
dmtsai   192.168.1.100
dmtsai   192.168.1.100
dmtsai   192.168.1.100
dmtsai   Fri

可以根据字段的某些条件进行匹配,例如匹配字段小于某个值的那一行数据。

awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename

示例:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。

$ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}'
root 0
bin 1
daemon 2

awk 变量:

变量名称 代表意义
NF 每一行拥有的字段总数
NR 目前所处理的是第几行数据
FS 目前的分隔字符,默认是空格键

示例:显示正在处理的行号以及每一行有多少字段

$ last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}'
dmtsai lines: 1 columns: 10
dmtsai lines: 2 columns: 10
dmtsai lines: 3 columns: 10
dmtsai lines: 4 columns: 10
dmtsai lines: 5 columns: 9

十、进程管理

查看进程

1. ps

查看某个时间点的进程信息。

示例:查看自己的进程

## ps -l

示例:查看系统所有进程

## ps aux

示例:查看特定的进程

## ps aux | grep threadx

2. pstree

查看进程树。

示例:查看所有进程树

## pstree -A

3. top

实时显示进程信息。

示例:两秒钟刷新一次

## top -d 2

4. netstat

查看占用端口的进程

示例:查看特定端口的进程

## netstat -anp | grep port

进程状态

状态 说明
R running or runnable (on run queue)
正在执行或者可执行,此时进程位于执行队列中。
D uninterruptible sleep (usually I/O)
不可中断阻塞,通常为 IO 阻塞。
S interruptible sleep (waiting for an event to complete)
可中断阻塞,此时进程正在等待某个事件完成。
Z zombie (terminated but not reaped by its parent)
僵死,进程已经终止但是尚未被其父进程获取信息。
T stopped (either by a job control signal or because it is being traced)
结束,进程既可以被作业控制信号结束,也可能是正在被追踪。


SIGCHLD

当一个子进程改变了它的状态时(停止运行,继续运行或者退出),有两件事会发生在父进程中:

  • 得到 SIGCHLD 信号;
  • waitpid() 或者 wait() 调用会返回。

其中子进程发送的 SIGCHLD 信号包含了子进程的信息,比如进程 ID、进程状态、进程使用 CPU 的时间等。

在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。


wait()

pid_t wait(int *status)

父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,之后 wait() 函数会销毁子进程并返回。

如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 -1,同时 errno 被置为 ECHILD。

参数 status 用来保存被收集的子进程退出时的一些状态,如果对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL。

waitpid()

pid_t waitpid(pid_t pid, int *status, int options)

作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。

pid 参数指示一个子进程的 ID,表示只关心这个子进程退出的 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。

options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。

孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。

孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。

由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。

僵尸进程

一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。

僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。

系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。

要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 进程所收养,这样 init 进程就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。

参考资料

Python爬虫学习

Python 爬虫入门学习。

Python 爬虫

1. 准备环境

借助 requests 网络库请求 html 页面,而 beautifulsoup4 库则是一个可以从 HTMLXML 文件中提取数据的 Python 库, 然后安装 lxml 解析器,BeautifulSoup 可以使用它来解析 HTML,然后提取内容。

pip install requests pip install beautifulsoup4 pip install lxml

2. BeautifulSoup 库的使用

3. 参考资料

Python基础学习

Linux 环境下的 Python 开发基础教程。

一、开发环境

1.安装依赖库

yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel

2.下载安装包

# wget下载安装包并解压至 /usr/local/python3
wget https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tgz
tar -C /usr/local -xzvf Python-3.7.0.tgz
cd /usr/local/Python-3.7.0

3.配置与安装

./configure --prefix=/usr/local/python3
make && make install
# 上面的参数,--prefix 是预期安装目录,--enable-optimizations 是优化选项(LTO,PGO 等)加上这个 flag 编译后,性能有 10% 左右的优化,但是这会明显的增加编译时间。
# 安装成功会提示如下消息:
Collecting setuptools
Collecting pip
Installing collected packages: setuptools, pip
Successfully installed pip-10.0.1 setuptools-39.0.1

4.创建软链接

ln -s /usr/local/python3/bin/python3.7 /usr/local/bin/python3
ln -s /usr/local/python3/bin/pip3.7 /usr/local/bin/pip3

5.正确使用 vim 编写第一个 python 程序

首先在 ~/.vimrc 配置 python 缩进:autocmd FileType python set tabstop=4 | set expandtab | set autoindent
示例程序:

#!/usr/local/bin/python3
# -*- coding:utf-8 -*-
for x in range(20):
    if x % 2 == 0:
        print(x)

6.可变对象与不可变对象

二、运算符

1. 格式化输出——%s 与 f’{exp}'

格式化字符串除了%s,还可以写为 f’{表达式}'

name = 'Tom'
years = 1998
print('name = %s, Year = %d' % (name, years))
print(f'name = {name}, Year = {years}')

2. 数据类型转换

2.1 input

  • 当程序执⾏到 input ,等待⽤户输⼊,输⼊完成之后才继续向下执⾏。
  • 在 Python 中, input 接收⽤户输⼊后,⼀般存储到变量,⽅便使⽤。
  • 在 Python 中, input 会把接收到的任意⽤户输⼊的数据都当做字符串处理。

2.2 转换数据类型的函数

函数 说明
int(x [,base ]) 将 x 转换为⼀个整数
float(x ) 将 x 转换为⼀个浮点数
str(x ) 将对象 x 转换为字符串
eval(str ) ⽤来计算在字符串中的有效 Python 表达式,并返回⼀个对象
tuple(s ) 将序列 s 转换为⼀个元组
list(s ) 将序列 s 转换为⼀个列表
chr(x ) 将⼀个整数转换为⼀个 Unicode 字符
ord(x ) 将⼀个字符转换为它的 ASCII 整数值
hex(x ) 将⼀个整数转换为⼀个⼗六进制字符串
oct(x ) 将⼀个整数转换为⼀个⼋进制字符串
bin(x ) 将⼀个整数转换为⼀个⼆进制字符串
complex(real [,imag ]) 创建⼀个复数,real 为实部,imag 为虚部
repr(x ) 将对象 x 转换为表达式字符串

2.3 示例

# 1. float() -- 转换成浮点型
num1 = 1
print(float(num1))
print(type(float(num1)))
# 2. str() -- 转换成字符串类型
num2 = 10
print(type(str(num2)))
# 3. tuple() -- 将⼀个序列转换成元组
list1 = [10, 20, 30]
print(tuple(list1))
print(type(tuple(list1)))
# 4. eval() -- 将字符串中的数据转换成Python表达式原本类型
str1 = '10'
str2 = '[1, 2, 3]'
str3 = '(1000, 2000, 3000)'
print(type(eval(str1)))
print(type(eval(str2)))
print(type(eval(str3)))

3. 运算符

3.1 分类

  • 算数运算符
  • 赋值运算符
  • 复合赋值运算符
  • ⽐较运算符
  • 逻辑运算符

3.2 py 风格的运算符

算数运算符

运算符 描述 实例
// 整除 9 // 4 输出结果为 2
** 指数 2 ** 4 输出结果为 16,即 2*2*2*2

逻辑运算符

运算符 描述 实例
and 逻辑与 True and False, 返回 False
or 逻辑或 False or True, 返回 True
not 逻辑非 not True 返回 False

3.3 拓展

数字之间的逻辑运算
and 运算符,只要有⼀个值为 0,则结果为 0,否则结果为最后⼀个⾮ 0 数字
or 运算符,只有所有值为 0 结果才为 0,否则结果为第⼀个⾮ 0 数字

a = 0
b = 1
c = 2
# and运算符,只要有⼀个值为0,则结果为0,否则结果为最后⼀个⾮0数字
print(a and b)  # 0
print(b and c)  # 2
print(c and b)  # 1
# or运算符,只有所有值为0结果才为0,否则结果为第⼀个⾮0数字
print(a or b)  # 1
print(a or c)  # 2
print(b or c)  # 1

三、字符串

1. 修改字符串大小写

firstName = 'ada'
lastName = 'LOVER'
print(firstName.title() + ' ' + lastName.title())  # Ada Lover
print(firstName.upper())  # ADA
print(lastName.lower())  # lover

2. 删除空白

favoriteLan = ' python3 '
print(favoriteLan.lstrip())  # 'python3 '
print(favoriteLan.rstrip())  # ' python3'
print(favoriteLan.strip())  # 'python3'

3. 字符串长度

favoriteLan = ' python3 '
print(favoriteLan.__len__())  # 9
print(len(favoriteLan))  # 9

4. if 示例程序

cars = ['audi', 'bmw', 'subaru', 'toyota']

for car in cars:
    if car == 'bmw':
        print(car.upper())
    else:
        print(car.title())

5. 条件测试

and, or, in, not in

cars = ['audi', 'bmw', 'subaru', 'toyota']
# and
if cars[0].lower() == 'audi' and cars[1] == 'bo':
    print(True)  # True
elif cars[1] == 'bmw':
    print(cars[1].title())  # Bmw
# in
print('audi' in cars)  # True
print('bo' in cars)  # False
# not in
print('bo' not in cars)  # True

四、列表

1. 列表

bicycles = ['trek', 'cannondale', 'redline', 'speci']
print(bicycles)  # ['trek', 'cannondale', 'redline', 'speci']

2. 增加元素

# add
bicycles.append('hongqi')
print(bicycles)  # ['trek', 'cannondale', 'redline', 'speci', 'hongqi']
# insert
bicycles.insert(0,'beijing')
print(bicycles)  # ['beijing', 'trek', 'cannondale', 'redline', 'speci', 'hongqi']

3. 删除元素

bicycles = ['trek', 'redline', 'speci']
print(bicycles)  # ['trek', 'redline', 'speci']

3.1 使用 del 语句删除元素

del bicycles[0]
print(bicycles)  # ['redline', 'speci']

3.2 使用方法 pop 删除元素

bics = bicycles.pop(1)
print(bics)  # redline
print(bicycles)  # ['trek', 'speci']
bicycles.pop()
print(bicycles)  # ['trek']

3.3 根据值删除元素

bicycles.remove('speci')
print(bicycles)  # ['trek', 'redline']

remove 方法只删除第一个指定的值

4. 组织列表

4.1 sort 永久性排序

bicycles.sort()
print(bicycles)  # ['redline', 'speci', 'trek']
bicycles.sort(reverse=True)
print(bicycles)  # ['trek', 'speci', 'redline']

4.2 sorted 函数临时对列表排序

print(sorted(bicycles))
print(sorted(bicycles, reverse=True))

4.3 reverse 方法反转列表

bicycles.reverse()
print(bicycles)  # ['speci', 'redline', 'trek']

5. 操作列表

5.1 列表解析

列表解析将 for 循环和创建新元素的代码合并为一行,并自动附加新元素

numbers = [value ** 2 for value in range(1, 11)]
print(numbers)  # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

5.2 列表切片

指定范围切片:

numbers = [value ** 2 for value in range(1, 11)]
print(numbers)  # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
print(numbers[0:2])  # [1, 4]
print(numbers[:4])  # [1, 4, 9, 16]
print(numbers[5:])  # [36, 49, 64, 81, 100]
print(numbers[-3:])  # [64, 81, 100]

遍历切片:

numbers = [value ** 2 for value in range(1, 11)]
print(numbers)  # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
for val in numbers[:4]:
    print(val, end=' ')  # 1 4 9 16

5.3 复制列表

必须使用整个列表切片的方法来确保复制的是列表的副本,而不是使用 ‘=‘进行复制

numbers = [value ** 2 for value in range(1, 6)]
myNumbers = numbers[:]
numbers.append(36)
myNumbers.append(49)
print(numbers)  # [1, 4, 9, 16, 25, 36]
print(myNumbers)  # [1, 4, 9, 16, 25, 49]

6. 元组

Python 将不能修改的值称为不可变的, 而不可变的列表被称为元组。 元组中元素虽然不可以被改变,但可以通过赋值的方法改变元组变量的值

numbers = tuple(value ** 2 for value in range(1, 6))
print(numbers)  # (1, 4, 9, 16, 25)
numbers = tuple(value ** 2 for value in range(1, 4))
print(numbers)  # (1, 4, 9)

五、字典

1. 示例程序

alien = {'color': 'green', 'points': 5}
# 访问字典键-值对
print(alien['color'], end=' ')
print(alien['points'])
# 添加键-值对
alien['x_position'] = 0
alien['y_position'] = 25
print(alien)  # {'color': 'green', 'points': 5, 'x_position': 0, 'y_position': 25}
# 删除键-值对
del alien['points']
print(alien)  # {'color': 'green', 'x_position': 0, 'y_position': 25}

2. 遍历字典

2.1 遍历所有键值对

alien = {'x_position': 0, 'y_position': 25}
for key, value in alien.items():
    print(f'Key = {key}, Value = {value}')
# 输出:
# Key = x_position, Value = 0
# Key = y_position, Value = 25

2.2 遍历字典所有的键

alien = {'x_position': 0, 'y_position': 25}
names = list(alien.keys())
print(names)  # ['x_position', 'y_position']

2.3 按顺序遍历字典中的所有键

alien = {'x_position': 0, 'y_position': 25, 'a': 'test1', 'b': 'test2'}
for key in sorted(alien.keys()):
    print(key, end=' ')

2.4 遍历字典所有的值

alien = {'x_position': 0, 'y_position': 25, 'a': 'test1', 'b': 'test2'}
for value in alien.values():
    print(value)

3. 嵌套

3.1 字典列表

即在列表中嵌套字典

alien = {'x_position': 0, 'y_position': 25}
aliens = []
for index in range(30):
    aliens.append(alien)
for app in aliens[:5]:
    print(app)

3.2 字典中存储列表

favoriteLan = {
    'jen': ['python', 'rust'],
    'sarah': ['c'],
    'edward': ['ruby', 'go'],
    'phil': ['shell', 'php'],
}

for name, languages in favoriteLan.items():
    print("\n" + name.title() + "'s favorite languages are: ")
    for language in languages:
        print("\t" + language.title())

3.3 字典中存储字典

users = {
    'torvalds': {
        'first': 'linus',
        'last': 'torvalds',
        'location': 'finland'
    },
    'buterin': {
        'first': 'vitalik',
        'last': 'buterin',
        'location': 'russia'
    }
}

for name, user in users.items():
    print(f"\nUsername: {name.title()}")
    print(f"first: {user['first'].title()}, last: {user['last'].title()}, location: {user['location'].title()}")

输出:

Username: Torvalds
first: Linus, last: Torvalds, location: Finland

Username: Buterin
first: Vitalik, last: Buterin, location: Russia

六、函数

1. 函数示例

def greet_user(username):
    """greet example"""
    print(f"Hello, {username.title()}")


greet_user('siya')
print(greet_user.__doc__)

2. 传递实参

2.1 位置实参

实参和形参完全对应的函数参数关联方式

def describe_pet(animal_type, pet_name):
    print(f"I have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")


describe_pet('hamster', 'harry')

2.2 关键字实参

describe_pet(animal_type='hamster', pet_name='harry')
describe_pet(pet_name='harry', animal_type='hamster')

2.3 默认值

def describe_pet(animal_type, pet_name='harry'):
    print(f"I have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")


describe_pet('hamster', 'vitalik')
describe_pet('dog')

3. 返回值

def get_formatted_name(first_name, last_name):
    full_name = first_name + ' ' + last_name
    return full_name.title()


full_name = get_formatted_name('vitalik', 'buterin')
print(full_name)  # Vitalik Buterin

4. 传递列表

def aTob(nums1, nums2):
    while nums1:
        nums2.append(nums1.pop())


nums1 = [val * 2 for val in range(1, 10)]
nums2 = []
aTob(nums1[:], nums2)

5. 传递任意数量的实参

5.1 示例程序

def aTob(*nums):
    print(nums)
aTob(1, 2, 4)  # (1, 2, 4)

5.2 结合使用位置实参与任意数量实参

def aTob(nums1, *nums2):
    print(nums1)
    print(nums2)
aTob(1, 2, 4)
# 1
# (2, 4)

5.3 使用任意数量的关键字实参

def aTob(first, last, **nums):
    print(f"{first.title()} {last.title()}.")
    print(nums)
aTob('vitalik', 'buterin', age=31, sex='male')
# Vitalik Buterin.
# {'age': 31, 'sex': 'male'}

七、类

1. 类示例

class Dog():
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def sit(self):
        print(f"{self.name.title()} is now sitting.")

    def roll_over(self):
        print(f"{self.name.title()} rolled over.")


dog = Dog('vitalik', 20)
dog.sit()
dog.roll_over()

2. 类的继承

2.1 继承示例

基类为 Car , 派生类为 ElectricCar

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        long_name = f"{self.year} {self.make} {self.model}"
        return long_name

    def read_odometer(self):
        print(f"This car has {self.odometer_reading} miles on it.")

    def update_odometer(self, mileage):
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")

    def increment_odometer(self, miles):
        self.odometer_reading += miles


class ElectricCar(Car):
    def __init__(self, make, model, year):
        super().__init__(make, model, year)
        self.battery_size = 70

    def describe_battery(self):
        print(f"This car has a {self.battery_size}-kWh battery")


car = ElectricCar('tesla', 'model s', 2016)
longName = car.get_descriptive_name()
print(longName)  # 2016 tesla model s
car.describe_battery()  # This car has a 70-kWh battery

2.2 重写父类方法

class Car:
    def fill_gas_tank(self):
        pass
class ElectricCar(Car):
    def fill_gas_tank(self):
        print("This car doesn't need a gas tank!")
car = ElectricCar()
car.fill_gas_tank()

3. 使用 OrderedDict 类

from collections import OrderedDict
favorite_languages = OrderedDict()
favorite_languages['jen'] = ['python', 'rust']
favorite_languages['sarah'] = ['c']
favorite_languages['edward'] = ['ruby', 'go']
favorite_languages['phil'] = ['shell', 'php']

for name, languages in favorite_languages.items():
    print(f"\n{name.title()}'s languages: ", end=' ')
    for language in languages:
        print(f"{language.title()}", end=' ')

八、GUI 编程

1. GUI 编程基础(Tkinter)

常用 Python GUI 库如下:

  • Tkinter: Tkinter 模块(Tk 接口)是 Python 的标准 Tk GUI 工具包的接口 .Tk 和 Tkinter 可以在大多数的 Unix 平台下使用,同样可以应用在 Windows 和 Macintosh 系统里。Tk8.0 的后续版本可以实现本地窗口风格,并良好地运行在绝大多数平台中。
  • wxPython:wxPython 是一款开源软件,是 Python 语言的一套优秀的 GUI 图形库,允许 Python 程序员很方便的创建完整的、功能健全的 GUI 用户界面。
  • Jython:Jython 程序可以和 Java 无缝集成。

1.1 Tkinter 编程

创建一个 GUI 程序:

  • 1、导入 Tkinter 模块
  • 2、创建控件
  • 3、指定这个控件的 master, 即这个控件属于哪一个
  • 4、告诉 GM(geometry manager) 有一个控件产生了。

示例:

# This is a sample Python script.
# Press Shift+F10 to execute it or replace it with your code.
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
import tkinter

top = tkinter.Tk()
# 进入消息循环
top.mainloop()

执行之后显示:
BUxYqA.png

1.2 Tkinter 组件

目前有 15 种 Tkinter 的部件。我们提出这些部件以及一个简短的介绍,在下面的表:

控件 描述
Button 按钮控件;在程序中显示按钮。
Canvas 画布控件;显示图形元素如线条或文本
Checkbutton 多选框控件;用于在程序中提供多项选择框
Entry 输入控件;用于显示简单的文本内容
Frame 框架控件;在屏幕上显示一个矩形区域,多用来作为容器
Label 标签控件;可以显示文本和位图
Listbox 列表框控件;在 Listbox 窗口小部件是用来显示一个字符串列表给用户
Menubutton 菜单按钮控件,用于显示菜单项。
Menu 菜单控件;显示菜单栏,下拉菜单和弹出菜单
Message 消息控件;用来显示多行文本,与 label 比较类似
Radiobutton 单选按钮控件;显示一个单选的按钮状态
Scale 范围控件;显示一个数值刻度,为输出限定范围的数字区间
Scrollbar 滚动条控件,当内容超过可视化区域时使用,如列表框。.
Text 文本控件;用于显示多行文本
Toplevel 容器控件;用来提供一个单独的对话框,和 Frame 比较类似
Spinbox 输入控件;与 Entry 类似,但是可以指定输入范围值
PanedWindow PanedWindow 是一个窗口布局管理的插件,可以包含一个或者多个子控件。
LabelFrame labelframe 是一个简单的容器控件。常用与复杂的窗口布局。
tkMessageBox 用于显示你应用程序的消息框。

1.3 标准属性

标准属性也就是所有控件的共同属性,如大小,字体和颜色等等。

属性 描述
Dimension 控件大小;
Color 控件颜色;
Font 控件字体;
Anchor 锚点;
Relief 控件样式;
Bitmap 位图;
Cursor 光标;

1.4 几何管理

Tkinter 控件有特定的几何状态管理方法,管理整个控件区域组织,以下是 Tkinter 公开的几何管理类:包、网格、位置

几何方法 描述
pack() 包装;
grid() 网格;
place() 位置;

2. Tkinter 编程实战

2.1 Frame 与 pack 的初体验

from tkinter import *
def say_hi():
    print("hello ~ !")
root = Tk()
frame1 = Frame(root, bd=128, cursor="plus", bg="white")
frame2 = Frame(root, bd=128, cursor="circle", bg="pink")
root.title("tkinter frame")
label = Label(frame1, text="Label", bg="red")
label.pack(fill=X)
hi_there = Button(frame2, text="say hi~", command=say_hi)
hi_there.pack()
frame1.pack(padx=10, pady=10, side=RIGHT)
frame2.pack(padx=10, pady=10, side=RIGHT)
root.mainloop()

执行之后显示:
BaVaoF.md.png

2.2 Python Tkinter 文本框(Entry)

  • 你如果需要输入多行文本,可以使用 Text 组件。
  • 你如果需要显示一行或多行文本且不允许用户修改,你可以使用 Label 组件。
import tkinter

top = tkinter.Tk()
top.title("emergency system")

frame = tkinter.Frame(top, bd=256, bg="white")
l1 = tkinter.Label(frame, text="name")
w = tkinter.Entry(frame, bd=10, highlightcolor="red", fg="black")
l1.pack(side=tkinter.LEFT)
w.pack(side=tkinter.LEFT)
frame.pack(padx=10, pady=10)

top.mainloop()

3. 廖雪峰图形编程入门

加入一个文本框,让用户可以输入文本,然后点按钮后,弹出消息对话框。

from tkinter import *
import tkinter.messagebox as messagebox


class Application(Frame):
    def __init__(self, master=None):
        Frame.__init__(self, master, bd=128, bg="green")
        self.pack()
        self.createWidgets()

    def createWidgets(self):
        self.nameInput = Entry(self, bd=12, bg="white")
        self.nameInput.pack()
        self.quitButton = Button(self, text='Quit', command=self.quit)
        self.helloButton = Button(self, text='hello', command=self.hello)
        self.helloButton.pack(side=LEFT, padx=10, pady=10)
        self.quitButton.pack(side=LEFT, padx=10, pady=10)

    def hello(self):
        name = self.nameInput.get()
        messagebox.showinfo("Message", "Hello %s" % name)


app = Application()
app.master.title("Hello World!")
app.mainloop()

32

4. 参考资料

[Tkinter 教程 12] 布局管理 (Pack Place Grid)
Python GUI 之 tkinter 窗口视窗教程大集合(看这篇就够了)

九、元类

廖雪峰使用元类

谈谈 Python 中元类 Metaclass(一):什么是元类

谈谈 Python 中元类 Metaclass(二):ORM 实践

深入理解 Python 中的元类(metaclass)