修改 Ceph 集群(主要是monitor
)的 IP 地址
现有 Ceph 集群两台服务器idv-master
和idv-node1
当前接入的是192.168.140.0/24
网段,现在需要通过网线连接至X
机房对应的交换机,接入192.168.136.0/24
网段。两个网段内部都是千兆环境,而互相之间则只有百兆,因此服务器的 IP 地址需要修改,Ceph Monitor 的 IP 地址也要对应进行修改。
主机名 | idv-master | idv-node1 |
---|---|---|
Ceph Mon | idv-master | 无 |
原 IP 地址 | 192.168.140.79/24 | 192.168.140.76/24 |
原网关地址 | 192.168.140.254 | 192.168.140.254 |
新 IP 地址 | 192.168.136.51/24 | 192.168.136.47/24 |
新网关地址 | 192.168.136.126 | 192.168.136.126 |
另外,两台服务器上分别做了网卡的bonding
:
idv-master
:em1、em2、em3、em4 -> bond0idv-node1
:em1、em2 、em3、em4-> bond0因此只需将idv-master
的em1
、em2
、em3
、em4
和idv-node1
的em1
、em2
、em3
、em4
这八个网卡通过网线换插到X
机房的交换机上,并修改bond0
的网络配置信息。
注:以下所有操作在换插网线之前完成,此时 Ceph 集群应处于
HEALTH_OK
状态
首先修改两台服务器bond0
的配置文件/etc/sysconfig/network-scripts/ifcfg-bond0
,在其中额外添加一个192.168.136.0/24
网段的 IP 地址:
idv-master
的ifcfg-bond0
修改如下:
DEVICE=bond0TYPE=BondNAME=bond0BOOTPROTO=noneONBOOT=yesDEFROUTE=yesIPV4_FAILURE_FATAL=noIPV6INIT=yesIPV6_AUTOCONF=yesIPV6_DEFROUTE=yesIPV6_FAILURE_FATAL=noIPV6_ADDR_GEN_MODE=stable-privacyIPADDR=192.168.140.79PREFIX=24IPADDR1=192.168.136.51PREFIX1=24GATEWAY=192.168.140.254DNS1=xxx.xxx.xxx.xxxBONDING_MASTER=yesBONDING_OPTS="mode=6 miimon=100"
idv-node1
的ifcfg-bond0
修改如下:
DEVICE=bond0TYPE=BondNAME=bond0BOOTPROTO=noneONBOOT=yesDEFROUTE=yesIPV4_FAILURE_FATAL=noIPV6INIT=yesIPV6_AUTOCONF=yesIPV6_DEFROUTE=yesIPV6_FAILURE_FATAL=noIPV6_ADDR_GEN_MODE=stable-privacyIPADDR=192.168.140.76PREFIX=24IPADDR1=192.168.136.47PREFIX1=24GATEWAY=192.168.140.254DNS1=xxx.xxx.xxx.xxxBONDING_MASTER=yesBONDING_OPTS="mode=6 miimon=100"
之后重启网络,测试两台服务器是否可以通过新的 IP 地址通信:
[root@idv-master ~] systemctl restart network[root@idv-master ~] nmclibond0: connected to bond0 "bond0" bond, 18:66:DA:F3:97:10, sw, mtu 1500 ip4 default, ip6 default inet4 192.168.136.51/24 inet4 192.168.140.79/24 route4 192.168.140.0/24 route4 192.168.136.0/24 route4 0.0.0.0/0......[root@idv-master ~] ping 192.168.136.47PING 192.168.136.47 (192.168.136.47) 56(84) bytes of data.64 bytes from 192.168.136.47: icmp_seq=1 ttl=64 time=0.204 ms64 bytes from 192.168.136.47: icmp_seq=2 ttl=64 time=0.192 ms64 bytes from 192.168.136.47: icmp_seq=3 ttl=64 time=0.167 ms^C
进入idv-master
的/root/ceph-deploy
目录,首先查看当前 Ceph 集群的 monitor 信息:
[root@idv-master ~] cd ceph-deploy[root@idv-master ~] ls -hltotal 216K-rw-------. 1 root root 113 Jun 15 18:13 ceph.bootstrap-mds.keyring-rw-------. 1 root root 113 Jun 15 18:13 ceph.bootstrap-mgr.keyring-rw-------. 1 root root 113 Jun 15 18:13 ceph.bootstrap-osd.keyring-rw-------. 1 root root 113 Jun 15 18:13 ceph.bootstrap-rgw.keyring-rw-------. 1 root root 151 Jun 15 18:13 ceph.client.admin.keyring-rw-r--r-- 1 root root 320 Jun 19 10:55 ceph.conf-rw-r--r--. 1 root root 188K Jun 16 11:27 ceph-deploy-ceph.log-rw-------. 1 root root 73 Jun 15 18:10 ceph.mon.keyring[root@idv-master ceph-deploy] ceph mon dumpdumped monmap epoch 3epoch 3fsid 37b37488-4f34-4ddd-bd28-ac4a5267c261last_changed 2020-06-15 21:19:38.124439created 2020-06-15 18:13:19.250180min_mon_release 14 (nautilus)0: [v2:192.168.140.79:3300/0,v1:192.168.140.79:6789/0] mon.idv-master
之后获取当前 Ceph 集群的monmap
文件,可通过ceph mon getmap
命令从当前集群动态获取(需要集群处于运行状态),或者通过monmaptool
根据配置文件ceph.conf
静态生成(无需集群处于运行状态):
# 从当前集群动态获取[root@idv-master ceph-deploy] ceph mon getmap -o ./monmap[root@idv-master ceph-deploy] monmaptool --print ./monmapmonmaptool: monmap file ./monmapepoch 3fsid 37b37488-4f34-4ddd-bd28-ac4a5267c261last_changed 2020-06-15 21:19:38.124439created 2020-06-15 18:13:19.250180min_mon_release 14 (nautilus)0: [v2:192.168.140.79:3300/0,v1:192.168.140.79:6789/0] mon.idv-master# 或者根据配置文件 ceph.conf 静态生成[root@idv-master ceph-deploy] monmaptool --create --generate -c ./ceph.conf ./monmap
获取完monmap
之后,接下来需要手动修改 monitor 的 IP 地址及端口。首先删除旧的 monitor,之后添加新的 monitor。此处由于只有idv-master
一个 monitor,因此只需对该 monitor 进行操作。当 Ceph 集群中存在多个 monitor 时,需要对所有的 monitor 进行相同的操作。
# 删除旧的 monitor[root@idv-master ceph-deploy] monmaptool --rm idv-master ./monmapmonmaptool: monmap file ./monmapmonmaptool: removing idv-mastermonmaptool: writing epoch 3 to ./monmap (0 monitors)# 查看删除后的 monmap,此时已经没有 mon.idv-master[root@idv-master ceph-deploy] monmaptool --print ./monmap monmaptool: monmap file ./monmapepoch 3fsid 37b37488-4f34-4ddd-bd28-ac4a5267c261last_changed 2020-06-15 21:19:38.124439created 2020-06-15 18:13:19.250180min_mon_release 14 (nautilus)# 添加新的 monitor,使用新的 IP 地址[root@idv-master ceph-deploy] monmaptool --add idv-master 192.168.136.51:6789 ./monmapmonmaptool: monmap file ./monmapmonmaptool: writing epoch 3 to ./monmap (1 monitors)# 查看新的 monmap,此时 monitor 的 IP 地址已修改[root@idv-master ceph-deploy] monmaptool --print ./monmap monmaptool: monmap file ./monmapepoch 3fsid 37b37488-4f34-4ddd-bd28-ac4a5267c261last_changed 2020-06-15 21:19:38.124439created 2020-06-15 18:13:19.250180min_mon_release 14 (nautilus)0: v1:192.168.136.51:6789/0 mon.idv-master
修改ceph.conf
中的配置信息,方便在换插网线之后通过ceph-deploy
推送到各个节点:
# 查看修改前的配置文件[root@idv-master ceph-deploy] cat ./ceph.conf [global]fsid = 37b37488-4f34-4ddd-bd28-ac4a5267c261mon_initial_members = idv-mastermon_host = 192.168.140.79auth_cluster_required = noneauth_service_required = noneauth_client_required = nonepublic_network=192.168.140.0/24cluster_network=192.168.140.0/24mon_clock_drift_allowed = 2[mgr]mgr modules = dashboard# 修改配置文件中的网络信息[root@idv-master ceph-deploy] vim ./ceph.conf# 查看修改后的配置文件[root@idv-master ceph-deploy] cat ./ceph.conf[global]fsid = 37b37488-4f34-4ddd-bd28-ac4a5267c261mon_initial_members = idv-mastermon_host = 192.168.136.51auth_cluster_required = noneauth_service_required = noneauth_client_required = nonepublic_network=192.168.136.0/24cluster_network=192.168.136.0/24mon_clock_drift_allowed = 2[mgr]mgr modules = dashboard
修改两台服务器/etc/hosts
文件中的主机名和 IP 地址的对应规则:
# 192.168.140.79 idv-master192.168.136.51 idv-master# 192.168.140.76 idv-node1192.168.136.47 idv-node1
在两台服务器上分别执行以下命令,停止 Ceph 相关服务的运行:
systemctl stop ceph.target
至此换插网线之前的准备工作已经完成,现在可以将网线换插至新的交换机设备。
注:以下所有操作在换插网线之前完成
换插网线后,首先需要做的就是恢复网络的连通状态。由于bond0
已经配置了两个 IP 地址,因此只需要修改bond0
中的GATEWAY
,并重启网络服务:
# 在 idv-master 上执行[root@idv-master ~] sed -i '/GATEWAY/c GATEWAY=192.168.136.126' /etc/sysconfig/network-scripts/ifcfg-bond0[root@idv-master ~] systemctl restart network# 在 idv-node1 上执行[root@idv-node1 ~] sed -i '/GATEWAY/c GATEWAY=192.168.136.126' /etc/sysconfig/network-scripts/ifcfg-bond0[root@idv-node1 ~] systemctl restart network
之后尝试访问网关地址和外部网络,测试网络的连通状态:
[root@idv-master ~] ping idv-node1[root@idv-master ~] ping 192.168.136.126[root@idv-master ~] ping baidu.com
在idv-master
的/root/ceph-deploy
目录下,使用ceph-deploy
将新的配置文件推送至所有节点上:
[root@idv-master ceph-deploy] ceph-deploy --overwrite-conf config push idv-master idv-node1
由于本文中只有idv-master
一个 monitor,因此只需在idv-master
上注入monmap
。对于有多个 monitor 的 Ceph 集群,需要在所有的 monitor 节点上进行下列操作:
[root@idv-master ceph-deploy] ceph-mon -i idv-master --inject-monmap ./monmap
修改目录权限:
[root@idv-master ceph-deploy] chown ceph:ceph /var/lib/ceph/mon/ceph-idv-master/store.db/*
之后启动 Ceph 集群,检查集群是否恢复运行:
# 在所有节点上启动 Ceph 相关服务[root@idv-master ~] systemctl start ceph.target[root@idv-node1 ~] systemctl start ceph.target# 检查集群是否恢复运行[root@idv-master ~] ceph -s
修改 Ceph Dashboard 至新的地址:
[root@idv-master ceph-deploy] ceph config set mgr mgr/dashboard/server_addr 192.168.136.51[root@idv-master ceph-deploy] systemctl restart ceph-mgr@idv-master.service
]]>
借助bonding
将多个物理网卡创建为单个虚拟逻辑网卡
网卡bond
是通过多张网卡绑定为一个逻辑网卡,实现本地网卡冗余、带宽扩容和负载均衡。内核版本2.4.12
以上均提供了bonding
模块,之前的版本可以通过patch
实现。
首先检查内核是否支持bonding
:
> cat /etc/redhat-releaseCentOS Linux release 7.8.2003 (Core)> uname -r3.10.0-1127.el7.x86_64> cat /boot/config-3.10.0-1127.el7.x86_64 | grep -i bondingCONFIG_BONDING=m
可以看到当前内核支持bonding
,查看bonding
模块信息:
> modinfo bonding | lessfilename: /lib/modules/3.10.0-1127.el7.x86_64/kernel/drivers/net/bonding/bonding.ko.xzauthor: Thomas Davis, tadavis@lbl.gov and many othersdescription: Ethernet Channel Bonding Driver, v3.7.1version: 3.7.1license: GPLalias: rtnl-link-bondretpoline: Yrhelversion: 7.8srcversion: 02BB340820F6F1A042A3033depends: intree: Yvermagic: 3.10.0-1127.el7.x86_64 SMP mod_unload modversions......
启用bonding
模块,并查看是否加载:
> modprobe bonding> lsmod | grep bondingbonding 152979 0
Dell PowerEdge R730 服务器拥有四路全双工千兆网口,先查看当前的网络连接与设备:
> nmcli c showNAME UUID TYPE DEVICE em1 d4d8562f-91e2-4f9f-af48-485d2b0d744f ethernet em1 em2 d82f2490-8fa3-49b9-8e40-275bec92d230 ethernet em2 em3 24c1fdcd-56b2-4d09-ab1b-ab347a73a3ad ethernet em3 em4 31a2eadc-2528-4f4f-907e-35a5801090a2 ethernet --> nmcli d statusDEVICE TYPE STATE CONNECTION em1 ethernet connected em1 em2 ethernet connected em2 em3 ethernet connected em3 em4 ethernet unavailable -- bond0 bond unmanaged -- lo loopback unmanaged --
可以看到此时系统中已经创建了bond0
设备,因为NetworkManager
在/etc/sysconfig/network-scripts/
目录下没有找到bond0
对应的配置文件ifcfg-bond0
,所以当前处于unmanaged
状态。
创建ifcfg-bond0
配置文件,并输入以下内容:
> vim /etc/sysconfig/network-scripts/ifcfg-bond0> cat /etc/sysconfig/network-scripts/ifcfg-bond0DEVICE=bond0TYPE=BondNAME=bond0BOOTPROTO=noneONBOOT=yesDEFROUTE=yesIPV4_FAILURE_FATAL=noIPV6INIT=yesIPV6_AUTOCONF=yesIPV6_DEFROUTE=yesIPV6_FAILURE_FATAL=noIPV6_ADDR_GEN_MODE=stable-privacyIPADDR=xxx.xxx.xxx.xxxPREFIX=24GATEWAY=xxx.xxx.xxx.xxxDNS1=xxx.xxx.xxx.xxxBONDING_MASTER=yesBONDING_OPTS="mode=6 miimon=100"
之后创建em1
和em2
对应的 Slave 配置文件:
> cat /etc/sysconfig/network-scripts/ifcfg-bond-slave-em1TYPE=EthernetNAME=bond-slave-em1DEVICE=em1ONBOOT=yesMASTER=bond0SLAVE=yes> cat /etc/sysconfig/network-scripts/ifcfg-bond-slave-em2TYPE=EthernetNAME=bond-slave-em2DEVICE=em2ONBOOT=yesMASTER=bond0SLAVE=yes
最后,将ifcfg-em1
和ifcfg-em2
中的ONBOOT=yes
替换为ONBOOT=no
:
> sed -i '/ONBOOT/c ONBOOT=no' /etc/sysconfig/network-scripts/ifcfg-em1> sed -i '/ONBOOT/c ONBOOT=no' /etc/sysconfig/network-scripts/ifcfg-em2
重启网络服务:
> systemctl restart network
检查当前的bonding
状态:
[root@idv-node1 ~] cat /proc/net/bonding/bond0 Ethernet Channel Bonding Driver: v3.7.1 (April 27, 2011)Bonding Mode: adaptive load balancingPrimary Slave: NoneCurrently Active Slave: em1MII Status: upMII Polling Interval (ms): 100Up Delay (ms): 0Down Delay (ms): 0Slave Interface: em1MII Status: upSpeed: 1000 MbpsDuplex: fullLink Failure Count: 0Permanent HW addr: 18:66:da:f3:b4:28Slave queue ID: 0Slave Interface: em2MII Status: upSpeed: 1000 MbpsDuplex: fullLink Failure Count: 0Permanent HW addr: 18:66:da:f3:b4:29Slave queue ID: 0
使用ethtool
查看bond0
速率,已达到2000Mb/s
:
[root@idv-node1 ~] ethtool bond0Settings for bond0: Supported ports: [ ] Supported link modes: Not reported Supported pause frame use: No Supports auto-negotiation: No Supported FEC modes: Not reported Advertised link modes: Not reported Advertised pause frame use: No Advertised auto-negotiation: No Advertised FEC modes: Not reported Speed: 2000Mb/s Duplex: Full Port: Other PHYAD: 0 Transceiver: internal Auto-negotiation: off Link detected: yes
手动编译安装 Open vSwitch
OVS 默认没有提供YUM
源,需要手动获取源码编译安装。
先在 这里 找到需要安装的版本,例如openvswitch-2.5.10.tar.gz
。
之后切换至root
,安装依赖:
[root@my-centos ~] yum -y install wget openssl-devel gcc make python-devel openssl-devel kernel-devel graphviz kernel-debug-devel autoconf automake rpm-build redhat-rpm-config libtool python-twisted-core python-zope-interface PyQt4 desktop-file-utils libcap-ng-devel groff checkpolicy selinux-policy-devel
新建ovs
用户并切换至ovs
登录:
[root@my-centos ~] adduser ovs[root@my-centos ~] su - ovs[ovs@my-centos ~]
下载源码并准备编译环境:
[ovs@my-centos ~] mkdir -p ~/rpmbuild/SOURCES[ovs@my-centos ~] cd ~/rpmbuild/SOURCES[ovs@my-centos SOURCES] wget http://openvswitch.org/releases/openvswitch-2.5.10.tar.gz[ovs@my-centos SOURCES] tar -zxvf openvswitch-2.5.10.tar.gz
以ovs
用户身份编译RPM
包,之后退出登录:
[ovs@my-centos SOURCES] rpmbuild -bb --nocheck openvswitch-2.5.10/rhel/openvswitch-fedora.spec[ovs@my-centos SOURCES] exit
以root
身份安装编译好的RPM
包:
[root@my-centos ~] yum localinstall /home/ovs/rpmbuild/RPMS/x86_64/openvswitch-2.5.10-1.el7.centos.x86_64.rpm -y
检查ovs-vsctl
命令是否可用:
> ovs-vsctl --versionovs-vsctl (Open vSwitch) 2.5.10Compiled Aug 9 2020 17:29:38DB Schema 7.12.1
启动服务,根据需要设置是否开机自启:
# 启动服务> systemctl start openvswitch.service# 检查服务状态> systemctl status openvswitch.service● openvswitch.service - Open vSwitch Loaded: loaded (/usr/lib/systemd/system/openvswitch.service; disabled; vendor preset: disabled) Active: active (exited) since Sun 2020-08-09 17:33:45 CST; 2s ago Process: 15621 ExecStart=/bin/true (code=exited, status=0/SUCCESS) Main PID: 15621 (code=exited, status=0/SUCCESS) CGroup: /system.slice/openvswitch.serviceAug 09 17:33:45 VM_0_17_centos systemd[1]: Starting Open vSwitch...Aug 09 17:33:45 VM_0_17_centos systemd[1]: Started Open vSwitch.# 如有需要,设置服务开机自启> systemctl enable openvswitch.service
最后检查服务是否已经启动:
> ovs-vsctl show93415cc9-53b0-44da-a2d7-17e42b4a5ed1 ovs_version: "2.5.10"
- CentOS 7 安装 Open vSwitch | 简书
- OVS - Open vSwitch | Github
- Open vSwitch Documentation
- Open vSwitch 架构解析与功能实践 - 范桂飓 | CSDN
- Open vSwitch 的原理和常用命令 | 开源中国
- Open vSwitch 详解 | 简书
- Open vSwitch 的 ovs-vsctl 命令详解 | 八戒
- 研究 Open vSwitch | jeremy 的技术点滴
- OVS 初级教程:使用 Open vSwitch 构建虚拟网络 | SDNLAB
- 云计算底层技术 - 使用 Open vSwitch | opengers
- Linux 上实现 vxlan 网络 | Cizixs【提到了多播模式下的 VXLAN】
- vxlan 协议原理简介 | Cizixs
- VXLAN Series – How VTEP Learns and Creates Forwarding Table – Part 5 | VMware vSphere Blog
- VXLAN 基础教程:VXLAN 协议原理介绍 | 云原生实验室
- VXLAN 基础教程:在 Linux 上配置 VXLAN 网络 | 云原生实验室
- 【华为悦读汇】技术发烧友:认识 VXLAN | 华为企业互动社区
- 什么是vxlan网络 | Luckylau’s Blog【对上面文章的总结】
- VXLAN 技术研究 | CSDN
- VXLAN Gateway Overview
- 搭建基于 Open vSwitch 的 VxLAN 隧道实验 | SDNLAB
所有文章快速导航,定期汇总更新… 💻
分类传送门:🚀KVM ,共计
14
篇
分类传送门:🚀Go,共计
24
篇
分类传送门:🚀Ceph,共计
2
篇
分类传送门:🚀CentOS
分类传送门:🚀Fedora
CentOS 7 升级git
至最新版本
CentOS 7 自带的git
版本为1.8.x
太过陈旧,需要手动编译源码升级:
> cat /etc/redhat-releaseCentOS Linux release 7.6.1810 (Core)> git --versiongit version 1.8.3.1
安装依赖:
> yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel> yum install gcc perl-ExtUtils-MakeMaker
获取最新版的git
源码包:
> cd /usr/src/> wget https://github.com/git/git/archive/v2.25.1.zip> unzip v2.25.1.zip && rm v2.25.1.zip> cd git-2.25.1
先编译,看有无报错:
> make prefix=/usr/local/git all
若编译成功,则先卸载旧版本的git
,再安装新版本:
> rpm -e --nodeps git> make prefix=/usr/local/git install
创建软链接:
> ln -s /usr/local/git/bin/git /usr/bin/git
检查版本:
> git --versiongit version 2.25.1
]]>
记一次车祸现场,参考 修改根目录所在 VG 名称 | 简书
最近对根目录所在 VG 进行了重命名操作,如下所示,根分区所在 VG 为centos
:
> pvs PV VG Fmt Attr PSize PFree /dev/sda7 centos lvm2 a-- 102.56g 0 /dev/sdb centos lvm2 a-- <223.57g 584.00m> vgs VG #PV #LV #SN Attr VSize VFree centos 2 3 0 wz--n- <326.13g 584.00m> lvs LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert home centos -wi-ao---- <34.81g root centos -wi-ao---- 283.00g
回想起 LVM 有vgrename
命令,于是就想当然的进行了如下操作:
# 先将 centos 这个 VG 重命名为 centos-new> vgrename centos centos-new# 之后修改 /etc/fstab > grep centos /etc/fstab/dev/mapper/centos-root / xfs defaults 0 0/dev/mapper/centos-home /home xfs defaults 0 0/dev/mapper/centos-swap swap swap defaults 0 0> sed 's/centos/centos-new/g' /etc/fstab | grep centos-new/dev/mapper/centos-new-root / xfs defaults 0 0/dev/mapper/centos-new-home /home xfs defaults 0 0/dev/mapper/centos-new-swap swap swap defaults 0 0
之后忘了改grub
就直接重启,于是grub
找不到之前的 LV 进不去系统。。。Fine,作死成功T_T
注:要想进入系统,在
grub
引导界面按e
进行编辑,将 LV 修改为对应的新名称即可,之后再按照下面的流程修改grub
配置文件
> vgs VG #PV #LV #SN Attr VSize VFree centos 2 3 0 wz--n- <326.13g 584.00m> vgrename centos centos-new
> grep centos /etc/fstab/dev/mapper/centos-root / xfs defaults 0 0/dev/mapper/centos-home /home xfs defaults 0 0/dev/mapper/centos-swap swap swap defaults 0 0> sed 's/centos/centos-new/g' /etc/fstab | grep centos-new/dev/mapper/centos-new-root / xfs defaults 0 0/dev/mapper/centos-new-home /home xfs defaults 0 0/dev/mapper/centos-new-swap swap swap defaults 0 0
> vim /etc/default/grubGRUB_TIMEOUT=5GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"GRUB_DEFAULT=savedGRUB_DISABLE_SUBMENU=trueGRUB_TERMINAL_OUTPUT="console"GRUB_CMDLINE_LINUX="crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet"GRUB_DISABLE_RECOVERY="true"
修改GRUB_CMDLINE_LINUX
:
centos/root
修改为centos-new/root
centos/swap
修改为centos-new/swap
# For UEFI devices> grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg# For Legacy devices> grub2-mkconfig -o /boot/grub2/grub.cfg
重启系统,问题解决。
]]>
LVM (Logical Volume Manager) 使用示例
// TODO: 待整理更新…
例如已有 VG 名为centos
,新加的磁盘为/dev/sdb
,用来扩容根目录/
:
使用下列命令会自动将整块/dev/sdb
创建为一个新的 PV,并将其加入centos
以供使用:
> vgextend centos /dev/sdb> pvdisplay /dev/sdb --- Physical volume --- PV Name /dev/sdb VG Name centos PV Size 223.57 GiB / not usable <4.59 MiB Allocatable yes PE Size 4.00 MiB Total PE 57233 Free PE 5266 Allocated PE 51967 PV UUID d1SdU8-r927-8b0r-Q5qs-zGl9-XhZu-SE8rSw> vgdisplay centos --- Volume group --- VG Name centos System ID Format lvm2 Metadata Areas 2 Metadata Sequence No 12 VG Access read/write VG Status resizable MAX LV 0 Cur LV 3 Open LV 2 Max PV 0 Cur PV 2 Act PV 2 VG Size <326.13 GiB PE Size 4.00 MiB Total PE 83489 Alloc PE / Size 78223 / <305.56 GiB Free PE / Size 5266 / 20.57 GiB VG UUID O2IM4T-7sbU-ONuI-maTG-Zrkq-paGl-5f054x
注:CentOS 7 默认文件系统为 XFS, 须使用
xfs_growfs
替代resize2fs
# 当前有三个 LV, 计划为 root 扩容> lvs centos LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert home centos -wi-ao---- <34.81g root centos -wi-ao---- 263.00g swap centos -wi-a----- 7.75g # 为 root 这个 LV 增加 10G 的空间> lvextend -L +10G /dev/centos/root# CentOS 7 默认文件系统为 XFS, 须使用 xfs_growfs 替代 resize2fs > resize2fs /dev/centos/root
查看各分区使用情况:
> df -hT -t xfsFilesystem Type Size Used Avail Use% Mounted on/dev/mapper/centos-root xfs 283G 144G 140G 51% //dev/sda6 xfs 1014M 167M 848M 17% /boot/dev/mapper/centos-home xfs 35G 360M 35G 2% /home
卸载/home
目录:
> umount /home
如果提示无法卸载,则是有进程占用/home
目录,可使用fuser
命令查看并终止占用的进程:
# 查看占用 /home 目录的进程> fuser -m /home# 查看占用 /home 目录的进程> fuser -k /home
调整/home
分区大小至30G
:
> resize2fs -p /dev/mapper/centos-home 30G
注:
xfs
是CentOS 7
默认的文件系统,只能扩大不能缩小,所以在必须缩小文件系统时,需要利用xfsdump
和xfsrestore
工具来备份和还原资料。具体参见:CentOS 7 调整 XFS 格式的 LVM 大小 | CSDN
可能会提示要先运行e2fsck
对文件系统进行检查:
> e2fsck -f /dev/mapper/centos-home
之后再重新执行命令:
> resize2fs -p /dev/mapper/centos-home 30G
挂载上/home
,查看该目录的空间是否变为30G
:
> mount /home> df -hT /home
此时文件系统已经缩减到30G
,但centos-home
这个Logical Volume
的大小还没有进行调整。使用lvreduce
命令减少centos-home
的空间大小:
# 将 centos-home 这个 LV 调整至 30G> lvreduce -L 30G /dev/mapper/centos-home# 以下的语法是相对加减的语法,仅作示例> lvreduce -L +5G /dev/mapper/centos-home> lvreduce -L -5G /dev/mapper/centos-home
最后把闲置空间增加给centos-root
,并生效至文件系统:
> lvextend -L +5G /dev/mapper/centos-root> resize2fs -p /dev/mapper/centos-root# 或者 xfs_growfs /dev/centos/root
]]>
- CentOS 7 中使用 LVM 管理磁盘和挂载磁盘 | 博客园
- 使用 lvm 管理磁盘分区 | 且听风吟
- Linux 使用 lvm 管理磁盘 | 阿文的博客
- Linux 磁盘管理系列三:LVM的使用 | Linux 运维日志
- 一张图让你学会 LVM | Linux 就该这么学
- LVM - ArchWiki
- 3 分钟看懂 Linux 磁盘划分 | 知乎
- Linux 系统 /dev/mapper 目录浅谈 | CSDN
- XFS vs EXT4 | 夏天的风
- CentOS 6.5 重新调整 /home 和根目录 / 大小 - FEFJay | 博客园
- CentOS 7 调整 XFS 格式的 LVM 大小 | CSDN
快速搭建 Ceph Jewel(v10.x)
虚拟机集群
>=200G
(估计值,非必须,满足实际需求即可。例如我只用来存放一个20G
的系统镜像,足够使用了)我的本地宿主机环境如下:
> cat /etc/redhat-releaseFedora release 31 (Thirty One)> uname -r5.4.8-200.fc31.x86_64> df -hT /Filesystem Type Size Used Avail Use% Mounted on/dev/mapper/fedora-root ext4 395G 123G 254G 33% /> virt-manager --version2.2.1> free -h total used free shared buff/cache availableMem: 15Gi 2.4Gi 10Gi 434Mi 2.3Gi 12GiSwap: 0B 0B 0B> screenfetch /:-------------:\ root@ins-7590 :-------------------:: OS: Fedora 31 ThirtyOne :-----------/shhOHbmp---:\ Kernel: x86_64 Linux 5.4.8-200.fc31.x86_64 /-----------omMMMNNNMMD ---: Uptime: 40m :-----------sMMMMNMNMP. ---: Packages: 2237 :-----------:MMMdP------- ---\ Shell: zsh 5.7.1 ,------------:MMMd-------- ---: Resolution: 3600x1080 :------------:MMMd------- .---: DE: GNOME :---- oNMMMMMMMMMNho .----: WM: GNOME Shell :-- .+shhhMMMmhhy++ .------/ WM Theme: :- -------:MMMd--------------: GTK Theme: Adwaita-dark [GTK2/3] :- --------/MMMd-------------; Icon Theme: Adwaita :- ------/hMMMy------------: Font: Cantarell 11 :-- :dMNdhhdNMMNo------------; CPU: Intel Core i7-9750H @ 12x 4.5GHz :---:sdNMMMMNds:------------: GPU: Mesa DRI Intel(R) UHD Graphics 630 (Coffeelake 3x8 GT2) :------:://:-------------:: RAM: 2469MiB / 15786MiB :---------------------://
计划使用三台虚拟机搭建三节点的 Ceph 集群:
hostname | IP | 备注 |
---|---|---|
ceph-node1 | 192.168.200.101 | deploy, 1 mon, 2 osd |
ceph-node2 | 192.168.200.102 | 1 mon, 2 osd |
ceph-node3 | 192.168.200.103 | 1 mon, 2 osd |
注:
ceph-node1
同时也作为ceph-deploy
的运行节点,为自身及其他节点安装ceph
创建一个目录(例如/mnt/ceph/
)用来存放三台虚拟机的磁盘镜像:
~ > mkdir -p /mnt/ceph~ > cd /mnt/ceph//mnt/ceph > mkdir ceph-node1 ceph-node2 ceph-node3/mnt/ceph > ls -ltotal 12drwxr-xr-x 2 root root 4096 Mar 2 15:23 ceph-node1drwxr-xr-x 2 root root 4096 Mar 2 15:23 ceph-node2drwxr-xr-x 2 root root 4096 Mar 2 15:23 ceph-node3
注:先准备
ceph-node1
,其余两台之后可通过virt-clone
复制
使用qemu-img
命令为ceph-node1
创建一块容量为100G
的系统盘,以及两块容量各为2T
的磁盘,镜像格式均为qcow2
:
/mnt/ceph > qemu-img create -f qcow2 ceph-node1/ceph-node1.qcow2 100GFormatting 'ceph-node1/ceph-node1.qcow2', fmt=qcow2 size=107374182400 cluster_size=65536 lazy_refcounts=off refcount_bits=16/mnt/ceph > qemu-img create -f qcow2 ceph-node1/disk-1.qcow2 2TFormatting 'ceph-node1/disk-1.qcow2', fmt=qcow2 size=2199023255552 cluster_size=65536 lazy_refcounts=off refcount_bits=16/mnt/ceph > qemu-img create -f qcow2 ceph-node1/disk-2.qcow2 2TFormatting 'ceph-node1/disk-2.qcow2', fmt=qcow2 size=2199023255552 cluster_size=65536 lazy_refcounts=off refcount_bits=16/mnt/ceph > tree -h.├── [ 4.0K] ceph-node1│ ├── [ 194K] ceph-node1.qcow2│ ├── [ 224K] disk-1.qcow2│ └── [ 224K] disk-2.qcow2├── [ 4.0K] ceph-node2└── [ 4.0K] ceph-node33 directories, 3 files
在virt-manager
里创建虚拟网络ceph-net
,模式NAT
,转发至本机的上网接口。规划的网段为192.168.200.0/24
,DHCP 范围192.168.200.101~254
,可自己修改调整:
ceph-net
自动生成的 XML 定义如下:
<network> <name>ceph-net</name> <uuid>cc046613-11c4-4db7-a478-ac6d568e69ec</uuid> <forward dev="wlo1" mode="nat"> <nat> <port start="1024" end="65535"/> </nat> <interface dev="wlo1"/> </forward> <bridge name="virbr1" stp="on" delay="0"/> <mac address="52:54:00:86:86:e8"/> <domain name="ceph-net"/> <ip address="192.168.200.1" netmask="255.255.255.0"> <dhcp> <range start="192.168.200.101" end="192.168.200.254"/> </dhcp> </ip></network>
在virt-manager
中新建虚拟机:
选择下载好的CentOS-7-x86_64-Minimal-1908.iso
作为系统安装镜像:
2 核 1G,默认即可:
选择之前创建好的ceph-node1.qcow2
作为系统镜像:
设置虚拟机名为ceph-node1
,网络选择ceph-net
,并勾选Customize configuration before install
,添加两块 2T 磁盘:
将disk-1.qcow2
和disk-2.qcow2
添加为VirtIO Disk
:
最后确认Boot Option
中CDROM
为第一启动项,即可开始安装:
之后就进入了熟悉的CentOS
安装界面,选择100G
的盘作为系统盘,顺便观察两块2T
的磁盘是否识别:
打开网络开关,确认是否已获得动态分配的192.168.200.0/24
网段内的 IP 地址,具体的地址之后进入系统可以修改。另外,左下角将主机名修改为ceph-node1
并Apply
现在即可开始安装,记得设置root
密码。安装完重启进入系统:
[root@ceph-node1 ~]$ lsblkNAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTsr0 11:0 1 1024M 0 rom vda 252:0 0 100G 0 disk ├─vda1 252:1 0 1G 0 part /boot└─vda2 252:2 0 99G 0 part ├─centos_ceph--node1-root 253:0 0 50G 0 lvm / ├─centos_ceph--node1-swap 253:1 0 2G 0 lvm [SWAP] └─centos_ceph--node1-home 253:2 0 47G 0 lvm /homevdb 252:16 0 2T 0 disk vdc 252:32 0 2T 0 disk
之前在安装过程中看到,eth0
自动获取了分配的 IP 地址,现在我们需要将eth0
的 IP 地址修改为我们计划的静态 IP 地址,即192.168.200.101
。
修改网卡的配置文件:
vi /etc/sysconfig/network-scripts/ifcfg-eth0# 修改以下几个配置项BOOTPROTO="static"IPADDR=192.168.200.101NETMASK=255.255.255.0GATEWAY=192.168.200.1DNS1=192.168.200.1ONBOOT="yes"
重启网络,之后检查一下内网和外网的连通性:
[root@ceph-node1 ~]$ systemctl restart network # 重启网络[root@ceph-node1 ~]$ ip addr list eth02: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 52:54:00:32:af:61 brd ff:ff:ff:ff:ff:ff inet 192.168.200.101/24 brd 192.168.200.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::5054:ff:fe32:af61/64 scope link valid_lft forever preferred_lft forever[root@ceph-node1 ~]$ nmclieth0: connected to eth0 "Red Hat Virtio" ethernet (virtio_net), 52:54:00:32:AF:61, hw, mtu 1500 ip4 default inet4 192.168.200.101/24 route4 192.168.200.0/24 route4 0.0.0.0/0 inet6 fe80::916d:cd1f:bb97:ca22/64 route6 fe80::/64 route6 ff00::/8lo: unmanaged "lo" loopback (unknown), 00:00:00:00:00:00, sw, mtu 65536DNS configuration: servers: 192.168.200.1 interface: eth0[root@ceph-node1 ~]$ ping 192.168.200.1 # ping 网关[root@ceph-node1 ~]$ ping baidu.com # ping 外网
简便起见,关闭firewalld
和selinux
:
# 关闭 firewalld 并禁用[root@ceph-node1 ~]$ systemctl stop firewalld[root@ceph-node1 ~]$ systemctl disable firewalld# 临时关闭 selinux[root@ceph-node1 ~]$ getenforceEnforcing[root@ceph-node1 ~]$ setenforce 0[root@ceph-node1 ~]$ getenforcePermissive# 永久关闭 selinux,重启后生效[root@ceph-node1 ~]$ sed -i 's/SELINUX=.*/SELINUX=disabled/' /etc/selinux/config
将yum
源修改为阿里云的源,并添加ceph
的源:
[root@ceph-node1 ~]$ yum clean all[root@ceph-node1 ~]$ curl http://mirrors.aliyun.com/repo/Centos-7.repo >/etc/yum.repos.d/CentOS-Base.repo[root@ceph-node1 ~]$ curl http://mirrors.aliyun.com/repo/epel-7.repo >/etc/yum.repos.d/epel.repo[root@ceph-node1 ~]$ sed -i '/aliyuncs/d' /etc/yum.repos.d/CentOS-Base.repo[root@ceph-node1 ~]$ sed -i '/aliyuncs/d' /etc/yum.repos.d/epel.repo[root@ceph-node1 ~]$ vim /etc/yum.repos.d/ceph.repo# 添加以下内容[ceph]name=cephbaseurl=http://mirrors.163.com/ceph/rpm-jewel/el7/x86_64/gpgcheck=0[ceph-noarch]name=cephnoarchbaseurl=http://mirrors.163.com/ceph/rpm-jewel/el7/noarch/gpgcheck=0[root@ceph-node1 ~]$ yum makecache[root@ceph-node1 ~]$ yum repolistLoaded plugins: fastestmirror, prioritiesLoading mirror speeds from cached hostfilerepo id repo name statusbase/7/x86_64 CentOS-7 - Base - mirrors.aliyun.com 10,097ceph ceph 499ceph-noarch cephnoarch 16epel/x86_64 Extra Packages for Enterprise Linux 7 - x86_64 13,196extras/7/x86_64 CentOS-7 - Extras - mirrors.aliyun.com 323updates/7/x86_64 CentOS-7 - Updates - mirrors.aliyun.com 1,478repolist: 25,609
安装ceph
客户端、ntpdate
以及其他软件:
# 安装 ceph 客户端[root@ceph-node1 ~]$ yum install ceph ceph-radosgw[root@ceph-node1 ~]$ ceph --versionceph version 10.2.11 (e4b061b47f07f583c92a050d9e84b1813a35671e)# 必须安装,借助 ntpdate 同步时间[root@ceph-node1 ~]$ yum install ntp ntpdate ntp-doc # 非必须,方便监控性能数据[root@ceph-node1 ~]$ yum install wget vim htop dstat iftop tmux
设置开机运行ntpdate
自动同步时间:
[root@ceph-node1 ~]$ ntpdate ntp.sjtu.edu.cn 2 Mar 09:49:39 ntpdate[1915]: adjust time server 84.16.73.33 offset 0.051449 sec[root@ceph-node1 ~]$ echo ntpdate ntp.sjtu.edu.cn >> /etc/rc.d/rc.local[root@ceph-node1 ~]$ chmod +x /etc/rc.d/rc.local
添加各节点主机名,生成 SSH 密钥并配置免密登录:
[root@ceph-node1 ~]$ echo ceph-node1 >/etc/hostname[root@ceph-node1 ~]$ vim /etc/hosts# 添加以下主机名192.168.200.101 ceph-node1192.168.200.102 ceph-node2192.168.200.103 ceph-node3# 三次回车,生成密钥[root@ceph-node1 ~]$ ssh-keygen# 设置本机免密登录# 由于虚拟机克隆后密钥是同一份# 因此只需执行这一次 ssh-copy-id# 三台虚拟机相互之间均可以免密登录[root@ceph-node1 ~]$ ssh-copy-id root@ceph-node1
在宿主机上同样添加以上三个节点的主机名,并ssh-copy-id
到虚拟机配置免密登录:
[root@ceph-host ~]$ vim /etc/hosts# 添加以下主机名192.168.200.101 ceph-node1192.168.200.102 ceph-node2192.168.200.103 ceph-node3[root@ceph-host ~]$ ssh-copy-id root@ceph-node1
至此虚拟机环境已准备完毕,可以执行shutdown -h now
关机,记得在virt-manager
里将CDROM
移除掉,并确保VirtIO Disk 1
为第一启动项。
使用virt-clone
命令基于ceph-node1
克隆出ceph-node2
、ceph-node3
另外两台虚拟机,它将为网卡生成新的 MAC 地址,并将镜像复制到指定路径:
> virsh domblklist ceph-node1 Target Source------------------------------------------------- vda /mnt/ceph/ceph-node1/ceph-node1.qcow2 vdb /mnt/ceph/ceph-node1/disk-1.qcow2 vdc /mnt/ceph/ceph-node1/disk-2.qcow2> virt-clone --original ceph-node1 --name ceph-node2 \--file /mnt/ceph/ceph-node2/ceph-node2.qcow2 \--file /mnt/ceph/ceph-node2/disk-1.qcow2 \--file /mnt/ceph/ceph-node2/disk-2.qcow2> virt-clone --original ceph-node1 --name ceph-node3 \--file /mnt/ceph/ceph-node3/ceph-node3.qcow2 \--file /mnt/ceph/ceph-node3/disk-1.qcow2 \--file /mnt/ceph/ceph-node3/disk-2.qcow2> tree -h /mnt/ceph//mnt/ceph├── [ 4.0K] ceph-node1│ ├── [ 1.8G] ceph-node1.qcow2│ ├── [ 224K] disk-1.qcow2│ └── [ 224K] disk-2.qcow2├── [ 4.0K] ceph-node2│ ├── [ 1.8G] ceph-node2.qcow2│ ├── [ 224K] disk-1.qcow2│ └── [ 224K] disk-2.qcow2└── [ 4.0K] ceph-node3 ├── [ 1.8G] ceph-node3.qcow2 ├── [ 224K] disk-1.qcow2 └── [ 224K] disk-2.qcow23 directories, 9 files> virsh list --all Id Name State------------------------------------ - ceph-node1 shut off - ceph-node2 shut off - ceph-node3 shut off
登录ceph-node2
,修改主机名及 IP 地址:
# 修改主机名,下次登录生效[root@ceph-node2 ~]$ hostname ceph-node2[root@ceph-node2 ~]$ echo ceph-node2 > /etc/hostname# 注销重新登录,使新的主机名生效[root@ceph-node2 ~]$ exit # 修改为对应的 IP 地址[root@ceph-node2 ~]$ vim /etc/sysconfig/network-scripts/ifcfg-eth0IPADDR=192.168.200.102# 重启网络[root@ceph-node2 ~]$ systemctl restart network
登录ceph-node3
,进行同样的操作,至此三台虚拟机已经准备完毕。
计划使用三台虚拟机搭建三节点的 Ceph 集群:
hostname | IP | 备注 |
---|---|---|
ceph-node1 | 192.168.200.101 | deploy, 1 mon, 2 osd |
ceph-node2 | 192.168.200.102 | 1 mon, 2 osd |
ceph-node3 | 192.168.200.103 | 1 mon, 2 osd |
通过ceph-deploy
工具即可很方便的从一个节点(此例中为ceph-node1
)部署ceph
集群,因此在ceph-node1
上执行以下操作:
注:以下命令在
ceph-node1
上执行
安装ceph-deploy
:
[root@ceph-node1 ~]$ yum info ceph-deployLoaded plugins: fastestmirrorLoading mirror speeds from cached hostfileAvailable PackagesName : ceph-deployArch : noarchVersion : 1.5.39Release : 0Size : 284 kRepo : ceph-noarchSummary : Admin and deploy tool for CephURL : http://ceph.com/License : MITDescription : An easy to use admin tool for deploy ceph storage clusters.[root@ceph-node1 ~]$ yum install ceph-deploy -y[root@ceph-node1 ~]$ ceph-deploy --version1.5.39
注:以下命令在
ceph-node1
上执行
创建部署目录my-cluster
:
[root@ceph-node1 ~]$ mkdir my-cluster[root@ceph-node1 ~]$ cd my-cluster/# 生成初始配置文件[root@ceph-node1 my-cluster]$ ceph-deploy new ceph-node1 ceph-node2 ceph-node3[root@ceph-node1 my-cluster]$ ls -hltotal 12K-rw-r--r-- 1 root root 203 Mar 2 09:18 ceph.conf-rw-r--r-- 1 root root 3.0K Mar 2 09:18 ceph-deploy-ceph.log-rw------- 1 root root 73 Mar 2 09:18 ceph.mon.keyring[root@ceph-node1 my-cluster]$ cat ceph.conf[global]fsid = 86537cd8-270c-480d-b549-1f352de6c907mon_initial_members = ceph-node1, ceph-node2, ceph-node3mon_host = 192.168.200.101,192.168.200.102,192.168.200.103auth_cluster_required = cephxauth_service_required = cephxauth_client_required = cephx
注:在任何时候当你陷入困境并希望从头开始部署时,就执行以下命令以清空
ceph
的package
,并擦除各节点的数据和配置:
[root@ceph-node1 my-cluster]$ ceph-deploy purge ceph-node1 ceph-node2 ceph-node3[root@ceph-node1 my-cluster]$ ceph-deploy purgedata ceph-node1 ceph-node2 ceph-node3[root@ceph-node1 my-cluster]$ ceph-deploy forgetkeys[root@ceph-node1 my-cluster]$ rm ceph.*
根据此前的 IP 配置向ceph.conf
中添加public_network
,并稍微增大mon
之间的时差允许范围(默认为0.05s
,现改为2s
):
[root@ceph-node1 my-cluster]$ echo public_network=192.168.200.0/24 >> ceph.conf[root@ceph-node1 my-cluster]$ echo mon_clock_drift_allowed = 2 >> ceph.conf[root@ceph-node1 my-cluster]$ cat ceph.conf [global]fsid = 86537cd8-270c-480d-b549-1f352de6c907mon_initial_members = ceph-node1, ceph-node2, ceph-node3mon_host = 192.168.200.101,192.168.200.102,192.168.200.103auth_cluster_required = cephxauth_service_required = cephxauth_client_required = cephxpublic_network=192.168.200.0/24mon_clock_drift_allowed = 2
开始部署monitor
:
[root@ceph-node1 my-cluster]$ ceph-deploy mon create-initial
查看集群状态,此时health
为HEALTH_ERR
是因为还没有部署osd
:
[root@ceph-node1 my-cluster]$ ceph -s cluster 86537cd8-270c-480d-b549-1f352de6c907 health HEALTH_ERR no osds monmap e2: 3 mons at {ceph-node1=192.168.200.101:6789/0,ceph-node2=192.168.200.102:6789/0,ceph-node3=192.168.200.103:6789/0} election epoch 6, quorum 0,1,2 ceph-node1,ceph-node2,ceph-node3 osdmap e1: 0 osds: 0 up, 0 in flags sortbitwise,require_jewel_osds pgmap v2: 64 pgs, 1 pools, 0 bytes data, 0 objects 0 kB used, 0 kB / 0 kB avail 64 creating
开始部署osd
:
[root@ceph-node1 my-cluster]$ ceph-deploy --overwrite-conf osd prepare \ceph-node1:/dev/vdb ceph-node1:/dev/vdc \ceph-node2:/dev/vdb ceph-node2:/dev/vdc \ceph-node3:/dev/vdb ceph-node3:/dev/vdc --zap-disk[root@ceph-node1 my-cluster]$ ceph-deploy --overwrite-conf osd activate \ceph-node1:/dev/vdb1 ceph-node1:/dev/vdc1 \ceph-node2:/dev/vdb1 ceph-node2:/dev/vdc1 \ceph-node3:/dev/vdb1 ceph-node3:/dev/vdc1
查看集群状态:
[root@ceph-node1 my-cluster]$ ceph -s cluster 86537cd8-270c-480d-b549-1f352de6c907 health HEALTH_OK monmap e2: 3 mons at {ceph-node1=192.168.200.101:6789/0,ceph-node2=192.168.200.102:6789/0,ceph-node3=192.168.200.103:6789/0} election epoch 6, quorum 0,1,2 ceph-node1,ceph-node2,ceph-node3 osdmap e30: 6 osds: 6 up, 6 in flags sortbitwise,require_jewel_osds pgmap v72: 64 pgs, 1 pools, 0 bytes data, 0 objects 646 MB used, 12251 GB / 12252 GB avail 64 active+clean
至此,集群部署完成。
首先在ceph-node1
上修改my-cluster
目录下的ceph.conf
:
[root@ceph-node1 my-cluster]$ vim ceph.conf# 将 cephx 全部改为 noneauth_cluster_required = noneauth_service_required = noneauth_client_required = none
之后通过ceph-deploy
将该配置文件推送到三个节点上:
[root@ceph-node1 my-cluster]$ ceph-deploy --overwrite-conf config push ceph-node1 ceph-node2 ceph-node3
最后分别在三个节点上重启mon
和osd
:
# ceph-node1[root@ceph-node1 ~]$ systemctl restart ceph-mon.target[root@ceph-node1 ~]$ systemctl restart ceph-osd.target# ceph-node2[root@ceph-node2 ~]$ systemctl restart ceph-mon.target[root@ceph-node2 ~]$ systemctl restart ceph-osd.target# ceph-node3[root@ceph-node3 ~]$ systemctl restart ceph-mon.target[root@ceph-node3 ~]$ systemctl restart ceph-osd.target
稍后可观察到集群恢复:
[root@ceph-node1 ~]$ ceph -s cluster 86537cd8-270c-480d-b549-1f352de6c907 health HEALTH_OK monmap e2: 3 mons at {ceph-node1=192.168.200.101:6789/0,ceph-node2=192.168.200.102:6789/0,ceph-node3=192.168.200.103:6789/0} election epoch 12, quorum 0,1,2 ceph-node1,ceph-node2,ceph-node3 osdmap e42: 6 osds: 6 up, 6 in flags sortbitwise,require_jewel_osds pgmap v98: 64 pgs, 1 pools, 0 bytes data, 0 objects 647 MB used, 12251 GB / 12252 GB avail 64 active+clean[root@ceph-node1 ~]$ ceph osd treeID WEIGHT TYPE NAME UP/DOWN REWEIGHT PRIMARY-AFFINITY -1 11.96457 root default -2 3.98819 host ceph-node1 0 1.99409 osd.0 up 1.00000 1.00000 5 1.99409 osd.5 up 1.00000 1.00000 -3 3.98819 host ceph-node2 1 1.99409 osd.1 up 1.00000 1.00000 2 1.99409 osd.2 up 1.00000 1.00000 -4 3.98819 host ceph-node3 3 1.99409 osd.3 up 1.00000 1.00000 4 1.99409 osd.4 up 1.00000 1.00000
首先回到宿主机,安装ceph
客户端:
[root@ceph-host ~]$ yum install ceph ceph-radosgw
之后创建/mnt/ceph/rbd-pool.xml
:
<pool type='rbd'> <name>rbd</name> <source> <host name='ceph-node1' port='6789'/> <name>rbd</name> </source></pool>
定义rbd
存储池并启动:
[root@ceph-host ~]$ virsh pool-create /mnt/ceph/rbd-pool.xmlPool rbd defined from /mnt/ceph/rbd-pool.xml[root@ceph-host ~]$ virsh pool-start rbdPool rbd started[root@ceph-host ~]$ virsh pool-info rbdName: rbdUUID: 0e3115e5-87c8-41c6-979b-3b8277deef78State: runningPersistent: yesAutostart: noCapacity: 11.96 TiBAllocation: 1.32 KiBAvailable: 11.96 TiB[root@ceph-host ~]$ virsh pool-dumpxml rbd<pool type='rbd'> <name>rbd</name> <uuid>0e3115e5-87c8-41c6-979b-3b8277deef78</uuid> <capacity unit='bytes'>13155494166528</capacity> <allocation unit='bytes'>1349</allocation> <available unit='bytes'>13154814738432</available> <source> <host name='ceph-node1' port='6789'/> <name>rbd</name> </source></pool>
使用qemu-img
尝试创建一个rbd
镜像:
[root@ceph-host ~]$ qemu-img create -f rbd rbd:rbd/test-from-host 10GFormatting 'rbd:rbd/test-from-host', fmt=rbd size=10737418240[root@ceph-host ~]$ qemu-img info rbd:rbd/test-from-hostimage: json:{"driver": "raw", "file": {"pool": "rbd", "image": "test-from-host", "driver": "rbd"}}file format: rawvirtual size: 10 GiB (10737418240 bytes)disk size: unavailablecluster_size: 4194304
使用rbd
命令与virsh
查看该镜像:
[root@ceph-host ~]$ rbd lstest-from-host[root@ceph-host ~]$ rbd duNAME PROVISIONED USEDtest-from-host 10 GiB 0 B[root@ceph-host ~]$ virsh vol-list rbd Name Path-------------------------------------- test-from-host rbd/test-from-host[root@ceph-host ~]$ virsh vol-info rbd/test-from-hostName: test-from-hostType: networkCapacity: 10.00 GiBAllocation: 10.00 GiB[root@ceph-host ~]$ virsh vol-dumpxml rbd/test-from-host<volume type='network'> <name>test-from-host</name> <key>rbd/test-from-host</key> <source> </source> <capacity unit='bytes'>10737418240</capacity> <allocation unit='bytes'>10737418240</allocation> <target> <path>rbd/test-from-host</path> <format type='raw'/> </target></volume>
宿主机若要关机,关闭三台虚拟机即可。宿主机开机后,再重新启动三台虚拟机,集群会自动恢复至HEALTH_OK
状态。
]]>
配置yum-cron
禁止Yum
在后台自动下载更新
新安装 CentOS 7 后,Yum
自动下载更新默认是开启状态,需要借助yum-cron
将其关闭,否则后台会定期产生下行流量。
首先安装yum-cron
包:
> yum install -y yum-cron> yum info yum-cronLoaded plugins: fastestmirror, langpacksLoading mirror speeds from cached hostfile * epel: nrt.edge.kernel.org * rpmforge: mirror.fairway.ne.jpInstalled PackagesName : yum-cronArch : noarchVersion : 3.4.3Release : 163.el7.centosSize : 51 kRepo : installedFrom repo : baseSummary : Files needed to run yum updates as a cron jobURL : http://yum.baseurl.org/License : GPLv2+Description : These are the files needed to run yum updates as a cron job. : Install this package if you want auto yum updates nightly via : cron.
编辑/etc/yum/yum-cron.conf
文件,将update_messages
和download_updates
均修改为no
:
> vim /etc/yum/yum-cron.conf......# Whether a message should be emitted when updates are available,# were downloaded, or applied.update_messages = no# Whether updates should be downloaded when they are available.download_updates = no......
重启yum-cron
服务:
systemctl enable yum-cronsystemctl restart yum-cron
]]>
将 Vue 项目快速部署至 Nginx 访问
以CentOS 7
为例:
# 安装 epel 库,若已安装则跳过> sudo yum install epel-release# 安装 nginx> sudo yum install nginx> sudo yum info nginxLoaded plugins: fastestmirror, langpacksLoading mirror speeds from cached hostfile * epel: mirrors.njupt.edu.cnInstalled PackagesName : nginxArch : x86_64Epoch : 1Version : 1.16.1Release : 1.el7Size : 1.6 MRepo : installedFrom repo : epelSummary : A high performance web server and reverse proxy serverURL : http://nginx.org/License : BSDDescription : Nginx is a web server and a reverse proxy server for HTTP, SMTP, POP3 and : IMAP protocols, with a strong focus on high concurrency, performance and : low memory usage.> rpm -qa | grep nginxnginx-mod-mail-1.16.1-1.el7.x86_64nginx-1.16.1-1.el7.x86_64nginx-mod-http-image-filter-1.16.1-1.el7.x86_64nginx-all-modules-1.16.1-1.el7.noarchnginx-mod-stream-1.16.1-1.el7.x86_64nginx-mod-http-perl-1.16.1-1.el7.x86_64nginx-filesystem-1.16.1-1.el7.noarchnginx-mod-http-xslt-filter-1.16.1-1.el7.x86_64
启动服务并设置开机自启(如果需要):
> sudo systemctl start nginx # 启动 nginx 服务> sudo systemctl enable nginx # 设置开机自启
/usr/share/nginx/
/etc/nginx/
一般在 Vue 项目中,执行
npm run build
或其他类似命令,会生成dist
目录,即为我们需要部署的 Web 项目文件
首先在 Vue 项目的根目录下进行构建:
idv-web> pwd/home/idv-webidv-web> yarn run build # 或 npm run build 等类似命令idv-web> ls hltotal 536K-rw-r--r-- 1 root root 53 Jan 7 17:28 babel.config.jsdrwxr-xr-x 2 root root 22 Jan 7 17:28 builddrwxr-xr-x 3 root root 57 Jan 12 17:40 dist # dist 目录即为需要部署的文件-rw-r--r-- 1 root root 766 Jan 7 17:28 jest.config.js-rw-r--r-- 1 root root 137 Jan 7 17:28 jsconfig.json-rw-r--r-- 1 root root 1.1K Jan 7 17:28 LICENSEdrwxr-xr-x 2 root root 75 Jan 10 10:34 mockdrwxr-xr-x 1050 root root 32K Jan 7 17:30 node_modules-rw-r--r-- 1 root root 2.0K Jan 7 17:28 package.json-rw-r--r-- 1 root root 197 Jan 7 17:28 postcss.config.jsdrwxr-xr-x 2 root root 43 Jan 7 17:28 public-rw-r--r-- 1 root root 3.2K Jan 7 17:28 README-en.md-rw-r--r-- 1 root root 7.1K Jan 7 17:28 README.mddrwxr-xr-x 13 root root 230 Jan 7 20:56 srcdrwxr-xr-x 3 root root 18 Jan 7 17:28 tests-rw-r--r-- 1 root root 4.3K Jan 7 22:29 vue.config.js-rw-r--r-- 1 root root 434K Jan 7 17:30 yarn.lock
之后将dist
目录复制到nginx
的网页目录下:
idv-web> cp -r ./dist /usr/share/nginx/idv-webidv-web> cd /usr/share/nginx/nginx> ls -hltotal 0drwxr-xr-x 3 root root 136 Jan 15 11:27 htmldrwxr-xr-x 3 root root 57 Jan 15 11:30 idv-webdrwxr-xr-x 2 root root 143 Jan 15 11:27 modules
Nginx 的配置文件位于/etc/nginx
目录下:
> cd /etc/nginx/nginx> ls -hltotal 68Kdrwxr-xr-x 2 root root 26 Jan 15 17:19 conf.ddrwxr-xr-x 2 root root 6 Oct 3 13:15 default.d-rw-r--r-- 1 root root 1.1K Oct 3 13:15 fastcgi.conf-rw-r--r-- 1 root root 1.1K Oct 3 13:15 fastcgi.conf.default-rw-r--r-- 1 root root 1007 Oct 3 13:15 fastcgi_params-rw-r--r-- 1 root root 1007 Oct 3 13:15 fastcgi_params.default-rw-r--r-- 1 root root 2.8K Oct 3 13:15 koi-utf-rw-r--r-- 1 root root 2.2K Oct 3 13:15 koi-win-rw-r--r-- 1 root root 5.2K Oct 3 13:15 mime.types-rw-r--r-- 1 root root 5.2K Oct 3 13:15 mime.types.default-rw-r--r-- 1 root root 2.5K Oct 3 13:15 nginx.conf-rw-r--r-- 1 root root 2.6K Oct 3 13:15 nginx.conf.default-rw-r--r-- 1 root root 636 Oct 3 13:15 scgi_params-rw-r--r-- 1 root root 636 Oct 3 13:15 scgi_params.default-rw-r--r-- 1 root root 664 Oct 3 13:15 uwsgi_params-rw-r--r-- 1 root root 664 Oct 3 13:15 uwsgi_params.default-rw-r--r-- 1 root root 3.6K Oct 3 13:15 win-utf
主配置文件为nginx.conf
,内容如下:
# For more information on configuration, see:# * Official English Documentation: http://nginx.org/en/docs/# * Official Russian Documentation: http://nginx.org/ru/docs/user nginx;worker_processes auto;error_log /var/log/nginx/error.log;pid /run/nginx.pid;# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.include /usr/share/nginx/modules/*.conf;events { worker_connections 1024;}http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; # Load modular configuration files from the /etc/nginx/conf.d directory. # See http://nginx.org/en/docs/ngx_core_module.html#include # for more information. include /etc/nginx/conf.d/*.conf; server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /usr/share/nginx/html; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } } # 省略部分配置}
可以看到:
server
节点对应一个网站目录conf.d
目录下的子配置文件server
节点又会导入default.d
目录下的配置这样一来,只需在conf.d
目录下添加网站配置即可:
注意:尽量避免使用 Chrome 中默认的非安全端口,参见 Chrome 错误代码:ERR_UNSAFE_PORT | CSDN
nginx> pwd/etc/nginxnginx> ls conf.d/idv-web.confnginx> cat conf.d/idv-web.confserver { listen 4000; # 绑定的端口 server_name localhost; root /usr/share/idv-web; # 网站对应的根目录 index index.html; location / { root /usr/share/nginx/idv-web; try_files $uri $uri/ @router; # 需要指向下面的 @router,否则 Vue 的路由在 Nginx 中刷新会报 404 index index.html; } # 对应上面的 @router # 主要原因是路由的路径资源并不是一个真实的路径, 所以无法找到具体的文件 # 因此需要 rewrite 到 index.html 中,然后交给路由进行处理 location @router { rewrite ^.*$ /index.html last; } }
重新加载 Nginx 的配置文件 (无需重启nginx
服务):
> nginx -s reload
即可通过http://<SERVER_IP>:<PORT>
访问项目首页。
]]>
在 腾讯云 COS 上部署 Hexo 博客,绑定自定义域名并开启 CDN 加速
// TODO: TO be updated…
]]>
手动编译 QEMU 源码,开启对 Ceph RBD 的支持。QEMU 版本 4.0.50
最近尝试使用 Ceph 作为 KVM 虚拟机的镜像存储方案,已经搭建好了三节点的 Ceph 集群。
先是在本地使用virsh
定义了名为rbd
的 Ceph 存储池:
注意:为了简便起见,暂时关闭了
cephx
认证。生产环境请务必启用
> virsh pool-dumpxml rbd<pool type='rbd'> <name>rbd</name> <uuid>908261b9-942e-4d7d-9fb5-66a170a27afb</uuid> <capacity unit='bytes'>1607391510528</capacity> <allocation unit='bytes'>9669956428</allocation> <available unit='bytes'>1575195836416</available> <source> <host name='ceph-master' port='6789'/> <name>rbd</name> </source></pool>> virsh vol-list rbd Name Path ------------------------------------------------------------------------------ centos7 rbd/centos7 win10-base rbd/win10-base win7 rbd/win7
使用virt-install
生成虚拟机的 XML 配置文件:
> virt-install --name ceph-test \ --machine pc --cpu host-model,disable=vmx \ --vcpus 2 --memory 2048 \ --disk vol=rbd/win10-base \ --disk device=cdrom,bus=ide,path=/mnt/kvm-idv/iso/win10-1809-x64.iso \ --boot cdrom --print-xml > ceph-test.xml
ceph-test.xml
内容如下:
<domain type="kvm"> <name>ceph-test</name> <uuid>238bd909-04da-42cc-9e0c-11841b58d470</uuid> <memory>2097152</memory> <currentMemory>2097152</currentMemory> <vcpu>2</vcpu> <os> <type arch="x86_64" machine="pc">hvm</type> <boot dev="cdrom"/> </os> <features> <acpi/> <apic/> <vmport state="off"/> </features> <cpu mode="host-model"> <feature policy="disable" name="vmx"/> </cpu> <clock offset="utc"> <timer name="rtc" tickpolicy="catchup"/> <timer name="pit" tickpolicy="delay"/> <timer name="hpet" present="no"/> </clock> <pm> <suspend-to-mem enabled="no"/> <suspend-to-disk enabled="no"/> </pm> <devices> <emulator>/usr/bin/kvm-spice</emulator> <disk type="network" device="disk"> <driver name="qemu" type="raw"/> <source protocol="rbd" name="rbd/win10-base"> <host name="ceph-master" port="6789"/> </source> <target dev="hda" bus="ide"/> </disk> <disk type="file" device="cdrom"> <driver name="qemu" type="raw"/> <source file="/mnt/kvm-idv/iso/win10-1809-x64.iso"/> <target dev="hdb" bus="ide"/> <readonly/> </disk> <controller type="usb" index="0" model="ich9-ehci1"/> <controller type="usb" index="0" model="ich9-uhci1"> <master startport="0"/> </controller> <controller type="usb" index="0" model="ich9-uhci2"> <master startport="2"/> </controller> <controller type="usb" index="0" model="ich9-uhci3"> <master startport="4"/> </controller> <interface type="network"> <source network="default"/> <mac address="52:54:00:c0:63:ee"/> </interface> <graphics type="spice" port="-1" tlsPort="-1" autoport="yes"> <image compression="off"/> </graphics> <console type="pty"/> <channel type="spicevmc"> <target type="virtio" name="com.redhat.spice.0"/> </channel> <sound model="ich6"/> <video> <model type="qxl"/> </video> <redirdev bus="usb" type="spicevmc"/> <redirdev bus="usb" type="spicevmc"/> </devices></domain>
然而在启动虚拟机时出现了问题:
> virsh define ./ceph-test.xmlDomain ceph-test defined from ./ceph-test.xml> virsh start ceph-testerror: Failed to start domain ceph-testerror: internal error: process exited while connecting to monitor: 2020-01-02T06:58:54.789212Z qemu-system-x86_64: -drive file=rbd:rbd/win10-base:auth_supported=none:mon_host=ceph-master\:6789,format=raw,if=none,id=drive-ide0-0-0: Unknown protocol 'rbd'
报错信息提示 QEMU 不支持rbd
协议,突然想起来这台机器上的 QEMU 是自己手动编译的,推测是编译的时候并未开启对 Ceph RBD 的支持。检查一下果然:
> qemu-system-x86_64 --versionQEMU emulator version 4.0.50 (v4.0.0-142-ge0fb2c3d89-dirty)Copyright (c) 2003-2019 Fabrice Bellard and the QEMU Project developers> qemu-img -h | grep 'Supported formats'Supported formats: blkdebug blklogwrites blkreplay blkverify bochs cloop copy-on-read dmg file ftp ftps host_cdrom host_device http https luks nbd null-aio null-co nvme parallels qcow qcow2 qed quorum raw replication sheepdog throttle vdi vhdx vmdk vpc vvfat
并没有rbd
的身影。没办法,只好重新编译一下了。
注意:
- 关于如何从源码编译安装 QEMU,请参考之前的文章:编译和安装 QEMU
- 本文仅添加对 Ceph RBD 的支持
进入 QEMU 源码目录(此处版本为4.0.50
):
> ./configure --help | grep rbd rbd rados block device (rbd)
上次配置时使用的参数如下:
> ./configure --prefix=/usr \ --enable-kvm \ --enable-libusb \ --enable-usb-redir \ --enable-debug \ --enable-debug-info \ --enable-curl \ --enable-sdl \ --enable-vhost-net \ --enable-spice \ --enable-vnc \ --enable-opengl \ --enable-gtk \ --target-list=x86_64-softmmu
只需添加--enable-rbd
,即:
> ./configure --prefix=/usr \ --enable-kvm \ --enable-libusb \ --enable-usb-redir \ --enable-debug \ --enable-debug-info \ --enable-curl \ --enable-sdl \ --enable-vhost-net \ --enable-spice \ --enable-vnc \ --enable-opengl \ --enable-gtk \ --enable-rbd \ --target-list=x86_64-softmmu
配置前还需安装一些依赖包,可根据错误提示自行搜索安装。例如 Ubuntu 需要安装下面三个包:
> apt install libcephfs-dev librados-dev librbd-dev
配置完成后检查一下:
> cat ./config-host.mak | grep -i rbdCONFIG_RBD=mRBD_CFLAGS=RBD_LIBS=-lrbd -lrados
可以看到已经开启了对 Ceph RBD 的支持。
# 编译 QEMU# 本机共 8 核,故开启 16 线程> make -j 16# 验证此时 qemu-img 是否支持 rbd> ./qemu-img -h | grep rbd# 安装 QEMU> make install
之后即可正常启动使用 Ceph RBD 的虚拟机,问题解决。
- 通过 libvirt 使用 Ceph RBD | Ceph Documentation
- QEMU 与块设备 | Ceph Documentation
- 使用 Ceph 作为 QEMU KVM 虚拟机的存储 - 冬日の草原
- OpenStack 使用 Ceph 存储后端创建虚拟机快照原理剖析 | int32bit’s Blog
- OpenStack 使用 Ceph 存储,Ceph 到底做了什么? | int32bit’s Blog
- libvirt 使用多个 Ceph 集群 | 李睿的博客
- CentOS KVM + Ceph | 李小波
- 初始 Ceph | jeremy 的技术点滴
- virt-install 工具安装基于 rbd 磁盘的虚拟机 | opengers
- QEMU3 - 使用 Ceph 来存储 QEMU 镜像 | 三木的博客
- 使用 Ceph 来存储 QEMU 镜像 | 腾讯云+社区
- Ceph 常用命令 | CSDN
- Ceph 运维常用指令 | 51CTO
- OpenStack 对接 Ceph 时的错误 | Linux 公社
- KVM + Ceph RBD 快照创建问题 | 51CTO
- SOSP19’ Ceph 的十年经验总结:文件系统是否适合做分布式文件系统的后端 | Ceph 开源社区
- 通过 libvirt 使用 Ceph 块设备 | 51CTO
- Ceph 基础知识和基础架构认识 | 博客园
- 验证 RBD 的缓存是否开启 | 磨渣
- 管理 Ceph 缓存池 | 博客园
- Ceph 的正确玩法之 SSD 作为 HDD 的缓存池 | 华云数据
]]>
Vue.js:渐进式 JavaScript 框架,相关资料整理
// TODO: To be updated…
安装vue-cli
:
# 安装 Vue CLI> npm install -g @vue/cli# 或者使用 yarn> npm i -g yarn> yarn global add @vue/cli# 验证 Node.js 与 vue-cli 版本> node -vv12.13.0> npm -v6.13.0> vue -V@vue/cli 4.0.5> vueUsage: vue <command> [options] uOptions: -V, --version output the version number -h, --help output usage informationCommands: create [options] <app-name> create a new project powered by vue-cli-service add [options] <plugin> [pluginOptions] install a plugin and invoke its generator in an already created project invoke [options] <plugin> [pluginOptions] invoke the generator of a plugin in an already created project inspect [options] [paths...] inspect the webpack config in a project with vue-cli-service serve [options] [entry] serve a .js or .vue file in development mode with zero config build [options] [entry] build a .js or .vue file in production mode with zero config ui [options] start and open the vue-cli ui init [options] <template> <app-name> generate a project from a remote template (legacy API, requires @vue/cli-init) config [options] [value] inspect and modify the config outdated [options] (experimental) check for outdated vue cli service / plugins upgrade [options] [plugin-name] (experimental) upgrade vue cli service / plugins info print debugging information about your environment Run vue <command> --help for detailed usage of given command.
构建一个新项目:
> vue create my-project
或者使用可视化界面:
> vue ui
vue-cli
脚手架生成的项目目录结构大致如下所示:
├── node_modules # 项目依赖包目录├── public│ ├── favicon.ico # ico 图标│ └── index.html # 首页模板|├── src │ ├── assets/ # 样式图片目录│ ├── components/ # 组件目录│ ├── views/ # 页面目录│ ├── App.vue # 父组件│ ├── main.js # 入口文件│ ├── router.js # 路由配置文件│ └── store.js # vuex 状态管理文件|├── .gitignore # git 忽略文件├── .postcssrc.js # postcss 配置文件├── babel.config.js # babel 配置文件├── package.json # 包管理文件└── yarn.lock # yarn 依赖信息文件
包管理文件package.json
的内容大致如下:
详细的
package.json
文件配置项可以参考:npm-package.json | npm Documentaion
{ "name": "my-project", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "vue": "^2.5.16", "vue-router": "^3.0.1", "vuex": "^3.0.1" }, "devDependencies": { "@vue/cli-plugin-babel": "^3.0.0-beta.15", "@vue/cli-service": "^3.0.0-beta.15", "less": "^3.0.4", "less-loader": "^4.1.0", "vue-template-compiler": "^2.5.16" }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ]}
在项目的构建和开发阶段,常用的npm
命令有:
# 生成 package.json 文件(需要手动选择配置)npm init# 生成 package.json 文件(使用默认配置)npm init -y# 一键安装 package.json 下的依赖包npm i# 在项目中安装包名为 xxx 的依赖包npm i xxx# 在项目中安装包名为 xxx 的依赖包(配置在 dependencies 下)npm i xxx --save# 在项目中安装包名为 xxx 的依赖包(配置在 devDependencies 下)npm i xxx --save-dev# 全局安装包名为 xxx 的依赖包npm i -g xxx# 运行 package.json 中 scripts 下的命令npm run xxx
比较陌生但实用的有:
# 打开 xxx 包的主页npm home xxx# 打开 xxx 包的代码仓库npm repo xxx# 将当前模块发布到 npmjs.com,需要先登录npm publish
yarn 是由 Facebook 推出并开源的包管理工具,具有速度快、安全性高、可靠性强等优势
yarn
的常用命令如下:
# 生成 package.json 文件(需要手动选择配置)yarn init# 生成 package.json 文件(使用默认配置)yarn init -y# 一键安装 package.json 下的依赖包yarn# 在项目中安装包名为 xxx 的依赖包(配置在 dependencies 下),同时 yarn.lock 也会被更新yarn add xxx# 在项目中安装包名为 xxx 的依赖包(配置在配置在 devDependencies 下),同时 yarn.lock 也会被更新yarn add xxx --dev# 全局安装包名为 xxx 的依yarn global add xxx# 运行 package.json 中 scripts 下的命令yarn xxx# 查看 yarn 配置项yarn config list
比较陌生但实用的有:
# 列出 xxx 包的版本信息yarn outdated xxx# 验证当前项目 package.json 里的依赖版本和 yarn 的 lock 文件是否匹配yarn check# 将当前模块发布到 npmjs.com,需要先登录yarn publish
例如在package.json
中的browserslist
配置项,其主要作用是在不同的前端工具之间共享目标浏览器和 Node.js 版本:
"browserslist": [ "> 1%", // 表示包含所有使用率 > 1% 的浏览器 "last 2 versions", // 表示包含浏览器最新的两个版本 "not ie <= 8" // 表示不包含 ie8 及以下版本]
也可以单独写在.browserslistrc
文件中:
# Browsers that we support > 1%last 2 versionsnot ie <= 8
或者在项目终端运行如下命令查看:
> npx browserslistand_chr 78and_ff 68and_qq 1.2and_uc 12.12android 76baidu 7.12bb 10bb 7chrome 78chrome 77chrome 76edge 18edge 17firefox 70firefox 69ie 11ie 10ie_mob 11ie_mob 10ios_saf 13.0-13.2ios_saf 12.2-12.4kaios 2.5op_mini allop_mob 46op_mob 12.1opera 64opera 63safari 13safari 12.1samsung 10.1samsung 9.2
除了使用npm
和yarn
命令对包进行安装和配置之外,vue-cli
从3.0
版本开始还提供了专属的vue add
命令:
@vue/cli-plugin
或者vue-cli-plugin
开头的包# vue-cli 会将 eslint 解析为 @vue/cli-plugin-eslintvue add eslint
注意:不同于
npm
或yarn
的安装,vue add
不仅会将包安装到项目中,还会改变项目的代码或文件结构
另外vue add
中还有两个特例:
# 安装 vue-routervue add router# 安装 vuexvue add vuex
这两个命令会直接安装vue-router
和vuex
并改变你的代码结构,使你的项目集成这两个配置。
vue-cli 3.x
提供了一种开箱即用的模式,无需配置webpack
即可运行项目。并且还提供了一个vue.config.js
文件来满足开发者对其封装的webpack
默认配置的修改:
// TODO: To be updated…
先在<el-table-column>
中指定:formatter
:
<el-table :data="rows" ref="datagrid" border="true" highlight-current-row="true" style="width: 100%"> <el-table-column prop="tableId" label="表id" :show-overflow-tooltip="true"> </el-table-column> <el-table-column prop="pk" label="是否为主键" :formatter="formatBoolean" :show-overflow-tooltip="true"> </el-table-column> </el-table>
之后在methods
中添加formatBoolean
:
// 布尔值格式化:cellValue为后台返回的值formatBoolean: function (row, column, cellValue) { var ret = ''; //你想在页面展示的值 if (cellValue) { ret = "是"; //根据自己的需求设定 } else { ret = "否"; } return ret;},
设置formatter
或filter
,参见:
坑在于<el-table-column>
要加prop
和column-key
,参见:
先手动添加一个<el-table-column>
,并设置其type
属性为selection
:
<el-table-column type="selection" width="55" />
添加一个<el-button>
,方便清除所有选择:
<el-button type="primary" @click="toggleSelection()">取消选择</el-button>
最后在methods
中实现toggleSelection()
:
toggleSelection(rows) { let selectedRows = this.$refs.userTable.store.states.selection; selectedRows.forEach(row => { console.log("id: ", row.id, " name: ", row.name); }); if (rows) { rows.forEach(row => { this.$refs.userTable.toggleRowSelection(row); }); } else { this.$refs.userTable.clearSelection(); }}
多选的行数据这样获取:
let selectedRows = this.$refs.userTable.store.states.selection;
参见:
设置<el-button>
的:disabled
:
<template slot-scope="scope"> //这里需要注意一下 <el-button type="primary" :disabled="scope.row.state == '已完成'" size="small" @click="dialogFormVisible = true"> 操作 </el-button></template>
调用done()
关闭:
this.$msgbox({ title: '用户下线', message: '是否下线当前用户?', showCancelButton: true, confirmButtonText: '确定', cancelButtonText: '取消', beforeClose: (action, instance, done) => { if (action === 'confirm') { done() } else { done() } }}).then(() => { const axiosIns = axios.create({ baseURL: 'http://localhost:8080/api/v1', timeout: 3000 }) axiosIns .delete(`login/${this.list[index].id}`) .then(res => { console.log(res) }) .catch(err => { console.error(err) }) this.list[index].online = false this.$message({ message: `用户 ${this.list[index].id} 已下线`, type: 'success', duration: 2000, showClose: true })})
参见:
]]>
近期整理备忘,待更新…
- 云计算底层技术 - 使用 Open vSwitch | opengers
- Open vSwitch 使用指南:OVS 常用操作总结 | Fishcried
- Open vSwitch 架构解析与功能实践 - 范桂飓 | CSDN
- Open vSwitch 的原理和常用命令 | 开源中国
- Open vSwitch 详解 | 简书
- Open vSwitch 的 ovs-vsctl 命令详解 | 八戒
- 研究 Open vSwitch | jeremy 的技术点滴
- 研究 Open vSwitch | 腾讯云+社区
- OVS 初级教程:使用 Open vSwitch 构建虚拟网络 | SDNLAB
- 云计算底层技术 - 使用 Open vSwitch | opengers
- VM 跨主机通信 OVS 配置 | 神评网
- 整合 Open vSwitch 与 DNSmasq 为虚拟机提供 DHCP 功能 | 博客园
- 整合 Open vSwitch 与 DNSmasq 为虚拟机提供 DHCP 功能 | CSDN
- 【重要】虚拟化调试和优化指南 | Red Hat
- 快速创建 KVM 虚拟机 | jeremy 的技术点滴
- 基于 OpenStack Ironic 实现 x86 裸机自动化装机实践与优化 | int32bit’s Blog
- 如何探测虚拟化环境是物理机、虚拟机还是容器?| int32bit’s Blog
- KVM 中开启嵌套虚拟化 | Fishcried
- VM 电源管理 Xen 和 KVM 比较 | 任思绪在这里飞
- 试用 WebVirtMgr | jeremy 的技术点滴
- 云平台核心架构设计要点 | 神评网
- KVM 虚拟化环境中的时区设置 | opengers
- KVM 虚拟化之嵌套虚拟化 Nested | opengers
- KVM 在线添加硬件 | opengers
- 虚拟化 iothread 特性 | 腾讯云+社区
- OpenStack 底层技术 - 实例磁盘扩容及格式转换 | opengers
> osinfo-query os
- kvm libvirt qemu实践系列(一)-kvm介绍 | opengers
- kvm libvirt qemu实践系列(二)-虚拟机管理 | opengers
- kvm libvirt qemu实践系列(三)-虚拟机xml文件使用 | opengers
- kvm libvirt qemu实践系列(四)-kvm虚拟机在线调整配置 | opengers
- kvm libvirt qemu实践系列(五)-虚拟机快照链 | opengers
- QEMU 中 VNC 流程详解+代码分析 | 任思绪在这里飞
- QEMU 2: 参数解析 | 三木的博客
- Accelerating QEMU on Windows with HAXM | QEMU
- Intel Hardware Accelerated Execution Manager (HAXM) | Github
]]>
gops 相关资料整理,待更新…
// TODO: To be updated…
]]>
Gogs:一款极易搭建的自助 Git 服务, by Unknwon@Github
本文基于
Gogs 0.11.91.0811
>=5.7
(InnoDB 引擎)、PostgreSQL、MSSQL、TiDB>=1.8.3
ssh-keygen
到%PATH%
环境变量中Bash
是默认的 Shell 程序,而不是PowerShell
Gogs 默认以git
用户运行,首先以root
身份新建用户git
并为其设置密码:
> sudo adduser git> sudo passwd git
之后切换至git
用户,在/home/git/
目录下创建.ssh
目录:
> su git # 切换至 git 用户> cd /home/git//home/git > mkdir .ssh # 创建 .ssh 目录/home/git > ls -aldrwx------ 6 git git 154 Nov 1 22:58 .drwxr-xr-x. 6 root root 58 Nov 1 18:35 ..-rw------- 1 git git 146 Nov 1 22:50 .bash_history-rw-r--r-- 1 git git 18 Apr 11 2018 .bash_logout-rw-r--r-- 1 git git 193 Apr 11 2018 .bash_profile-rw-r--r-- 1 git git 231 Apr 11 2018 .bashrcdrwxrwxr-x 3 git git 18 Nov 1 18:36 .cachedrwxrwxr-x 3 git git 18 Nov 1 18:36 .configdrwxr-xr-x 4 git git 39 Nov 13 2018 .mozilladrwxrwxr-x 2 git git 6 Nov 1 22:58 .ssh-rw-r--r-- 1 git git 658 Oct 31 2018 .zshrc
注:之后的操作全部以
git
用户进行操作
Gogs 支持二进制、源码、包管理、Docker、Vagrant、基于 K8s 的 Helm Charts 等多种安装方式
从 Gogs 的Releases
页面下载linux_amd64zip
,之后解压:
/home/git > wget https://github.com/gogs/gogs/releases/download/v0.11.91/linux_amd64.zip/home/git > unzip linux_amd64.zip/home/git > cd gogs/home/git/gogs > ls -hltotal 55M-rwxr-xr-x 1 git git 55M Aug 12 10:26 gogs-rw-r--r-- 1 git git 1.1K Jun 5 2018 LICENSEdrwxr-xr-x 8 git git 101 Aug 12 10:26 public-rw-r--r-- 1 git git 8.4K Aug 12 10:25 README.md-rw-r--r-- 1 git git 5.5K Aug 12 10:25 README_ZH.mddrwxr-xr-x 7 git git 195 Aug 12 10:26 scriptsdrwxr-xr-x 11 git git 174 Aug 12 10:26 templates/home/git/gogs > pwd/home/git/gogs
Gogs 的默认配置文件位于源码中的
conf/app.ini
,该文件从v0.6.0
版本开始被嵌入到二进制中
为了使自定义配置能覆盖原有的默认配置,需要在gogs
目录下手动创建自定义配置文件custom/conf/app.ini
,在该文件中修改相应选项的值即可:
/home/git/gogs > mkdir -p custom/conf/home/git/gogs > vim custom/conf/app.ini
参见官方文档:配置与运行 - Gogs
例如自定义仓库根目录、数据库配置:
[repository]ROOT = /home/gogs-repos[database]USER = adminPASSWD = ******
这样可以保护自定义配置不被破坏:
首先建立好数据库gogs
,文件scripts/mysql.sql
是数据库的初始化 SQL 语句:
SET GLOBAL innodb_file_per_table = ON, innodb_file_format = Barracuda, innodb_large_prefix = ON;DROP DATABASE IF EXISTS gogs;CREATE DATABASE IF NOT EXISTS gogs CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
执行下列命令即可初始化gogs
数据库:
mysql -u root -p < scripts/mysql.sql
此外还需要登录 MySQL 创建新用户gogs
,并将数据库gogs
的所有权限都赋予该用户:
mysql> CREATE USER 'gogs'@'localhost' identified by 'YOUR_PASSWORD';mysql> GRANT ALL PRIVILEGES ON gogs.* to 'gogs'@'localhost';mysql> FLUSH PRIVILEGES;
如需更新密码:
mysql> ALTER USER 'gogs'@'localhost' identified by 'NEW_PASSWORD';
./gogs web
前台运行:
/home/gogs > ./gogs web2019/11/01 17:48:31 [TRACE] Custom path: /home/gogs/custom2019/11/01 17:48:31 [TRACE] Log path: /home/gogs/log2019/11/01 17:48:31 [TRACE] Log Mode: Console (Trace)2019/11/01 17:48:31 [ INFO] Gogs 0.11.91.08112019/11/01 17:48:31 [ INFO] Cache Service Enabled2019/11/01 17:48:31 [ INFO] Session Service Enabled2019/11/01 17:48:31 [ INFO] SQLite3 Supported2019/11/01 17:48:31 [ INFO] Run Mode: Development2019/11/01 17:48:31 [ INFO] Listen: http://0.0.0.0:3000...
或者nohup ./gogs web > gogs.out 2>&1 &
后台运行:
# 后台运行 gogs,stderr 重定向至 stdout,stdout 重定向至 gogs.out/home/gogs $ nohup ./gogs web > gogs.out 2>&1 &[1] 8346/home/gogs $ jobs -l # 查看后台任务[1] + 8346 running nohup ./gogs web > gogs.out 2>&1/home/gogs $ cat gogs.out # 查看 gogs 在 gogs.out 中的输出nohup: ignoring input2019/11/01 18:01:33 [TRACE] Custom path: /home/gogs/custom2019/11/01 18:01:33 [TRACE] Log path: /home/gogs/log2019/11/01 18:01:33 [TRACE] Log Mode: Console (Trace)2019/11/01 18:01:33 [ INFO] Gogs 0.11.91.08112019/11/01 18:01:33 [ INFO] Cache Service Enabled2019/11/01 18:01:33 [ INFO] Session Service Enabled2019/11/01 18:01:33 [ INFO] SQLite3 Supported2019/11/01 18:01:33 [ INFO] Run Mode: Development2019/11/01 18:01:33 [ INFO] Listen: http://0.0.0.0:3000/home/gogs $ fg %1 # 切至前台运行[1] + 8346 running nohup ./gogs web > gogs.out 2>&1^Z # 暂停并放入后台[1] + 8346 suspended nohup ./gogs web > gogs.out 2>&1/home/gogs $ jobs -l[1] + 8346 suspended nohup ./gogs web > gogs.out 2>&1/home/gogs $ bg %1 # 后台继续运行[1] + 8346 continued nohup ./gogs web > gogs.out 2>&1/home/gogs $ jobs -l[1] + 8346 running nohup ./gogs web > gogs.out 2>&1
参见:
最后访问http://localhost:3000/install
,即可根据提示进行安装配置:
// TODO: To be updated…
]]>
swaggo: Automatically generate RESTful API documentation with Swagger 2.0 for Go.
// TODO: To be updated…
待更新
下载swaggo
:
> go get -u github.com/swaggo/swag/cmd/swag
> swag init -hNAME: swag init - Create docs.goUSAGE: swag init [command options] [arguments...]OPTIONS: --generalInfo value, -g value Go file path in which 'swagger general API Info' is written (default: "main.go") --dir value, -d value Directory you want to parse (default: "./") --propertyStrategy value, -p value Property Naming Strategy like snakecase,camelcase,pascalcase (default: "camelcase") --output value, -o value Output directory for all the generated files(swagger.json, swagger.yaml and doc.go) (default: "./docs") --parseVendor Parse go files in 'vendor' folder, disabled by default --parseDependency Parse go files in outside dependency folder, disabled by default
// TODO: To be updated…
- BindJSON validation failed for a required integer field that has zero value - gin | Github
- bug:c.BindJSON(&req) - gin | Github
- Binding JSON having fields with empty string generates validation error - gin | Github
- Binding failed on int required field when value is 0 - gin | Github
- Bind validation bug with int zero value - gin | Github
- 模型绑定和验证 | Gin Web Framework
- Model binding and validation - gin-gonic/gin | Github
]]>
节约空间,能省一点是一点
go build
使用的是静态编译,会将程序的依赖一起打包,这样一来编译得到的可执行文件可以直接在目标平台运行,无需运行环境(例如 JRE)或动态链接库(例如 DLL)的支持。
虽然 Go 的静态编译很方便,但也存在一个问题:打包生成的可执行文件体积较大,毕竟相关的依赖都被打包进来了。今天就来尝试一下压缩 Go 编译得到的可执行文件的体积。
在程序编译的时候可以加上-ldflags "-s -w"
参数来优化编译,原理是通过去除部分链接和调试等信息来减小编译生成的可执行程序体积,具体参数如下:
-a
:强制编译所有依赖包-s
:去掉符号表信息,不过panic
的时候stace trace
就没有任何文件名/行号信息了-w
:去掉DWARF
调试信息,不过得到的程序就不能使用gdb
进行调试了注:不建议
-w
和-s
同时使用
未添加编译参数,main.exe
原体积约为19.6M
:
> go build main.go # 直接编译> ls -al main.exe # 约为 19.6M-rwxr-xr-x 1 abelsu7 197609 20556800 10月 25 15:52 main.exe*
添加-w
参数,去掉调试信息,体积减小至约15.8M
:
> go build -ldflags "-w" main.go # 添加 -w,去掉调试信息> ls -al main.exe # 约为 15.8M-rwxr-xr-x 1 abelsu7 197609 16569344 10月 25 15:54 main.exe*
添加-s
参数,去掉符号表,体积减小至约14.7M
:
> go build -ldflags "-s" main.go # 添加 -s,去掉符号表> ls -al main.exe # 约为 14.7M-rwxr-xr-x 1 abelsu7 197609 15397888 10月 25 15:59 main.exe*
同时添加-w -s
参数,体积同样约为14.7M
:
> go build -ldflags "-w -s" main.go # 同时添加 -w -s> ls -al main.exe # 同样约为 14.7M-rwxr-xr-x 1 abelsu7 197609 15397888 10月 25 16:04 main.exe*
- 可以看到添加
-s
参数时,可执行文件体积减小最多- 若对符号表无需求,
-ldflags
直接添加"-s"
即可
UPX - the Ultimate Packer for eXecutables 是一款开源的可执行文件压缩程序,可以压缩常见平台下的可执行程序包。
在 Releases 页面下载对应平台的
upx
,MacOS 和 Linux 可以直接安装发行版:
# For MacOS> brew install upx# For CentOS/Fedora/RHEL> yum install upx> yum info upxInstalled PackagesName : upxArch : x86_64Version : 3.95Release : 4.el7Size : 1.8 MRepo : installedFrom repo : epelSummary : Ultimate Packer for eXecutablesURL : http://upx.sourceforge.net/License : GPLv2+ and Public DomainDescription : UPX is a free, portable, extendable, high-performance executable : packer for several different executable formats. It achieves an : excellent compression ratio and offers very fast decompression. Your : executables suffer no memory overhead or other drawbacks.
直接go build
后压缩,main.exe
体积从19.6M
压缩至9.1M
,为原体积的46.48%
:
> go build main.go> ls -al main.exe # 约为 19.6M-rwxr-xr-x 1 abelsu7 197609 20556800 10月 25 16:32 main.exe*> upx main.exe # 压缩至原体积的 46.48% Ultimate Packer for eXecutables Copyright (C) 1996 - 2018UPX 3.95w Markus Oberhumer, Laszlo Molnar & John Reiser Aug 26th 2018 File size Ratio Format Name -------------------- ------ ----------- ----------- 20556800 -> 9554944 46.48% win64/pe main.exePacked 1 file.> ls -al main.exe # 约为 9.1M-rwxr-xr-x 1 abelsu7 197609 9554944 10月 25 16:32 main.exe*
添加-ldflags "-s"
后压缩,main.exe
体积从14.7M
压缩至4.8M
,为原体积的32.42%
:
> go build -ldflags "-s" main.go> ls -al main.exe # 约为 14.7M-rwxr-xr-x 1 abelsu7 197609 15397888 10月 25 16:38 main.exe*> upx main.exe # 压缩至原体积的 32.42% Ultimate Packer for eXecutables Copyright (C) 1996 - 2018UPX 3.95w Markus Oberhumer, Laszlo Molnar & John Reiser Aug 26th 2018 File size Ratio Format Name -------------------- ------ ----------- ----------- 15397888 -> 4992512 32.42% win64/pe main.exePacked 1 file.> ls -al main.exe # 约为 4.8M-rwxr-xr-x 1 abelsu7 197609 4992512 10月 25 16:38 main.exe*
最终经过
-ldflags
编译优化和upx
压缩,main.exe
体积从19.6M
压缩至4.8M
,约为原体积的24.5%
,效果还是很明显的
另外,upx
不仅可以压缩当前主机平台的可执行程序,只要是支持的平台都可以进行压缩。因此可以先在本机进行交叉编译,之后使用上述方法压缩可执行程序,最后再将可执行程序传至目标平台运行。
关于交叉编译,可以参考上一篇文章:Go 程序的交叉编译、选择性编译 | 苏易北
以在windows/amd64
下交叉编译linux/amd64
为例,main
体积从19.9M
压缩至4.8M
,约为原体积的24.2%
,压缩比例大致相同:
# 设置交叉编译环境变量> set GOOS=Linux> set GOARCH=amd64> set CGO_ENABLED=0# 直接交叉编译> go build main.go> ls -al main # 约为 19.9M-rw-r--r-- 1 abelsu7 197609 20837787 10月 25 16:51 main# 开启编译优化,去掉符号表> go build -ldflags "-s" main.go> ls -al main # 约为 14.7M-rw-r--r-- 1 abelsu7 197609 15464736 10月 25 16:51 main> upx main # 压缩至原体积的 32.60% Ultimate Packer for eXecutables Copyright (C) 1996 - 2018UPX 3.95w Markus Oberhumer, Laszlo Molnar & John Reiser Aug 26th 2018 File size Ratio Format Name -------------------- ------ ----------- ----------- 15464736 -> 5041696 32.60% linux/amd64 mainPacked 1 file.> ls -al main # 约为 4.8M-rw-r--r-- 1 abelsu7 197609 5041696 10月 25 16:51 main
下面是upx
的一些常用参数:
-o
:指定输出的文件名-k
:保留备份原文件-1
:最快压缩,共1-9
九个级别-9
:最优压缩,与上面对应-d
:解压缩decompress
,恢复原体积-l
:显示压缩文件的详情,例如upx -l main.exe
-t
:测试压缩文件,例如upx -t main.exe
-q
:静默压缩be quiet
-v
:显示压缩细节be verbose
-f
:强制压缩-V
:显示版本号-h
:显示帮助信息--brute
:尝试所有可用的压缩方法,slow
--ultra-brute
:比楼上更极端,very slow
]]>
在 Windows、Linux、MacOS 下交叉编译 Golang
从Golang 1.5
开始,交叉编译变得非常便捷:
CGO
的程序,只需设置GOOS
、GOARCH
、CGO_ENABLED
这几个环境变量,即可直接利用编译器自带的跨平台特性实现跨平台编译CGO
的程序,大部分情况下可以通过配置CC
环境变量使用自行准备的交叉编译工具进行编译关于使用
CGO
情况下的交叉编译,参见 交叉编译 Go 程序 | Holmesian Blog
> go versiongo version go1.13 windows/amd64> go tool dist listaix/ppc64android/386android/amd64android/armandroid/arm64darwin/386 # Mac i386darwin/amd64 # Mac amd64darwin/armdarwin/arm64dragonfly/amd64freebsd/386freebsd/amd64freebsd/armillumos/amd64js/wasmlinux/386 # Linux i386 linux/amd64 # Linux amd64linux/armlinux/arm64linux/mipslinux/mips64linux/mips64lelinux/mipslelinux/ppc64linux/ppc64lelinux/s390xnacl/386nacl/amd64p32nacl/armnetbsd/386netbsd/amd64netbsd/armnetbsd/arm64openbsd/386openbsd/amd64openbsd/armopenbsd/arm64plan9/386plan9/amd64plan9/armsolaris/amd64windows/386 # Windows i386windows/amd64 # Windows amd64windows/arm> go envset GOHOSTARCH=amd64 # 本机的架构set GOHOSTOS=windows # 本机的系统set GOARCH=amd64 # 目标平台的架构,交叉编译时需要设置set GOOS=windows # 目标平台的系统,交叉编译时需要设置set CGO_ENABLED=0 # 是否启用 CGO...
最常用的大概是x86
和amd64
架构:
darwin/386
:对应 Mac x86darwin/amd64
:对应 Mac amd64linux/386
:对应 Linux x86linux/amd64
:对应 Linux amd64Windows/386
:对应 Windows x86Windows/amd64
:对应 Windows amd64在 Windows 下编译 MacOS 和 Linux 的 64 位程序:
# For MacOS/amd64set CGO_ENABLED=0set GOOS=darwinset GOARCH=amd64go build main.go# For Linux/amd64set CGO_ENABLED=0set GOOS=linuxset GOARCH=amd64go build main.go
在 Linux 下编译 MacOS 和 Windows 的 64 位程序:
# For MacOS/amd64CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build main.go# For Windows/amd64CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
在 MacOS 下编译 Windows 和 Linux 的 64 位程序:
# For Windows/amd64CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go# For Linux/amd64CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
虽然 Golang 可以跨平台编译,但系统之间的差异性仍然存在。有些时候我们会直接调用操作系统函数,不同操作系统下的库可能会有不同的实现,比如syscall
库。
命令go build
没有内置#define
或者预处理器之类的处理平台相关的代码取舍,而是采用 Tag 标记和文件后缀的方式实现选择性编译。
为了实现根据不同的目标平台编译对应的源文件,需要在文件顶部添加构建标记build tag
:
// +build
标记遵循以下规则:
简单翻译一下:
为或,
为且!
为非例如:
// +build A,B !C,D// (A && B) || ((!C) && D)
再例如:
// +build !windows,386//此文件在非 Windows 操作系统,且为 x86 处理器时编译
构建标记必须出现在文件顶部,可以有多个build tag
,之间是AND
的关系:
// +build linux darwin// +build 386
另外需要注意build tag
和package xxx
语句之间需要有空行分隔,也就是:
// +build linux darwin// +build 386package mypkg
以_$GOOS.go
为后缀的文件只在此平台上编译,其他平台上编译时就当此文件不存在,完整的后缀如:
_$GOOS_$GOARCH.go
例如:
syscall_linux_amd64.go
:只在 Linux/amd64 下编译syscall_windows_386.go
:只在 Windows/i386 下编译syscall_windows.go
:只在 Windows 下编译package mainimport ( "fmt" "runtime")func main() { fmt.Printf("OS: %s\nArchitecture: %s\n", runtime.GOOS, runtime.GOARCH)}
Windows/amd64 下为 Windows/amd64 与 Linux/amd64 编译:
# For Windows/amd64> go build cross.go# For Linux/amd64> set CGO_ENABLED=0> set GOOS=linux> set GOARCH=amd64> go build cross.go# Cmder> ls -hltotal 3.0M-rw-r--r-- 1 abelsu7 197609 1014K 10月 24 17:26 cross-rwxr-xr-x 1 abelsu7 197609 2.0M 10月 24 17:15 cross.exe*-rw-r--r-- 1 abelsu7 197609 140 10月 24 17:12 cross.go-rw-r--r-- 1 abelsu7 197609 198 10月 23 17:58 go.mod-rw-r--r-- 1 abelsu7 197609 1.5K 10月 23 17:57 go.sum-rw-r--r-- 1 abelsu7 197609 1.9K 10月 24 15:48 main.go
Windows 下运行:
> .\cross.exeOS: windowsArchitecture: amd64
WSL 下运行:
> ./crossOS: linuxArchitecture: amd64
]]>
- 交叉编译 Go 程序 | Holmesian Blog
- 交叉编译 Go 程序 | 鸟窝
- Golang 交叉编译与选择性编译 | CSDN
- Golang 在 Mac、Linux、Windows 下如何交叉编译 | CSDN
- Golang 交叉编译中的那些坑 | CSDN
- Cross compilation with Go 1,5 | Dave Cheney
- Building windows go programs on linux - golang/go | Github
- Cross Compile in Go (Golang) | Medium.com
- TinyGo Brings Go To Arduino | Hackaday
- Better way to install Golang (Go) on Raspberry Pi | E-Tinkers
- Recipe: Cross Compliling | The Go Cookbook
- Gin 实践 番外:Golang 交叉编译 - 煎鱼 | SegmentFault
- Gin 实践 番外:请入门 Makefile - 煎鱼 | SegmentFault
// TODO: To be updated…
下载安装present
:
> go get -u -v golang.org/x/tools/cmd/present
演示文档目录下执行命令:
> present
访问http://127.0.0.1:3999
等我有时间就回来更新!
flag++
]]>
使用 Golang 执行 Shell 命令的各种姿势
// TODO: To be updated…
执行命令可以使用Run()
或者Start()
方法,Run()
是阻塞的执行,Start()
是非阻塞的执行:
package mainimport ( "fmt" "os/exec")func main() { command := exec.Command("ping","www.baidu.com") err := command.Run() // 阻塞执行 if err != nil{ fmt.Println(err.Error()) }}
// TODO: To be updated…
- Go 语言中执行命令的几种方式 | 杨彦星
- Golang exec 命令执行 | 简书
- Golang os/exec 执行外部命令 | 简书
- Golang 执行系统命令 os/exec | 01happy
- 如何用 Go 调用 Windows API | Razeen’s Blog
- Go 学习笔记 (八) - 使用 os/exec 执行命令 | Razeen’s Blog
- [译]使用 os/exec 执行命令 | 鸟窝
- golang-ssh-01: 执行远程命令 | MojoTech
- Golang 远程执行命令 | CSDN
- Go 执行远程 ssh 命令 | bbsmax
- 如何使用 Go 语言实现远程执行命令 | TeaKKi
]]>
参考 CentOS 7 下 yum 安装和配置 NFS | Zhanming’s blog,补充整理部分内容
本文中的服务器环境如下:
Role | Hostname | OS |
---|---|---|
NFS 服务端 | centos-2 | CentOS 7.5 |
NFS 客户端 | abelsu7-ubuntu | Ubuntu 18.04 |
注:为简略起见,以下命令均以
root
身份运行,省略sudo
注:对应的 Apt 包为
nfs-kernel-server
和nfs-common
> yum info nfs-utilsAvailable PackagesName : nfs-utilsArch : x86_64Epoch : 1Version : 1.3.0Release : 0.65.el7Size : 412 kRepo : base/7/x86_64Summary : NFS utilities and supporting clients and daemons for the kernel NFS serverURL : http://sourceforge.net/projects/nfsLicense : MIT and GPLv2 and GPLv2+ and BSDDescription : The nfs-utils package provides a daemon for the kernel NFS server and : related tools, which provides a much higher level of performance than the : traditional Linux NFS server used by most users. : : This package also contains the showmount program. Showmount queries the : mount daemon on a remote host for information about the NFS (Network File : System) server on the remote host. For example, showmount can display the : clients which are mounted on that host. : : This package also contains the mount.nfs and umount.nfs program.> yum install nfs-utils# rpcbind 作为依赖会自动安装
允许rpcbind.service
、nfs.service
开机自启:
# 允许服务开机自启> systemctl enable rpcbind> systemctl enable nfs
启动相关服务:
# 启动相关服务> systemctl start rpcbind> systemctl start nfs
防火墙允许服务通过:
# 防火墙允许服务通过> firewall-cmd --zone=public --permanent --add-service={rpc-bind,mountd,nfs}success> firewall-cmd --reloadsuccess
例如需要共享的目录为/mnt/kvm/
:
# 创建 /mnt/kvm 并修改权限> cd /mnt/mnt > mkdir kvm/mnt > chmod 755 kvm# 验证目录权限/mnt > ls -ltotal 0drwxr-xr-x 2 root root 59 Oct 17 17:49 kvm
之后修改/etc/exports
,将/mnt/kvm/
添加进去:
> cat /etc/exports# 1. 只允许 abelsu7-ubuntu 访问/mnt/kvm/ abelsu7-ubuntu(rw,sync,no_root_squash,no_all_squash)# 2. 根据 IP 地址范围限制访问/mnt/kvm/ 192.168.0.0/24(rw,sync,no_root_squash,no_all_squash)# 3. 使用 * 表示访问不加限制/mnt/kvm/ *(rw,sync,no_root_squash,no_all_squash)
关于/etc/exports
中的参数含义:
/mnt/kvm/
:需要共享的目录192.168.0.0/24
:客户端 IP 范围,*
表示无限制rw
:权限设置,可读可写sync
:同步共享目录no_root_squash
:可以使用root
授权no_all_squash
:可以使用普通用户授权保存之后,重启nfs
服务:
> systemctl restart nfs
在centos-2
本地查看:
> showmount -e localhostExport list for localhost:/mnt/kvm abelsu7-ubuntu
# CentOS/Fedora, etc.> yum install nfs-utils# Ubuntu/Debian, etc.> apt install nfs-common
设置rpcbind
服务开机启动:
> systemctl enable rpcbind
启动rpcbind
:
> systemctl start rpcbind
客户端不需要打开防火墙,也不需要开启 NFS 服务
先查看服务端的共享目录:
> showmount -e centos-2Export list for centos-2:/mnt/kvm abelsu7-ubuntu
在客户端创建并挂载对应目录:
> mkdir -p /mnt/kvm> mount -t nfs centos-2:/mnt/kvm /mnt/kvm
最后检查一下是否挂载成功:
> df -hT /mnt/kvmFilesystem Type Size Used Avail Use% Mounted oncentos-2:/mnt/kvm nfs4 500G 119G 382G 24% /mnt/kvm> mount | grep /mnt/kvmcentos-2:/mnt/kvm on /mnt/kvm type nfs4 (rw,relatime,vers=4.2,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=222.xxx.xxx.xxx,local_lock=none,addr=116.xxx.xxx.xxx)
在客户端编辑/etc/fstab
:
# /etc/fstab: static file system information.## Use 'blkid' to print the universally unique identifier for a# device; this may be used with UUID= as a more robust way to name devices# that works even if disks are added and removed. See fstab(5).## <file system> <mount point> <type> <options> <dump> <pass># / was on /dev/sda8 during installationUUID=26d36e85-367a-4200-87fb-0505c5837078 / ext4 errors=remount-ro 0 1# /boot/efi was on /dev/sda1 during installationUUID=000E-274F /boot/efi vfat umask=0077 0 1# swap was on /dev/sda9 during installation# UUID=ee4da9a3-0288-4f8e-a86e-ab8ac3faa6bc none swap sw 0 0# For nfscentos-2:/mnt/kvm /mnt/kvm nfs defaults 0 0
最后重新加载systemctl
,即可实现重启后自动挂载:
> systemctl daemon-reload> mount | grep /mnt/kvmcentos-2:/mnt/kvm on /mnt/kvm type nfs4 (rw,relatime,vers=4.2,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=222.xxx.xxx.xxx,local_lock=none,addr=116.xxx.xxx.xxx)
待更新…
> time dd if=/dev/zero of=/mnt/kvm-lun/test-nfs-speed bs=8k count=1024> time dd if=/mnt/kvm-lun/test-nfs-speed of=/dev/null bs=8k count=1024
]]>
前后端分离,Mock Server 初体验
To be updated…
- postman
- Restlet Client - REST API Testing
- Swagger
]]>
相关资料整理,更新中…
编制(Orchestration):
编排(Choreography):
编排(Choreography)的难点:
编排的模型包括:
编排框架还提供了例如本地调用、REST 调用、同步/异步调用等活动,从而在使用上更加方便。
在调用的时候我们知道有同步和异步的区别:
当Service A
调用Service B
,失败次数达到一定阈值时,Service A
就不会再去调用Service B
,而是降级去执行本地的方法。
微服务基础架构的核心模块包括:
K8s 的架构本身就是微服务,支持定制化与横向扩展:
在 K8s 中几乎所有的组件都是无状态化的,状态都保存在统一的 etcd 里,扩展性很好,组件之间异步完成自己的任务,将结果放在 etcd 里面,互相不耦合:
网易云对容器创建流程进行了定制化。由于大促和非大促期间,节点的数目相差比较大,因而不能采用事先全部创建好节点的方式,这样会造成资源的浪费,因而中间添加了网易云自己的模块 Controller 和 IaaS 的管理层,使得当创建容器资源不足的时候,动态调用 IaaS 的接口,动态的创建资源。这一切对于客户端和 kubelet 无感知:
K8s 的每个组件都可进行独立的优化,互不影响:
新浪混合云系统 DCP 在设计之初遇到了一个很明显的问题:很难用单一的 IaaS,PaaS 或 CaaS 去定义我们的场景:
不同的层是存在不同的编排方法的:
对于新浪混合云 DCP,主要有以下两大方面:
微博目前开发的 DCP 主要包含三层:
- 微服务编排之道 | 掘金
- 微服务核心研究之编排 | 简书
- 编排的艺术:K8S 中的容器编排和应用编排 - Kuberneteschina | 知乎专栏
- 几种常见的微服务编排模式 | Neohope’s Blog
- kubernetes 容器编排系统介绍 | 腾讯云+社区
- 容器编排的作用和要实现的内容 | CSDN
- 从 kubernetes 看如何设计超大规模资源调度系统 | CSDN
- 编排管理成容器云关键 Kubernetes(K8s)和Swarm对比分析 | kubernetes 中文社区
- 云编排技术:探索您的选择 | IBM Developer
- Netflix Conductor: A microservices orchestrator | Medium.com
- Kubernetes 容器编排的三大支柱 | 51CTO
- 深入 kubernetes 调度之原理分析 | 腾讯云+社区
- 混合云编排工具 Terraform 简介 | int32bit
打开 Golang 开发的百宝箱,更新中…
强烈推荐:
// Print/Sprint/Fprintfunc index(w http.ResponseWriter, r *http.Request) { fmt.Println("Inside handler: index") name := fmt.Sprintf("%s", "abel") fmt.Fprintf(w, "Hello %s!\n", name)}
参见 2.1 strings — 字符串操作 | The-Golang-Standard-Library-by-Example
// 字符串分割array := strings.Split(s, ",")// 字符串清理func stringsClean(value string) string { newReplacer := strings.NewReplacer("\n", " ","\t", " ") newValue := newReplacer.Replace(value) return strings.TrimSpace(newValue)}
// 数组排序sort.Slice(prev, func(i, j int) bool { return prev[i] > prev[j]})
package mainimport ( "bufio" "fmt" "os" "strings")func main() { r := bufio.NewReader(os.Stdin) line, _, err := r.ReadLine() if err != nil { fmt.Printf("error happend: %s\n", err) } s := string(line) s = strings.TrimFunc(s, func(r rune) bool { if r == '[' || r == ']' { return true } return false }) fmt.Println(s) array := strings.Split(s, ", ") fmt.Println(array)}------[1, 2, 3, 4, 5]1, 2, 3, 4, 5[1 2 3 4 5]
推荐一个知乎专栏作者:谢伟,知乎专栏『Gopher』- Go 上手指南
其他不错的文章:
相关资料整理,更新中…
参考资料:
大部分公司在使用的微服务技术架构体系示意图:
微服务基础架构的核心模块包括:
其中粉红色标注的模块是和微服务关系最密切的模块
待更新…
微服务设计 | Kubernetes 功能 |
---|---|
1. API 网关 | Ingress |
2. 无状态化,区分有状态化和无状态的应用 | 无状态对应 Deployment,有状态对应 StatefulSet |
3. 数据库的横向扩展 | headless service 指向 PaaS 服务,或者 StatefulSet 部署 |
4. 缓存 | headless service 指向 PaaS 服务,或者 StatefulSet 部署 |
5. 服务拆分和服务发现 | Service |
6. 服务编排与弹性伸缩 | Deployment 的 Replicas |
7. 统一配置中心 | ConfigMap |
8. 统一日志中心 | DaemonSet 部署日志 Agent |
9. 熔断、限流、降级 | Service Mesh |
10. 全方位的监控(智能管控?) | Cadvisor, DaemonSet 部署监控 Agent |
在我们微服务设计的十个要点中,我们会发现Kubernetes都能够有相应的组件和概念,提供相应的支持。
其中最后的一块拼图就是服务发现,与熔断限流降级。
众所周知,Kubernetes 的服务发现是通过Service
来实现的,服务之间的转发是通过kube-proxy
下发 iptables 规则来实现的,这个只能实现最基本的服务发现和转发能力,不能满足高并发应用下的高级的服务特性,比较 SpringCloud 和 Dubbo 有一定的差距,于是 Service Mesh 诞生了,他期望将熔断,限流,降级等特性,从应用层,下沉到基础设施层去实现,从而使得 Kubernetes 和容器全面接管微服务。
待更新…
Service Mesh
中文名称为服务网格,因为它的部署图看起来就像一个网格:
图中的绿色小块可以理解为微服务应用,蓝色小块可以理解为 Service Mesh 的轻量级网络代理
简单来说:Service Mesh
就是一个基础设施层,它是用于处理微服务中服务与服务之间通信的一种技术。
有了 Service Mesh 之后,在微服务框架中,服务与服务之间的通信就是靠这些网络代理模块来保障。
在传统的微服务架构中,随着业务越来越复杂,拆分的服务实例也越来越多,那么各个服务之间的依赖就变成了非常复杂的网络拓扑结构,类似下图:
在如此复杂的分布式部署架构下,微服务中服务依赖调用和数据传输所面临的问题也成倍增加,极大的提高了服务治理的难度。
同时,由于容器化技术的成熟和规模化,微服务都会采用容器化,并朝着云原生应用的方向发展。而传统的微服务架构中,虽然也有服务治理的组件,但是这些组件大多需要在应用代码里进行集成,并不符合云原生的思想。因此,急需一个标准化、能高效部署和运维的微服务体系方案。
因此,Service Mesh 就应运而生了——其目的就是用来解决微服务架构中服务间可靠调用、服务治理等问题。
Service Mesh 由两个核心模块组成,SideCar
和Control Plane
:
1. Sidecar:
Sidecar
即上面提到的与服务部署在一起的轻量级网络代理,它的作用就是实现服务框架的各项功能,这样就可以让服务 (Service A 或 B) 回归业务本质。
传统的微服务架构中,各种服务框架的功能 (例如服务发现、负载均衡、限流熔断等) 的代码逻辑或多或少都需要耦合到服务实例的代码中,给服务实例增加了很多业务无关的代码,也提升了复杂度
有了Sidecar
之后,服务节点只做业务逻辑自身的功能,服务之间的调用交给了Sidecar
,由Sidecar
去注册服务、去做服务发现、去做请求路由、去实现熔断限流、去做日志统计。
在这种新的微服务架构中,所有的Sidecar
组合在一起,就是一个服务网格了。不过,这个大型的服务网格并不是完全自治的,它还需要一个统一的控制节点,也就是Control Plane
。
2. Control Plane:
Control Plane
是用来从全局的角度控制Sidecar
的。例如它负责所有Sidecar
的注册,并存储一个统一的路由表,帮助各个Sidecar
进行负载均衡和请求调度。
此外,Control Plane
还会收集所有Sidecar
的监控信息和日志数据,相当于Service Mesh
架构的大脑。Control Plane
控制着Sidecar
来实现服务治理的各项功能。
Mesos 基于 master/slave 架构,框架决定如何利用资源,master 负责管理机器,slave 会定期的将机器情况报告给 master,master再将信息给框架。
官网地址:轻舟微服务 | 网易云
轻舟微服务是围绕应用和微服务打造的一站式 PaaS 平台,帮助用户快速实现易接入、易运维的微服务解决方案。为致力于数字化转型的企业提供中台服务治理,帮助企业实现建立生态统一标准,优化管理能力及自动化能力。
基于开源、兼容开源:
低成本、易接入:
智能运维&立体化监控:
微服务治理框架:
智能 API 网关:
分布式事务:
容器应用管理服务:
DevOps:
AIOps:
官网地址:腾讯微服务平台 TSF | 腾讯云
腾讯微服务平台(Tencent Service Framework,TSF)是一个围绕应用和微服务的 PaaS 平台,提供一站式应用全生命周期管理能力和数据化运营支持,提供多维度应用和服务的监控数据,助力服务性能优化。提供基于 Spring Cloud 和 Service Mesh 两种微服务架构的商业化支持。
拥抱开源社区:
应用全生命周期管理:
细粒度服务治理:
分布式事务:
灵活运维:
构建分布式系统:
金融业务往往有严格合规性要求,用户能够将业务部署在专用宿主机的云服务器上,在资源共享的同时保证与其他用户的子机物理隔离,满足敏感业务数据保护、磁盘消磁需求。
应用发布和管理:
相对于传统的应用发布需要运维人员登录到每一台服务器进行发布和部署,TSF 针对分布式系统的应用发布和管理,提供了简单易用的可视化控制台。用户通过控制台可以发布应用,包括创建、部署、启动应用,也支持查看应用的部署状态。除此之外,用户可以通过控制台管理应用,包括回滚应用、扩容、缩容和删除应用。
服务治理:
支持服务级别和 API 级别的服务治理能力,包括服务路由、服务限流、服务鉴权功能。服务路由功能支持将请求按权重路由到不同版本的服务上。
官网地址:微服务引擎 CSE | 华为云
微服务引擎(Cloud Service Engine)提供高性能微服务框架和一站式服务注册、服务治理、动态配置和分布式事务管理控制台,帮助用户实现微服务应用的快速开发和高可用运维;支持 ServiceComb、Spring Cloud 和 Service Mesh 运行环境。
微服务开发框架:
微服务治理中心:
微服务安全管控:
微服务灰度发布:
分布式事务管理:
非侵入式接入:
统一配置中心:
微服务仪表盘:
微服务应用高可用运维:
多语言微服务解决方案:
开源框架微服务应用接入与管理:
]]>
Go, please
折腾 VS Code 写 Go 的朋友都有这种体会,VS Code 的 Go 插件体验上还是输给 GoLand 不少,尤其是代码补全和提示,GOPATH
没配置好或者项目路径在GOPATH
之外就基本残废。
但是我还是坚持用 VS Code,毕竟人家开源,又有微软爸爸背书,相比 GoLand 更加轻量,快捷键用的也更顺手,其实最主要还是没钱 QAQ。
gopls 实现了 VS Code 的 Language Server Protocol (LSP),发音为go please
。
Go Team 目前正在积极维护gopls
,有望成为之后 VS Code Go 插件的默认补全工具,但目前还是有很多小问题,例如下面这个Known issue
:
相似的问题还有下面这个:
只在 Windows 环境下出现,因为 Windows 默认的换行符是CRLF
,而目前gopls
的格式化只支持LF
换行。
Windows 可以设置
"files.eol": "\n"
暂时规避一下,评论里提到之后会解决这个问题
目前已知的 Known issues:
期待 Go Team 早日解决🍻
"go.useLanguageServer": true,"[go]": { "editor.snippetSuggestions": "none", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": true }},"gopls": { "completeUnimported": true, "usePlaceholders": true, "completionDocumentation": true, "hoverKind": "SynopsisDocumentation" // No/Synopsis/Full, default Synopsis},"files.eol": "\n", // formatting only supports LF line endings
此外还有一些ExperimentalFeatures
:
"go.languageServerExperimentalFeatures": { "format": true, "autoComplete": true, "rename": true, "goToDefinition": true, "hover": true, "signatureHelp": true, "goToTypeDefinition": true, "goToImplementation": true, "documentSymbols": true, "workspaceSymbols": true, "findReferences": true, "diagnostics": false},
最后提供一份自用的 VS Code 用户设置,仅供参考:用户设置 - VS Code | Coding-Notes
- gopls documentation - golang/tools | Github
- x/tools/gopls: formatting resets cursor after save with CRLF line endings - golang/go | Github
- Cursor flies to top of file on save and text flashes - microsoft/vscode-go | Github
- gopls - 及时的代码补全 | arbent
- 在 VS Code 中使用 gopls | SegmentFault
- VS Code 中的代码自动补全和自动导入包 | 茶歇驿站
- VSCode 写 Golang,请切换到 Google 官方语言服务器 gopls,有质的提升 | 论坛爱好者
]]>
virtio 框架学习,更新中…
KVM 是必须依赖硬件虚拟化技术辅助(例如 Intel VT-x、AMD-V)的 Hypervisor:
VM-Exit
到用户态由 QEMU 进行模拟。传统的方式是使用纯软件的方式来模拟 I/O 设备,效率并不高为了解决 I/O 虚拟化效率低下的问题,可以在客户机中使用半虚拟化驱动(Paravirtualized Drivers,PV Drivers)来提高客户机的 I/O 性能。目前,KVM 中实现半虚拟化驱动的方式就是采用了
virtio
这个 Linux 下的设备驱动标准框架。
virtio 是 Linux 平台下一种 I/O 半虚拟化框架,由澳大利亚程序员 Rusty Russell 开发。他当时的目的是支持自己的虚拟化解决方案 Lguest,而在 KVM 中也广泛使用了 Virtio 作为半虚拟化 I/O 框架。
下图是 QEMU 以纯软件方式模拟 I/O 设备的示意图:
需要注意的是:
VM-Entry
、VM-Exit
发生,需要多次上下文切换,也需要多次数据复制,因此性能较差如图所示,virtio 分为了前端驱动和后端驱动:
virtio_blk
、virtio_net
等,是在客户机中存在的驱动程序模块在前后端驱动之间,还定义了两层来支持客户机和 QEMU 之间的通信:
例如:
virtio_net
网络驱动程序使用两个虚拟队列(接收/发送),而virtio_blk
驱动仅使用一个虚拟队列。
虚拟队列实际上被实现为客户机操作系统和 Hypervisor 之间的衔接点,但它可以通过任意方式实现,前提是客户机操作系统和 virtio 后端程序都遵循一定的标准,以相互匹配的方式实现它。
这样做就可以根据约定实现批量处理而不是客户机中每次 I/O 请求都需要处理一次,从而提高了客户机与 Hypervisor 之间信息交换的效率
virtio 是半虚拟化的解决方案,是半虚拟化 Hypervisor 的一组通用 I/O 设备的抽象。它提供了一套上层应用与各 Hypervisor 虚拟化设备(KVM、Xen、VMware 等)之间的通信框架和编程接口,减少了跨平台所带来的兼容性问题。客户机需要知道自己运行在虚拟化环境中,进而根据 Virtio 标准和 Hypervisor 协作,从而提高 I/O 性能。
virtio 是半虚拟化驱动框架,可以提供接近 Native 的 I/O 性能,但是客户机中必须安装特定的 virtio 驱动,并按照 virtio 的规定格式进行数据传输
Kernel >= 2.6.25
的内核都支持 virtio。由于 virtio 的后端处理程序是在位于用户空间中的 QEMU 中实现的,所以宿主机只需要比较新的内核即可,不需要特别编译 virtio 相关驱动。而客户机需要有特定 virtio 驱动程序的支持,以便客户机处理 I/O 操作请求时调用前端驱动。
客户机内核中关于 virtio 的部分配置如下:
# 需要启用的选项CONFIG_VIRTIO_PCI=mCONFIG_VIRTIO_BALLOON=mCONFIG_VIRTIO_BLK=mCONFIG_VIRTIO_NET=mCONFIG_VIRTIO=mCONFIG_VIRTIO_RING=y# 其他相关的选项CONFIG_VIRTIO_VSOCKETS=mCONFIG_VIRTIO_VSOCKETS_COMMON=mCONFIG_SCSI_VIRTIO=mCONFIG_VIRTIO_CONSOLE=mCONFIG_HW_RANDOM_VIRTIO=mCONFIG_DRM_VIRTIO_GPU=mCONFIG_VIRTIO_PCI_LEGACY=yCONFIG_VIRTIO_INPUT=m# CONFIG_VIRTIO_MMIO is not set# 在子机中查看 VIRTIO 相关内核模块> lsmod | grep virtiovirtio_balloon 18015 0 virtio_net 28063 0 virtio_blk 18166 2 virtio_pci 22934 0 virtio_ring 22746 4 virtio_blk,virtio_net,virtio_pci,virtio_balloonvirtio 14959 4 virtio_blk,virtio_net,virtio_pci,virtio_balloon
如图所示,virtio 大致分为三个层次:前端驱动(位于客户机)、后端驱动(位于 QEMU)以及中间的传输层。
每一个 virtio 设备(块设备、网卡等)在系统层面看来,都是一个 PCI 设备。这些设备之间有共性部分,也有差异部分。
共性部分:
差异部分:
net_device
,与协议栈系统关联起来。同时,向队列中写入什么数据、数据的含义如何,各个设备也不相同。队列中来了什么数据,是什么含义,如何处理,各个设备也不相同如果每个 virtio 设备都完整的实现自己的功能,就会造成不必要的代码冗余。针对这个问题,virtio 又设计了
virtio_pci
模块,以处理所有 virtio 设备的共性部分。这样一来所有的 virtio 设备在系统看来都是一个 PCI 设备,其设备驱动都是virtio_pci
但是,virtio_pci
并不能完整的驱动任何一个设备。因此,virtio_pci
在调用probe()
接管每一个设备时,会根据其virtio_device_id
来识别出具体是哪一种设备,然后相应的向内核注册一个 virtio 类型的设备。
在注册设备之前,virtio_pci
驱动已经为该设备做了许多共性操作,同时还为该设备提供了各种操作的适配接口,这些都通过virtio_config_ops
来适配。
Kernel 3.10.0
中关于 virito 的重要源码文件如下:
drivers/block/virtio_blk.cdrivers/char/hw_random/virtio_rng.cdrivers/char/virtio_console.cdrivers/net/virtio_net.cdrivers/scsi/virtio_scsi.cdrivers/virtio/virtio_balloon.cdrivers/virtio/virtio_mmio.cdrivers/virtio/virtio_pci.cdrivers/virtio/virtio_ring.cdrivers/virtio/virtio.cinclude/linux/virtio_caif.hinclude/linux/virtio_config.hinclude/linux/virtio_console.hinclude/linux/virtio_mmio.hinclude/linux/virtio_ring.hinclude/linux/virtio_scsi.hinclude/linux/virtio.hinclude/linux/vring.hinclude/uapi/linux/virtio_9p.hinclude/uapi/linux/virtio_balloon.hinclude/uapi/linux/virtio_blk.hinclude/uapi/linux/virtio_config.hinclude/uapi/linux/virtio_console.hinclude/uapi/linux/virtio_ids.hinclude/uapi/linux/virtio_net.hinclude/uapi/linux/virtio_pci.hinclude/uapi/linux/virtio_ring.hinclude/uapi/linux/virtio_rng.htools/virtio/linux/virtio_config.htools/virtio/linux/virtio_ring.htools/virtio/linux/virtio.htools/virtio/linux/vring.htools/virtio/vitrio_test.ctools/virtio/vringh_test.clinux-3.10 ├─ drivers | ├─ block | | └─ virtio_blk.c | | | ├─ char | | ├─ hw_random | | | └─ virtio_rng.c | | | | | └─ virtio_console.c | | | ├─ net | | └─ virtio_net.c | | | ├─ scsi | | └─ virtio_scsi.c | | | └─ virtio | ├─ virtio_balloon.c | ├─ virtio_mmio.c | ├─ virtio_pci.c | ├─ virtio_ring.c | └─ virtio.c | ├─ include | ├─ linux | | ├─ virtio_caif.h | | ├─ virtio_config.h | | ├─ virtio_console.h | | ├─ virtio_mmio.h | | ├─ virtio_ring.h | | ├─ virtio_scsi.h | | ├─ virtio.h | | └─ vring.h | | | └─ uapi | └─ linux | ├─ virtio_9p.h | ├─ virtio_balloon.h | ├─ virtio_blk.h | ├─ virtio_config.h | ├─ virtio_console.h | ├─ virtio_ids.h | ├─ virtio_net.h | ├─ virtio_pci.h | ├─ virtio_ring.h | └─ virtio_rng.h | └─ tools └─ virtio ├─ linux | ├─ virtio_config.h | ├─ virtio_ring.h | ├─ virtio.h | └─ vring.h | ├─ virtio_test.c └─ vringh_test.c
在 virtio 前端驱动即客户机内核中,virtio 的类层次结构如下图所示:
最顶级的是virtio_driver
,在客户机 OS 中表示前端驱动程序,在include/linux/virtio.h
中定义:
/** * virtio_driver - operations for a virtio I/O driver * @driver: underlying device driver (populate name and owner). * @id_table: the ids serviced by this driver. * @feature_table: an array of feature numbers supported by this driver. * @feature_table_size: number of entries in the feature table array. * @probe: the function to call when a device is found. Returns 0 or -errno. * @remove: the function to call when a device is removed. * @config_changed: optional function to call when the device configuration * changes; may be called in interrupt context. */struct virtio_driver { struct device_driver driver; const struct virtio_device_id *id_table; const unsigned int *feature_table; unsigned int feature_table_size; int (*probe)(struct virtio_device *dev); void (*scan)(struct virtio_device *dev); void (*remove)(struct virtio_device *dev); void (*config_changed)(struct virtio_device *dev);#ifdef CONFIG_PM int (*freeze)(struct virtio_device *dev); int (*restore)(struct virtio_device *dev);#endif};
每个 virtio 设备都有其对应的virtio_device_id
,该结构体在include/linux/mod_devicetable.h
中定义:
struct virtio_device_id { __u32 device; __u32 vendor;};#define VIRTIO_DEV_ANY_ID 0xffffffff
与驱动程序匹配的设备由virtio_device
封装,它表示在客户机 OS 中的设备,在include/linux/virtio.h
中定义:
/** * virtio_device - representation of a device using virtio * @index: unique position on the virtio bus * @dev: underlying device. * @id: the device type identification (used to match it with a driver). * @config: the configuration ops for this device. * @vringh_config: configuration ops for host vrings. * @vqs: the list of virtqueues for this device. * @features: the features supported by both driver and device. * @priv: private pointer for the driver's use. */struct virtio_device { int index; struct device dev; struct virtio_device_id id; const struct virtio_config_ops *config; const struct vringh_config_ops *vringh_config; struct list_head vqs; /* Note that this is a Linux set_bit-style bitmap. */ unsigned long features[1]; void *priv;};
每一个virtio_device
都有一个virtio_config_ops
类型的指针*config
,它定义了配置 virtio 设备的操作,该结构体在include/linux/virtio_config.h
中定义:
/** * virtio_config_ops - operations for configuring a virtio device * @get: read the value of a configuration field * @set: write the value of a configuration field * @get_status: read the status byte * @set_status: write the status byte * @reset: reset the device * @find_vqs: find virtqueues and instantiate them. * @del_vqs: free virtqueues found by find_vqs(). * @get_features: get the array of feature bits for this device. * @finalize_features: confirm what device features we'll be using. * @bus_name: return the bus name associated with the device * @set_vq_affinity: set the affinity for a virtqueue. */struct virtio_config_ops { void (*get)(struct virtio_device *vdev, unsigned offset, void *buf, unsigned len); void (*set)(struct virtio_device *vdev, unsigned offset, const void *buf, unsigned len); u8 (*get_status)(struct virtio_device *vdev); void (*set_status)(struct virtio_device *vdev, u8 status); void (*reset)(struct virtio_device *vdev); int (*find_vqs)(struct virtio_device *, unsigned nvqs, struct virtqueue *vqs[], vq_callback_t *callbacks[], const char *names[]); void (*del_vqs)(struct virtio_device *); u32 (*get_features)(struct virtio_device *vdev); void (*finalize_features)(struct virtio_device *vdev); const char *(*bus_name)(struct virtio_device *vdev); int (*set_vq_affinity)(struct virtqueue *vq, int cpu);};
每一个virtqueue
包含了对应的virtio_device
以及对应的队列操作回调函数,它在include/linux/virtio.h
中定义:
/** * virtqueue - a queue to register buffers for sending or receiving. * @list: the chain of virtqueues for this device * @callback: the function to call when buffers are consumed (can be NULL). * @name: the name of this virtqueue (mainly for debugging) * @vdev: the virtio device this queue was created for. * @priv: a pointer for the virtqueue implementation to use. * @index: the zero-based ordinal number for this queue. * @num_free: number of elements we expect to be able to fit. * * A note on @num_free: with indirect buffers, each buffer needs one * element in the queue, otherwise a buffer will need one element per * sg element. */struct virtqueue { struct list_head list; void (*callback)(struct virtqueue *vq); const char *name; struct virtio_device *vdev; unsigned int index; unsigned int num_free; void *priv;};
在include/linux/virtio.h
中定义:
/** * virtio_driver - operations for a virtio I/O driver * @driver: underlying device driver (populate name and owner). * @id_table: the ids serviced by this driver. * @feature_table: an array of feature numbers supported by this driver. * @feature_table_size: number of entries in the feature table array. * @probe: the function to call when a device is found. Returns 0 or -errno. * @remove: the function to call when a device is removed. * @config_changed: optional function to call when the device configuration * changes; may be called in interrupt context. */struct virtio_driver { struct device_driver driver; const struct virtio_device_id *id_table; const unsigned int *feature_table; unsigned int feature_table_size; int (*probe)(struct virtio_device *dev); void (*scan)(struct virtio_device *dev); void (*remove)(struct virtio_device *dev); void (*config_changed)(struct virtio_device *dev);#ifdef CONFIG_PM int (*freeze)(struct virtio_device *dev); int (*restore)(struct virtio_device *dev);#endif};
这里的virtio_device_id
有两个字段:
struct virtio_device_id { __u32 device; __u32 vendor;};#define VIRTIO_DEV_ANY_ID 0xffffffff
在include/linux/virtio.h
中定义:
/** * virtio_device - representation of a device using virtio * @index: unique position on the virtio bus * @dev: underlying device. * @id: the device type identification (used to match it with a driver). * @config: the configuration ops for this device. * @vringh_config: configuration ops for host vrings. * @vqs: the list of virtqueues for this device. * @features: the features supported by both driver and device. * @priv: private pointer for the driver's use. */struct virtio_device { int index; struct device dev; struct virtio_device_id id; const struct virtio_config_ops *config; const struct vringh_config_ops *vringh_config; struct list_head vqs; /* Note that this is a Linux set_bit-style bitmap. */ unsigned long features[1]; void *priv;};
在include/linux/virtio_config.h
中定义:
/** * virtio_config_ops - operations for configuring a virtio device * @get: read the value of a configuration field * vdev: the virtio_device * offset: the offset of the configuration field * buf: the buffer to write the field value into. * len: the length of the buffer * @set: write the value of a configuration field * vdev: the virtio_device * offset: the offset of the configuration field * buf: the buffer to read the field value from. * len: the length of the buffer * @get_status: read the status byte * vdev: the virtio_device * Returns the status byte * @set_status: write the status byte * vdev: the virtio_device * status: the new status byte * @reset: reset the device * vdev: the virtio device * After this, status and feature negotiation must be done again * Device must not be reset from its vq/config callbacks, or in * parallel with being added/removed. * @find_vqs: find virtqueues and instantiate them. * vdev: the virtio_device * nvqs: the number of virtqueues to find * vqs: on success, includes new virtqueues * callbacks: array of callbacks, for each virtqueue * include a NULL entry for vqs that do not need a callback * names: array of virtqueue names (mainly for debugging) * include a NULL entry for vqs unused by driver * Returns 0 on success or error status * @del_vqs: free virtqueues found by find_vqs(). * @get_features: get the array of feature bits for this device. * vdev: the virtio_device * Returns the first 32 feature bits (all we currently need). * @finalize_features: confirm what device features we'll be using. * vdev: the virtio_device * This gives the final feature bits for the device: it can change * the dev->feature bits if it wants. * @bus_name: return the bus name associated with the device * vdev: the virtio_device * This returns a pointer to the bus name a la pci_name from which * the caller can then copy. * @set_vq_affinity: set the affinity for a virtqueue. */typedef void vq_callback_t(struct virtqueue *);struct virtio_config_ops { void (*get)(struct virtio_device *vdev, unsigned offset, void *buf, unsigned len); void (*set)(struct virtio_device *vdev, unsigned offset, const void *buf, unsigned len); u8 (*get_status)(struct virtio_device *vdev); void (*set_status)(struct virtio_device *vdev, u8 status); void (*reset)(struct virtio_device *vdev); int (*find_vqs)(struct virtio_device *, unsigned nvqs, struct virtqueue *vqs[], vq_callback_t *callbacks[], const char *names[]); void (*del_vqs)(struct virtio_device *); u32 (*get_features)(struct virtio_device *vdev); void (*finalize_features)(struct virtio_device *vdev); const char *(*bus_name)(struct virtio_device *vdev); int (*set_vq_affinity)(struct virtqueue *vq, int cpu);};
在include/linux/virtio.h
中定义:
/** * virtqueue - a queue to register buffers for sending or receiving. * @list: the chain of virtqueues for this device * @callback: the function to call when buffers are consumed (can be NULL). * @name: the name of this virtqueue (mainly for debugging) * @vdev: the virtio device this queue was created for. * @priv: a pointer for the virtqueue implementation to use. * @index: the zero-based ordinal number for this queue. * @num_free: number of elements we expect to be able to fit. * * A note on @num_free: with indirect buffers, each buffer needs one * element in the queue, otherwise a buffer will need one element per * sg element. */struct virtqueue { struct list_head list; void (*callback)(struct virtqueue *vq); const char *name; struct virtio_device *vdev; unsigned int index; unsigned int num_free; void *priv;};
在hw/virtio/virtio.c
中定义:
struct VirtQueue{ VRing vring; /* Next head to pop */ uint16_t last_avail_idx; /* Last avail_idx read from VQ. */ uint16_t shadow_avail_idx; uint16_t used_idx; /* Last used index value we have sjjignalled on */ uint16_t signalled_used; /* Last used index value we have signalled on */ bool signalled_used_valid; /* Notification enabled? */ bool notification; uint16_t queue_index; int inuse; uint16_t vector; void (*handle_output)(VirtIODevice *vdev, VirtQueue *vq); void (*handle_aio_output)(VirtIODevice *vdev, VirtQueue *vq); VirtIODevice *vdev; EventNotifier guest_notifier; EventNotifier host_notifier; QLIST_ENTRY(VirtQueue) node;};
在hw/virtio/virtio.c
中定义:
typedef struct VRing{ unsigned int num; unsigned int num_default; unsigned int align; hwaddr desc; hwaddr avail; hwaddr used;} VRing;
未完待续…
- Virtio 概述和基本原理(KVM 半虚拟化驱动)| 笑遍世界
- Virtio:针对 Linux 的 I/O 虚拟化框架 | IBM Developer
- virtio 基本原理 (kvm 半虚拟化驱动) | 开源中国
- 说一说虚拟化绕不开的 io 半虚拟化 | 腾讯云加社区
- Virtio Vring 工作机制分析 | OenHan
- KVM Virtio Block 源代码分析 | OenHan
- vring的创建(基于kernel 3.10, qemu2.0.0) - leoufung| CSDN
- QEMU 通过virtio接收报文处理流程(QEMU2.0.0)- leoufung | CSDN
- VIRTIO 的 vring 收发队列创建流程 - leoufung | CSDN
- VIRTIO 中的前后端配合限速分析 - leoufung | CSDN
- virtio 路径 | 随便写写
- Virtio 前端驱动详解 - 太初有道 | cnblogs
- Virtio 后端驱动详解 - 太初有道 | cnblogs
- Virtio 前后端 notify 机制详解 - 太初有道 | cnblogs
- intel EPT 机制详解 - 太初有道 | cnblogs
- KVM 中 EPT 逆向映射机制分析 - 太初有道 | cnblogs
- QEMU 进程页表和 EPT 的同步问题 - 太初有道 | cnblogs
- KVM vCPU 线程调度问题的讨论 - 太初有道 | cnblogs
- virtio 之 vhost 工作原理简析 - 太初有道 | cnblogs
- PCI 设备详解一 - 太初有道 | cnblogs
]]>
Linux 系统常用监控命令总结
To be updated…
排查 Linux 问题方法以及常用命令
cat /proc/cpuinfo# 物理 CPU 个数cat /proc/cpuinfo | grep 'physical id' | sort | uniq | wc -l# 每个 CPU 核心数cat /proc/cpuinfo | grep 'core id' | sort | uniq | wc -l# 逻辑 CPUcat /proc/cpuinfo | grep 'processor' | sort | uniq | wc -l# mpstatmpstatmpstat 2 10
cat /proc/meminfofree -gtdf -hTdu -csh ./*
操作系统 IPC 共享内存/队列:
ipcs #(shmems, queues, semaphores)
平时我们经常需要监控内存的使用状态,常用的命令有free
、vmstat
、top
、dstat -m
等。
推荐阅读:
> free -h total used free shared buffers cachedMem: 7.7G 6.2G 1.5G 17M 33M 184M-/+ buffers/cache: 6.0G 1.7GSwap: 24G 581M 23G
第一行Mem
:
total
:内存总数7.7G
,物理内存大小,就是机器实际的内存used
:已使用内存6.2G
,这个值包括了cached
和应用程序实际使用的内存free
:空闲的内存1.5G
,未被使用的内存大小shared
:共享内存的大小,17M
buffers
:被缓冲区占用的内存大小,33M
cached
:被缓存占用的内存大小,184M
其中有:
total = used + free
第二行-/+ buffers/cache
,代表应用程序实际使用的内存:
used - buffers/cached
,表示应用程序实际使用的内存free + buffers/cached
,表示理论上都可以被使用的内存可以看到,这两个值加起来也是
total
第三行swap
,代表交换分区的使用情况:总量、使用的和未使用的
cache
代表缓存,当系统读取文件时,会先把数据从硬盘读到内存里,因为硬盘比内存慢很多,所以这个过程会很耗时。
为了提高效率,Linux 会把读进来的文件在内存中缓存下来(局部性原理),即使程序结束,cache 也不会被自动释放。因此,当有程序进行大量的读文件操作时,就会发现内存使用率升高了。
当其他程序需要使用内存时,Linux 会根据自己的缓存策略(例如 LRU)将这些没人使用的 cache 释放掉,给其他程序使用,当然也可以手动释放缓存:
echo 1 > /proc/sys/vm/drop_caches
考虑内存写文件到硬盘的场景,因为硬盘太慢了,如果内存要等待数据写完了之后才继续后面的操作,效率会非常低,也会影响程序的运行速度,所以就有了缓冲区buffer
。
当内存需要写数据到硬盘中时会先放到 buffer 里面,内存很快把数据写到 buffer 中,可以继续其他工作,而硬盘可以在后台慢慢读出 buffer 中的数据并保存起来,这样就提高了读写的效率。
例如把电脑中的文件拷贝到 U 盘时,如果文件特别大,有时会出现这样的情况:明明看到文件已经拷贝完,但系统还是会提示 U 盘正在使用中。这就是 buffer 的原因:拷贝程序虽然已经把数据放到 buffer 中,但是还没有全部写入到 U 盘中
同样的,可以使用sync
命令来手动flush buffer
中的内容:
> sync --helpUsage: sync [OPTION] [FILE]...Synchronize cached writes to persistent storageIf one or more files are specified, sync only them,or their containing file systems. -d, --data sync only file data, no unneeded metadata -f, --file-system sync the file systems that contain the files --help display this help and exit --version output version information and exitGNU coreutils online help: <http://www.gnu.org/software/coreutils/>Full documentation at: <http://www.gnu.org/software/coreutils/sync>or available locally via: info '(coreutils) sync invocation'
交换分区swap
是实现虚拟内存的重要概念。swap
就是把硬盘上的一部分空间当作内存来使用,正在运行的程序会使用物理内存,把未使用的内存放到硬盘,叫做swap out
。而把硬盘交换分区中的内存重新放到物理内存中,叫做swap in
。
交换分区可以在逻辑上扩大内存空间,但是也会拖慢系统速度,因为硬盘的读写速度很慢。Linux 系统会将不经常使用的内存放到交换分区中。
cache
:作为page cache
的内存,是文件系统的缓存,在文件层面上的数据会缓存到page cache
中buffer
:作为buffer cache
的内存,是磁盘块的缓存,直接对磁盘进行操作的数据会缓存到 buffer cache 中简单来说:page cache
用来缓存文件数据,buffer cache
用来缓存磁盘数据。在有文件系统的情况下,对文件操作,那么数据会缓存到page cache
中。如果直接采用dd
等工具对磁盘进行读写,那么数据会缓存到buffer cache
中。
vmstat (Virtual Memory Statics,虚拟内存统计) 是对系统的整体情况进行统计,包括内核进程、虚拟内存、磁盘、中断和 CPU 活动的统计信息:
> vmstat --helpUsage: vmstat [options] [delay [count]]Options: -a, --active active/inactive memory -f, --forks number of forks since boot -m, --slabs slabinfo -n, --one-header do not redisplay header -s, --stats event counter statistics -d, --disk disk statistics -D, --disk-sum summarize disk statistics -p, --partition <dev> partition specific statistics -S, --unit <char> define display unit -w, --wide wide output -t, --timestamp show timestamp -h, --help display this help and exit -V, --version output version information and exitFor more details see vmstat(8).> vmstat -SM 1 100 # 1 表示刷新间隔(秒),100 表示打印次数,单位 MBprocs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 1 0 0 470 188 1154 0 0 0 4 3 0 0 0 99 0 0 0 0 0 470 188 1154 0 0 0 0 112 231 1 1 98 0 0 0 0 0 470 188 1154 0 0 0 0 91 176 0 0 100 0 0 0 0 0 470 188 1154 0 0 0 0 118 229 1 0 99 0 0 0 0 0 470 188 1154 0 0 0 0 78 156 0 0 100 0 0 0 0 0 470 188 1154 0 0 0 64 84 186 0 1 97 2 0
r
列:表示运行和等待 CPU 时间片的进程数,这个值如果长期大于 CPU 个数,就说明 CPU 资源不足,可以考虑增加 CPUb
列:表示在等待资源的进程数,例如正在等待 I/O 或者内存交换swpn
列:表示切换到交换分区的内存大小,如果swpd
的值不为 0 或者比较大,且si
、so
的值长期为 0,那么这种情况暂时不会影响系统性能free
列:当前空闲的物理内存大小buff
列:表示buffers cache
的内存大小,一般对块设备的读写才需要缓冲cache
列:表示page cache
的内存大小,一般作为文件系统的缓存,频繁访问的文件都会被 cached。如果 cache 值比较大,就说明 cached 文件数量较多。如果此时 I/O 中的bi
比较小,就说明文件系统效率比较好si
列:表示swap in
,即内存由交换分区放入物理内存中so
列:表示swap out
,即将未使用的内存放到硬盘的交换分区中bi
列:表示从块设备读取的数据总量,即读磁盘,单位KB/s
bo
列:表示写入块设备的数据总量,即写磁盘,单位KB/s
这里设置的
bi+bo
参考值为1000
,如果超过1000
,且wa
值比较大,则表示系统磁盘 I/O 性能瓶颈
in
列:表示在某一时间间隔中观察到的每秒设备中断数cs
列:表示每秒产生的上下文切换次数上面这两个值越大,内核消耗的 CPU 时间就越多
us
列:表示用户进程消耗 CPU 的时间百分比。us
值比较高时,说明用户进程消耗的 CPU 时间多,如果长期大于 50%,可以考虑优化程序sy
列:表示内核进程消耗 CPU 的时间百分比。sy
值比较高时,说明内核消耗的 CPU 时间多,如果us+sy
超过 80%,就说明 CPU 资源存在不足id
列:表示 CPU 处在空闲状态的时间百分比wa
列:表示 I/O Wait 所占 CPU 的时间百分比。wa
值越高,说明 I/O Wait 越严重。如果wa
值超过 20%,说明 I/O Wait 严重st
列:表示 CPU Steal Time,针对虚拟机ifconfigiftopethtool
# 端口netstat -ntlp # TCPnetstat -nulp # UDPnetstat -nxlp # UNIXnetstat -nalp # 不仅展示监听端口,还展示其他阶段的连接lsof -p <PID> -Plsof -i :5900sar -n DEV 1 # 网络流量ssss -s
sudo tcpdump -i any udp port 20112 and ip[0x1f:02]=0x4e91 -XNnvvvsudo tcpdump -i any -XNnvvvsudo tcpdump -i any udp -XNnvvvsudo tcpdump -i any udp port 20112 -XNnvvvsudo tcpdump -i any udp port 20112 and ip[0x1f:02]=0x4e91 -XNnvvv
监控各进程的网络流量
nethogs
iotopiostatiostat -kx 2vmstat -SMvmstat 2 10dstatdstat --top-io --top-bio
toptop -Hhtopps auxfps -eLf # 展示线程ls /proc/<PID>/task
例如最常用的top
命令:
Help for Interactive Commands - procps version 3.2.8Window 1:Def: Cumulative mode Off. System: Delay 3.0 secs; Secure mode Off. Z,B Global: 'Z' change color mappings; 'B' disable/enable bold l,t,m Toggle Summaries: 'l' load avg; 't' task/cpu stats; 'm' mem info 1,I Toggle SMP view: '1' single/separate states; 'I' Irix/Solaris mode f,o . Fields/Columns: 'f' add or remove; 'o' change display order F or O . Select sort field <,> . Move sort field: '<' next col left; '>' next col right R,H . Toggle: 'R' normal/reverse sort; 'H' show threads c,i,S . Toggle: 'c' cmd name/line; 'i' idle tasks; 'S' cumulative time x,y . Toggle highlights: 'x' sort field; 'y' running tasks z,b . Toggle: 'z' color/mono; 'b' bold/reverse (only if 'x' or 'y') u . Show specific user only n or # . Set maximum tasks displayed k,r Manipulate tasks: 'k' kill; 'r' renice d or s Set update interval W Write configuration file q Quit ( commands shown with '.' require a visible task display window ) Press 'h' or '?' for help with Windows,any other key to continue
1
: 显示各个 CPU 的使用情况c
: 显示进程完整路径H
: 显示线程P
: 排序 - CPU 使用率M
: 排序 - 内存使用率R
: 倒序Z
: Change color mappingsB
: Disable/enable boldl
: Toggle load avgt
: Toggle task/cpu statsm
: Toggle mem infous - Time spent in user spacesy - Time spent in kernel spaceni - Time spent running niced user processes (User defined priority)id - Time spent in idle operationswa - Time spent on waiting on IO peripherals (eg. disk)hi - Time spent handling hardware interrupt routines. (Whenever a peripheral unit want attention form the CPU, it literally pulls a line, to signal the CPU to service it)si - Time spent handling software interrupt routines. (a piece of code, calls an interrupt routine...)st - Time spent on involuntary waits by virtual cpu while hypervisor is servicing another processor (stolen from a virtual machine)
lsof -P -p 123
stress --cpu 8 \ --io 4 \ --vm 2 \ --vm-bytes 128M \ --timeout 60s
time
命令
wwhoami
uptimehtopvmstatmpstatdstat
lspcilscpulsblklsblk -fm # 显示文件系统、权限lshw -c displaydmidecode
# 挂载mountumountcat /etc/fstab# LVMpvdisplaypvslvdisplaylvsvgdisplayvgsdf -hTlsof
cat /proc/modulessysctl -a | grep ...cat /proc/interrupts
dmesgless /var/log/messagesless /var/log/secureless /var/log/auth
crontab -lcrontab -l -u nobody # 查看所有用户的cronsudo find /var/spool/cron/ | sudo xargs cat
strace
命令用于打印系统调用、信号:
strace -pstrace -p 5191 -fstrace -e trace=signal -p 5191-e trace=open-e trace=file-e trace=process-e trace=network-e trace=signal-e trace=ipc-e trace=desc-e trace=memory
ltrace
命令用于打印动态链接库访问:
ltrace -p <PID>ltrace -S # syscall
w # 显示当前登录的用户、登录 IP、正在执行的进程等last # 看看最近谁登录了服务器、服务器重启时间uptime # 开机时间、登录用户、平均负载history # 查看历史命令
cat /proc/...cgroupscmdlinecpuinfocryptodevicesdiskstatsfilesystemsiomemioportskallsymsmeminfomodulespartitionsuptimeversionvmstat
nohup <command> &>[some.log] &
# 综合tophtop glancesdstat & sarmpstat# 性能分析perf# 进程pspstree -ppgreppkillpidofCtrl+z & jobs & fg# 网络ipifconfigdigpingtracerouteiftop pingtop nloadnetstatvnstatslurmscptcpdump# 磁盘 I/Oiotop iostat# 虚拟机virt-top# 用户wwhoami# 运行时间uptime# 磁盘dudflsblk# 权限chownchmod# 服务systemctl list-unit-files# 定位findlocate# 性能测试time
- 常用 Linux 系统监控命令 | 神奕的博客
- Linux 工具快速教程 | Linux Tools Quick Tutorial
- 穷佐罗的 Linux 书 | GitBook
- Brendan D. Gregg
- BPF Performance Tools | Brendan D. Gregg
- Systems Performance: Enterprise and the Cloud | Brendan D. Gregg
- How to use strace and ltrace commands in Linx | The Geek Diary
- First 5 Minutes Troubleshooting A Server | devo.ps
- First 5 Commands When I Connect on a Linux Server | Linux.com
- Linux Performance Analysis in 60,000 Milliseconds | The Netfilx Tech Blog
- 【PPT】Shared Memory Segments and POSIX Semaphores
- 通过 free 命令理解 Linux 内存管理 | Cizixs
- free 命令中 cached 和 buffers 的区别 - 踏雪无痕 | 博客园
- Linux du 命令和 df 命令区别 | CSDN
- du 和 df 文件大小不一致问题排查 | CSDN
- Linux CPU 资源高、内存高分析 - milkty | 博客园
关于load average
的几点总结:
5s
一采样并计算移动平均n
核,满载的负载就是n
摘自 单独编译 KVM 模块 - llwszjj | CSDN
To be updated…
# 进入 KVM 代码目录cd /root/kernel-src/kvm-2.6.32/arch/x86/kvm# 开始编译 make -C /lib/modules/`uname -r`/build M=`pwd` cleanmake -C /lib/modules/`uname -r`/build M=`pwd` modules# 拷贝编译结果出来,并使用cp *.ko /root/kvm/tools/modules/cd /root/kvm/tools/modules/# 卸载旧版本模块modprobe -r kvm_intelmodprobe -r kvm# 安装新版本模块modprobe irqbypassinsmod kvm.koinsmod kvm-intel.ko
]]>
摘自 Go 语言之旅
main
包开始运行import ( // 标准库 "fmt" "math" // 本地包 "apiserver/model" "apiserver/log" // 外部包 "github.com/spf13/viper")
导出名以大写字母开头:
package mainimport ( "fmt" "math")func main() { fmt.Println(math.Pi)}------3.141592653589793
函数可以有多个返回值:
package mainimport "fmt"func swap(x, y string) (string, string) { return y, x}func main() { a, b := swap("Hello", "world") fmt.Println(a, b)}------world Hello
也可以分别为返回值命名:
package mainimport "fmt"func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return}func main() { fmt.Println(split(26))}------11 15
Go 语言的基本类型如下:
boolstringint int8 int16 int32 int64uint uint8 uint16 uint32 uint64 uintptrbyte // uint8 的别名rune // int32 的别名, 表示一个 Unicode 码点float32 float64complex64 complex128
测试一下:
package mainimport ( "fmt" "math/cmplx")var ( ToBe bool = false MaxInt uint64 = 1<<64 - 1 z complex128 = cmplx.Sqrt(-5 + 12i))func main() { fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe) fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt) fmt.Printf("Type: %T Value: %v\n", z, z)}------Type: bool Value: falseType: uint64 Value: 18446744073709551615Type: complex128 Value: (2+3i)
0
false
""
package mainimport "fmt"func main() { var i int var f float64 var b bool var s string fmt.Printf("%v %v %v %q\n", i, f, b, s)}------0 0 false ""
表达式T(v)
将值v
转换为类型T
:
package mainimport ( "fmt" "math")func main() { var x, y int = 3, 4 var f float64 = math.Sqrt(float64(x*x + y*y)) var z uint = uint(f) fmt.Println(x, y, z)}------3 4 5
package mainimport "fmt"func main() { v1 := 42 fmt.Printf("v1 is of type %T\n", v1) v2 := 3.1415926 fmt.Printf("v2 is of type %T\n", v2) v3 := 0.867 + 0.5i fmt.Printf("v3 is of type %T\n", v3)}------v1 is of type intv2 is of type float64v3 is of type complex128
常量使用const
关键字声明,可以是字符、字符、布尔值或数值,且不能用:=
语法声明:
package mainimport "fmt"const Pi = 3.14func main() { const World = "世界" fmt.Println("Hello", World) fmt.Println("Happy", Pi, "Day") const Truth = true fmt.Println("Go rules?", Truth)}------Hello 世界Happy 3.14 DayGo rules? true
数值常量是高精度的值,一个未指定类型的常量由上下文来决定其类型:
package mainimport "fmt"const ( // 将 1 左移 100 位来创建一个非常大的数字 // 即这个数的二进制是 1 后面跟着 100 个 0 Big = 1 << 100 // 再往右移 99 位,即 Small = 1 << 1,或者说 Small = 2 Small = Big >> 99)func needInt(x int) int { return x * 10 + 1 }func needFloat(x float64) float64 { return x * 0.1}func main() { fmt.Println(needInt(Small)) fmt.Println(needFloat(Small)) fmt.Println(needFloat(Big))}------210.21.2676506002282295e+29
for i := 0; i < 10; i++ { sum += i}
省略初始语句及后置语句,可用作while
:
for sum < 1000 { sum += sum}
无限循环:
for { // do something}
if v := math.Pow(x, n); v <lim { return v} else { fmt.Printf("%g >= %g\n", v, lim)}return lim
switch
的case
语句从上到下顺次执行,直到匹配成功时停止:
package mainimport ( "fmt" "runtime")func main() { fmt.Print("Go runs on ") switch os := runtime.GOOS; os { case "windows": fmt.Println("Windows.") case "darwin": fmt.Println("OS X.") case "linux": fmt.Println("Linux.") default: fmt.Printf("%s.\n", os) }}
没有条件的switch
同switch true
一样,可以将一长串的if-else
写得更清晰:
package mainimport ( "fmt" "time")func main() { t := time.Now() switch { case t.Hour() < 12: fmt.Println("Good morning!") case t.Hour() < 17: fmt.Println("Good afternoon!") default: fmt.Println("Good evening!") }}
defer
语句会将函数推迟到外层函数返回之后执行:
推迟调用的函数其参数会立即求值,但直到外层函数返回之前,该函数都不会被调用
package mainimport "fmt"func main() { defer fmt.Println("world") fmt.Println("Hello")}------Helloworld
推迟的函数调用会被压入栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用:
package mainimport "fmt"func main() { fmt.Println("counting") for i := 0; i < 10; i++ { defer fmt.Println(i) } fmt.Println("done")}------countingdone9876543210
指针保存了值的内存地址,类型*T
是指向T
类型值的指针,其零值为nil
:
var p *intfmt.Println(p)------0xc000062070<nil>
与 C 不同,Go 没有指针运算。
package mainimport "fmt"func main() { i, j := 42, 2701 p := &i // 指向 i fmt.Println(*p) // 通过指针读取 i 的值 *p = 21 // 通过指针设置 i 的值 fmt.Println(i) // 查看 i 的值 p = &j // 指向 *p = *p / 37 // 通过指针对 j 进行除法运算 fmt.Println(j) // 查看 j 的值 fmt.Print(p) // 查看 j 的地址}------4221730xc000062070
一个结构体struct
就是一组字段field
。当有一个指向结构体的指针p
时,Go 允许使用隐式间接引用,直接通过p.X
访问其字段:
package mainimport "fmt"type Vertex struct { X int Y int}func main() { v := Vertex{ X: 1, Y: 2, } p := &v p.X = 1e9 fmt.Println(v)}------{1000000000 2}
结构体文法通过直接列出字段的值来新分配一个结构体,使用Name:
语法可以仅列出部分字段,特殊的前缀&
返回一个指向结构体的指针:
package mainimport "fmt"type Vertex struct { X int Y int}var ( v1 = Vertex{1, 2} v2 = Vertex{ X: 1, Y: 6, } v3 = Vertex{} p = &Vertex{1, 2})func main() { fmt.Println(v1, p, v2, v3)}------{1 2} &{1 2} {1 6} {0 0}
类型[n]T
是独立的类型:
var a [10]int
即数组的长度是其类型的一部分,因此数组不能改变大小。
package mainimport "fmt"func main() { var a [2]string a[0] = "Hello" a[1] = "World" fmt.Println(a[0], a[1]) fmt.Println(a) primes := [6]int{2, 3, 5, 7, 11, 13} fmt.Println(primes)}------Hello World[Hello World][2 3 5 7 11 13]
每个数组的大小都是固定的,而切片则为数组元素提供动态大小的灵活视角。
类型[]T
表示一个元素类型为T
的切片,通过上界和下界来界定:
a[low:high]
这是一个左开右闭区间,两个下标均可以省略:
package mainimport "fmt"func main() { primes := [6]int{2, 3, 5, 7, 11, 13} fmt.Println(primes, len(primes), cap(primes)) var s1 = primes[1:4] fmt.Println(s1, len(s1), cap(s1)) var s2 = primes[:] fmt.Println(s2, len(s2), cap(s2))}------[2 3 5 7 11 13] 6 6[3 5 7] 3 5[2 3 5 7 11 13] 6 6
切片就像数组的引用
package mainimport "fmt"func main() { names := [4]string{ "John", "Paul", "George", "Ringo", } fmt.Println("The original array:", names) a := names[0:2] b := names[1:3] fmt.Println("\nSlice a:", a) fmt.Println("Slice b:", b) b[0] = "XXX" fmt.Println("\nNow slice a:", a) fmt.Println("Now slice b:", b) fmt.Println("\nThe modified array:", names)}------The original array: [John Paul George Ringo]Slice a: [John Paul]Slice b: [Paul George]Now slice a: [John XXX]Now slice b: [XXX George]The modified array: [John XXX George Ringo]
切片文法类似于没有长度的数组文法:
package mainimport "fmt"func main() { q := []int{2, 3, 5, 7, 11, 13} fmt.Println(q) r := []bool{true, false, true, true, false, true} fmt.Println(r) s := []struct { i int b bool }{ {2, true}, {3, false}, {5, true}, {7, true}, {11, false}, {13, true}, } fmt.Println(s)}------[2 3 5 7 11 13][true false true true false true][{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}]
切片下界默认值为0
,上界则是该切片的长度。
package mainimport ( "fmt" "unsafe")func main() { s := []int{2, 3, 5, 7, 11, 13} fmt.Println(" s:", s) s1 := s[1:4] fmt.Println("s1: ", s1) s2 := s1[:2] fmt.Println("s2: ", s2) s3 := s2[1:] fmt.Println("s3: ", s3) s3[0] = 0 fmt.Println("\nSizeof int on a 64-bit machine:", unsafe.Sizeof(s[0])) fmt.Println("\ns[0] location:", &s[0]) fmt.Println("s[1] location:", &s[1]) fmt.Println("s[2] location:", &s[2]) fmt.Println("s[3] location:", &s[3])}------ s: [2 3 5 7 11 13]s1: [3 5 7]s2: [3 5]s3: [5]Sizeof int on a 64-bit machine: 8s[0] location: 0xc00008c030s[1] location: 0xc00008c038s[2] location: 0xc00008c040s[3] location: 0xc00008c048
切片拥有长度和容量:
package mainimport "fmt"func main() { s := []int{2, 3, 5, 7, 11, 13} printSlice(s) // 截取切片使其长度为 0 s = s[:0] printSlice(s) // 拓展其长度 s = s[:4] printSlice(s) // 舍弃前两个值 s = s[2:] printSlice(s)}func printSlice(s []int) { fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)}------len=6 cap=6 [2 3 5 7 11 13]len=0 cap=6 []len=4 cap=6 [2 3 5 7]len=2 cap=4 [5 7]
切片的零值是nil
,长度和容量均为0
且没有底层数组:
package mainimport "fmt"func main() { var s []int fmt.Println(s, len(s), cap(s)) if s == nil { fmt.Println("nil!") }}------[] 0 0nil!
切片可以用内建函数make
来创建,make
函数会分配一个元素为零值的数组并返回一个引用它的切片:
package mainimport "fmt"func main() { a := make([]int, 5) printSlice("a", a) b := make([]int, 0, 5) printSlice("b", b) c := b[:2] printSlice("c", c) d := c[2:5] printSlice("d", d)}func printSlice(s string, x []int) { fmt.Printf("%s len=%d cap=%d %v\n", s, len(x), cap(x), x)}------a len=5 cap=5 [0 0 0 0 0]b len=0 cap=5 []c len=2 cap=5 [0 0]d len=3 cap=3 [0 0 0]
切片可以包含切片,类似二维切片的概念:
package mainimport ( "fmt" "strings")func main() { board := [][]string{ []string{"_", "_", "_"}, []string{"_", "_", "_"}, []string{"_", "_", "_"}, } board[0][0] = "X" board[2][2] = "O" board[1][2] = "X" board[1][0] = "O" board[0][2] = "X" for i := 0; i < len(board); i++ { fmt.Printf("%s\n", strings.Join(board[i], " ")) }}------X _ XO _ X_ _ O
Go 提供了内建的append()
函数,用于向切片追加新元素:
package mainimport "fmt"func main() { var s []int printSlice(s) // 向切片添加一个 0 s = append(s, 0) printSlice(s) // 这个切片会按需增长 s = append(s, 1) printSlice(s) // 可以一次性添加多个元素 s = append(s, 2, 3, 4) printSlice(s)}func printSlice(s []int) { fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)}------len=0 cap=0 []len=1 cap=2 [0]len=2 cap=2 [0 1]len=5 cap=8 [0 1 2 3 4]// go1.12.6 windows/amd64:len=5 cap=6 [0 1 2 3 4]
当切片长度不超过
1024
时,每次扩容为原切片长度的两倍(有待考证)
for
循环的range
形式可遍历切片slice
或映射map
:
package mainimport "fmt"var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}func main() { for i, v := range pow { fmt.Printf("2^%d = %d\n", i, v) }}------2^0 = 12^1 = 22^2 = 42^3 = 82^4 = 162^5 = 322^6 = 642^7 = 128
当使用for range
遍历时,每次迭代都会返回两个值:
i
为当前元素的下标v
为该下标对应元素的一份副本因此,通过下标
s[i]
取值比直接通过v
效率更高
可以将下标或值赋予_
表示忽略:
for i, _ := range powfor _, value := range pow
若只需要索引,忽略第二个变量即可:
for i := range pow
映射map
将键映射到值。映射的文法与结构体相似,不过必须有键名:
package mainimport "fmt"type Vertex struct { Lat, Long float64}var m = map[string]Vertex{ "Bell Labs": Vertex{ 40.68433, -74.39967, }, "Google": Vertex{ 37.42202, -122.08408, },}func main() { fmt.Println(m)}------map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]
可以直接省略顶级类型的类型名:
var m = map[string]Vertex{ "Bell Labs": {40.68433, -74.39967}, "Google": {37.42202, -122.08408},}
当从映射中读取某个不存在的键时,结果是映射的元素类型的零值。
// 插入或修改元素m[key] = elem// 删除元素delete(m, key)// 通过双赋值检测某个键是否存在elem, ok := m[key]
函数也是值,可以向其他值一样传递。
函数值可以用作函数的参数或返回值:
package mainimport ( "fmt" "math")func compute(fn func(float64, float64) float64) float64 { return fn(3, 4)}func main() { hypot := func(x, y float64) float64 { return math.Sqrt(x*x + y*y) } fmt.Println(hypot(5, 12)) fmt.Println(compute(hypot)) fmt.Println(compute(math.Pow))}------13581
Go 的函数可以是一个闭包clojure
。
闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。
例如,函数adder()
返回一个闭包,每个闭包都被绑定在其各自的sum
变量上:
package mainimport "fmt"func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum }}func main() { pos, neg := adder(), adder() for i := 0; i < 10; i++ { fmt.Println( pos(i), neg(-2*i), ) }}------0 01 -23 -66 -1210 -2015 -3021 -4228 -5636 -7245 -90
Go 没有类,不过可以为结构体类型定义方法。
方法就是一类带特殊的接收者参数的函数,接收者位于func
关键字和方法名之间:
package mainimport ( "fmt" "math")type Vertex struct { X, Y float64}func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y)}func main() { v := Vertex{3, 4} fmt.Println(v.Abs())}------5
也可以为非结构体类型声明方法,例如在下面的例子中看到了一个带Abs()
方法的数值类型MyFloat
:
package mainimport ( "fmt" "math")type MyFloat float64func (f MyFloat) Abs() float64 { if f < 0 { return float64(-f) } return float64(f)}func main() { f := MyFloat(-math.Sqrt2) fmt.Println(f.Abs())}------1.4142135623730951
需要注意:
MyFloat
)的类型定义和方法声明必须在同一包内可以为指针接收者声明方法,这意味着方法内部可以修改接收者指向的值:
package mainimport ( "fmt" "math")type Vertex struct { X, Y float64}func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y)}func (v *Vertex) Scale(f float64) { v.X *= f v.Y *= f}func main() { v := Vertex{3, 4} v.Scale(10) fmt.Println(v.Abs())}------50
带指针参数的函数必须接受一个指针:
var v VertexScaleFunc(v, 5) // 编译错误!ScaleFunc(&v, 5) // OK
而以指针为接收者的方法被调用时,接收者既能为值又能为指针:
var v Vertexv.Scale(5) // OKp := &vp.Scale(10) // OK
这是因为 Go 会对语句做以下翻译:
v.Scale(5)// 以上语句解释为:(&v).Scale(5)
同样的,以值为接收者的方法被调用时,接收者既能为值又能为指针:
var v Vertexfmt.Println(v.Abs()) // OKp := &vfmt.Println(p.Abs()) // OK
同样会做以下翻译:
p.Abs()// 以上语句翻译为(*p).Abs()
通常来说,所有给定类型的方法都应该有值或指针接收者,但不应该二者混用
使用指针接收者的原因有二:
例如在下面的示例中,Scale()
和Abs()
接收者的类型均为*Vertex
,即便Abs()
并不需要修改其接收者:
package mainimport ( "fmt" "math")type Vertex struct { X, Y float64}func (v *Vertex) Scale(f float64) { v.X = v.X * f v.Y = v.Y * f}func (v *Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y)}func main() { v := &Vertex{3, 4} fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs()) v.Scale(5) fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())}------Before scaling: &{X:3 Y:4}, Abs: 5After scaling: &{X:15 Y:20}, Abs: 25
接口类型是由一组方法签名定义的集合,接口类型的变量可以保存任何实现了这些方法的值:
package mainimport ( "fmt" "math")type Abser interface { Abs() float64}func main() { var a Abser f := MyFloat(-math.Sqrt2) v := Vertex{3, 4} a = f // a MyFloat 实现了 Abser fmt.Println(a.Abs()) a = &v // a *Vertex 实现了 Abser fmt.Println(a.Abs())}type MyFloat float64func (f MyFloat) Abs() float64 { if f < 0 { return float64(-f) } return float64(f)}type Vertex struct { X, Y float64}func (v *Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y)}------1.41421356237309515
在 Go 中,接口是隐式实现的,即类型通过实现一个接口的所有方法来实现该接口。
既然无需专门显式声明,也就没有implements
关键字。
(value, type)
package mainimport ( "fmt" "math")type I interface { M()}type T struct { S string}func (t *T) M() { fmt.Println(t.S)}type F float64func (f F) M() { fmt.Println(f)}func main() { var i I i = &T{"Hello"} describe(i) i.M() i = F(math.Pi) describe(i) i.M()}func describe(i I) { fmt.Printf("(%v, %T)\n", i, i)}------(&{Hello}, *main.T)Hello(3.141592653589793, main.F)3.141592653589793
即便接口内的具体值为nil
,方法仍然会被nil
接收者调用:
package mainimport "fmt"type I interface { M()}type T struct { S string}func (t *T) M() { if t == nil { fmt.Println("<nil>") return } fmt.Println(t.S)}func main() { var i I var t *T i = t describe(i) i.M() i = &T{"hello"} describe(i) i.M()}func describe(i I) { fmt.Printf("(%v, %T)\n", i, i)}------(<nil>, *main.T)<nil>(&{hello}, *main.T)hello
注意:保存了
nil
具体值的接口,其自身并不为nil
nil
接口值既不保存值也不保存具体类型nil
接口调用方法会产生运行时错误package mainimport "fmt"type I interface { M()}func main() { var i I describe(i) i.M()}func describe(i I) { fmt.Printf("(%v, %T)\n", i, i)}------(<nil>, <nil>)panic: runtime error: invalid memory address or nil pointer dereference[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xd9864]goroutine 1 [running]:main.main() /tmp/sandbox062767685/prog.go:12 +0x84
指定了零个方法的接口值被称为空接口:
interface{}
package mainimport "fmt"func main() { var i interface{} describe(i) i = 42 describe(i) i = "hello" describe(i)}func describe(i interface{}) { fmt.Printf("(%v, %T)\n", i, i)}------(<nil>, <nil>)(42, int)(hello, string)
类型断言提供了访问接口值底层具体值的方式:
t, ok := i.(T)
i
保存了一个T
,那么t
将会是其底层值,而ok
为true
ok
将为false
,而t
将为T
类型的零值,程序并不会产生panic
package mainimport "fmt"func main() { var i interface{} = "hello" s := i.(string) fmt.Println(s) s, ok := i.(string) fmt.Println(s, ok) f, ok := i.(float64) fmt.Println(f, ok) f = i.(float64) // 报错(panic) fmt.Println(f)}------hellohello true0 falsepanic: interface conversion: interface {} is string, not float64goroutine 1 [running]:main.main() /tmp/sandbox590758285/prog.go:17 +0x220
类型选择是一种按顺序从几个类型断言中选择分支的结构:
package mainimport "fmt"func do(i interface{}) { switch v := i.(type) { case int: fmt.Printf("Twice %v is %v\n", v, v*2) case string: fmt.Printf("%q is %v bytes long\n", v, len(v)) default: fmt.Printf("I don't know about type %T!\n", v) }}func main() { do(21) do("hello") do(true)}------Twice 21 is 42"hello" is 5 bytes longI don't know about type bool!
fmt
包中定义的Stringer
接口是最普遍的接口之一:
type Stringer interface { String() string}
因此只要实现了String()
方法,就可以打印结构体的信息:
package mainimport "fmt"type Person struct { Name string Age int}func (p Person) String() string { return fmt.Sprintf("%v (%v years)", p.Name, p.Age)}func main() { a := Person{"Arthur Dent", 42} z := Person{"Zaphod Beeblebrox", 9001} fmt.Println(a, z)}------Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)
Go 程序使用error
值来表示错误状态。与fmt.Stringer
类似,error
类型也是一个内建接口:
type error interface { Error() string}
通常函数会返回一个error
值,调用它的代码应当判断这个错误是否等于nil
来进行错误处理:
package mainimport ( "fmt" "time")type MyError struct { When time.Time What string}func (e *MyError) Error() string { return fmt.Sprintf("at %v, %s", e.When, e.What)}func run() error { return &MyError{ time.Now(), "it didn't work", }}func main() { if err := run(); err != nil { fmt.Println(err) }}------at 2019-08-18 12:53:38.540727 +0800 CST m=+0.002985501, it didn't work
io
包指定了io.Reader
接口,它表示从数据流的末尾进行读取。
io.Reader
接口有一个Read
方法:
func (T) Read(b []byte) (n int, err error)
该方法用数据填充给定的字节切片,并返回填充的字节数和错误值。在遇到数据流的结尾时,会返回一个io.EOF
错误:
package mainimport ( "fmt" "io" "strings")func main() { r := strings.NewReader("Hello, Reader!") b := make([]byte, 8) for { n, err := r.Read(b) fmt.Printf("n = %v err = %v b = %v\n", n, err, b) fmt.Printf("b[:n] = %q\n", b[:n]) if err == io.EOF { break } }}------n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]b[:n] = "Hello, R"n = 6 err = <nil> b = [101 97 100 101 114 33 32 82]b[:n] = "eader!"n = 0 err = EOF b = [101 97 100 101 114 33 32 82]b[:n] = ""
image
包定义了Image
接口:
package imagetype Image interface { ColorModel() color.Model Bounds() Rectangle At(x, y int) color.Color}
这些接口和类型由image/color
包定义:
package mainimport ( "fmt" "image")func main() { m := image.NewRGBA(image.Rect(0, 0, 100, 100)) fmt.Println(m.Bounds()) fmt.Println(m.At(0, 0).RGBA())}------(0,0)-(100,100)0 0 0 0
Go 程goroutine
是由 Go 运行时管理的轻量级线程:
go f(x, y, z)
上面的语句会启动一个新的 Go 程并执行:
f(x, y, z)
f, x, y, z
的求值发生在当前 Go 程中f
的执行发生在新的 Go 程中package mainimport ( "fmt" "time")func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) }}func main() { go say("world") say("hello")}------helloworldworldhelloworldhelloworldhellohello
Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync
包提供了这种能力,不过也可以利用其它方式实现。
信道channel
是带有类型的管道,可以通过它使用信道操作符<-
来发送或者接收值:
ch <- v // 将 v 发送至信道 chv := <-ch // 从 ch 接收值并赋予 v
根据箭头在信道的方向,左读右写。
和映射与切片一样,信道在使用前必须创建:
ch := make(chan int)
默认情况下是阻塞的,这使得 Go 程序可以在没有显式的锁或竞态变量的情况下进行同步:
package mainimport "fmt"func sum(s []int, c chan int) { sum := 0 for i := range s { sum += s[i] } c <- sum // 将和送入 c}func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x, y := <-c, <-c // 从 c 中接收 fmt.Println(x, y, x+y)}-------5 17 12
信道可以是带缓冲的:
ch := make(chan int, 100)
仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接收方会阻塞。
发送者可通过close
关闭一个信道来表示没有需要发送的值了。
接收者可通过以下语句判断信道是否已被关闭:
v, ok := <-ch // 关闭时 v 为默认零值,ok 为 false
循环for i := range c
会不断从信道接收值,直到它被关闭:
package mainimport ( "fmt")func fibonacci(n int, c chan int) { x, y := 0, 1 for i := 0; i < n; i++ { c <- x x, y = y, x+y } close(c)}func main() { c := make(chan int, 10) go fibonacci(cap(c), c) for i := range c { fmt.Println(i) } v, ok := <-c fmt.Println(v, ok)}------01123581321340 false
panic
panic
range
循环select
语句使一个 Go 程可以等待多个通信操作。
select
会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时,会随机选择一个执行:
package mainimport "fmt"func fibonacci(c, quit chan int) { x, y := 0, 1 for { select { case c <- x: x, y = y, x+y case <-quit: fmt.Println("quit") return } }}func main() { c := make(chan int) quit := make(chan int) go func() { for i := 0; i < 10; i++ { fmt.Println(<-c) } quit <- 0 }() fibonacci(c, quit)}------0112358132134quit
当select
中的其他分支都没有准备好时,default
分支就会执行:
select {case i := <-c: // 使用 idefault: // 从 c 中接收会阻塞时执行}
为了在尝试发送或者接收时不发生阻塞,可使用default
分支:
package mainimport ( "fmt" "time")func main() { tick := time.Tick(100 * time.Millisecond) boom := time.After(500 * time.Millisecond) for { select { case <-tick: fmt.Println("tick.") case <-boom: fmt.Println("BOOM!") default: fmt.Println(" .") time.Sleep(50 * time.Millisecond) } }}------ . .tick. . .tick. . .tick. . .tick. . .BOOM!
Go 标准库中提供了sync.Mutex
互斥锁类型及其两个方法:Lock()
、Unlock()
:
package mainimport ( "fmt" "sync" "time")// SafeCounter 的并发使用是安全的type SafeCounter struct { v map[string]int mux sync.Mutex}// Inc 增加给定 key 的计数器的值func (c *SafeCounter) Inc(key string) { c.mux.Lock() c.v[key]++ c.mux.Unlock()}// Value 返回给定 key 的计数器的当前值func (c *SafeCounter) Value(key string) int { c.mux.Lock() defer c.mux.Unlock() return c.v[key]}func main() { c := SafeCounter{ v: make(map[string]int), } for i := 0; i < 1000; i++ { go c.Inc("somekey") } time.Sleep(time.Second) fmt.Println(c.Value("somekey"))}------1000
Lock()
方法、在代码后调用Unlock()
方法来保证一段代码的互斥执行defer
语句来保证互斥锁一定会被解锁// fmtfmt.Errorf("%s", "db connect fail")// io// mathmath.Pi// sync// stringsstrings.Join(board[i], " ")fileds := strings.Fileds(s)// strconv// net/http// net/url// log// types// json// xml// randfmt.Println("My favorite number is", rand.Intn(10)) %d 整型 %s 字符串 %f 浮点数 %T 类型 %v 值,例如 {3 4}%+v 域+值,例如 {X:3 Y:4} %q 带引号的字符串, "s"// timetime.Now()time.Now().Hour()time.Sleep(time.Second)// unsafeunsafe.Sizeof()// runtimeruntime.GOOSruntime.GOARCHruntime.Version()fmt.Println(runtime.GOMAXPROCS(0))// errorserrors.New()// os.Open()
make([]int, 4, 8)// 切片 slicelen()cap()append()copy()// 映射 mapdelete(m, key)// 内建接口type error interface { Error()}type Stringer interface { String() string}// 通道for i := range chclose(ch)
package mainimport ( "fmt" "math")func sqrt(x float64) float64 { z := float64(1) for { y := z - (z*z-x)/(2*z) if math.Abs(y-z) < 1e-10 { return y } z = y }}func main() { fmt.Println(sqrt(2)) fmt.Println(math.Sqrt(2))}------1.41421356237309511.4142135623730951
package mainimport ( "golang.org/x/tour/pic")func Pic(dx, dy int) [][]uint8 { ret := make([][]uint8, dy) for x := 0; x < dy; x++ { ret[x] = make([]uint8, dx) for y := 0; y < dx; y++ { ret[x][y] = uint8(x ^ y) // ret[x][y] = uint8((x + y) / 2) // ret[x][y] = uint8(x * y) // ret[x][y] = uint8(float64(x) * math.Log(float64(y))) // ret[x][y] = uint8(x % (y + 1)) } } return ret}func main() { pic.Show(Pic)}
package mainimport ( "strings" "golang.org/x/tour/wc")func WordCount(s string) map[string]int { count := make(map[string]int) for _, word := range strings.Fields(s) { count[word]++ } return count}func main() { wc.Test(WordCount)}
package mainimport "fmt"// 返回一个“返回int的函数”func fibonacci() func() int { one := 0 two := 1 return func() int { three := one + two one = two two = three return three }}func main() { f := fibonacci() for i := 0; i < 10; i++ { fmt.Println(f()) }}------123581321345589
package mainimport "fmt"type IPAddr [4]bytefunc (ip IPAddr) String() string { return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])}func main() { hosts := map[string]IPAddr{ "loopback": {127, 0, 0, 1}, "googleDNS": {8, 8, 8, 8}, } for name, ip := range hosts { fmt.Printf("%v: %v\n", name, ip) }}------loopback: 127.0.0.1googleDNS: 8.8.8.8
package mainimport ( "fmt" "math")type ErrNegativeSqrt float64func (e ErrNegativeSqrt) Error() string { return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e))}func sqrt(x float64) (float64, error) { if x < 0 { return 0, ErrNegativeSqrt(x) } z := float64(1) for { y := z - (z*z-x)/(2*z) if math.Abs(y-z) < 1e-10 { return y, nil } z = y }}func main() { fmt.Println(sqrt(2)) fmt.Println(sqrt(-2))}------1.4142135623730951 <nil>0 cannot Sqrt negative number: -2
package mainimport ( "strings" "golang.org/x/tour/reader")type MyReader struct{}func (MyReader) Read(b []byte) (int, error) { r := strings.NewReader("A") n, err := r.Read(b) return n, err}func main() { reader.Validate(MyReader{})}
package mainimport ( "io" "os" "strings")type rot13Reader struct { r io.Reader}func (self rot13Reader) Read(buf []byte) (int, error) { length, err := self.r.Read(buf) if err != nil { return length, err } for i := 0; i < length; i++ { v := buf[i] switch { case 'a' <= v && v <= 'm': fallthrough case 'A' <= v && v <= 'M': buf[i] = v + 13 case 'n' <= v && v <= 'z': fallthrough case 'N' <= v && v <= 'Z': buf[i] = v - 13 } } return length, nil}func main() { s := strings.NewReader("Lbh penpxrq gur pbqr!") r := rot13Reader{s} io.Copy(os.Stdout, &r)}------You cracked the code!
package mainimport ( "image" "image/color" "golang.org/x/tour/pic")type Image struct { w int h int}func (self Image) ColorModel() color.Model { return color.RGBAModel}func (self Image) Bounds() image.Rectangle { return image.Rect(0, 0, self.w, self.h)}func (self Image) At(x, y int) color.Color { r := (uint8)((float64)(x) / (float64)(self.w) * 255.0) g := (uint8)((float64)(y) / (float64)(self.h) * 255.0) b := (uint8)((float64)(x*y) / (float64)(self.w*self.h) * 255.0) return color.RGBA{r, g, b, 255}}func main() { m := Image{255, 255} pic.ShowImage(m)}
package mainimport ( "fmt" "golang.org/x/tour/tree")// Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。func Walk(t *tree.Tree, ch chan int) { if t == nil { return } Walk(t.Left, ch) ch <- t.Value Walk(t.Right, ch)}// Same 检测树 t1 和 t2 是否含有相同的值。func Same(t1, t2 *tree.Tree) bool { ch1 := make(chan int) ch2 := make(chan int) go Walk(t1, ch1) go Walk(t2, ch2) for i := 0; i < 10; i++ { x, y := <-ch1, <-ch2 fmt.Println(x, y) if x != y { return false } } return true}func main() { fmt.Println(Same(tree.New(1), tree.New(1)))}------1 12 23 34 45 56 67 78 89 910 10true
package mainimport ( "fmt" "sync")type Fetcher interface { // Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。 Fetch(url string) (body string, urls []string, err error)}// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。func Crawl(url string, depth int, fetcher Fetcher, crawled Crawled, out chan string, end chan bool) { // TODO: 并行的抓取 URL。 // TODO: 不重复抓取页面。 // 下面并没有实现上面两种情况: if depth <= 0 { end <- true return } crawled.mux.Lock() if _, ok := crawled.crawled[url]; ok { crawled.mux.Unlock() end <- true return } crawled.crawled[url] = 1 crawled.mux.Unlock() _, urls, err := fetcher.Fetch(url) if err != nil { fmt.Println(err) end <- true return } out <- url //fmt.Println("found: ", url, body) for _, u := range urls { go Crawl(u, depth-1, fetcher, crawled, out, end) } for i := 0; i < len(urls); i++ { <-end } end <- true return}type Crawled struct { crawled map[string]int mux sync.Mutex}func main() { crawled := Crawled{make(map[string]int), sync.Mutex{}} out := make(chan string) end := make(chan bool) go Crawl("http://golang.org/", 4, fetcher, crawled, out, end) for { select { case url := <-out: fmt.Println("found: ", url) case <-end: return } }}// fakeFetcher 是返回若干结果的 Fetcher。type fakeFetcher map[string]*fakeResulttype fakeResult struct { body string urls []string}func (f fakeFetcher) Fetch(url string) (string, []string, error) { if res, ok := f[url]; ok { return res.body, res.urls, nil } return "", nil, fmt.Errorf("not found: %s", url)}// fetcher 是填充后的 fakeFetcher。var fetcher = fakeFetcher{ "http://golang.org/": &fakeResult{ "The Go Programming Language", []string{ "http://golang.org/pkg/", "http://golang.org/cmd/", }, }, "http://golang.org/pkg/": &fakeResult{ "Packages", []string{ "http://golang.org/", "http://golang.org/cmd/", "http://golang.org/pkg/fmt/", "http://golang.org/pkg/os/", }, }, "http://golang.org/pkg/fmt/": &fakeResult{ "Package fmt", []string{ "http://golang.org/", "http://golang.org/pkg/", }, }, "http://golang.org/pkg/os/": &fakeResult{ "Package os", []string{ "http://golang.org/", "http://golang.org/pkg/", }, },}------found: http://golang.org/found: http://golang.org/pkg/found: http://golang.org/pkg/os/found: http://golang.org/pkg/fmt/not found: http://golang.org/cmd/
推荐一个知乎专栏作者:谢伟,知乎专栏『Gopher』- Go 上手指南
其他不错的文章:
Go Web Programming Notes. To Be Updated…
package mainimport ( "fmt" "net/http")func handler(writer http.ResponseWriter, request *http.Request) { fmt.Fprintf(writer, "Hello Web, %s!", request.URL.Path[1:])}func main() { http.HandleFunc("/", handler) http.ListenAndServe(":8080", nil)}
HTTP 请求的 URL 格式:
http://<servername>/<handlername>?<parameters>
相关参考:
package mainimport ( "fmt" "log" "net/http" "github.com/julienschmidt/httprouter")func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "Welcome!\n")}func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { fmt.Fprintf(w, "hello %s!\n", ps.ByName("name"))}func getUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { uid := ps.ByName("uid") fmt.Fprintf(w, "you are get user %s", uid)}func modifyUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { uid := ps.ByName("uid") fmt.Fprintf(w, "you are modify user %s", uid)}func deleteUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { uid := ps.ByName("uid") fmt.Fprintf(w, "you are delete user %s", uid)}func addUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { uid := ps.ByName("uid") fmt.Fprintf(w, "you are add user %s", uid)}func main() { router := httprouter.New() router.GET("/", Index) router.GET("/hello/:name", Hello) router.GET("/user/:uid", getUser) router.POST("/adduser/:uid", addUser) router.DELETE("/deluser/:uid", deleteUser) router.PUT("/moduser/:uid", modifyUser) log.Fatal(http.ListenAndServe(":8080", router))}
参见 3.2 Go 搭建一个 Web 服务器 - build-web-application-with-golang | Github
package mainimport ( "fmt" "log" "net/http" "strings")func sayHelloName(w http.ResponseWriter, r *http.Request) { r.ParseForm() fmt.Println(r.Form) // 解析参数,默认是不会解析的 fmt.Println("path", r.URL.Path) // 在服务器端打印 fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) for k, v := range r.Form { fmt.Println("key:", k) fmt.Println("val:", strings.Join(v, "")) } fmt.Fprintf(w, "Hello Abel!") // 写入到 Response 中}func main() { http.HandleFunc("/", sayHelloName) // 设置访问的路由 log.Fatal(http.ListenAndServe(":8080", nil)) // 设置监听的端口}
GET
请求:
GET http://localhost:8080?url_long=111&url_long=222 HTTP/1.1
响应:
HTTP/1.1 200 OKDate: Mon, 02 Sep 2019 10:24:36 GMTContent-Length: 11Content-Type: text/plain; charset=utf-8Connection: closeHello Abel!
服务端输出:
> go run main.gomap[url_long:[111 222]]path /scheme[111 222]key: url_longval: 111222
先理清几个基本概念:
POST
、GET
、Cookie
、URL
等信息 下图是 Go 实现 Web 服务的工作模式流程图:
HTTP 包的执行流程:
Listen Socket
,监听指定端口,等待客户端请求的到来Listen Socket
接收客户端的请求,得到Client Socket
,接下来通过Client Socket
与客户端通信Client Socket
读取 HTTP 请求的协议头,如果是POST
方法,还可能要读取客户端提交的数据,然后交给相应的handler
处理请求。处理完毕后,handler
会准备好客户端需要的数据,通过Client Socket
写给客户端对于上述过程,要想了解 Go 是如何让 Web 运行起来的,需要搞清楚以下三点:
Go Version:
1.12.6
在之前的代码中可以看到,监听端口的实现是在http.ListenAndServe()
函数中:
http.ListenAndServe(":8080", nil)
该函数首先会初始化一个Server
对象,之后调用该对象的同名方法:
// ListenAndServe listens on the TCP network address addr and then calls// Serve with handler to handle requests on incoming connections.// Accepted connections are configured to enable TCP keep-alives.//// The handler is typically nil, in which case the DefaultServeMux is used.//// ListenAndServe always returns a non-nil error.func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe()}
Server
结构体的ListenAndServe()
方法又调用了net.Listen("tcp", addr)
,也就是底层用 TCP 协议搭建了一个服务,开始监听指定的端口:
// ListenAndServe listens on the TCP network address srv.Addr and then// calls Serve to handle requests on incoming connections.// Accepted connections are configured to enable TCP keep-alives.//// If srv.Addr is blank, ":http" is used.//// ListenAndServe always returns a non-nil error. After Shutdown or Close,// the returned error is ErrServerClosed.func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed } addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) // 监听指定的端口 if err != nil { return err } // 接收并处理客户端的请求 return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})}
监听端口之后,上述代码最后又调用了srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
作为返回值,该函数的作用就是接收并处理客户端的请求信息。该函数的具体实现如下:
// Serve accepts incoming connections on the Listener l, creating a// new service goroutine for each. The service goroutines read requests and// then call srv.Handler to reply to them.//// HTTP/2 support is only enabled if the Listener returns *tls.Conn// connections and they were configured with "h2" in the TLS// Config.NextProtos.//// Serve always returns a non-nil error and closes l.// After Shutdown or Close, the returned error is ErrServerClosed.func (srv *Server) Serve(l net.Listener) error { // 省略部分代码 for { rw, e := l.Accept() // 1. 接收客户端请求 if e != nil { select { case <-srv.getDoneChan(): return ErrServerClosed default: } if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 c := srv.newConn(rw) // 2. 创建一个新的 Conn c.setState(c.rwc, StateNew) // before Serve can return go c.serve(ctx) // 3. 为每个连接单独开一个 goroutine }}
省略部分代码,重点关注其中的for{}
循环:
l.Accept()
:接收请求,并处理可能出现的错误srv.newConn(rw)
:创建一个新的连接Conn
go c.serve(ctx)
:为新连接单独开一个goroutine
,把请求的数据当作参数扔给这个Conn
去服务那么如何具体分配到相应的函数来处理请求呢?可以看到,在上面的代码中,最后实际调用go c.serve(ctx)
处理请求,该函数的实现代码较长,仅截取重要语句如下:
// Serve a new connection.func (c *conn) serve(ctx context.Context) { // 省略部分代码 for { // 1. 解析请求,获取 ResponseWriter 及 Request w, err := c.readRequest(ctx) // 省略部分代码 // HTTP cannot have multiple simultaneous active requests.[*] // Until the server replies to this request, it can't read another, // so we might as well run the handler in this goroutine. // [*] Not strictly true: HTTP pipelining. We could let them all process // in parallel even if their responses need to be serialized. // But we're not going to implement HTTP pipelining because it // was never deployed in the wild and the answer is HTTP/2. // 2. 进一步处理请求 serverHandler{c.server}.ServeHTTP(w, w.req) } // 省略部分代码}
c.readRequest(ctx)
:解析请求,获取对应的ResponseWriter
及Request
serverHandler.ServeHTTP(w, w.req)
:进一步处理请求结构体serverHandler
的ServeHTTP()
方法具体实现如下:
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { // 1. 获取 Server 对应的 Handler handler := sh.srv.Handler // 2. 若对应的 Handler 为 nil,则使用 DefaultServeMux if handler == nil { handler = DefaultServeMux } if req.RequestURI == "*" && req.Method == "OPTIONS" { handler = globalOptionsHandler{} } // 3. 调用相应的函数处理请求 handler.ServeHTTP(rw, req)}
首先通过handler := sh.srv.Handler
获取对应的Handler
,也就是最开始调用ListenAndServe()
时传入的第二个参数。实际上Handler
是一个接口类型,只定义了一个方法ServeHTTP()
:
type Handler interface { ServeHTTP(ResponseWriter, *Request)}
例如之前传入的参数是nil
,就会使用默认的DefaultServeMux
。该变量是一个路由器(或者说,HTTP 请求多路复用器),用来匹配 URL 并跳转到其相应的 handle 函数,它在 Go 源码的server.go
中定义:
// ServeMux is an HTTP request multiplexer.// It matches the URL of each incoming request against a list of registered// patterns and calls the handler for the pattern that// most closely matches the URL.//// Patterns name fixed, rooted paths, like "/favicon.ico",// or rooted subtrees, like "/images/" (note the trailing slash).// Longer patterns take precedence over shorter ones, so that// if there are handlers registered for both "/images/"// and "/images/thumbnails/", the latter handler will be// called for paths beginning "/images/thumbnails/" and the// former will receive requests for any other paths in the// "/images/" subtree.//// Note that since a pattern ending in a slash names a rooted subtree,// the pattern "/" matches all paths not matched by other registered// patterns, not just the URL with Path == "/".//// If a subtree has been registered and a request is received naming the// subtree root without its trailing slash, ServeMux redirects that// request to the subtree root (adding the trailing slash). This behavior can// be overridden with a separate registration for the path without// the trailing slash. For example, registering "/images/" causes ServeMux// to redirect a request for "/images" to "/images/", unless "/images" has// been registered separately.//// Patterns may optionally begin with a host name, restricting matches to// URLs on that host only. Host-specific patterns take precedence over// general patterns, so that a handler might register for the two patterns// "/codesearch" and "codesearch.google.com/" without also taking over// requests for "http://www.google.com/".//// ServeMux also takes care of sanitizing the URL request path and the Host// header, stripping the port number and redirecting any request containing . or// .. elements or repeated slashes to an equivalent, cleaner URL.type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry // slice of entries sorted from longest to shortest. hosts bool // whether any patterns contain hostnames}type muxEntry struct { h Handler pattern string}// NewServeMux allocates and returns a new ServeMux.func NewServeMux() *ServeMux { return new(ServeMux) }// DefaultServeMux is the default ServeMux used by Serve.var DefaultServeMux = &defaultServeMuxvar defaultServeMux ServeMux
最开始在main()
函数中调用http.HandleFunc("/", sayHelloName)
时,就注册了请求/
的路由规则:
func main() { http.HandleFunc("/", sayHelloName) // 设置访问的路由 log.Fatal(http.ListenAndServe(":8080", nil)) // 设置监听的端口}// HandleFunc registers the handler function for the given pattern// in the DefaultServeMux.// The documentation for ServeMux explains how patterns are matched.func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler)}
这样一来当请求的 URI 为/
时,路由就会跳转到/
对应的Handler
,也就是sayHelloName()
本身,最后把结果写入 Response 并反馈给客户端。
一个 HTTP 连接的处理流程示意图如下:
参见 3.4 Go 的 http 包详解 - build-web-application-with-golang | Github
Go 的http
包有两个核心功能:Conn、ServeMux。
与其他一些语言编写的 HTTP 服务器不同,Go 为了实现高并发和高性能,使用了goroutine
来处理Conn
的读写事件,这样每个请求都能保持独立,相互不会阻塞,可以高效的响应网络事件,这是 Go 高效的保证。
Go 在等待客户端请求的Serve()
函数里是这样写的:
func (srv *Server) Serve(l net.Listener) error { // 省略部分代码 for { rw, e := l.Accept() // 省略错误处理 c := srv.newConn(rw) c.setState(c.rwc, StateNew) // before Serve can return go c.serve(ctx) }}
可以看到客户端的每次请求都会创建一个Conn
,函数newConn()
在server.go
中的实现如下:
// Create new connection from rwc.func (srv *Server) newConn(rwc net.Conn) *conn { c := &conn{ server: srv, rwc: rwc, } if debugServerConnections { c.rwc = newLoggingConn("server", c.rwc) } return c}
可以看到Conn
实际上在net
包中定义,是一个接口类型,在net.go
中的定义如下:
// Conn is a generic stream-oriented network connection.//// Multiple goroutines may invoke methods on a Conn simultaneously.type Conn interface { Read(b []byte) (n int, err error) Write(b []byte) (n int, err error) Close() error LocalAddr() Addr RemoteAddr() Addr SetDeadline(t time.Time) error SetReadDeadline(t time.Time) error SetWriteDeadline(t time.Time) error}
客户端的每次请求都会创建一个
Conn
,这个Conn
里面保存了该次请求的信息,然后再传递到对应的handler
,该handler
中便可以读取到相应的 Header 信息,这样就保证了每个请求的独立性
之前调用http.ListenAndServe(":8080", nil)
时,实际上内部时调用了http
包默认的路由器DefaultServeMux
,通过路由器把本次请求的信息传递到了后端的处理函数,它是一个ServeMux
类型的变量:
// DefaultServeMux is the default ServeMux used by Serve.var DefaultServeMux = &defaultServeMuxvar defaultServeMux ServeMux
结构体ServeMux
就是 Go 中的路由器,它在server.go
中的定义如下:
type ServeMux struct { // 锁,由于请求涉及到并发处理,因此这里需要一个锁机制 mu sync.RWMutex // 路由规则,一个路由表达式 string 对应一个 muxEntry 实体 m map[string]muxEntry es []muxEntry // slice of entries sorted from longest to shortest. // 是否在任意的规则中带有 host 信息 hosts bool // whether any patterns contain hostnames}type muxEntry struct { h Handler // 路由表达式对应哪个 handler pattern string // 路径匹配字符串}type Handler interface { ServeHTTP(ResponseWriter, *Request) // 路由实现器}
Handler
是一个接口,但是之前示例代码中的sayHelloName()
函数并没有实现ServeHTTP()
这个方法,为什么能作为 Handler 添加到路由器中呢?
这是因为在http
包中还定义了一个类型HandlerFunc
,回顾一下之前设置访问路由的语句:
http.HandleFunc("/", sayHelloName)
这里我们调用了HandleFunc()
将sayHelloName()
设置为"/"
路由对应的Handler
,而HandleFunc()
实际进行的操作如下:
// HandleFunc registers the handler function for the given pattern.func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { if handler == nil { panic("http: nil handler") } mux.Handle(pattern, HandlerFunc(handler))}
可以看到这里将handler
转换为了HandlerFunc
,而它默认实现了ServeHTTP()
方法,即我们调用了HandlerFunc(f)
,将f
强制类型转换为HandlerFunc
类型,这样f
就拥有了ServeHTTP()
方法:
这也是适配器模式在 Go 中的应用
// The HandlerFunc type is an adapter to allow the use of// ordinary functions as HTTP handlers. If f is a function// with the appropriate signature, HandlerFunc(f) is a// Handler that calls f.type HandlerFunc func(ResponseWriter, *Request)// ServeHTTP calls f(w, r).func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r)}
路由器里存储好了相应的路由规则,那么具体的请求又是怎样分发的呢?实际上,默认的路由器ServeMux
实现了ServeHTTP()
方法:
// ServeHTTP dispatches the request to the handler whose// pattern most closely matches the request URL.func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { if r.RequestURI == "*" { if r.ProtoAtLeast(1, 1) { w.Header().Set("Connection", "close") } w.WriteHeader(StatusBadRequest) return } h, _ := mux.Handler(r) h.ServeHTTP(w, r)}
如上所示,路由器接收到请求之后,如果是*
则关闭连接,否则会调用mux.Handler(r)
返回对应设置路由的处理 handler,然后执行h.ServeHTTP(w, r)
,也就是调用对应路由的 handler 的ServeHTTP
接口。
继续来看mux.Handler(r)
是如何处理的:
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { // CONNECT requests are not canonicalized. if r.Method == "CONNECT" { // If r.URL.Path is /tree and its handler is not registered, // the /tree -> /tree/ redirect applies to CONNECT requests // but the path canonicalization does not. if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok { return RedirectHandler(u.String(), StatusMovedPermanently), u.Path } return mux.handler(r.Host, r.URL.Path) } // 省略部分代码 return mux.handler(host, r.URL.Path)}// handler is the main implementation of Handler.// The path is known to be in canonical form, except for CONNECT methods.func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { mux.mu.RLock() defer mux.mu.RUnlock() // Host-specific pattern takes precedence over generic ones if mux.hosts { h, pattern = mux.match(host + path) } if h == nil { h, pattern = mux.match(path) } if h == nil { h, pattern = NotFoundHandler(), "" } return}
可以看到在mux.handler()
中是调用mux.match()
进行匹配的,函数定义如下:
// Find a handler on a handler map given a path string.// Most-specific (longest) pattern wins.func (mux *ServeMux) match(path string) (h Handler, pattern string) { // Check for exact match first. v, ok := mux.m[path] if ok { return v.h, v.pattern } // Check for longest valid match. mux.es contains all patterns // that end in / sorted from longest to shortest. for _, e := range mux.es { if strings.HasPrefix(path, e.pattern) { return e.h, e.pattern } } return nil, ""}
这样一来就清楚了,在match()
方法中,会根据mux.m[path]
获取请求路径对应的muxEntry
,返回muxEntry
中保存的Handler
以及pattern
字符串,最后调用Handler
的ServeHTTP()
方法就可以执行相应的函数了。
通过上面的介绍,我们大致了解了 Go 的整个路由过程。除了默认路由器DefaultServeMux
,Go 同时也支持外部实现的路由器。
http.ListenAndServe()
方法的第二个参数就是用来配置外部路由器的,它是一个Handler
接口,即外部路由器只要实现了Handler
接口的ServeHTTP()
方法,就可以在自己实现的路由器的ServeHTTP()
中实现自定义路由功能。
如下所示,实现一个简单的外部路由器MyMux
:
package mainimport ( "fmt" "net/http")type MyMux struct {}func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { sayHelloName(w, r) return } http.NotFound(w, r) return}func sayHelloName(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello My Router!")}func main() { mux := &MyMux{} http.ListenAndServe(":8080", mux)}
请求报文:
GET http://localhost:8080 HTTP/1.1
响应报文:
HTTP/1.1 200 OKDate: Tue, 03 Sep 2019 02:57:42 GMTContent-Length: 16Content-Type: text/plain; charset=utf-8Connection: closeHello My Router!
Go Version:
1.12.6
分析完http
包后,现在梳理一下代码的执行过程。例如下面这段代码:
package mainimport ( "fmt" "net/http")func index(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello %s!\n", r.URL.Path[1:])}func main() { http.HandleFunc("/", index) http.ListenAndServe(":8080", nil)}
首先调用http.HandleFunc()
:
http.HandleFunc("/", index) └─ DefaultServeMux.HandleFunc(pattern, handler) // 若 handler 为 nil,则触发 panic └─ mux.Handle(pattern, HandlerFunc(handler)) // 注册路由
DefaultServeMux.HandleFunc()
DefaultServeMux.Handle()
,注册请求路径所对应的 handlerDefaultServeMux
的map[string]muxEntry
中增加对应的 handler 和路由规则之后调用http.ListenAndServe(":8080", nil)
:
http.ListenAndServe(":8080", nil) ├─ server := &Server{Addr: addr, Handler: handler} └─ server.ListenAndServe() ├─ net.Listen("tcp", addr) └─ srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) ├─ l.Accept() ├─ c := srv.newConn(rw) └─ go c.serve(ctx) ├─ w, err := c.readRequest(ctx) └─ serverHandler{c.server}.ServeHTTP(w, w.req) ├─ handler := sh.srv.Handler // nil 则为 DefaultServeMux └─ handler.ServeHTTP(rw, req) ├─ h, _ := mux.Handler(r) └─ h.ServeHTTP(w, r)
Server
server.ListenAndServe()
net.Listen("tcp", addr)
监听端口,即Listen
srv.Serve()
处理请求,即Serve
Serve()
中启动一个 for 循环,在循环中调用Accept()
接收请求Conn
,开启一个 goroutine 并调用go c.serve(ctx)
,为这个请求进行服务c.readRequest(ctx)
读取每个请求内容nil
则设为DefaultServeMux
handler.ServeHTTP(rw, req)
,上面的例子中就进入到DefaultServeMux.ServeHTTP(rw, req)
ServeHTTP()
中选择 handler:
ServeMux
的muxEntry
,判断是否有路由能满足这个 RequestServeHTTP()
NotFoundHandler
的ServeHTTP()
例如下面的表单login.gtpl
:
<html><head> <title></title></head><body> <form action="/login" method="post"> 用户名:<input type="text" name="username"> 密码:<input type="password" name="password"> <input type="submit" value="登录"> </form></body></html>
处理表单:
package mainimport ( "fmt" "html/template" "log" "net/http" "strings")func sayHelloName(w http.ResponseWriter, r *http.Request) { r.ParseForm() // 解析 URL 传递的参数,对于 POST 则解析 Request Body // 注意:如果没有调用 ParseForm 方法,下面无法获取表单的数据 log.Println("Inside sayHelloName") fmt.Printf("Form:\t%v\n", r.Form) fmt.Printf("path:\t%s\n", r.URL.Path) fmt.Printf("scheme:\t%s\n", r.URL.Scheme) fmt.Println(r.Form["url_long"]) for k, v := range r.Form { fmt.Println("key:", k) fmt.Println("val:", strings.Join(v, "")) } fmt.Fprintf(w, "Hello abel!\n")}func login(w http.ResponseWriter, r *http.Request) { fmt.Println("method:", r.Method) // 获取请求的方法 if r.Method == "GET" { t, _ := template.ParseFiles("login.gtpl") log.Println(t.Execute(w, nil)) } else { // 请求的是登录数据,那么执行登录的逻辑判断 r.ParseForm() fmt.Println("username:", r.Form["username"]) fmt.Println("password:", r.Form["password"]) }}func main() { http.HandleFunc("/", sayHelloName) http.HandleFunc("/login", login) log.Fatal(http.ListenAndServe(":8080", nil))}
request.Form
是一个url.Values
类型,里面存储了key=value
的信息:
package mainimport ( "fmt" "net/url")func main() { v := url.Values{} v.Set("name", "abel") v.Add("friend", "arjen") v.Add("friend", "frank") fmt.Println(v.Encode()) fmt.Println(v.Get("name")) fmt.Println(v.Get("friend")) fmt.Println(v["friend"])}------friend=arjen&friend=frank&name=abelabelarjen[arjen frank]
- Go Web Programming - sausheong | Github
- 08.3. REST - Go Web 编程 | Learnku
- httprouter - julienschmidt | Github
- build-web-application-with-golang - astaxie | Github
- Golang: Building a Basic Web Server in Go | Ruan Bekker’s Blog
- project-layout - Standard Go Project Layout | Github
- Go Developer Roadmap - Go 开发者路线图 | Github
- 明白了,原来 Go Web 框架中的中间件都是这样实现的 | 鸟窝
- Go 语言的修饰器编程 | 酷壳 CoolShell
- 教程:使用 go 的 gin 和 gorm 框架来构建 RESTful API 微服务 | LearnKu
- Build RESTful API service in golang using gin-gonic framework | Medium
]]>
KVM API 概述,基于 Kernel 2.6.32
KVM 的 API 是通过/dev/kvm
这个字符设备进行访问的:
> ls /dev/kvm -lcrw-rw---- 1 root root 10, 232 Jul 29 16:24 /dev/kvm
/dev/kvm
作为 Linux 的一个标准字符型设备,可以使用常见的系统调用如open()
、close()
、ioctl()
等指令进行操作。不过在 KVM 字符型设备的实现函数中,并没有包含write()
、read()
等操作,因此所有对 KVM 的操作都是通过ioctl()
发送相应的控制字实现的。
内核源码中的
Documentation/kvm/api.txt
是 KVM API 的说明文档,可以点击 这里 查看
根据 API 所提供的功能,又可将其分为以下三类:
System ioctl
:系统指令,针对 KVM 的全局性参数设置,例如通过KVM_CREATE_VM
创建虚拟机VM ioctl
:虚拟机指令,针对指定的 VM 进行控制,例如创建 vCPU、设置虚拟机内存。需要在创建 VM 的进程中调用 VM 指令,以确保进程安全vCPU ioctl
:vCPU 指令,针对指定的 vCPU 进行参数设置,例如寄存器读写、中断控制。需要在创建 vCPU 的线程中调用 vCPU 指令,以确保线程安全通常情况下,对于 KVM API 的操作是从打开/dev/kvm
设备文件开始的:
open
系统调用打开/dev/kvm
设备文件后,会获得一个文件描述符fd
,然后再通过ioctl
发送相应的控制字进行之后的操作KVM_CREATE_VM
指令将创建一个虚拟机并返回该虚拟机对应的fd
。然后再根据返回的fd
对该虚拟机进行控制KVM_CREATE_VCPU
指令将创建一个 vCPU,并返回该 vCPU 对应的fd
KVM_RUN
,运行 vCPU,启动虚拟机需要注意的是,通过fork()
调用创建的子进程将继承父进程的文件描述符fd
,从而实现多进程访问。而在 KVM API 的内部实现中,并没有针对这种情况进行保护。因此api.txt
文档也有提示:VM 指令需要在创建该 VM 的进程中调用,vCPU 指令也需要在创建 vCPU 的线程中调用。
上述流程的伪代码示例如下所示:
open("/dev/kvm")ioctl(KVM_CREATE_VM) // 创建 VMioctl(KVM_CREATE_VCPU) // 为 VM 创建 vCPUfor (;;) { ioctl(KVM_RUN) // 运行 vCPU,启动 VM switch (exit_reason) { // 捕捉 VM-EXIT 原因进行处理 case KVM_EXIT_IO: /* ... */ case KVM_EXIT_HLT: /* ... */ }}
System ioctls 用于控制 KVM 全局的运行环境及参数设置,例如创建虚拟机、检查扩展支持。
通过kvm_main.c
中的kvm_dev_ioctl()
进行处理,与架构相关的指令(例如 x86)则通过x86.c
中的kvm_arch_dev_ioctl()
进行处理。
主要指令字如下所示:
指令字 | 功能说明 | 返回值 |
---|---|---|
KVM_GET_API_VERSION | 查询当前 KVM API 版本 | 当前版本为 12 |
KVM_CREATE_VM | 创建 KVM 虚拟机 | 返回创建的 KVM 虚拟机 fd |
KVM_GET_MSR_INDEX_LIST | 获得 MSR 索引列表 | 返回 kvm_msr_list 类型的链表 msr_list |
KVM_CHECK_EXTENSION | 检查扩展支持情况 | 返回 0 则不支持,非 0 则支持 |
KVM_GET_VCPU_MMAP_SIZE | 返回ioctl(KVM_RUN) 与用户空间共享内存区域大小 | mmap 区域大小,单位 bytes |
其中最重要的是KVM_CREATE_VM
。通过该指令字,KVM 将返回一个文件描述符fd
,指向内核空间中新创建的 KVM 虚拟机。
VM ioctl 用于对虚拟机进行控制,例如内存、vCPU、中断、时钟。
通过kvm_main.c
中的kvm_vm_ioctl()
进行处理,与架构相关的指令(例如 x86)则通过x86.c
中的kvm_arch_vm_ioctl()
进行处理。
主要指令字如下所示:
指令字 | 功能说明 | 返回值 |
---|---|---|
KVM_CREATE_VCPU | 为已经创建好的 VM 添加 vCPU | 成功则返回 vCPU fd,失败 -1 |
KVM_SET_MEMORY_REGION | 添加或修改 VM 的内存 | 成功 0,失败 -1 |
KVM_SET_USER_MEMORY_REGION | api.txt 中推荐替代KVM_SET_MEMORY_REGION 的新 API | 成功 0,失败为负 |
KVM_GET_DIRTY_LOG | 返回上次调用后给定 memory slot 的脏页位图 | 成功 0,失败 -1 |
KVM_SET_MEMORY_ALIAS | 定义kvm_memory_alias | 成功 0,失败 -1 |
KVM_CREATE_IRQCHIP | 创建一个虚拟的 APIC,并且之后创建的 vCPU 都将连接到该 APIC | 成功 0,失败 -1 |
KVM_IRQ_LINE | 对给定的虚拟 APIC 触发中断信号 | 成功 0,失败 -1 |
KVM_GET_IRQCHIP | 读取 APIC 的中断标志信息 | 成功 0,失败 -1 |
KVM_SET_IRQCHIP | 设置 APIC 的中断标志信息 | 成功 0,失败 -1 |
KVM_GET_CLOCK | 读取当前 VM kvmclock 中的 timestamp | 成功 0,失败 -1 |
KVM_SET_CLOCK | 设置当前 VM kvmclock 中的 timestamp | 成功 0,失败 -1 |
VM ioctl 需要借助通过KVM_CREATE_VM
返回的 VM fd 进行操作,例如KVM_CREATE_VCPU
:
static long kvm_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg){ struct kvm *kvm = filp->private_data; // VM 对应的 kvm 结构体 void __user *argp = (void __user *)arg; // ioctl 入参 int r; if (kvm->mm != current->mm) return -EIO; switch (ioctl) { case KVM_CREATE_VCPU: r = kvm_vm_ioctl_create_vcpu(kvm, arg); // 需要借助 kvm 创建 vCPU if (r < 0) goto out; break; /* ... */ }out: return r;}
vCPU ioctl 用于对具体的 vCPU 进行配置,例如执行 vCPU 指令,设置寄存器、中断等。
通过kvm_main.c
中的kvm_vcpu_ioctl()
进行处理,与架构相关的指令(例如 x86)则通过x86.c
中的kvm_arch_vcpu_ioctl()
进行处理。
主要指令字如下所示:
指令字 | 功能说明 | 返回值 |
---|---|---|
KVM_RUN | 运行 vCPU | 成功 0,失败 -1 |
KVM_GET_REGS | 获取通用寄存器信息 | 成功 0,失败 -1 |
KVM_SET_REGS | 设置通用寄存器信息 | 成功 0,失败 -1 |
KVM_GET_SREGS | 获取特殊寄存器信息 | 成功 0,失败 -1 |
KVM_SET_SREGS | 设置特殊寄存器信息 | 成功 0,失败 -1 |
KVM_TRANSLATE | 将 GVA 翻译为 GPA | 成功 0,失败 -1 |
KVM_INTERRUPT | 通过插入一个中断向量,在 vCPU 上产生中断(当 APIC 无效时) | 成功 0,失败 -1 |
KVM_DEBUG_GUEST | 开启 Guest OS 的调试模式 | 成功 0,失败 -1 |
KVM_GET_MSRS | 获取 MSR 寄存器信息 | 成功 0,失败 -1 |
KVM_SET_MSRS | 设置 MSR 寄存器信息 | 成功 0,失败 -1 |
KVM_SET_CPUID | 设置 vCPU 的 CPUID 信息 | 成功 0,失败 -1 |
KVM_SET_SIGNAL_MASK | 设置 vCPU 的中断信号屏蔽 | 成功 0,失败 -1 |
KVM_GET_FPU | 获取浮点寄存器信息 | 成功 0,失败 -1 |
KVM_SET_FPU | 设置浮点寄存器信息 | 成功 0,失败 -1 |
其中最重要的指令是KVM_RUN
。在通过KVM_CREATE_CPU
为虚拟机创建 vCPU,并取得 vCPU 对应的 fd 后,就可以调用KVM_RUN
启动虚拟机:
static long kvm_vcpu_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg){ struct kvm_vcpu *vcpu = filp->private_data; void __user *argp = (void __user *)arg; int r; /* ... */ switch (ioctl) { case KVM_RUN: // 运行 vCPU r = -EINVAL; if (arg) goto out; r = kvm_arch_vcpu_ioctl_run(vcpu, vcpu->run); // 实际的执行函数 break; /* ... */ }out: kfree(fpu); kfree(kvm_sregs); return r;}
可以看到调用kvm_arch_vcpu_ioctl_run()
时传递了两个参数:vcpu
即为当前的 vCPU,而vcpu->run
则指向一个kvm_run
结构体(省略部分字段):
/* for KVM_RUN, returned by mmap(vcpu_fd, offset=0) */struct kvm_run { /* in */ __u8 request_interrupt_window; __u8 padding1[7]; /* out */ __u32 exit_reason; __u8 ready_for_interrupt_injection; __u8 if_flag; __u8 padding2[2]; /* in (pre_kvm_run), out (post_kvm_run) */ __u64 cr8; __u64 apic_base; union { /* KVM_EXIT_UNKNOWN */ struct { __u64 hardware_exit_reason; } hw; /* KVM_EXIT_FAIL_ENTRY */ struct { __u64 hardware_entry_failure_reason; } fail_entry; /* KVM_EXIT_EXCEPTION */ struct { __u32 exception; __u32 error_code; } ex; /* KVM_EXIT_IO */ struct {#define KVM_EXIT_IO_IN 0#define KVM_EXIT_IO_OUT 1 __u8 direction; __u8 size; /* bytes */ __u16 port; __u32 count; __u64 data_offset; /* relative to kvm_run start */ } io; struct { struct kvm_debug_exit_arch arch; } debug; /* KVM_EXIT_MMIO */ struct { __u64 phys_addr; __u8 data[8]; __u32 len; __u8 is_write; } mmio; /* ... */ };};
kvm_run
定义在include/linux/kvm.h
中,通过读取该结构体可以了解 KVM 内部的运行状态,可以类比为计算机芯片中的寄存器组。
#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>#include <sys/ioctl.h>#include <fcntl.h>#include <unistd.h>#include <linux/kvm.h>#define KVM_FILE "/dev/kvm"int main(){ int dev; int ret; dev = open(KVM_FILE, O_RDWR|O_NDELAY); ret = ioctl(dev, KVM_GET_API_VERSION, 0); printf("----KVM API version is %d----\n", ret); ret = ioctl(dev, KVM_CHECK_EXTENSION, KVM_CAP_NR_VCPUS); printf("----KVM supports MAX_VCPUS per guest(VM) is %d----\n", ret); ret = ioctl(dev, KVM_CHECK_EXTENSION, KVM_CAP_NR_MEMSLOTS); printf("----KVM supports MEMORY_SLOTS per guset(VM) is %d----\n", ret); ret = ioctl(dev, KVM_CHECK_EXTENSION, KVM_CAP_IOMMU); if(ret != 0) printf("----KVM supports IOMMU (i.e. Intel VT-d or AMD IOMMU).----\n"); else printf("----KVM doesn't support IOMMU (i.e. Intel VT-d or AMD IOMMU).----\n"); return 0;}
编译运行:
> vim kvm-api-test.c> gcc kvm-api-test.c -o kvm-api-test> ./kvm-api-test----KVM API version is 12--------KVM supports MAX_VCPUS per guest(VM) is 160--------KVM supports MEMORY_SLOTS per guset(VM) is 32--------KVM doesn't support IOMMU (i.e. Intel VT-d or AMD IOMMU).----
]]>
QEMU 源码vl.c
中的main()
函数调用流程分析,基于1.2.0
版本
KVM 虚拟化由用户空间的 QEMU 和内核中的 KVM 模块配合完成,QEMU 通过ioctl()
向/dev/kvm
发送指令字,对虚拟机进行操作。配合流程如下:
QEMU 的入口main()
函数位于vl.c
中,重点关注以下几点:
在以上几点做了详细的调用流程展开,其他函数不再深入,部分函数省略,整个main()
函数的处理逻辑如下图所示:
int main() ├─ atexit(qemu_run_exit_notifiers) // 注册 QEMU 的退出处理函数 ├─ module_call_init(MODULE_INIT_QOM) // 初始化 QOM ├─ runstate_init() ├─ init_clocks() // 初始化时钟源 ├─ module_call_init(MODULE_INIT_MACHINE) ├─ switch(popt->index) case QEMU_OPTION_XXX // 解析 QEMU 参数 ├─ socket_init() ├─ os_daemonize() ├─ configure_accelerator() // 启用 KVM 加速支持 | └─ kvm_init() // 【1】创建 KVM 虚拟机并获取对应的 fd | ├─ kvm_ioctl(KVM_GET_API_VERSION) // 检查 KVM API 版本 | ├─ kvm_ioctl(KVM_CREATE_VM) // 创建虚拟机,并获取 vmfd | ├─ kvm_arch_init() | └─ memory_listener_register(&kvm_memory_listener) // 注册 kvm_memory_listener | ├─ qemu_init_cpu_loop() // 初始化 vCPU 线程竞争的锁 ├─ qemu_init_main_loop() | └─ main_loop_init() | ├─ qemu_spice_init() // 初始化 SPICE ├─ cpu_exec_init_all() // 【2】初始化虚拟机的地址空间,主要是 QEMU 侧的内存布局 | ├─ memory_map_init() // 初始化 MemoryRegion 及其对应的 FlatView | | ├─ memory_region_init() // 初始化 system_memory/io 这两个全局 MemoryRegion | | ├─ set_system_memory_map() // address_space_memory->root = system_memory | | | └─ memory_region_update_topology() // 为 MemoryRegion 生成 FlatView | | | └─ address_space_update_topology() // as->current_map = new_view | | | ├─ generate_memory_topology() // 将 MemoryRegion 的拓扑结构渲染为 FlatRange 数组 | | | | ├─ flatview_init(&view) | | | | ├─ render_memory_region(&view, mr, ...) // 根据 mr 生成 view | | | | └─ flatview_simplify(&view) // 合并相邻的 FlatRange | | | | | | | ├─ address_space_update_topology_pass() | | | | └─ kvm_region_add() // region_add 对应的回调实现 | | | | └─ kvm_set_phys_mem() // 根据传入的 section 填充 KVMSlot | | | | └─ kvm_set_user_memory_region() // 将 QEMU 侧的内存布局注册到 KVM 中 | | | | └─ kvm_ioctl(KVM_SET_USER_MEMORY_REGION) | | | | | | | └─ address_space_update_ioeventfds() | | | | | └─ memory_listener_register() // 注册 core_memory_listener、io_memory_listener | | └─ listener_add_address_space() | | | └─ io_mem_init() // 初始化 I/O MemoryRegion | └─ memory_region_init_io() // ram/rom/unassigned/notdirty/subpage-ram/watch | └─ memory_region_init() | ├─ bdrv_init_with_whitelist() ├─ blk_mig_init() | └─ register_savevm_live("block", &savevm_block_handlers, ...) // 注册块设备热迁移的处理函数 | ├─ register_savevm_live("ram", &savevm_ram_handlers, ...) // 注册内存热迁移的处理函数 ├─ select_vgahw(vga_model) // 选择 VGA 显卡设备,std/cirrus/vmware/xenfb/qxl/none ├─ select_watchdog(watchdog) ├─ qdev_machine_init() ├─ machine->init() // QEMU 1.2.0 默认的 QEMUMachine 为 pc_machine_v1_2 | └─ pc_init_pci() | └─ pc_init1() | ├─ pc_cpus_init(cpu_model) // 【3】CPU 初始化,根据 smp_cpus 参数创建对应数量的 vCPU 子线程 | | └─ pc_new_cpu(cpu_model) | | └─ cpu_x86_init(cpu_model) | | └─ x86_cpu_realize() | | └─ qemu_init_vcpu() | | └─ qemu_kvm_start_vcpu() | | └─ qemu_thread_create() // 顺序创建 vCPU 子线程,失败会阻塞 | | └─ qemu_kvm_cpu_thread_fn() | | ├─ kvm_init_vcpu() | | | ├─ kvm_ioctl(KVM_CREATE_VCPU) // 获取 vCPU 对应的 fd | | | └─ kvm_arch_init_vcpu() | | | | | ├─ qemu_kvm_init_cpu_signals() | | ├─ kvm_cpu_exec() | | | └─ kvm_vcpu_ioctl(KVM_RUN) // 运行 vCPU | | | └─ kvm_arch_vcpu_ioctl_run() // 进入内核,由 KVM 处理 | | | └─ __vcpu_run() | | | └─ vcpu_enter_guest() | | | └─ kvm_mmu_reload() | | | └─ kvm_mmu_load() // spin_lock(&vcpu->kvm->mmu_lock) | | | | | └─ qemu_kvm_wait_io_event() | | | ├─ kvmclock_create() | ├─ pc_memory_init() // 【4】内存初始化,从 QEMU 进程的地址空间中进行实际的内存分配 | | ├─ memory_region_init_ram() // 创建 pc.ram, pc.rom 并分配内存 | | | ├─ memory_region_init() | | | └─ qemu_ram_alloc() | | | └─ qemu_ram_alloc_from_ptr() | | | | | ├─ vmstate_register_ram_global() // 将 MR 的 name 写入 RAMBlock 的 idstr | | | └─ vmstate_register_ram() | | | └─ qemu_ram_set_idstr() | | | | | ├─ memory_region_init_alias() // 初始化 ram_below_4g, ram_above_4g | | └─ memory_region_add_subregion() // 在 system_memory 中添加 subregions | | └─ memory_region_add_subregion_common() | | └─ memory_region_update_topology() // 为 MemoryRegion 生成 FlatView | | └─ address_space_update_topology() // as->current_map = new_view | | ├─ generate_memory_topology() // 将 MemoryRegion 的拓扑结构渲染为 FlatRange 数组 | | | ├─ flatview_init(&view) | | | ├─ render_memory_region(&view, mr, ...) // 根据 mr 生成 view | | | └─ flatview_simplify(&view) // 合并相邻的 FlatRange | | | | | ├─ address_space_update_topology_pass() | | | └─ kvm_region_add() // region_add 对应的回调实现 | | | └─ kvm_set_phys_mem() // 根据传入的 section 填充 KVMSlot | | | └─ kvm_set_user_memory_region() // 将 QEMU 侧的内存布局注册到 KVM 中 | | | └─ kvm_ioctl(KVM_SET_USER_MEMORY_REGION) | | | | | └─ address_space_update_ioeventfds() | | | ├─ i440fx_init() | ├─ ioapic_init(gsi_state) | ├─ pc_vga_init() | ├─ pc_basic_device_init() | ├─ pci_piix3_ide_init() | ├─ audio_init() | ├─ pc_cmos_init() | └─ pc_pci_device_init() | ├─ cpu_synchronize_all_post_init() ├─ set_numa_modes() // 设置 NUMA ├─ vnc_display_init() // 初始化 VNC ├─ qemu_spice_display_init() ├─ qemu_run_machine_init_done_notifiers() ├─ os_setup_post() ├─ resume_all_vcpus() ├─ main_loop() // 【5】主线程开启循环,监听事件 | └─ main_loop_wait() | └─ os_host_main_loop_wait() | └─ select() | ├─ bdrv_close_all() ├─ pause_all_vcpus() ├─ net_cleanup() └─ res_free()
大致流程如下图所示(图源自网络),对应 VMX 模式下 root 和 non-root 模式的概念:
ioctl(KVM_RUN)
进入内核运行 vCPU,并处理 I/O 请求VM-Entry
进入非根模式,运行 Guest OS,并处理VM-Exit
。如果能在内核处理,则处理后再次通过VM-Entry
进入 Guest OS;如果不能处理(例如 I/O 请求),则退出到用户空间,由 QEMU 进行处理通过virsh
启动一台 32 核 CPU 的虚拟机,使用pstack
打印堆栈验证一下:
> pstack $(pidof qemu-system-x86_64)...(省略重复的堆栈)Thread 6 (Thread 0x7fdcd4dfa700 (LWP 37340)):#0 0x00007fdd46002307 in ioctl () from /lib64/libc.so.6#1 0x00000000005e4bcb in kvm_vcpu_ioctl ()#2 0x00000000005e57d8 in kvm_cpu_exec ()#3 0x00000000005a2601 in qemu_kvm_cpu_thread_fn ()#4 0x00007fdd462d8893 in start_thread () from /lib64/libpthread.so.0#5 0x00007fdd46009bfd in clone () from /lib64/libc.so.6Thread 5 (Thread 0x7fdcbbfff700 (LWP 37341)):#0 0x00007fdd46002307 in ioctl () from /lib64/libc.so.6#1 0x00000000005e4bcb in kvm_vcpu_ioctl ()#2 0x00000000005e57d8 in kvm_cpu_exec ()#3 0x00000000005a2601 in qemu_kvm_cpu_thread_fn ()#4 0x00007fdd462d8893 in start_thread () from /lib64/libpthread.so.0#5 0x00007fdd46009bfd in clone () from /lib64/libc.so.6Thread 4 (Thread 0x7fdcbb5fe700 (LWP 37342)):#0 0x00007fdd46002307 in ioctl () from /lib64/libc.so.6#1 0x00000000005e4bcb in kvm_vcpu_ioctl ()#2 0x00000000005e57d8 in kvm_cpu_exec ()#3 0x00000000005a2601 in qemu_kvm_cpu_thread_fn ()#4 0x00007fdd462d8893 in start_thread () from /lib64/libpthread.so.0#5 0x00007fdd46009bfd in clone () from /lib64/libc.so.6Thread 3 (Thread 0x7fdcbabfd700 (LWP 37343)):#0 0x00007fdd46002307 in ioctl () from /lib64/libc.so.6#1 0x00000000005e4bcb in kvm_vcpu_ioctl ()#2 0x00000000005e57d8 in kvm_cpu_exec ()#3 0x00000000005a2601 in qemu_kvm_cpu_thread_fn ()#4 0x00007fdd462d8893 in start_thread () from /lib64/libpthread.so.0#5 0x00007fdd46009bfd in clone () from /lib64/libc.so.6Thread 2 (Thread 0x7fc4a73fd700 (LWP 37451)):#0 0x00007fdd462dc115 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0#1 0x0000000000568781 in qemu_cond_wait ()#2 0x00000000005952c3 in vnc_worker_thread_loop ()#3 0x0000000000595778 in vnc_worker_thread ()#4 0x00007fdd462d8893 in start_thread () from /lib64/libpthread.so.0#5 0x00007fdd46009bfd in clone () from /lib64/libc.so.6Thread 1 (Thread 0x7fdd47cce700 (LWP 37247)):#0 0x00007fdd460029f3 in select () from /lib64/libc.so.6#1 0x000000000053b325 in main_loop_wait ()#2 0x0000000000536ef4 in main ()
]]>2019-09-22 更新
公司 | 日期 | 时间 | 平台 |
---|---|---|---|
招银网络科技 | 9.24 (周二) | 15:00-? | 待定 |
电信云计算 | 9.25/26 (周三) | 面试 | 待定 |
校招官网 | 微信 | 时间 |
---|---|---|
腾讯 | 腾讯招聘 | 9.15 简历截止 |
阿里 | 阿里技术栈 | 9.12 简历截止,Q&A |
字节跳动 | 字节跳动招聘 | 9.22 笔试,Q&A |
华为 | 华为招聘 | 已投递 |
百度 | 百度招聘 | 9.17 19:00 笔试,Q&A |
电信云计算 | 电信云计算校招内推 | 9.25/26 广州面试,牛客内推 |
网易游戏 | 网易游戏综合招聘 | 9.19 网申截止,Q&A |
网易游戏-互娱 | 网易游戏互娱校园招聘 | 9.19 内推截止 |
网易游戏-雷火 | 网易游戏雷火伏羲招聘 | 9.12 网申截止,Q&A |
滴滴 | 滴滴出行校园招聘 | 9.15 网申截止,Q&A |
美团点评 | 美团点评招聘 | 9.18 15:00 笔试,笔试攻略 |
深信服 | 深信服招聘 | 9.18 笔试,岗位描述 |
微众银行 | WeBank招聘 | 9.18 网申截止,9.19 笔试 |
招银网络科技 | 招银网络科技 | 9.23 网申截止 |
校招官网 | 微信 | 时间 |
---|---|---|
网易互联网 | 网易招聘 | |
网易有道 | 有道招聘 | 9.17 网申截止, |
哔哩哔哩 | 哔哩哔哩招聘 | |
DJI大疆 | DJI大疆招聘 | |
小米 | 小米招聘 | |
顺丰科技 | 顺丰科技招聘 | |
360 | 360招聘 |
常用排序算法复杂度及稳定性:
关于时间复杂度:
O(n^2)
:各类简单排序,包括直接插入、直接选择、冒泡排序O(nlogn)
:快速排序、堆排序、归并排序关于稳定性:
TCP 三次握手、四次挥手:
参见:
src_port
和目标端口dst_port
(src_ip, src_port, dst_ip, dst_port)
来表示是同一个连接Sequence Number
:包的序号seq
,用来解决网络包乱序 (reordering) 的问题Acknowledgement Number
:就是ACK
,表示确认收到了包,用来解决丢包的问题Window
:又叫Advertised-Window
,也就是著名的滑动窗口 (Sliding Window),用于解决流量控制问题对于建立连接的三次挥手:
Sequence Number
的值。通信的双方要互相通知对方自己的初始Sequence Number
,缩写为ISN
即Initial Sequence Number
,所以叫SYN
,全称Synchronize Sequence Numbers
。这个号要作为以后数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输问题而乱序 (TCP 会用这个序号来拼接数据)对于断开连接的四次挥手:
FIN
和ACK
。当有一方要关闭连接时,会发送指令告知对方,我要关闭连接了,这时对方会返回一个ACK
。此时一个方向的连接关闭,但是另一个方向仍然可以继续传输数据,等到发送完所有的数据后,会发送一个FIN
来关闭此方向上的连接,最后由接收方发送ACK
确认关闭连接FIN
报文的一方只能回复一个ACK
,是无法马上返回给对方一个FIN
报文段的,因为是否结束数据传输由上层的应用层控制为什么关闭时需要四次挥手?
Server
端收到Client
端的SYN
连接请求报文后,可以直接发送SYN+ACK
报文,其中ACK
报文是用来应答的,SYN
报文是用来同步的Server
端收到FIN
报文时,很可能并不会立即关闭Socket
ACK
报文,告诉Client
端,你发的FIN
报文我收到了,等到我Server
端所有的报文都发送完了,才能发送FIN
报文Server
端的ACK
和FIN
不能一起发送,故需要四次挥手为什么不能用两次握手进行连接?
B
,避免产生错误A
发出的第一个连接请求报文段并没有丢失,而是在某些网络节点长时间滞留了,以致延误到连接释放后的某个时间才到达B
。本来这是一个早已失效的报文段。但B
受到此失效的连接请求报文段后,就误以为是A
又发出一次新的连接请求,于是就向A
发出确认报文段,同意建立连接。假定不采用第三次报文握手,那么只要B
发出确认,新的连接就建立了A
并没有发出建立连接的请求,因此不会理睬B
的确认,也不会向B
发送数据,但B
却以为新的运输连接已经建立了,并一直等待A
发来的数据。B
的许多资源就这样白白浪费了A
不会向B
的确认发出确认,B
由于收不到确认,就知道A
并没有要求建立连接,于是B
就不会再建立连接为什么 TIME_WAIT 状态需要 2MSL?
Client
发送的最后一个ACK
报文段能够到达Server
:Server
如果没有收到ACK
,将不断重复发送FIN
,所以Client
不能立即关闭,它必须确认Server
接收到了该ACK
A
在发送完最后一个ACK
报文段后,再经过2MSL
时间,就可以使本连接持续时间内所产生的所有报文段都从网络中消失,这样就可以使下一个新的连接中不会出现旧连接中的请求报文段I/O 多路复用:select、poll、epoll:
Linux IPC 通信方式:
package mainimport ( "fmt")type Node struct { value int left *Node right *Node}func main() { preOrder := []int{1, 2, 4, 7, 3, 5, 6, 8} inOrder := []int{4, 7, 2, 1, 5, 3, 8, 6} tree := constructBTree(preOrder, inOrder) preCatTree(tree) inCatTree(tree)}// 重建二叉树func constructBTree(preOrder, inOrder []int) *Node { l := len(preOrder) if l == 0 { return nil } root := &Node{ value: preOrder[0], } if l == 1 { return root } leftLen, rightLen := 0, 0 for _, v := range inOrder { if v == root.value { break } leftLen++ } rightLen = l - leftLen - 1 if leftLen > 0 { fmt.Println("左子树", preOrder[1:leftLen+1], inOrder[0:leftLen]) root.left = constructBTree(preOrder[1:leftLen+1], inOrder[0:leftLen]) } if rightLen > 0 { fmt.Println("右子树", preOrder[leftLen+1:], inOrder[leftLen+1:]) root.right = constructBTree(preOrder[leftLen+1:], inOrder[leftLen+1:]) } return root}func preCatTree(t *Node) { fmt.Println(t.value) if t.left != nil { preCatTree(t.left) } if t.right != nil { preCatTree(t.right) }}func inCatTree(t *Node) { if t.left != nil { inCatTree(t.left) } fmt.Println(t.value) if t.right != nil { inCatTree(t.right) }}
1.落单的数:
package mainimport ( "fmt")func main() { n := 0 ans := 0 cur := 0 fmt.Scan(&n) for i := 0; i < n; i++ { fmt.Scan(&cur) ans ^= cur } fmt.Println(ans)}------71 2 2 1 3 4 34
2.同构字符串:
package mainimport ( "fmt" "strings")func main() { var s, s1, s2 string fmt.Scan(&s) split := strings.Split(s, ";") s1, s2 = split[0], split[1] len1 := len(s1) len2 := len(s2) if len1 != len2 { fmt.Println("False") return } else if len1 == 1 { fmt.Println("True") return } record := make(map[byte]byte) for i := 0; i < len1; i++ { if record[s1[i]] == s2[i] { continue } else if record[s1[i]] == 0 { record[s1[i]] = s2[i] } else { fmt.Println("False") return } } fmt.Println("True") return}------ababa;ststsTrue
3.最大连续子序列的和:
package mainimport ( "fmt")func main() { array := make([]int, 0) var a int var ch byte fmt.Scan(&ch) for { n, err := fmt.Scanf("%d", &a) if n == 0 { break } if err != nil { fmt.Println(err) break } array = append(array, a) } fmt.Println(search(array))}func search(a []int) int { l := len(a) curSum := 0 maxSum := 0 for i := 0; i < l; i++ { curSum = 0 for j := i; j < l; j++ { curSum += a[j] if curSum > maxSum { maxSum = curSum } } } return maxSum}------[2, 4, -2, 5, -6]9
1.距离最近的厕所:
package mainimport ( "fmt")var ( n int s string wcDis [1000000]int)const ( maxDistance int = 1000001)// 离最近厕所的距离,O代表有厕所,保证至少有一个厕所//// [Input]// 9// XXOXOOXXX//// [Output]// 2 1 0 1 0 0 1 2 3func main() { fmt.Scan(&n) fmt.Scan(&s) for i := 0; i < 1000000; i++ { wcDis[i] = maxDistance } for i := 0; i < n; i++ { findWC(i) }}func findWC(cur int) { if s[cur] == 'O' { wcDis[cur] = 0 fmt.Printf("%d ", 0) return } curDis := min(searchLeft(cur), searchRight(cur)) wcDis[cur] = curDis fmt.Printf("%d ", curDis)}func min(a, b int) int { if a < b { return a } return b}func searchLeft(cur int) int { if cur == 0 { return maxDistance } if s[cur-1] == 'O' { return 1 } return wcDis[cur-1] + 1}func searchRight(cur int) int { disRight := 0 for i := cur + 1; i < n; i++ { disRight++ if s[i] == 'O' { return disRight } } return maxDistance}------9XXOXOOXXX2 1 0 1 0 0 1 2 3
2.考试跳过的题目:
package mainimport ( "fmt" "sort")// 第一行: 测试用例个数// 第二行: n, m, 分别代表题目总数、时间总数// 输出: 至少要跳过前面的几道题//// [Input]// 2// 5 5// 1 2 3 4 5// 4 4// 4 3 2 1//// [Output]// 0 0 1 2 4// 0 1 2 2func main() { var total int fmt.Scan(&total) for i := 0; i < total; i++ { solve() }}func solve() { var n, m int fmt.Scan(&n, &m) questions := make([]int, n) for i := 0; i < n; i++ { fmt.Scan(&questions[i]) } var curSum = 0 for i := 0; i < n-1; i++ { curSum += questions[i] printAns(questions, i, curSum, n, m) fmt.Print(" ") } curSum += questions[n-1] printAns(questions, n-1, curSum, n, m) fmt.Println()}func printAns(questions []int, curPos, curSum, n, m int) { if curSum <= m { fmt.Print(0) return } prev := make([]int, curPos) copy(prev, questions[0:curPos]) sort.Slice(prev, func(i, j int) bool { return prev[i] > prev[j] }) var curGiveup = 0 for i := 0; i < curPos; i++ { curSum -= prev[i] curGiveup++ if curSum <= m { fmt.Print(curGiveup) return } }}------25 51 2 3 4 54 44 3 2 10 0 1 2 40 1 2 2
]]>Sync once, enjoy everywhere!
To be updated…
]]>
Farewell, Source Insight!
更新中…
]]>
- GNU Global
- VS Code + GNU Global - 打造 Trace Linux Kernel 環境 | Jayce’s Shared Memory
- Visual Studio Code + GNU Global打造代码编辑神器 | 腾讯云+社区
- Ubuntu 安裝 GNU Global(gtags) 阅读Linux内核源码 | CSDN
- Source Insight 4.0
- Source Insight 使用方法逆天整理 | cnblogs
- Source Insight 4.0 破解和使用 | CSDN
- Source Insight 常用设置和快捷键大全 | cnblogs
- 如何使用 Visual Studio Code 阅读 Android 源码 | 程序员虾饺
- bootlin/elixir - The Elixir Cross Referencer | Github
- 开源免费的源码阅读神器 Sourcetrail | 知乎
- Sourcetrail - 开源跨平台的源码浏览器
保护眼睛,猿猿有责
在.bashrc
或.zshrc
中添加:
export LS_COLORS=${LS_COLORS}:'di=01;37;44'
之后source ~/.bashrc
,即可在当前终端生效。
]]>
摘自 通过 qemu-nbd 方式挂载 qcow2 镜像格式 | CSDN
更新中…
]]>
QEMU-KVM 的内存虚拟化是由 QEMU 和 KVM 二者共同实现的,其本质上是一个将 Guest 虚拟内存转换成 Host 物理内存的过程。概括来看,主要有以下几点:
64 位 CPU 上支持 48 位的虚拟地址寻址空间,和 52 位的物理地址寻址空间。Linux 采用 4 级页表机制将虚拟地址(VA)转换成物理地址(PA),先从页表的基地址寄存器CR3
中读取页表的起始地址,然后加上页号得到对应的页表项,从中取出页的物理地址,加上偏移量就得到 PA。
QEMU 利用mmap
系统调用,在进程的虚拟地址空间中申请连续大小的空间,作为 Guest 的物理内存。
QEMU 作为 Host 上的一个进程运行,Guest 的每个 vCPU 都是 QEMU 进程的一个子线程。而 Guest 实际使用的仍是 Host 上的物理内存,因此对于 Guest 而言,在进行内存寻址时需要完成以下地址转换过程:
Guest虚拟内存地址(GVA) | Guest线性地址 | Guest物理地址(GPA) | Guest ------------------ | Host Host虚拟地址(HVA) | Host线性地址 | Host物理地址(HPA)
其中,虚拟地址到线性地址的转换过程可以省略,因此 KVM 的内存寻址主要涉及以下四种地址的转换:
Guest虚拟内存地址(GVA) | Guest物理地址(GPA) | Guest ------------------ | Host Host虚拟地址(HVA) | Host物理地址(HPA)
其中,GVA->GPA
的映射由 Guest OS 维护,HVA->HPA
的映射由 Host OS 维护,因此需要一种机制,来维护GPA->HVA
之间的映射关系。
常用的实现有SPT(Shadow Page Table)
和EPT/NPT
,前者通过软件维护影子页表,后者通过硬件特性实现二级映射。
KVM 通过维护记录GVA->HPA
的影子页表 SPT,减少了地址转换带来的开销,可以直接将 GVA 转换为 HPA。
在软件虚拟化的内存转换中,GVA 到 GPA 的转换通过查询 CR3 寄存器来完成,CR3 中保存了 Guest 的页表基地址,然后载入 MMU 中进行地址转换。
在加入了 SPT 技术后,当 Guest 访问 CR3 时,KVM 会捕获到这个操作EXIT_REASON_CR_ACCESS
,之后 KVM 会载入特殊的 CR3 和影子页表,欺骗 Guest 这就是真实的 CR3。之后就和传统的访问内存方式一致,当需要访问物理内存的时候,只会经过一层影子页表的转换。
影子页表由 KVM 维护,实际上就是一个 Guest 页表到 Host 页表的映射。KVM 会将 Guest 的页表设置为只读,当 Guest OS 对页表进行修改时就会触发 Page Fault,VM-EXIT 到 KVM,之后 KVM 会对 GVA 对应的页表项进行访问权限检查,结合错误码进行判断:
GVA-HPA
表项当 Guest 切换进程时,会把带切换进程的页表基址载入到 Guest 的 CR3 中,导致 VM-EXIT 到 KVM 中。KVM 再通过哈希表找到对应的 SPT,然后加载到机器的 CR3 中。
影子页表的引入,减少了GVA->HPA
的转换开销,但是缺点在于需要为 Guest 的每个进程都维护一个影子页表,这将带来很大的内存开销。同时影子页表的建立是很耗时的,如果 Guest 的进程过多,将导致影子页表频繁切换。因此 Intel 和 AMD 在此基础上提供了基于硬件的虚拟化技术。
Intel EPT 技术引入了 EPT(Extended Page Table)和 EPTP(EPT base pointer)的概念。EPT 中维护着 GPA 到 HPA 的映射,而 EPTP 负责指向 EPT。
在 Guest OS 运行时,Guest 对应的 EPT 地址被加载到 EPTP,而 Guest OS 当前运行的进程页表基址被加载到 CR3。于是在进行地址转换时,首先通过 CR3 指向的页表实现 GVA 到 GPA 的转换,再通过 EPTP 指向的 EPT 完成 GPA 到 HPA 的转换。当发生 EPT Page Fault 时,需要 VM-EXIT 到 KVM,更新 EPT。
内存虚拟化的目的就是让虚拟机能够无缝的访问内存。有了 Intel EPT 的支持后,CPU 在 VMX non-root 状态时进行内存访问会再做一次 EPT 转换。在这个过程中,QEMU 会负责以下内容:
GPA->HVA
QEMU 和 KVM 之间是通过 KVM 提供的ioctl()
接口进行交互的。在内核的kvm_vm_ioctl()
中,设置虚拟机内存的系统调用为KVM_SET_USER_MEMORY_REGION
:
static long kvm_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg){ /* ... */ case KVM_SET_USER_MEMORY_REGION: { // 在 KVM 中注册用户空间传入的内存信息 struct kvm_userspace_memory_region kvm_userspace_mem; r = -EFAULT; // 将传入的数据结构复制到内核空间 if (copy_from_user(&kvm_userspace_mem, argp, sizeof kvm_userspace_mem)) goto out; // 实际进行处理的函数 r = kvm_vm_ioctl_set_memory_region(kvm, &kvm_userspace_mem, 1); if (r) goto out; break; } /* ... */}
可以看到这里需要传递的参数类型为kvm_userspace_memory_region
:
/* for KVM_SET_USER_MEMORY_REGION */struct kvm_userspace_memory_region { __u32 slot; // slot 编号 __u32 flags; // 标志位,例如是否追踪脏页、是否可用等 __u64 guest_phys_addr; // Guest 物理地址,即 GPA __u64 memory_size; // 内存大小,单位 bytes __u64 userspace_addr; // 从 QEMU 进程地址空间中分配内存的起始地址,即 HVA};
KVM_SET_USER_MEMORY_REGION
这个 ioctl 主要目的就是设置GPA->HVA
的映射关系,KVM 会继续调用kvm_vm_ioctl_set_memory_region()
,在内核空间维护并管理 Guest 的内存。
QEMU 用 AddressSpace 结构体表示 Guest 中 CPU/设备看到的内存,类似于物理机中地址空间的概念,但在这里表示的是 Guest 的一段地址空间,如内存地址空间address_space_memory
、I/O 地址空间address_space_io
,它在 QEMU 源码memory.c
中定义:
/* A system address space - I/O, memory, etc. */struct AddressSpace { MemoryRegion *root; // 根级 MemoryRegion FlatView current_map; // 对应的平面展开视图 FlatView int ioeventfd_nb; MemoryRegionIoeventfd *ioeventfds;};
每个 AddressSpace 一般包含一系列的 MemoryRegion:root
指针指向根级 MemoryRegion,而root
可能有自己的若干个 subregions,于是形成树状结构。这些 MemoryRegion 通过树连接起来,树的根即为 AddressSpace 的root
域。
另外,QEMU 中有两个全局的静态 AddressSpace,在memory.c
中定义:
static AddressSpace address_space_memory; // 内存地址空间static AddressSpace address_space_io; // I/O 地址空间
其root
域分别指向之后会提到的两个 MemoryRegion 类型变量:system_memory
、system_io
。
MemoryRegion 表示在 Guest Memory Layout 中的一段内存区域,它是联系 GPA 和 RAMBlocks(描述真实内存)之间的桥梁,在memory.h
中定义:
struct MemoryRegion { /* All fields are private - violators will be prosecuted */ const MemoryRegionOps *ops; // 回调函数集合 void *opaque; MemoryRegion *parent; // 父 MemoryRegion 指针 Int128 size; // 该区域内存的大小 target_phys_addr_t addr; // 在 Address Space 中的地址,即 HVA void (*destructor)(MemoryRegion *mr); ram_addr_t ram_addr; // MemoryRegion 的起始地址,即 GPA bool subpage; bool terminates; bool readable; bool ram; // 是否表示 RAM bool readonly; /* For RAM regions */ bool enabled; // 是否已经通知 KVM 使用这段内存 bool rom_device; bool warning_printed; /* For reservations */ MemoryRegion *alias; // 是否为 MemoryRegion alias target_phys_addr_t alias_offset; // 若为 alias,在原 MemoryRegion 中的 offset unsigned priority; bool may_overlap; QTAILQ_HEAD(subregions, MemoryRegion) subregions; // 子区域链表头 QTAILQ_ENTRY(MemoryRegion) subregions_link; // 子区域链表节点 QTAILQ_HEAD(coalesced_ranges, CoalescedMemoryRange) coalesced; const char *name; // MemoryRegion 的名字,调试时使用 uint8_t dirty_log_mask; // 表示哪一种 dirty map 被使用,共分三种 unsigned ioeventfd_nb; MemoryRegionIoeventfd *ioeventfds;};
在 QEMU 的exec.c
中也定义了两个静态的 MemoryRegion 指针变量:
static MemoryRegion *system_memory; // 内存 MemoryRegion,对应 address_space_memorystatic MemoryRegion *system_io; // I/O MemoryRegion,对应 address_space_io
与两个全局 AddressSpace 对应,即 AddressSpace 的root
域指向这两个 MemoryRegion。
MemoryRegion 有多种类型,可以表示一段 RAM、ROM、MMIO、alias。
若为 alias 则表示一个 MemoryRegion 的部分区域,例如 QEMU 会为pc.ram
这个表示 RAM 的 MemoryRegion 添加两个 alias:ram-below-4g
和ram-above-4g
,之后会看到具体的代码实例。
另外,MemoryRegion 也可以表示一个 container,这就表示它只是其他若干个 MemoryRegion 的容器
那么要如何创建不同类型的 MemoryRegion 呢?在 QEMU 中实际上是通过调用不同的初始化函数区分的。根据不同的初始化函数及其功能,可以将 MemoryRegion 划分为以下三种类型:
memory_region_init
初始化,没有自己的内存,用于管理 subregion,例如system_memory
:void memory_region_init(MemoryRegion *mr, const char *name, uint64_t size){ mr->ops = NULL; mr->parent = NULL; mr->size = int128_make64(size); if (size == UINT64_MAX) { mr->size = int128_2_64(); } mr->addr = 0; mr->subpage = false; mr->enabled = true; mr->terminates = false; // 非实体 MemoryRegion,搜索时会继续前往其 subregions mr->ram = false; // 根级 MemoryRegion 不分配内存 mr->readable = true; mr->readonly = false; mr->rom_device = false; mr->destructor = memory_region_destructor_none; mr->priority = 0; mr->may_overlap = false; mr->alias = NULL; QTAILQ_INIT(&mr->subregions); memset(&mr->subregions_link, 0, sizeof mr->subregions_link); QTAILQ_INIT(&mr->coalesced); mr->name = g_strdup(name); mr->dirty_log_mask = 0; mr->ioeventfd_nb = 0; mr->ioeventfds = NULL;}
可以看到mr->addr
被设置为 0,而mr->ram_addr
则并没有初始化。
memory_region_init_ram()
初始化,有自己的内存(从 QEMU 进程地址空间中分配),大小为size
,例如ram_memory
、pci_memory
:void *pc_memory_init(MemoryRegion *system_memory, const char *kernel_filename, const char *kernel_cmdline, const char *initrd_filename, ram_addr_t below_4g_mem_size, ram_addr_t above_4g_mem_size, MemoryRegion *rom_memory, MemoryRegion **ram_memory){ MemoryRegion *ram, *option_rom_mr; /* ...*/ /* Allocate RAM. We allocate it as a single memory region and use * aliases to address portions of it, mostly for backwards compatibility * with older qemus that used qemu_ram_alloc(). */ ram = g_malloc(sizeof(*ram)); // 调用 memory_region_init_ram 对 ram_memory 进行初始化 memory_region_init_ram(ram, "pc.ram", below_4g_mem_size + above_4g_mem_size); vmstate_register_ram_global(ram); *ram_memory = ram; /* ... */}
void memory_region_init_ram(MemoryRegion *mr, const char *name, uint64_t size){ memory_region_init(mr, name, size); mr->ram = true; mr->terminates = true; mr->destructor = memory_region_destructor_ram; mr->ram_addr = qemu_ram_alloc(size, mr);}
可以看到这里是先调用了memory_region_init()
,之后设置 RAM 属性,并继续调用qemu_ram_alloc()
分配内存。
memory_region_init_alias()
初始化,没有自己的内存,表示实体 MemoryRegion 的一部分。通过 alias 成员指向实体 MemoryRegion,alias_offset
为在实体 MemoryRegion 中的偏移量,例如ram_below_4g
、ram_above_4g
:void *pc_memory_init(MemoryRegion *system_memory, const char *kernel_filename, const char *kernel_cmdline, const char *initrd_filename, ram_addr_t below_4g_mem_size, ram_addr_t above_4g_mem_size, MemoryRegion *rom_memory, MemoryRegion **ram_memory){ MemoryRegion *ram_below_4g, *ram_above_4g; /* ... */ ram_below_4g = g_malloc(sizeof(*ram_below_4g)); // 调用 memory_region_init_alias 对 ram_below_4g 进行初始化 memory_region_init_alias(ram_below_4g, "ram-below-4g", ram, 0, below_4g_mem_size); /* ... */}
void memory_region_init_alias(MemoryRegion *mr, const char *name, MemoryRegion *orig, target_phys_addr_t offset, uint64_t size){ memory_region_init(mr, name, size); mr->alias = orig; // 指向实体 MemoryRegion mr->alias_offset = offset;}
MemoryRegion 用来描述一段逻辑层面上的内存区域,而记录实际分配的内存地址信息的结构体则是 RAMBlock,在cpu-all.h
中定义:
typedef struct RAMBlock { struct MemoryRegion *mr; // 唯一对应的 MemoryRegion uint8_t *host; // RAMBlock 关联的内存,即 HVA ram_addr_t offset; // RAMBlock 在 VM 物理内存中的偏移量,即 GPA ram_addr_t length; // RAMBlock 的长度 uint32_t flags; char idstr[256]; // RAMBlock 的 id QLIST_ENTRY(RAMBlock) next; // 指向下一个 RAMBlock#if defined(__linux__) && !defined(TARGET_S390X) int fd;#endif} RAMBlock;
可以看到在 RAMBlock 中host
和offset
域分别对应了 HVA 和 GPA,因此也可以说 RAMBlock 中存储了GPA->HVA
的映射关系,另外每一个 RAMBlock 都会指向其所属的 MemoryRegion。
QEMU 在cpu-all.h
中定义了一个全局变量ram_list
,以链表的形式维护了所有的 RAMBlock:
typedef struct RAMList { uint8_t *phys_dirty; QLIST_HEAD(, RAMBlock) blocks; uint64_t dirty_pages;} RAMList;extern RAMList ram_list;
每一个新分配的 RAMBlock 都会被插入到ram_list
的头部。如需查找地址所对应的 RAMBlock,则需要遍历ram_list
,当目标地址落在当前 RAMBlock 的地址区间时,该 RAMBlock 即为查找目标。
AddressSpace、MemoryRegion、RAMBlock 之间的关系如下所示:
可以看到 AddressSpace 的root
域指向根级 MemoryRegion,AddressSpace 是由root
域指向的 MemoryRegion 及其子树共同表示的。MemoryRegion 作为一个逻辑层面的内存区域,还需借助分布在其中的 RAMBlock 来存储真实的地址映射关系。
下图是我根据自己的理解绘制的三者之间的关系图:
如图所示,以address_space_memory
为例,其root
域对应的 MemoryRegion 为system_memory
。system_memory
的 subregions 为两个 alias MemoryRegion:ram_below_4g
、ram_above_4g
,均指向pc.ram
这个实体 MemoryRegion。pc.ram
的内存实际上通过 RAMBlock 分配,其addr
与ram_addr
域分别对应了 RAMBlock 的 HVA、GPA。QEMU 从自己的进程地址空间中为该 RAMBlock 分配内存后,将其mr
域指向pc.ram
,至此就完成了 QEMU 侧的内存分配。
AddressSpace 的root
域及其子树共同构成了 Guest 的物理地址空间,但这些都是在 QEMU 侧定义的。要传入 KVM 进行设置时,复杂的树状结构是不利于内核进行处理的,因此需要将其转换为一个“平坦”的地址模型,也就是一个从零开始、只包含地址信息的数据结构,这在 QEMU 中通过 FlatView 来表示。每个 AddressSpace 都有一个与之对应的 FlatView 指针current_map
,表示其对应的平面展开视图。
FlatView 在memory.c
中定义:
/* Flattened global view of current active memory hierarchy. Kept in sorted * order. */struct FlatView { FlatRange *ranges; // 对应的 FlatRange 数组 unsigned nr; // FlatRange 的数目 unsigned nr_allocated; // 当前数组的项数};
其中,ranges
是一个数组,记录了 FlatView 下所有的 FlatRange。
在 FlatView 中,FlatRange 表示在 FlatView 中的一段内存范围,同样在memory.c
中定义:
/* Range of memory in the global map. Addresses are absolute. */struct FlatRange { MemoryRegion *mr; // 指向所属的 MemoryRegion target_phys_addr_t offset_in_region; // 在全局 MemoryRegion 中的 offset,对应 GPA AddrRange addr; // 代表的地址区间,对应 HVA uint8_t dirty_log_mask; bool readable; bool readonly;};
每个 FlatRange 对应一段虚拟机物理地址区间,各个 FlatRange 不会重叠,按照地址的顺序保存在数组中,具体的地址范围由一个 AddrRange 结构来描述:
/* * AddrRange 用于表示 FlatRange 的起始地址及大小 */struct AddrRange { Int128 start; Int128 size;};
在 QEMU 中,还有几个起到中介作用的结构体,MemoryRegionSection 就是其中之一。
之前介绍的 FlatRange 代表一个物理地址空间的片段,偏向于描述在 Host 侧即 AddressSpace 中的分布,而 MemoryRegionSection 则代表在 Guest 侧即 MemoryRegion 中的片段。MemoryRegionSection 在memory.h
中定义:
/** * MemoryRegionSection: describes a fragment of a #MemoryRegion * * @mr: the region, or %NULL if empty * @address_space: the address space the region is mapped in * @offset_within_region: the beginning of the section, relative to @mr's start * @size: the size of the section; will not exceed @mr's boundaries * @offset_within_address_space: the address of the first byte of the section * relative to the region's address space * @readonly: writes to this section are ignored */struct MemoryRegionSection { MemoryRegion *mr; // 所属的 MemoryRegion MemoryRegion *address_space; // 关联的 AddressSpace target_phys_addr_t offset_within_region; // 在 MemoryRegion 内部的 offset uint64_t size; // Section 的大小 target_phys_addr_t offset_within_address_space; // 在 AddressSpace 内部的 offset bool readonly; // 是否为只读};
offset_within_region
:在所属 MemoryRegion 中的 offset。一个 AddressSpace 可能由多个 MemoryRegion 组成,因此该 offset 是局部的offset_within_address_space
:在所属 AddressSpace 中的 offset,它是全局的root
指向对应的根级 MemoryRegion,current_map
指向root
通过generate_memory_topology()
生成的 FlatViewranges
数组表示该 MemoryRegion 所表示的 Guest 地址区间,并按照地址的顺序进行排列ranges
数组中的 FlatRange 对应生成,作为注册到 KVM 中的基本单位QEMU 在用户空间申请内存后,需要将内存信息通过一系列系统调用传入内核空间的 KVM,由 KVM 侧进行管理,因此 QEMU 侧也定义了一些用于向 KVM 传递参数的结构体。
在kvm-all.c
中定义,是 KVM 中内存管理的基本单位:
typedef struct KVMSlot{ target_phys_addr_t start_addr; // Guest 物理地址,GPA ram_addr_t memory_size; // 内存大小 void *ram; // QEMU 用户空间地址,HVA int slot; // Slot 编号 int flags; // 标志位,例如是否追踪脏页、是否可用等} KVMSlot;
KVMSlot 类似于内存插槽的概念,在 KVMState 的定义中可以看到,最多支持 32 个 KVMSlot:
struct KVMState{ KVMSlot slots[32]; // 最多支持 32 个 KVMSlot /* ... */}KVMState *kvm_state;
调用ioctl(KVM_SET_USER_MEMORY_REGION)
时需要向 KVM 传递的参数,在kvm.h
中定义
/* for KVM_SET_USER_MEMORY_REGION */struct kvm_userspace_memory_region { __u32 slot; // slot 编号 __u32 flags; // 标志位,例如是否追踪脏页、是否可用等 __u64 guest_phys_addr; // Guest 物理地址,GPA __u64 memory_size; // 内存大小,bytes __u64 userspace_addr; // 从 QEMU 进程空间分配的起始地址,HVA};
为了监控虚拟机的物理地址访问,对于每一个 AddressSpace,都会有一个 MemoryListener 与之对应。每当物理映射GPA->HVA
发生改变时,就会回调这些函数。MemoryListener 是对一些事件的回调函数合集,在memory.h
中定义:
/** * MemoryListener: callbacks structure for updates to the physical memory map * * Allows a component to adjust to changes in the guest-visible memory map. * Use with memory_listener_register() and memory_listener_unregister(). */struct MemoryListener { void (*begin)(MemoryListener *listener); void (*commit)(MemoryListener *listener); void (*region_add)(MemoryListener *listener, MemoryRegionSection *section); void (*region_del)(MemoryListener *listener, MemoryRegionSection *section); void (*region_nop)(MemoryListener *listener, MemoryRegionSection *section); void (*log_start)(MemoryListener *listener, MemoryRegionSection *section); void (*log_stop)(MemoryListener *listener, MemoryRegionSection *section); void (*log_sync)(MemoryListener *listener, MemoryRegionSection *section); void (*log_global_start)(MemoryListener *listener); void (*log_global_stop)(MemoryListener *listener); void (*eventfd_add)(MemoryListener *listener, MemoryRegionSection *section, bool match_data, uint64_t data, EventNotifier *e); void (*eventfd_del)(MemoryListener *listener, MemoryRegionSection *section, bool match_data, uint64_t data, EventNotifier *e); /* Lower = earlier (during add), later (during del) */ unsigned priority; MemoryRegion *address_space_filter; QTAILQ_ENTRY(MemoryListener) link;};
所有的 MemoryListener 都会挂在全局变量memory_listeners
链表上,在memory.c
中定义:
static QTAILQ_HEAD(memory_listeners, MemoryListener) memory_listeners = QTAILQ_HEAD_INITIALIZER(memory_listeners);
在memory.c
中枚举了 ListenerDireciton:
enum ListenerDirection { Forward, Reverse };
另外,system_memory
、system_io
这两个全局 MemoryRegion 分别注册了core_memory_listener
和io_memory_listener
,在exec.c
中定义:
// 对应 system_memory 这个 MemoryRegionstatic MemoryListener core_memory_listener = { .begin = core_begin, .commit = core_commit, .region_add = core_region_add, .region_del = core_region_del, .region_nop = core_region_nop, .log_start = core_log_start, .log_stop = core_log_stop, .log_sync = core_log_sync, .log_global_start = core_log_global_start, .log_global_stop = core_log_global_stop, .eventfd_add = core_eventfd_add, .eventfd_del = core_eventfd_del, .priority = 0,};// 对应 system_io 这个 MemoryRegionstatic MemoryListener io_memory_listener = { .begin = io_begin, .commit = io_commit, .region_add = io_region_add, .region_del = io_region_del, .region_nop = io_region_nop, .log_start = io_log_start, .log_stop = io_log_stop, .log_sync = io_log_sync, .log_global_start = io_log_global_start, .log_global_stop = io_log_global_stop, .eventfd_add = io_eventfd_add, .eventfd_del = io_eventfd_del, .priority = 0,};
除此之外,QEMU 还在全局注册了kvm_memory_listener
,在kvm-all.c
中定义,用于将 QEMU 侧内存拓扑结构的改动同步更新至 KVM 中:
// 同时监听 system_memory、system_iostatic MemoryListener kvm_memory_listener = { .begin = kvm_begin, .commit = kvm_commit, .region_add = kvm_region_add, .region_del = kvm_region_del, .region_nop = kvm_region_nop, .log_start = kvm_log_start, .log_stop = kvm_log_stop, .log_sync = kvm_log_sync, .log_global_start = kvm_log_global_start, .log_global_stop = kvm_log_global_stop, .eventfd_add = kvm_eventfd_add, .eventfd_del = kvm_eventfd_del, .priority = 10,};
结构体名 | 定义 | 说明 |
---|---|---|
AddressSpace | memory.c | VM 能看到的一段地址空间,偏向 Host 侧 |
MemoryRegion | memory.h | 地址空间中一段逻辑层面的内存区域,偏向 Guest 侧 |
RAMBlock | cpu-all.h | 记录实际分配的内存地址信息,存储了GPA->HVA 的映射关系 |
FlatView | memory.c | MemoryRegion 对应的平面展开视图,包含一个 FlatRange 类型的 ranges 数组 |
FlatRange | memory.c | 对应一段虚拟机物理地址区间,各个 FlatRange 不会重叠,按照地址的顺序保存在数组中 |
MemoryRegionSection | memory.h | 表示 MemoryRegion 中的片段 |
MemoryListener | memory.h | 回调函数集合 |
KVMSlot | kvm-all.c | KVM 中内存管理的基本单位,表示一个内存插槽 |
kvm_userspace_memory_region | kvm.h | 调用ioctl(KVM_SET_USER_MEMORY_REGION) 时需要向 KVM 传递的参数 |
memory.c
中定义:static AddressSpace address_space_memory; // 内存地址空间,对应 system_memorystatic AddressSpace address_space_io; // I/O 地址空间,对应 system_io
exec.c
中定义:static MemoryRegion *system_memory; // 用于管理内存 subregion 的根级 MemoryRegionstatic MemoryRegion *system_io; // 用于管理 I/O subregion 的根级 MemoryRegion
exec.c
中定义:RAMList ram_list = { .blocks = QLIST_HEAD_INITIALIZER(ram_list.blocks) }; // 用于管理全局的 RAMBlock
memory.c
中定义static QTAILQ_HEAD(memory_listeners, MemoryListener) memory_listeners = QTAILQ_HEAD_INITIALIZER(memory_listeners);
exec.c
和kvm-all.c
中定义:// 对应 system_memory 这个 MemoryRegionstatic MemoryListener core_memory_listener = { .begin = core_begin, .commit = core_commit, .region_add = core_region_add, .region_del = core_region_del, .region_nop = core_region_nop, .log_start = core_log_start, .log_stop = core_log_stop, .log_sync = core_log_sync, .log_global_start = core_log_global_start, .log_global_stop = core_log_global_stop, .eventfd_add = core_eventfd_add, .eventfd_del = core_eventfd_del, .priority = 0,};// 对应 system_io 这个 MemoryRegionstatic MemoryListener io_memory_listener = { .begin = io_begin, .commit = io_commit, .region_add = io_region_add, .region_del = io_region_del, .region_nop = io_region_nop, .log_start = io_log_start, .log_stop = io_log_stop, .log_sync = io_log_sync, .log_global_start = io_log_global_start, .log_global_stop = io_log_global_stop, .eventfd_add = io_eventfd_add, .eventfd_del = io_eventfd_del, .priority = 0,};// 在全局注册,同时监听 system_memory、system_iostatic MemoryListener kvm_memory_listener = { .begin = kvm_begin, .commit = kvm_commit, .region_add = kvm_region_add, .region_del = kvm_region_del, .region_nop = kvm_region_nop, .log_start = kvm_log_start, .log_stop = kvm_log_stop, .log_sync = kvm_log_sync, .log_global_start = kvm_log_global_start, .log_global_stop = kvm_log_global_stop, .eventfd_add = kvm_eventfd_add, .eventfd_del = kvm_eventfd_del, .priority = 10,};
QEMU 的内存申请流程大致可分为三个部分:回调函数的注册、AddressSpace 的初始化、实际内存的分配。下面将根据在vl.c
的main()
函数中的调用顺序分别介绍。
int main() └─ static int configure_accelerator() └─ int kvm_init() // 初始化 KVM ├─ int kvm_ioctl(KVM_CREATE_VM) // 创建 VM ├─ int kvm_arch_init() // 针对不同的架构进行初始化 └─ void memory_listener_register() // 注册 kvm_memory_listener └─ static void listener_add_address_space() // 调用 region_add 回调 └─ static void kvm_region_add() // region_add 对应的回调实现 └─ static void kvm_set_phys_mem() // 根据传入的 section 填充 KVMSlot └─ static int kvm_set_user_memory_region() └─ int ioctl(KVM_SET_USER_MEMORY_REGION)
进入configure_accelerator()
后,QEMU 会先调用configure_accelerator()
设置 KVM 的加速支持,之后进入kvm_init()
。该函数主要完成对 KVM 的初始化,包括一些常规检查如 CPU 个数、KVM 版本等,之后通过kvm_ioctl(KVM_CREATE_VM)
与内核交互,创建 KVM 虚拟机。在kvm_init()
的最后,会调用memory_listener_register()
注册kvm_memory_listener
:
int kvm_init(void){ /* ... */ s->vmfd = kvm_ioctl(s, KVM_CREATE_VM, 0); // 创建 VM /* ... */ ret = kvm_arch_init(s); // 针对不同的架构进行初始化 if (ret < 0) { goto err; } /* ... */ memory_listener_register(&kvm_memory_listener, NULL); // 注册回调函数 /* ... */}
该注册函数本身并不复杂,结合备注来看:
void memory_listener_register(MemoryListener *listener, MemoryRegion *filter){ MemoryListener *other = NULL; listener->address_space_filter = filter; /* 若 memory_listeners 为空或当前 listener 的优先级大于最后一个 listener 的优先级,则直接在末尾插入 */ if (QTAILQ_EMPTY(&memory_listeners) || listener->priority >= QTAILQ_LAST(&memory_listeners, memory_listeners)->priority) { QTAILQ_INSERT_TAIL(&memory_listeners, listener, link); } else { /* 遍历链表,按照优先级升序排列 */ QTAILQ_FOREACH(other, &memory_listeners, link) { if (listener->priority < other->priority) { break; } } /* 插入 listener */ QTAILQ_INSERT_BEFORE(other, listener, link); } /* 对于以下 AddressSpace,设置其对应的 listener */ listener_add_address_space(listener, &address_space_memory); listener_add_address_space(listener, &address_space_io);}
最后的listener_add_address_space()
主要是将listener
注册到其对应的 AddressSpace 上,并根据 AddressSpace 对应的 FlatRange 数组,生成 MemoryRegionSection,并注册到 KVM 中:
static void listener_add_address_space(MemoryListener *listener, AddressSpace *as){ FlatRange *fr; /* 若非注册的 AddressSpace,直接返回 */ if (listener->address_space_filter && listener->address_space_filter != as->root) { return; } /* 开启内存脏页记录 */ if (global_dirty_log) { listener->log_global_start(listener); } /* 遍历 AddressSpace 对应的 FlatRange 数组,并将其转换成 MemoryRegionSection */ FOR_EACH_FLAT_RANGE(fr, &as->current_map) { MemoryRegionSection section = { .mr = fr->mr, .address_space = as->root, .offset_within_region = fr->offset_in_region, .size = int128_get64(fr->addr.size), .offset_within_address_space = int128_get64(fr->addr.start), .readonly = fr->readonly, }; /* 将 section 所代表的内存区域注册到 KVM 中 */ listener->region_add(listener, §ion); }}
由于此时 AddressSapce 尚未初始化,所以此处的循环为空,仅是在全局注册了kvm_memory_listener
。最后调用了kvm_memory_listener->region_add()
,对应的实现是kvm_region_add()
,该函数最终会通过ioctl(KVM_SET_USER_MEMORY_REGION)
,将 QEMU 侧申请的内存信息传入 KVM 进行注册,这里的流程会在下一部分进行分析。
int main() └─ void cpu_exec_init_all() ├─ static void memory_map_init() | ├─ void memory_region_init() // 初始化 system_memory/io 这两个全局 MemoryRegion | ├─ void set_system_memory_map() // address_space_memory->root = system_memory | | └─ static void memory_region_update_topology() // 为 MemoryRegion 生成 FlatView | | └─ static void address_space_update_topology() // as->current_map = new_view | | └─ static void address_space_update_topology_pass() | | └─ static void kvm_region_add() // region_add 对应的回调实现 | | └─ static void kvm_set_phys_mem() // 根据传入的 section 填充 KVMSlot | | └─ static int kvm_set_user_memory_region() | | └─ int ioctl(KVM_SET_USER_MEMORY_REGION) | | | └─ void memory_listener_register() // 注册对应的 MemoryListener | └─ static void listener_add_address_space() | └─ static void io_mem_init() └─ void memory_region_init_io() // ram/rom/unassigned/notdirty/subpage-ram/watch └─ void memory_region_init()
第一部分在全局注册了kvm_memory_listener
,但由于 AddressSpace 尚未初始化,实际上并未向 KVM 中注册任何实际的内存信息。QEMU 在main()
函数中会继续调用cpu_exec_init_all()
对 AddressSpace 进行初始化,该函数实际上是对两个 init 函数的封装调用:
void cpu_exec_init_all(void){#if !defined(CONFIG_USER_ONLY) memory_map_init(); // 初始化两个全局 AddressSpace,以及对应的 MemoryRegion、FlatView io_mem_init(); // 初始化六个I/O MemoryRegion#endif}
先来看memory_map_init()
,主要用来初始化两个全局的系统地址空间system_memory
、system_io
:
static void memory_map_init(void){ system_memory = g_malloc(sizeof(*system_memory)); memory_region_init(system_memory, "system", INT64_MAX); // 1. 初始化 system_memory set_system_memory_map(system_memory); // 2. 设置 address_space_memory 关联 system_memory 及其对应的 FlatView system_io = g_malloc(sizeof(*system_io)); memory_region_init(system_io, "io", 65536); // 1. 初始化 system_io set_system_io_map(system_io); // 2. 设置 address_space_io 关联 system_io 及其对应的 FlatView memory_listener_register(&core_memory_listener, system_memory); // 3. 注册 core_memory_listener memory_listener_register(&io_memory_listener, system_io); // 3. 注册 io_memory_listener}
这样一来就完成了以下对应关系:
AddressSpace address_space_memory address_space_io ↓ ↓MemoryRegion system_memory system_io ↑ ↑MemoryRegionListener core_memory_listener io_memory_listener
AddressSpace | 对应的 MemoryRegion | 对应的 MemoryRegionListener |
---|---|---|
address_space_memory | system_memory | core_memory_listener |
address_space_io | system_io | io_memory_listener |
memory_region_init
主要是初始化system_memory
的各个字段,这里比较重要的是set_system_memory_map()
,先设置 AddressSpace 对应的 MemoryRegion,之后根据system_memory
更新address_space_memory
对应的 FlatView:
void set_system_memory_map(MemoryRegion *mr){ address_space_memory.root = mr; // 将 address_space_memory 的 root 域指向 system_memory memory_region_update_topology(NULL); // 根据 system_memory 更新 address_space_memory 对应的 FlatView}
而memory_region_update_topology()
则会继续调用address_space_update_topology()
,生成 AddressSpace 对应的 FlatView 视图:
static void memory_region_update_topology(MemoryRegion *mr){ // 此时仅在全局注册了 kvm_memory_listener,而 kvm_begin() 为空,无实际操作 MEMORY_LISTENER_CALL_GLOBAL(begin, Forward); if (address_space_memory.root) { // 更新 address_space_memory 的 FlatView address_space_update_topology(&address_space_memory); } if (address_space_io.root) { // 更新 address_space_io 的 FlatView address_space_update_topology(&address_space_io); } // 此时仅在全局注册了 kvm_memory_listener,而 kvm_commit() 为空,无实际操作 MEMORY_LISTENER_CALL_GLOBAL(commit, Forward); memory_region_update_pending = false;}
address_space_update_topology()
会先调用generate_memory_topology()
生成system_memory
更新后的视图new_view
,再将address_space_memory
的current_map
指向这个new_view
,最后销毁old_view
:
static void address_space_update_topology(AddressSpace *as){ FlatView old_view = as->current_map; FlatView new_view = generate_memory_topology(as->root); // 根据 system_memory 生成 new_view // 入参 adding 为 false 时将调用 kvm_region_del() address_space_update_topology_pass(as, old_view, new_view, false); // 入参 adding 为 true 时将调用 kvm_region_add() address_space_update_topology_pass(as, old_view, new_view, true); as->current_map = new_view; // 指向 new_view flatview_destroy(&old_view); // 销毁 old_view address_space_update_ioeventfds(as);}
在address_space_update_topology_pass()
的最后,会调用MEMORY_LISTENER_UPDATE_REGION
这个宏,触发region_add
对应的回调函数kvm_region_add()
:
static void address_space_update_topology_pass(AddressSpace *as, FlatView old_view, FlatView new_view, bool adding){ unsigned iold, inew; FlatRange *frold, *frnew; /* Generate a symmetric difference of the old and new memory maps. * Kill ranges in the old map, and instantiate ranges in the new map. */ /* ... */ } else { /* In new */ if (adding) { MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, region_add); } ++inew; } }}
这个宏在memory.c
中定义,会将 FlatView 中的 FlatRange 转换为 MemoryRegionSection,作为入参传递给kvm_region_add()
:
#define MEMORY_LISTENER_UPDATE_REGION(fr, as, dir, callback) \ MEMORY_LISTENER_CALL(callback, dir, (&(MemoryRegionSection) { \ .mr = (fr)->mr, \ .address_space = (as)->root, \ .offset_within_region = (fr)->offset_in_region, \ .size = int128_get64((fr)->addr.size), \ .offset_within_address_space = int128_get64((fr)->addr.start), \ .readonly = (fr)->readonly, \ }))
而kvm_region_add()
实际上是对kvm_set_phys_mem()
的封装调用。该函数比较复杂,会根据传入的section
填充 KVMSlot,再传递给kvm_set_user_memory_region()
:
static int kvm_set_user_memory_region(KVMState *s, KVMSlot *slot){ struct kvm_userspace_memory_region mem; mem.slot = slot->slot; // 根据 KVMSlot 填充 kvm_userspace_memory_region mem.guest_phys_addr = slot->start_addr; mem.memory_size = slot->memory_size; mem.userspace_addr = (unsigned long)slot->ram; mem.flags = slot->flags; if (s->migration_log) { mem.flags |= KVM_MEM_LOG_DIRTY_PAGES; } return kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem);}
可以看到这里又将 KVMSlot 转换为 kvm_userspace_memory_region,作为ioctl()
的参数,交给内核中的 KVM 进行内存的注册。至此 QEMU 侧负责管理内存的数据结构均已完成初始化,可以参考下面的图片了解各数据结构之间的对应关系:
最后简单看下io_mem_init()
,调用memory_region_init_io()
对六个 I/O MemoryRegion 进行初始化:
static void io_mem_init(void){ memory_region_init_io(&io_mem_ram, &error_mem_ops, NULL, "ram", UINT64_MAX); memory_region_init_io(&io_mem_rom, &rom_mem_ops, NULL, "rom", UINT64_MAX); memory_region_init_io(&io_mem_unassigned, &unassigned_mem_ops, NULL, "unassigned", UINT64_MAX); memory_region_init_io(&io_mem_notdirty, ¬dirty_mem_ops, NULL, "notdirty", UINT64_MAX); memory_region_init_io(&io_mem_subpage_ram, &subpage_ram_ops, NULL, "subpage-ram", UINT64_MAX); memory_region_init_io(&io_mem_watch, &watch_mem_ops, NULL, "watch", UINT64_MAX);}
而memory_region_init_io()
则会先调用memory_region_init()
对上述六个 MemoryRegion 进行初始化,之后设置一些字段的值:
void memory_region_init_io(MemoryRegion *mr, const MemoryRegionOps *ops, void *opaque, const char *name, uint64_t size){ memory_region_init(mr, name, size); mr->ops = ops; mr->opaque = opaque; mr->terminates = true; // 表示为实体类型的 MemoryRegion mr->destructor = memory_region_destructor_iomem; mr->ram_addr = ~(ram_addr_t)0;}
int main() └─ void machine->init(ram_size, ...) └─ static void pc_init_pci(ram_size, ...) // 初始化虚拟机 └─ static void pc_init1(system_memory, system_io, ram_size, ...) ├─ void memory_region_init(pci_memory, "pci", ...) // pci_memory, rom_memory └─ void pc_memory_init() // 初始化内存,分配实际的物理内存地址 ├─ void memory_region_init_ram() // 创建 pc.ram, pc.rom 并分配内存 | ├─ void memory_region_init() | └─ ram_addr_t qemu_ram_alloc() | └─ ram_addr_t qemu_ram_alloc_from_ptr() | ├─ void vmstate_register_ram_global() // 将 MR 的 name 写入 RAMBlock 的 idstr | └─ void vmstate_register_ram() | └─ void qemu_ram_set_idstr() | ├─ void memory_region_init_alias() // 初始化 ram_below_4g, ram_above_4g └─ void memory_region_add_subregion() // 在 system_memory 中添加 subregions └─ static void memory_region_add_subregion_common() └─ static void memory_region_update_topology() // 为 MemoryRegion 生成 FlatView └─ static void address_space_update_topology() // as->current_map = new_view └─ static void address_space_update_topology_pass() └─ static void kvm_region_add() // region_add 对应的回调实现 └─ static void kvm_set_phys_mem() // 根据传入的 section 填充 KVMSlot └─ static int kvm_set_user_memory_region() └─ int ioctl(KVM_SET_USER_MEMORY_REGION)
之前的回调函数注册、AddressSpace 的初始化,实际上均没有对应的物理内存。顺着main()
函数往下走,会来到pc_init_pci()
这个函数。
函数pc_init_pci()
负责在 QEMU 中初始化虚拟机,内存的虚拟化也是在这里完成的。调用machine->init()
时传入了ram_size
参数,表示申请内存的大小,一步步传递给了pc_init1()
。
在pc_init1()
中,先将ram_size
分为above_4g_mem_size
、below_4g_mem_size
,之后调用pc_memory_init()
对内存进行初始化:
void *pc_memory_init(MemoryRegion *system_memory, const char *kernel_filename, const char *kernel_cmdline, const char *initrd_filename, ram_addr_t below_4g_mem_size, ram_addr_t above_4g_mem_size, MemoryRegion *rom_memory, MemoryRegion **ram_memory){ MemoryRegion *ram, *option_rom_mr; // 两个实体 MR: pc.ram, pc.rom MemoryRegion *ram_below_4g, *ram_above_4g; // 两个别名 MR: ram_below_4g, ram_above_4g /* Allocate RAM. We allocate it as a single memory region and use * aliases to address portions of it, mostly for backwards compatibility * with older qemus that used qemu_ram_alloc(). */ ram = g_malloc(sizeof(*ram)); // 创建 ram // 分配具体的内存(实际上会创建一个 RAMBlock 并将其 offset 值写入 ram.ram_addr,对应 GPA) memory_region_init_ram(ram, "pc.ram", below_4g_mem_size + above_4g_mem_size); // 将 MR 的 name 写入 RAMBlock 的 idstr vmstate_register_ram_global(ram); *ram_memory = ram; // 创建 ram_below_4g 表示 4G 以下的内存 ram_below_4g = g_malloc(sizeof(*ram_below_4g)); memory_region_init_alias(ram_below_4g, "ram-below-4g", ram, 0, below_4g_mem_size); // 将 ram_below_4g 挂在 system_memory 下 memory_region_add_subregion(system_memory, 0, ram_below_4g); if (above_4g_mem_size > 0) { ram_above_4g = g_malloc(sizeof(*ram_above_4g)); memory_region_init_alias(ram_above_4g, "ram-above-4g", ram, below_4g_mem_size, above_4g_mem_size); memory_region_add_subregion(system_memory, 0x100000000ULL, ram_above_4g); } /* ... */}
这里的重点在于memory_region_init_ram()
,它通过qemu_ram_alloc()
获取ram
这个 MemoryRegion 对应的 RAMBlock 的offset
,并存入ram.ram_addr
,这样就可以在ram_list
中根据该字段查找 MR 对应的 RAMBlock:
void memory_region_init_ram(MemoryRegion *mr, const char *name, uint64_t size){ memory_region_init(mr, name, size); // 填充字段,初始化默认值 mr->ram = true; // 表示为 RAM mr->terminates = true; // 表示为实体 MemoryRegion mr->destructor = memory_region_destructor_ram; mr->ram_addr = qemu_ram_alloc(size, mr); // 这里保存 RAMBlock 的 offset,即 GPA}
而qemu_ram_alloc()
最终会调用qemu_ram_alloc_from_ptr()
,创建一个对应大小 RAMBlock 并分配内存,返回对应的 GPA 地址存入mr->ram_addr
中:
ram_addr_t qemu_ram_alloc_from_ptr(ram_addr_t size, void *host, MemoryRegion *mr){ RAMBlock *new_block; // 创建一个 RAMBlock size = TARGET_PAGE_ALIGN(size); // 页对齐 new_block = g_malloc0(sizeof(*new_block)); // 初始化 new_block new_block->mr = mr; // 将 new_block-> 指向入参的 MemoryRegion new_block->offset = find_ram_offset(size); // 从 ram_list 中的 RAMBlock 之间找到一段可以满足 size 需求的 gap,并返回起始地址的 offset,对应 GPA if (host) { // 新建的 RAMBlock host 字段为空,跳过 new_block->host = host; new_block->flags |= RAM_PREALLOC_MASK; } else { if (mem_path) { // 未指定 mem_path#if defined (__linux__) && !defined(TARGET_S390X) new_block->host = file_ram_alloc(new_block, size, mem_path); if (!new_block->host) { new_block->host = qemu_vmalloc(size); qemu_madvise(new_block->host, size, QEMU_MADV_MERGEABLE); }#else fprintf(stderr, "-mem-path option unsupported\n"); exit(1);#endif } else { if (xen_enabled()) { xen_ram_alloc(new_block->offset, size, mr); } else if (kvm_enabled()) { // 从这里继续 /* some s390/kvm configurations have special constraints */ new_block->host = kvm_vmalloc(size); // 实际上还是调用 qemu_vmalloc(size) } else { new_block->host = qemu_vmalloc(size); // 从 QEMU 的线性空间中分配 size 大小的内存,返回 HVA } qemu_madvise(new_block->host, size, QEMU_MADV_MERGEABLE); } } new_block->length = size; // 将 length 设置为 size QLIST_INSERT_HEAD(&ram_list.blocks, new_block, next); // 将该 RAMBlock 插入 ram_list 头部 ram_list.phys_dirty = g_realloc(ram_list.phys_dirty, // 重新分配 ram_list.phys_dirty 的内存空间 last_ram_offset() >> TARGET_PAGE_BITS); memset(ram_list.phys_dirty + (new_block->offset >> TARGET_PAGE_BITS), 0, size >> TARGET_PAGE_BITS); cpu_physical_memory_set_dirty_range(new_block->offset, size, 0xff); // 对该 RAMBlock 对应的内存标记为 dirty qemu_ram_setup_dump(new_block->host, size); if (kvm_enabled()) kvm_setup_guest_memory(new_block->host, size); return new_block->offset;}
这样一来ram
对应的 RAMBlock 中就分配好了 GPA 和 HVA,就可以将内存信息同步至 KVM 侧了。
最后回到pc_memory_init()
中,在分配完实际内存后,会先调用memory_region_init_alias()
初始化ram_below_4g
、ram_above_4g
这两个 alias,之后调用memory_region_add_subregion()
将这两个 alias 指向ram
这个实体 MemoryRegion。该函数最终会触发kvm_region_add()
回调,将实际的内存信息传入 KVM 注册。该过程如下图所示,与之前分析的流程相同,此处不再赘述。
qemu_ram_alloc_from_ptr()
从 QEMU 的进程地址空间中以 mmap 的方式分配内存,并负责维护该 MemoryRegion 对应内存的起始 GPA/HVA/size 等相关信息kvm_userspace_memory_region
结构体,作为ioctl()
的参数更新 KVM 中的kvm_memory_slot
ioctl()
创建 vcpu 时,调用kvm_mmu_create()
初始化 MMU 相关信息vcpu_enter_guest()=>kvm_mmu_reload()
会将根级页表地址加载到 VMCS,让 Guest 使用该页表]]>
- KVM,QEMU 核心分析 | 博客园
- 内存虚拟化到底是咋整的?- 腾讯云 TStack | 腾讯云+社区
- 从kvm场景下guest访问的内存被swap出去之后说起 | kernelnote
- linux 下 cpu load 和 cpu 使用率的关系 | kernelnote
- 关于linux下进程栈的研究 | kernelnote
- 虚拟化环境中的hypercall介绍 | kernelnote
- linux ksm 内存 merge机制研究 | kernelnote
- KVM 虚拟化之 VM Exit/Entry | Min’s Blog
- QEMU Internals: How guest physical RAM works | Stefan Hajnoczi
- kvm: virtual x86 mmu setup | Davidlohr Bueso
- GDB 调试 QEMU 源码记录 - 太初有道 | cnblogs
- SIG@QEMU-KVM - kernel-dev-environment | Github
- 向大家汇报,我们连续第二年登上KVM全球开源贡献榜 | 腾讯开源
Perf 使用速查
更新中…
# 1. Ctrl-C 结束执行后,生成采样数据 perf.data> perf record -g -e cpu-cycles -p $(pidof qemu-system-x86_64)# 2. 使用 perf script 对 perf.data 进行解析> perf script -i perf.data &> perf.unfold# 3. 将 perf.unfold 中的符号进行折叠> ./stackcollapse-perf.pl perf.unfold &> perf.folded# 4. 最后生成 SVG 火焰图> ./flamegraph.pl perf.folded > perf.svg
]]>
- 电子书:《Linux Perf Master》- RiboseYim | 知乎
- The Linux Perf Master | GitBook
- How to analyze your system with perf and Python | opensource.com
- Linux 效能分析工具: Perf | 成大資工 Wiki
- 2. 程序调试 | Linux Tools Quick Tutorial
- 5. pstack 跟踪进程栈 | Linux Tools Quick Tutorial
- perf-tools | Bolog
- Linux 性能诊断 perf 使用指南 | 阿里云栖社区
- Linux perf sched Summary | Oliver Yang
- Linux 性能优化 9:KVM 环境 | 知乎
- 了解 Linux Perf 报告输出
- 运维利器万能的 strace | 运维生存时间
- Perf 命令 | 云网牛站
- 系统级性能分析工具 perf 的介绍与使用 | 博客园
- brendangregg/FlameGraph | Github
- perf+火焰图分析程序性能 | 博客园
- Linux下的内核测试工具 —— perf使用简介 | 阿里云栖社区
- KVM 分析工具 | hanbaoying
- objdump 反汇编用法示例 | CSDN
理解 Shell 命令中 1>/dev/null、2>&1 的含义
更新中…
]]>
QEMU-KVM 热迁移原理及相关源码分析
// TODO: To be updated…
]]>
- Libvirt支持的三种CPU模式与热迁移(by Joshua)
- Libvirt支持的三种CPU模式与热迁移(by Joshua) | CSDN
- 虚拟化在线迁移优化实践(二):KVM虚拟化跨机迁移优化指南 | UCloud 云计算
- Linux KVM 在线迁移实战 | Cloud Atlas
- huataihuang/Cloud Atlas Draft | GitBook
- huataihuang/Cloud Atlas Draft | Github
- 热迁移、RTC 计时与安全加固…腾讯云 KVM 性能优化实践验谈 | InfoQ
- 热迁移、RTC 计时与安全增强…腾讯云 KVM 性能优化实践经验谈 | 腾讯云+社区
- 内存管理之:页和页框&地址变换结构 | CSDN
- KVM 虚拟迁移 | ICode9
- 浅析 QEMU 热迁移特性 - Multifd | 滴滴云
- QEMU 中 Bitmap 的应用 | 滴滴云
- 美团云“零感知”在线迁移解决方案 | 驱动中国
- docs: Fix generating qemu-doc.html with texinfo 5 | QEMU Development
- Errors in makefile for qemu 0.14.1 in ubuntu 15.04 64 bit | StackOverflow
- Windows 虚拟机对应的 QEMU 进程 CPU 占有率 116% | 代码天地
A Terminal Emulator based on UWP and web technologies.
PowerShell
、CMD
、WSL
或其他自定义终端iTerm
主题Ctrl-F
搜索shell profiles
注:需要更新至
Windows 10 Fall Creators Update
参考 How to install(as an end-user)
首先安装 Chocolatey,以管理员身份运行cmd.exe
,运行以下命令:
@"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"
安装完成后,使用choco -v
命令验证Chocolatey
是否已正确安装:
PS C:\Users\abel1> choco -v0.10.15PS C:\Users\abel1> choco list -lChocolatey v0.10.15chocolatey 0.10.15fluent-terminal 0.4.1.0PowerShell 5.1.14409.201808113 packages installed.
之后使用choco
安装fluent-terminal
:
PS C:\Users\abel1> choco install fluent-terminal
Function | Key |
---|---|
Tab 向前 | Ctrl-Tab |
Tab 向后 | Ctrl-Shift-Tab |
新建 Tab | Ctrl-T |
选择 Profile 新建 Tab | Ctrl-Shift-T |
更改 Tab 标题 | Ctrl-Shift-R |
关闭 Tab | Ctrl-W |
新建 Window | Ctrl-N |
选择 Profile 新建 Window | Ctrl-Shift-N |
打开 Settings | Ctrl-, |
复制 | Ctrl-Shift-C |
粘贴 | Ctrl-Shift-V |
搜索 | Ctrl-F |
全屏 | Alt-Enter |
全选 | Ctrl-A |
快速切换 Tab | Ctrl-1~9 |
]]>
插件地址:Vimium | Chrome Web Store
查看帮助按
?
Vimium | Function | Chrome |
---|---|---|
j | 向下滚动 | ↓ |
k | 向上滚动 | ↑ |
h | 向左滚动 | ← |
l | 向右滚动 | → |
d | 向下翻页 | PageDown |
u | 向上翻页 | PageUp |
Vimium | Function | Chrome |
---|---|---|
gg | 前往页面顶部 | Ctrl+Home |
G | 前往页面底部 | Ctrl+End |
Vimium | Function | Chrome |
---|---|---|
r | 刷新页面 | Ctrl+R |
gs | 查看页面源码 | Ctrl+U |
Vimium | Function |
---|---|
yy | 复制当前页面地址 |
yf | 复制某一链接地址 |
p | 在当前标签页中打开复制的 URL |
P | 在新标签页中打开复制的 URL |
i | 进入 insert mode ,使用网站默认的快捷键 |
Vimium | Function |
---|---|
f | 在当前标签页打开链接 |
F | 在新标签页打开链接 |
o | 在当前标签页打开 URL/书签/历史 |
O | 在新标签页打开 URL/书签/历史 |
T | 搜索已打开的标签页 |
b | 在当前标签页打开书签 |
B | 在新标签页打开书签 |
Vimium | Function |
---|---|
gf | 切换 Frame |
Vimium | Function | Chrome |
---|---|---|
/ | 进入 find mode | Ctrl+F |
n | 下一个匹配 | Enter |
N | 上一个匹配 | Shift+Enter |
Vimium | Function | Chrome |
---|---|---|
H | 后退 | Alt ← |
L | 前进 | Alt → |
Vimium | Function | Chrome |
---|---|---|
K ,gt | 前往右侧 Tab | Ctrl+PageDown |
J ,gT | 前往左侧 Tab | Ctrl+PageUp |
g0 | 第一个 Tab | Ctrl+1 |
g$ | 最后一个 Tab | |
t | 新建 Tab | Ctrl+T |
yt | 在新 Tab 中打开当前 Tab | |
Alt+P | 固定当前 Tab | |
Alt+M | 静音当前 Tab | |
x | 关闭当前 Tab | Ctrl+W |
X | 重新打开关闭的 Tab | Ctrl+Shift+T |
]]>
摘自 告别 Windows 终端的难看难用,从改造 PowerShell 的外观开始 | 少数派
待更新…
配置文件路径:
C:\Users\abelsu7\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
主要看这两篇:
]]>
- 告别 Windows 终端的难看难用,从改造 PowerShell 的外观开始 | 少数派
- 5 个 PowerShell 主题,让你的 Windows 终端更好看 | 少数派
- 为什么 Windows 的终端(如命令提示符、PowerShell)都这么丑?| 知乎
- 将美化进行到底,把 PowerShell 做成 oh-my-zsh 的样子 | walterlv 吕毅
- PowerShell 美化:oh my posh | Flymia
- oh-my-posh | Github
- Sarasa Gothic / 更纱黑体 | Github
- FluentTerminal | Github
- Chocolatey | The package manager for Windows
摘自 这些必备的 Linux shell知识你都掌握了吗 | Linux 学习
/root/sh-utils/test.sh para1 para2 para3$0 $1 $2 $3脚本名 第一个参数 第三个参数
$0
:执行的脚本名$1
:第一个参数$2
:第二个参数$#
:脚本后面传入的参数个数$@
:所有参数,并且可以被遍历$*
:所有参数,不加引号时与$@
相同,具体区别请移步 参考文章$?
:上一条命令的退出状态两个$
:当前脚本的进程 ID使用=
给变量赋值:
para1="hello world" # 字符串直接赋给变量 para1
注意:
=
两边不能有空格,等号右边有空格的字符串也必须用引号括起来
使用unset
取消变量:
unset para1
使用变量时,需要在变量前添加$
,或者变量名两边添加{}
:
#!/bin/bashpara1="hello world"echo "para1 is $para1"echo "para1 is ${para1}!"------para1 is hello worldpara1 is hello world!
#!/bin/bash# save command output into varhostname=`hostname`echo $hostname# call command in stringecho "Current path is $(pwd)"# use double (()) to calculate an expressionecho "36+52=$((36+52))"# command as a stringkernel="uname -r" echo "kernel is $($kernel)"# several commands as a stringcmd="ls;pwd"echo "$(eval $cmd)"------centos-2Current path is /root/GithubProjects/sh-utils36+52=88kernel is 3.10.0-862.14.4.el7.x86_6424-bit-color.shdocker-k8s-images.shexport-http-proxy.shget-wechat-cover.shssr.shstart-goland.shtest.shtmux-tools/root/GithubProjects/sh-utils
一般来说,如果命令成功执行,则其返回值为0
,因此可通过下面的方式判断上一条命令的执行结果:
if [ $? -eq 0 ]then echo "success"elif [ $? -eq 1 ]then echo "failed,code is 1"else echo "other code"fi
case
语句的使用方法如下:
name="aa"name="aa"case $name in "aa") echo "name is $name" ;; "") echo "name is empty" ;; "bb") echo "name is $name" ;; *) echo "other name" ;;esac
需要注意以下几点:
[]
前面要有空格,里面是逻辑表达式if elif
后面要跟then
,之后才是要执行的语句$?
赋给一个变量,因为一旦执行了一条命令,$?
的值就可能会变case
语句的每个分支最后以两个;;
结尾,最后是esac
有两种写法:
if [ 10 -gt 5 -o 10 -gt 4 ];then echo "10>5 or 10>4"fi# 或者if [ 10 -gt 5 ] || [ 10 -gt 4 ];then echo "10>5 or 10>4"fi
-a
,同&&
,表示与-o
,同||
,表示或!
,表示非-eq
:两数是否相等-ne
:两数是否不等-gt
:前者是否大于后者-lt
:前者是否小于后者-ge
:前者是否大于等于后者-le
:前者是否小于等于后者-f $filename
:是否为文件-e $filename
:是否存在-d $filename
:是否为目录-s $filename
:文件存在且不为空! -s $filename
:文件是否为空遍历输出脚本的参数:
# 遍历输出脚本的参数for i in $@; do echo $idone
还可以指定循环变量范围:
for i in {1..5}; do echo "Welcome $i"done
在此基础上指定循环步长:
for i in {5..15..3}; do echo "number is $i"done
for ((i = 0 ; i < 10 ; i++)); do echo $idone
while [ "$ans" != "yes" ]do read -p "please input yes to exit loop: " ansdone
ans="yes"until [ "$ans" != "yes" ]do read -p "please input yes to continue loop: " ansdone
函数定义如下:
myfunc(){ echo "Hello! $1"}
或者:
function myfunc(){ echo "Hello! $1"}
函数调用:
para1="abelsu7"myfunc $para1
通常函数的return
返回值只支持0-255
,因此想要获得其他形式的返回值,可以通过下面的方式:
function myfunc(){ local myresult="some value" echo $myresult}val=$(myfunc) # val 的值为 some value
通过return
的方式适用于判断函数的执行是否成功:
function myfunc(){ # do something return 0}if myfunc;then echo "success"else echo "failed"fi
#!/bin/bash# 这是单行注释: << !注释 1注释 2注释 3!: '注释 1注释 2注释 3': << EOF注释 1注释 2注释 3EOF: << 字符 # 数字或者字符均可注释 1注释 2注释 3字符 # 要与之前的字符相同
脚本执行后免不了要记录日志,常用的方法是重定向。
方式一,将标准输出保存到文件中,并在控制台打印标准错误:
./test.sh > log.dat
方式二,将标准输出和标准错误都保存到日志文件中:
./test.sh > log.dat 2>&1
方式三,保存日志文件的同时,也输出到控制台:
./test.sh |tee log.dat
./test.sh # 最常见的执行方式sh test.sh # 在子进程中执行sh -x test.sh # 会在终端打印执行的命令,适合调试source test.sh # 在父进程中执行. test.sh # 不需要赋予执行权限,临时执行
很多时候我们需要获取脚本的执行结果,即退出状态。通常0
表示执行成功,而非0
表示执行失败。
为了获得退出码,我们需要使用exit
,例如:
#!/bin/bashfunction myfun(){ if [ $# -lt 2 ] then echo "para num error" exit 1 fi echo "ok" exit 2}if [ $# -lt 1 ]then echo "para num error" exit 1fireturnVal=`myfun aa`echo "end shell"exit 0
这里需要注意的是,使用:
returnVal=`myfun aa`
这样的语句来执行函数,即使函数里面有exit
,它也不会退出脚本执行,而只是会退出该函数。这是因为exit
是退出当前进程,而这种方式执行函数,相当于fork
了一个子进程,因此不会退出当前脚本。
所以无论你的函数参数是什么,最后都会打印:
./test.sh;echo $?0
]]>
参见 Go in Visual Studio Code | VS Code
]]>
摘自 How to Monitor Your Computer’s CPU Temperature | How-To Geek
传送门 Core Temp
传送门 HWMonitor
sensors
命令]]>
goproxy.io is a global proxy for Go Modules.
# Enable the go modules featureexport GO111MODULE=on# Set the GOPROXY environment variableexport GOPROXY=https://goproxy.io
# Enable the go modules feature$env:GO111MODULE="on"# Set the GOPROXY environment variable$env:GOPROXY="https://goproxy.io"
go env -w GOPROXY=https://goproxy.io,direct# Set environment variable allow bypassing the proxy for selected modulesgo env -w GOPRIVATE=*.corp.example.com
]]>
- Using Go Modules | The Go Blog
- Migrating to Go Modules | The Go Blog
- 拜拜了,GOPATH 君!新版本 Golang 的包管理入门教程 | 知乎
- Go module 机制下升级 major 版本号的实践 | TonyBai
- VSCode 配置 Go 环境及 Go mod 使用 | YSICING
- go mod 使用 | 掘金
- VSCode 配置 Go 环境及 Go mod 使用 | 方缘之道
- 开始使用 Go Module - isLishude | 知乎
- Go Modules 详解 | 后端进阶
- Go Modules 不完全教程 | Golang 成神之路
- Go Modules 不完全教程 - Golang Inside | 知乎专栏
- Go Module 使用实践及问题解决 | banyu
- 【干货】Go Modules 内部分享 | Xuanwo’s Blog
GORM is a fantastic ORM library for Golang.
// TODO: To be updated…
> go get -u github.com/jinzhu/gorm
以MySQL
为例:
package mainimport ( "fmt" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql")type Product struct { gorm.Model Code string Price uint}func main() { // 初始化 MySQL 连接 config := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=%t&loc=%s", "your_username", "your_password", "host:port", "database", true, "Local") db, err := gorm.Open("mysql", config) if err != nil { panic("Failed to connect database") } defer db.Close() // Migrate the schema db.AutoMigrate(&Product{}) // 创建 db.Create(&Product{ Code: "L1212", Price: 1000, }) // 读取 var product Product db.First(&product, 1) // 查询 ID 为 1 的 Product db.First(&product, "code = ?", "L1212") // 查询 code 为 L1212 的 Product // 更新 - 更新 product 的 price 为 2000 db.Model(&product).Update("price", 2000) // 删除 - 删除 product db.Delete(&product)}
首先需要导入目标数据库的驱动程序。例如:
import _ "github.com/go-sql-driver/mysql"
为了方便记住导入路径,GORM
包装了一些驱动:
import _ "github.com/jinzhu/gorm/dialects/mysql"// import _ "github.com/jinzhu/gorm/dialects/postgres"// import _ "github.com/jinzhu/gorm/dialects/sqlite"// import _ "github.com/jinzhu/gorm/dialects/mssql"
注意:为了处理time.Time
,需要包括parseTime
作为参数。更多支持的参数
import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql")func main() { db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") defer db.Close()}
也可以参照之前的形式来写:
// 初始化 MySQL 连接config := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=%t&loc=%s", "your_username", "your_password", "host:port", "database", true, "Local")db, err := gorm.Open("mysql", config)if err != nil { panic("Failed to connect database")}defer db.Close()
import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/postgres")func main() { db, err := gorm.Open("postgres", "host=myhost user=gorm dbname=gorm sslmode=disable password=mypassword") defer db.Close()}
import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite")func main() { db, err := gorm.Open("sqlite3", "/tmp/gorm.db") defer db.Close()}
自动迁移(Auto Migrate)模式将自动保持更新到最新。
注意:自动迁移仅仅会创建表以及缺少的列和索引,并不会改变现有列的类型或删除未使用的列,以保护数据
db.AutoMigrate(&User{})db.AutoMigrate(&User{}, &Product{}, &Order{})// 创建表时添加表后缀db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&Product{})
// 检查模型`User`对应的表是否存在db.HasTable(&User{})// 检查表`users`是否存在db.HasTable("users")
// 为模型`User`创建表db.CreateTable(&User{})// 创建表`users`时将"ENGINE=InnoDB"附加到SQL语句db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&User{})
// 删除模型`User`的表db.DropTable(&User{})// 删除表`users`db.DropTable("users")// 删除模型`User`的表和表`products`db.DropTableIfExists(&User{}, "products")
// 修改模型`User`的description列的数据类型为`text`db.Model(&User{}).ModifyColumn("description", "text")
// 删除模型`User`的description列db.Model(&User{}).DropColumn("description")
// 添加外键// 1st param : 外键字段// 2nd param : 外键表(字段)// 3rd param : ONDELETE// 4th param : ONUPDATEdb.Model(&Product{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT")
// 为`name`列添加索引`idx_user_name`db.Model(&User{}).AddIndex("idx_user_name", "name")// 为`name`, `age`列添加索引`idx_user_name_age`db.Model(&User{}).AddIndex("idx_user_name_age", "name", "age")// 添加唯一索引db.Model(&User{}).AddUniqueIndex("idx_user_name", "name")// 为多列添加唯一索引db.Model(&User{}).AddUniqueIndex("idx_user_name_age", "name", "age")// 删除索引db.Model(&User{}).RemoveIndex("idx_user_name")
package modelimport ( "database/sql" "time" "github.com/jinzhu/gorm")type User struct { gorm.Model Birthday time.Time Age int Name string `gorm:"size:255"` // string 长度默认为 255 Num int `gorm:"AUTO_INCREMENT"` // 自增 CreditCard CreditCard // One-To-One (拥有一个 - CreditCard 表的 UserID 作外键) Emails []Email // One-To-Many (拥有多个 - Email 表的 UserID 作外键) BillingAddress Address // One-To-One (属于 - 本表的 BillingAddressID 作外键) BillingAddressID sql.NullInt64 ShippingAddress Address // One-To-One (属于 - 本表的 ShippingAddressID 作外键) ShippingAddressId int IgnoreMe int `gorm:"-"` // 忽略这个字段 Languages []Language `gorm:"many2many:user_languages;"` // Many-To-Many , `user_languages` 是连接表}type Email struct { ID int UserID int `gorm:"index"` // 外键 (属于), tag `index` 是为该列创建索引 Email string `gorm:"type:varchar(100);unique_index"` // `type` 设置 sql 类型,`unique_index` 为该列设置唯一索引 Subscribed bool}type Address struct { ID int Address1 string `gorm:"not null;unique"` // 设置该字段非空且唯一 Address2 string `gorm:"type:varchar(100);unique"` Post sql.NullString `gorm:"not null"`}type Language struct { ID int Name string `gorm:"index:idx_name_code"` // 创建索引并命名,如果找到其他相同名称的索引则创建组合索引 Code string `gorm:"index:idx_name_code"` // `unique_index` also works}type CreditCard struct { gorm.Model UserID uint Number string}
基本模型定义gorm.Model
,包括字段ID
、CreatedAt
、UpdatedAt
、DeletedAt
。
gorm.Model
在gorm
目录下的model.go
中定义:
// Model base model definition, including fields `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`, which could be embedded in your models// type User struct {// gorm.Model// }type Model struct { ID uint `gorm:"primary_key"` CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time `sql:"index"`}
可以将它嵌入你的模型,或者只写需要的字段:
// 添加字段 `ID`,`CreatedAt`,`UpdatedAt`,`DeletedAt`type User struct { gorm.Model Name string}// 只需要字段 `ID`,`CreatedAt`type User struct { ID uint CreatedAt time.Time Name string}
type User struct {} // 默认表名是 `users`// 设置 User 的表名为 `profiles`func (User) TableName() string { return "profiles"}func (u User) TableName() string { if u.Role == "admin" { return "admin_users" } else { return "users" }}// 全局禁用表名负数db.SingularTable(true) // 如果设置为 true,`User` 的默认表名为 `user`,使用 `TableName` 设置的表名不受影响
可以通过定义DefaultTableNameHandler
对默认表名应用任何规则:
gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string { return "prefix_" + defaultTableName;}
type User struct { ID uint // 列名为 `id` Name string // 列名为 `name` Birthday time.Time // 列名为 `birthday` CreatedAt time.Time // 列名为 `created_at`}// 重设列名type Animal struct { AnimalID int64 `gorm:"column:beast_id"` // 设置列名为 `beast_id` Birthday time.Time `gorm:"column:day_of_the_beast"` // 设置列名为 `day_of_the_beast` Age int64 `gorm:"column:age_of_the_beast"` // 设置列名为 `age_of_the_beast`}
type User struct { ID uint // 字段 `ID` 为默认主键 Name string}// 使用 tag `primary_key` 来设置主键type Animal struct { AnimalID int64 `gorm:"primary_key"` // 设置 AnimalID 为主键 Name string Age int64}
创建具有CreatedAt
字段的记录将被设置为当前时间:
db.Create(&user) // 将会设置 `CreatedAt` 为当前时间// 使用 `Update` 来更改它的值db.Model(&user).Update("created_at", time.Now())
保存具有UpdatedAt
字段的记录将被设置为当前时间:
db.Save(&user) // 将会设置 `UpdatedAt` 为当前时间db.Model(&user).Update("name", "jinzhu") // 将会设置 `UpdatedAt` 为当前时间
删除具有
DeletedAt
字段的记录时,记录本身不会从数据库中删除,只是将字段DeletedAt
设置为当前时间,并且该记录在查询时无法被找到,即软删除。
A
belongs to
association sets up a one-to-one connection with another model, such that each instance of the declaring model “belongs to” one instance of the other model.
例如,如果您的应用程序包含用户和配置文件,并且可以将每个配置文件分配给一个用户:
type User struct { gorm.Model Name string}// `Profile` belongs to `User`, `UserID` is the foreign keytype Profile struct { gorm.Model UserID int User User Name string}
// Enable Logger, show detailed logdb.LogMode(true)// Disable Logger, don't show any logdb.LogMode(false)// Debug a single operation, show detailed log for this operationdb.Debug().Where("name = ?", "jinzhu").First(&User{})
执行任何操作后,如果发生任何错误,GORM 会将其设置为*DB
的Error
字段:
if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil { // 错误处理...}// 如果有多个错误发生,用`GetErrors`获取所有的错误,它返回`[]error`db.First(&user).Limit(10).Find(&users).GetErrors()// 检查是否返回 RecordNotFound 错误db.Where("name = ?", "hello world").First(&user).RecordNotFound()if db.Model(&user).Related(&credit_card).RecordNotFound() { // 没有信用卡被发现处理...}
要在事务中执行一组操作,一般流程如下:
// 开始事务tx := db.Begin()// 在事务中做一些数据库操作(从这一点使用'tx',而不是'db')tx.Create(...)// ...// 发生错误时回滚事务tx.Rollback()// 或提交事务tx.Commit()
一个具体的例子:
func CreateAnimals(db *gorm.DB) err { tx := db.Begin() // 注意,一旦你在一个事务中,使用tx作为数据库句柄 if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { tx.Rollback() return err } if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { tx.Rollback() return err } tx.Commit() return nil}
db.Exec("DROP TABLE users;")db.Exec("UPDATE orders SET shipped_at=? WHERE id IN (?)", time.Now, []int64{11,22,33})// Scantype Result struct { Name string Age int}var result Resultdb.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result)
获取查询结果为*sql.Row
或*sql.Rows
:
row := db.Table("users").Where("name = ?", "jinzhu").Select("name, age").Row() // (*sql.Row)row.Scan(&name, &age)rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error)defer rows.Close()for rows.Next() { ... rows.Scan(&name, &age, &email) ...}// Raw SQLrows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows() // (*sql.Rows, error)defer rows.Close()for rows.Next() { ... rows.Scan(&name, &age, &email) ...}
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error)defer rows.Close()for rows.Next() { var user User db.ScanRows(rows, &user) // do something}
从*gorm.DB
连接获取通用数据库接口*sql.DB
:
// 获取通用数据库对象`*sql.DB`以使用其函数db.DB()// Pingdb.DB().Ping()
设置连接池:
db.DB().SetMaxIdleConns(10)db.DB().SetMaxOpenConns(100)
将多个字段设置为主键以启用复合主键:
type Product struct { ID string `gorm:"primary_key"` LanguageCode string `gorm:"primary_key"`}
Gorm 有内置的日志记录器支持,默认情况下,它会打印发生的错误:
// 启用Logger,显示详细日志db.LogMode(true)// 禁用日志记录器,不显示任何日志db.LogMode(false)// 调试单个操作,显示此操作的详细日志db.Debug().Where("name = ?", "jinzhu").First(&User{})
也可以自定义Logger
:
db.SetLogger(gorm.Logger{revel.TRACE})db.SetLogger(log.New(os.Stdout, "\r\n", 0))
参见:
// TODO: To be updated…
]]>
QEMU 3.1.0 源码学习,更新中…
To be updated…
摘自
qemu-3.1.0/docs/devel/migration.rst
QEMU 中关于保存/恢复正在运行客户机的状态的代码,有两个相对应的操作:
Saving the state
Restoring a guest
因此,QEMU 需要在目的宿主机上以相同的参数启动客户机,并且客户机所拥有的设备需要与迁移前保存时所拥有的设备保持一致。
当我们可以保存/恢复客户机之后,还需要另一项功能,即迁移Migration
:
迁移意味着源宿主机上运行的 QEMU 可以被迁移至目标宿主机继续运行
而 KVM 虚拟机的迁移又可分为以下两种:
static migration
,又称冷迁移cold migration
live migration
,又称热迁移hot migration
其中动态迁移值得更多关注,因为运行中的客户机有很多的状态(例如RAM
),而动态迁移可以保证客户机在保持运行的情况下,将这些状态一并迁移至目标宿主机。
当然,客户机并不是真的一直处于运行态。当客户机所有的相关数据都已迁移至目标宿主机后,源宿主机上的客户机就会停止运行。而在目标宿主机上的客户机重新运行之前,还会有一段停机时间service down-time
,通常情况下为几百毫秒以内。
迁移的数据流一般都是字节流byte stream
,可以通过常见的协议进行传递:
tcp migration
:使用 TCP 套接字TCP Sockets
完成迁移unix migration
:使用 UNIX 套接字UNIX Sockets
完成迁移exec migration
:使用进程的标准输入/输出stdin/stdout
完成迁移fd migration
:使用传递给 QEMU 的文件描述符fd
完成迁移,并且 QEMU 不需要关心这个fd
是如何打开的In addition, support is included for migration using
RDMA
, which transports the page data usingRDMA
, where the hardware takes care of transporting the pages, and the load on the CPU is much lower. While the internals ofRDMA
migration are a bit different, this isn’t really visible outside the RAM migration code.
所有的迁移协议使用相同的infrastructure
来保存/恢复虚拟机的设备。
持有迁移数据流的文件、套接字sockets
、文件描述符fd
都抽象在migration/qemu-file.h
中的QEMUFile
结构体中。
结构体QEMUFile
在qemu-file.c
中的定义如下:
#define IOV_MAX 1024 /* 定义在 include/qemu/osdep.h 中 */...#define IO_BUF_SIZE 32768#define MAX_IOV_SIZE MIN(IOV_MAX, 64)struct QEMUFile { const QEMUFileOps *ops; const QEMUFileHooks *hooks; void *opaque; int64_t bytes_xfer; int64_t xfer_limit; int64_t pos; /* start of buffer when writing, end of buffer when reading */ int buf_index; int buf_size; /* 0 when writing */ uint8_t buf[IO_BUF_SIZE]; DECLARE_BITMAP(may_free, MAX_IOV_SIZE); struct iovec iov[MAX_IOV_SIZE]; unsigned int iovcnt; int last_error;};
结构体QIOChannel
在include/io/channel.h
中的定义如下:
/** * QIOChannel: * * The QIOChannel defines the core API for a generic I/O channel * class hierarchy. It is inspired by GIOChannel, but has the * following differences * * - Use QOM to properly support arbitrary subclassing * - Support use of iovecs for efficient I/O with multiple blocks * - None of the character set translation, binary data exclusively * - Direct support for QEMU Error object reporting * - File descriptor passing * * This base class is abstract so cannot be instantiated. There * will be subclasses for dealing with sockets, files, and higher * level protocols such as TLS, WebSocket, etc. */struct QIOChannel { Object parent; unsigned int features; /* bitmask of QIOChannelFeatures */ char *name; AioContext *ctx; Coroutine *read_coroutine; Coroutine *write_coroutine;#ifdef _WIN32 HANDLE event; /* For use with GSource on Win32 */#endif};
大多数情况下,
QEMUFile
都和QIOChannel
的subtype
相互关联,例如QIOChannelTLS
、QIOChannelFile
、QIOChannelSocket
对于大多数的设备,只需要调用一次common infrastructure
即可,这些被称为non-iterative devices
。这些设备的数据在precopy migration
预拷贝迁移阶段的最后被传送,此时虚拟机的 CPU 处于暂停状态。
而对于iterative devices
,包含的数据量很大,例如内存RAM
或large tables
。
略
大部分的设备数据可以使用include/migration/vmstate.h
中的VMSTATE
宏定义来描述。
结构体VMStateDescription
在include/migration/vmstate.h
中定义如下:
struct VMStateDescription { const char *name; int unmigratable; int version_id; int minimum_version_id; int minimum_version_id_old; MigrationPriority priority; LoadStateHandler *load_state_old; int (*pre_load)(void *opaque); int (*post_load)(void *opaque, int version_id); int (*pre_save)(void *opaque); bool (*needed)(void *opaque); const VMStateField *fields; const VMStateDescription **subsections;};
而在hw/input/pckbd.c
中,vmstate_kdb
定义如下:
static const VMStateDescription vmstate_kbd = { .name = "pckbd", .version_id = 3, .minimum_version_id = 3, .post_load = kbd_post_load, .fields = (VMStateField[]) { VMSTATE_UINT8(write_cmd, KBDState), VMSTATE_UINT8(status, KBDState), VMSTATE_UINT8(mode, KBDState), VMSTATE_UINT8(pending, KBDState), VMSTATE_END_OF_LIST() }, .subsections = (const VMStateDescription*[]) { &vmstate_kbd_outport, NULL }};
与VMState
相对应的是 QEMU 早期的实现方式:每个被迁移的设备需要注册两个函数,一个用来保存状态,另一个用来恢复状态。
函数register_savevm_live
在migration/savevm.c
中的定义如下:
/* TODO: Individual devices generally have very little idea about the rest of the system, so instance_id should be removed/replaced. Meanwhile pass -1 as instance_id if you do not already have a clearly distinguishing id for all instances of your device class. */int register_savevm_live(DeviceState *dev, const char *idstr, int instance_id, int version_id, SaveVMHandlers *ops, void *opaque){ SaveStateEntry *se; se = g_new0(SaveStateEntry, 1); se->version_id = version_id; se->section_id = savevm_state.global_section_id++; se->ops = ops; se->opaque = opaque; se->vmsd = NULL; /* if this is a live_savem then set is_ram */ if (ops->save_setup != NULL) { se->is_ram = 1; } if (dev) { char *id = qdev_get_dev_path(dev); if (id) { if (snprintf(se->idstr, sizeof(se->idstr), "%s/", id) >= sizeof(se->idstr)) { error_report("Path too long for VMState (%s)", id); g_free(id); g_free(se); return -1; } g_free(id); se->compat = g_new0(CompatEntry, 1); pstrcpy(se->compat->idstr, sizeof(se->compat->idstr), idstr); se->compat->instance_id = instance_id == -1 ? calculate_compat_instance_id(idstr) : instance_id; instance_id = -1; } } pstrcat(se->idstr, sizeof(se->idstr), idstr); if (instance_id == -1) { se->instance_id = calculate_new_instance_id(se->idstr); } else { se->instance_id = instance_id; } assert(!se->compat || se->instance_id == 0); savevm_state_handler_insert(se); return 0;}
而ops
是一个指向SaveVMHanlers
的指针对象,结构体SaveVMHandlers
在include/migration/register.h
中的定义如下:
typedef struct SaveVMHandlers { /* This runs inside the iothread lock. */ SaveStateHandler *save_state; void (*save_cleanup)(void *opaque); int (*save_live_complete_postcopy)(QEMUFile *f, void *opaque); int (*save_live_complete_precopy)(QEMUFile *f, void *opaque); /* This runs both outside and inside the iothread lock. */ bool (*is_active)(void *opaque); bool (*has_postcopy)(void *opaque); /* is_active_iterate * If it is not NULL then qemu_savevm_state_iterate will skip iteration if * it returns false. For example, it is needed for only-postcopy-states, * which needs to be handled by qemu_savevm_state_setup and * qemu_savevm_state_pending, but do not need iterations until not in * postcopy stage. */ bool (*is_active_iterate)(void *opaque); /* This runs outside the iothread lock in the migration case, and * within the lock in the savevm case. The callback had better only * use data that is local to the migration thread or protected * by other locks. */ int (*save_live_iterate)(QEMUFile *f, void *opaque); /* This runs outside the iothread lock! */ int (*save_setup)(QEMUFile *f, void *opaque); void (*save_live_pending)(QEMUFile *f, void *opaque, uint64_t threshold_size, uint64_t *res_precopy_only, uint64_t *res_compatible, uint64_t *res_postcopy_only); /* Note for save_live_pending: * - res_precopy_only is for data which must be migrated in precopy phase * or in stopped state, in other words - before target vm start * - res_compatible is for data which may be migrated in any phase * - res_postcopy_only is for data which must be migrated in postcopy phase * or in stopped state, in other words - after source vm stop * * Sum of res_postcopy_only, res_compatible and res_postcopy_only is the * whole amount of pending data. */ LoadStateHandler *load_state; int (*load_setup)(QEMUFile *f, void *opaque); int (*load_cleanup)(void *opaque); /* Called when postcopy migration wants to resume from failure */ int (*resume_prepare)(MigrationState *s, void *opaque);} SaveVMHandlers;
可以看到有以下两个指针对象:
typedef struct SaveVMHandlers { /* This runs inside the iothread lock. */ SaveStateHandler *save_state; ... LoadStateHandler *load_state; ...} SaveVMHandlers;
而在include/qemu/typedefs.h
中:
typedef void SaveStateHandler(QEMUFile *f, void *opaque);typedef int LoadStateHandler(QEMUFile *f, void *opaque, int version_id);
值得注意的是:
load_state
需要接收version_id
作为参数,以便确认正在接收的状态数据的格式。而save_state
不需要version_id
参数,因为它总是会保存最新的状态
QEMU 之后应该会逐渐用VMState
的方式来替代现有的VMState macros
:
Note that because the VMState macros still save the data in a raw format, in many cases it’s possible to replace legacy code with a carefully constructed VMState description that matches the byte layout of the existing code.
未完待续…
先看一张图:
QEMU 作为设备模拟器,可以模拟多种处理器架构。其中,待模拟的架构称为Target
,而 QEMU 运行的系统环境称为Host
。
QEMU 中有一个模块叫做Tiny Code Generator
,简称TCG
,负责将Target Code
动态的翻译为Host Code
,也即TCG Target
。
因此我们也可以将在模拟处理器上运行的代码 (OS + UserTools) 称为Guest Code
。QEMU 的作用就是将Guest Code
提取出来,并将其转换为Host Specific Code
。
QEMU 的启动过程涉及以下几个重要的源文件:
vl.c
cpus.c
exec.c
cpu-exec.c
在vl.c
中定义了启动入口main
函数,负责根据传入的参数例如RAM
、CPU
、devices
来建立虚拟机的运行环境。CPU 的执行也是从此处开始的。
所有与虚拟硬件设备相关的代码都在hw/
目录下。
在target/
目录下:
> pwd/kvm/qemu-src/qemu-3.1.0/target> lltotal 28Kdrwxr-xr-x 2 ibm ibm 269 Dec 12 2018 alphadrwxr-xr-x 2 ibm ibm 4.0K Dec 12 2018 armdrwxr-xr-x 2 ibm ibm 296 Dec 12 2018 crisdrwxr-xr-x 2 ibm ibm 214 Dec 12 2018 hppadrwxr-xr-x 3 ibm ibm 4.0K Dec 12 2018 i386drwxr-xr-x 2 ibm ibm 219 Dec 12 2018 lm32drwxr-xr-x 2 ibm ibm 299 Dec 12 2018 m68kdrwxr-xr-x 2 ibm ibm 210 Dec 12 2018 microblazedrwxr-xr-x 2 ibm ibm 4.0K Dec 12 2018 mipsdrwxr-xr-x 2 ibm ibm 164 Dec 12 2018 moxiedrwxr-xr-x 2 ibm ibm 166 Dec 12 2018 nios2drwxr-xr-x 2 ibm ibm 319 Dec 12 2018 openriscdrwxr-xr-x 3 ibm ibm 4.0K Dec 12 2018 ppcdrwxr-xr-x 2 ibm ibm 243 Dec 12 2018 riscvdrwxr-xr-x 2 ibm ibm 4.0K Dec 12 2018 s390xdrwxr-xr-x 2 ibm ibm 192 Dec 12 2018 sh4drwxr-xr-x 2 ibm ibm 4.0K Dec 12 2018 sparcdrwxr-xr-x 2 ibm ibm 168 Dec 12 2018 tilegxdrwxr-xr-x 2 ibm ibm 223 Dec 12 2018 tricoredrwxr-xr-x 2 ibm ibm 179 Dec 12 2018 unicore32drwxr-xr-x 8 ibm ibm 4.0K Dec 12 2018 xtensa
在tcg
目录下:
> pwd/kvm/qemu-src/qemu-3.1.0/tcg> lltotal 576Kdrwxr-xr-x 2 ibm ibm 74 Dec 12 2018 aarch64drwxr-xr-x 2 ibm ibm 50 Dec 12 2018 armdrwxr-xr-x 2 ibm ibm 74 Dec 12 2018 i386-rw-r--r-- 1 ibm ibm 146 Dec 12 2018 LICENSEdrwxr-xr-x 2 ibm ibm 50 Dec 12 2018 mips-rw-r--r-- 1 ibm ibm 48K Dec 12 2018 optimize.cdrwxr-xr-x 2 ibm ibm 50 Dec 12 2018 ppc-rw-r--r-- 1 ibm ibm 22K Dec 12 2018 READMEdrwxr-xr-x 2 ibm ibm 50 Dec 12 2018 s390drwxr-xr-x 2 ibm ibm 50 Dec 12 2018 sparc-rw-r--r-- 1 ibm ibm 122K Dec 12 2018 tcg.c-rw-r--r-- 1 ibm ibm 1.6K Dec 12 2018 tcg-common.c-rw-r--r-- 1 ibm ibm 1.8K Dec 12 2018 tcg-gvec-desc.h-rw-r--r-- 1 ibm ibm 47K Dec 12 2018 tcg.h-rw-r--r-- 1 ibm ibm 3.0K Dec 12 2018 tcg-ldst.inc.c-rw-r--r-- 1 ibm ibm 2.0K Dec 12 2018 tcg-mo.h-rw-r--r-- 1 ibm ibm 94K Dec 12 2018 tcg-op.c-rw-r--r-- 1 ibm ibm 11K Dec 12 2018 tcg-opc.h-rw-r--r-- 1 ibm ibm 72K Dec 12 2018 tcg-op-gvec.c-rw-r--r-- 1 ibm ibm 15K Dec 12 2018 tcg-op-gvec.h-rw-r--r-- 1 ibm ibm 49K Dec 12 2018 tcg-op.h-rw-r--r-- 1 ibm ibm 11K Dec 12 2018 tcg-op-vec.c-rw-r--r-- 1 ibm ibm 5.2K Dec 12 2018 tcg-pool.inc.cdrwxr-xr-x 2 ibm ibm 64 Dec 12 2018 tci-rw-r--r-- 1 ibm ibm 39K Dec 12 2018 tci.c-rw-r--r-- 1 ibm ibm 394 Dec 12 2018 TODO
/vl.c
:包含了main
函数,负责启动虚拟机、运行 vCPU。main_loop()
也存在于此文件中,虚拟机的转换是在这个循环中进行调用的/target/i386/translate.c
:负责提取Guest Code
并将其转换为平台无关的TCG ops
。转换过程中的单位元是一个TB
即Translation Block
,只有当一个TB
的转换执行结束后,才会轮到下一个TB
。这里是 TCG 的前端/tcg/tcg.c
:TCG 的主要实现代码,这里是 TCG 的后端/tcg/i386/tcg-target.inc.c
:将TCG ops
转换为Host Code
/accel/tcg/cpu-exec.c
:函数int cpu_exec(CPUState *cpu)
会调用函数tb_find()
在 Code Buffer 中查找下一个 TB,这里的 TB 指的是已经翻译成 Host 相关指令的 TB。如果找到了,就会调用cpu_loop_exec_tb()
来在 Host 上执行QEMU 的作用就是,提取
Guest Code
,并将其转换为Host Code
整个转换过程由两部分组成:
第一步由前端完成,
Target Code
的代码块TB
被转换成TCG-ops
(独立于机器的中间代码)第二步由后端完成,利用 Host 架构对应的
TCG
,把由TB
生成的TCP-ops
转换成Host Code
定义在/vl.c
中,函数原型如下:
int main(int argc, char **argv, char **envp);
入口main
函数,解析传入 QEMU 的命令行参数,并以此初始化 VM,例如内存大小、磁盘大小、启动盘等。
部分内容参考 Living Migrating QEMU-KVM Virtual Machines | Red Hat Developer
QEMU/KVM 在早期版本中就引入了热迁移的支持。一般来说,热迁移需要迁移的src
和dst
可以同时访问虚拟机镜像。一个简单的例子,在同一台 Host 上将QEMU VM
迁移至另一台QEMU VM
。
首先在src
启动一台虚拟机vm1
:
qemu-system-x86_64 --accel kvm -m 2G -smp 2 -hda fedora30.qcow2
之后在dst
以相同的启动命令运行另一台虚拟机vm2
,指定相同的镜像文件,并添加-incoming
参数:
qemu-system-x86_64 --accel kvm -m 2G -smp 2 -hda fedora30.qcow2 -incoming tcp:0:6666
在vm1
中的QEMU monitor
中输入以下命令:
migrate tcp:localhost:6666
大概十几秒之后可以看到vm2
以vm1
暂停之前的状态继续运行,迁移成功。
Guest
的运行现状——内存区域,QEMU 将其视为the entire Guest
。不需要翻译任何关于内存区域的内容,这部分会被迁移代码当作一个黑盒Opaque
,只需将这些内容从src
发送到dst
。这个区域在上图中被标记为灰色Devices
即设备状态,这部分对Guest
来说是可见的,也即 QEMU 内部的状态(因为这些设备由 QEMU 进行模拟并提供给Guest
),因此 QEMU 使用自己的协议发送这部分数据,其中包含了所有对Guest
可见的设备状态Host
上的 QEMU 进程状态(例如通过-smp
指定的 CPU 核数、-m
指定的内存大小等),这部分由 Host 内核中的 KVM 模块提供,因此迁移过程不涉及这部分状态,但需要在迁移之前确保在src
和dst
上这部分状态保持一致,一般以相同的 QEMU 命令行参数启动 QEMU 即可实现迁移的src
和dst
需要满足以下前提条件才可实现热迁移:
NFS
NTP
实现machine type
(当进行跨 QEMU 版本的热迁移时很重要)、RAM
大小热迁移主要有以下三个阶段
Guest
的所有RAM
标记为dirty
dirty RAM page
发送至dst
,直到达到一定的终止条件src
上的Guest
,继续传送剩余的dirty RAM page
以及device state
阶段一、二对应上图中的灰色区域,阶段三对应灰色区域和左边的区域
可以看到热迁移大部分的工作都是在进行RAM
传输,尤其是dirty page
的传输,所以很多对于热迁移的优化也是针对RAM
传输进行优化。
注:
dirty page
指的是在迁移过程中产生变化的memory page
,内存迁移是先把没有变化的内存传输过去,然后逐渐减小dirty page
的大小,最后有短暂的downtime
,把剩下的dirty page
一并传输过去
之后就可以在dst
上继续运行 QEMU 程序了。
注意:当从阶段二向阶段三过渡时,要做一个很重要的决策,即
Guest
会在阶段三暂停运行,所以在第三阶段要尽可能少的迁移页面,以减少停机时间
先来看在QEMU Monitor
输入migrate
命令后,经过的一些函数:
注意:除了
hmp.c
在根目录之外,其他源文件均在migration
目录下
void hmp_migrate() /* hmp.c */ -> void qmp_migrate() /* migration.c */ -> void tcp_start_outgoing_migration() /* socket.c */ -> static void socket_start_outgoing_migration() /* socket.c */ -> static void socket_outgoing_migration() /* socket.c */ -> void migration_channel_connect() /* channel.c */ -> QEMUFile *qemu_fopen_channel_output() /* qemu-file-channel.c */ -> void migrate_fd_connect() /* migration.c */ -> static void *migration_thread() /* migration.c */
在hmp-commands.hx
中可以看到migrate
命令对应的入口函数为hmp_migrate
:
ETEXI { .name = "migrate", .args_type = "detach:-d,blk:-b,inc:-i,resume:-r,uri:s", .params = "[-d] [-b] [-i] [-r] uri", .help = "migrate to URI (using -d to not wait for completion)" "\n\t\t\t -b for migration without shared storage with" " full copy of disk\n\t\t\t -i for migration without " "shared storage with incremental copy of disk " "(base image shared between src and destination)" "\n\t\t\t -r to resume a paused migration", .cmd = hmp_migrate, },STEXI@item migrate [-d] [-b] [-i] @var{uri}@findex migrateMigrate to @var{uri} (using -d to not wait for completion). -b for migration with full copy of disk -i for migration with incremental copy of disk (base image is shared)ETEXI
函数hmp_migrate
在hmp.c
中定义:
void hmp_migrate(Monitor *mon, const QDict *qdict){ /* 省略部分代码 */ qmp_migrate(uri, !!blk, blk, !!inc, inc, false, false, &err); if (err) { hmp_handle_error(mon, &err); return; } /* 省略部分代码 */}
进行迁移逻辑处理的函数跳转到了qmp_migrate
,在migration.c
中定义:
void qmp_migrate(const char *uri, bool has_blk, bool blk, bool has_inc, bool inc, bool has_detach, bool detach, Error **errp){ Error *local_err = NULL; MigrationState *s = migrate_get_current(); const char *p; if (migration_is_setup_or_active(s->state) || s->state == MIGRATION_STATUS_CANCELLING || s->state == MIGRATION_STATUS_COLO) { error_setg(errp, QERR_MIGRATION_ACTIVE); return; } if (runstate_check(RUN_STATE_INMIGRATE)) { error_setg(errp, "Guest is waiting for an incoming migration"); return; } if (migration_is_blocked(errp)) { return; } /* 省略部分代码 */ migrate_init(s); if (strstart(uri, "tcp:", &p)) { tcp_start_outgoing_migration(s, p, &local_err);#ifdef CONFIG_RDMA } else if (strstart(uri, "rdma:", &p)) { rdma_start_outgoing_migration(s, p, &local_err);#endif } else if (strstart(uri, "exec:", &p)) { exec_start_outgoing_migration(s, p, &local_err); } else if (strstart(uri, "unix:", &p)) { unix_start_outgoing_migration(s, p, &local_err); } else if (strstart(uri, "fd:", &p)) { fd_start_outgoing_migration(s, p, &local_err); } else { error_setg(errp, QERR_INVALID_PARAMETER_VALUE, "uri", "a valid migration protocol"); migrate_set_state(&s->state, MIGRATION_STATUS_SETUP, MIGRATION_STATUS_FAILED); block_cleanup_parameters(s); return; } if (local_err) { migrate_fd_error(s, local_err); error_propagate(errp, local_err); return; }}
简单说下这个函数:首先通过migrate_get_current()
获取当前的MigrationState
指针对象,之后检查当前是否已经有迁移进程存在。之后下面的语句:
if (migration_is_blocked(errp)) { return;}.../* migration.c 中定义 */bool migration_is_blocked(Error **errp){ if (qemu_savevm_state_blocked(errp)) { return true; } if (migration_blockers) { error_propagate(errp, error_copy(migration_blockers->data)); return true; } return false;}
这里通过qemu_savevm_state_blocked()
来判断当前虚拟机状态适不适合进行迁移。
最后直接来说上面函数调用栈最下面的migrate_fd_connect()
,通过qemu_thread_create
调用migration_thread
在src
上创建一个迁移线程:
void migrate_fd_connect(MigrationState *s, Error *error_in){ /* 省略之前的语句 */ qemu_thread_create(&s->thread, "live_migration", migration_thread, s, QEMU_THREAD_JOINABLE); s->migration_thread_running = true;}
而migration_thread
同样在migration.c
中定义:
/* * Master migration thread on the source VM. * It drives the migration and pumps the data down the outgoing channel. */static void *migration_thread(void *opaque){ /* 省略部分代码 */ /* 对应 Stage 1 */ qemu_savevm_state_setup(s->to_dst_file); /* 省略部分代码 */ while (s->state == MIGRATION_STATUS_ACTIVE || s->state == MIGRATION_STATUS_POSTCOPY_ACTIVE) { int64_t current_time; if (!qemu_file_rate_limit(s->to_dst_file)) { /* 对应 Stage 2 */ MigIterateState iter_state = migration_iteration_run(s); if (iter_state == MIG_ITERATE_SKIP) { continue; } else if (iter_state == MIG_ITERATE_BREAK) { break; } } if (qemu_file_get_error(s->to_dst_file)) { if (migration_is_setup_or_active(s->state)) { migrate_set_state(&s->state, s->state, MIGRATION_STATUS_FAILED); } trace_migration_thread_file_err(); break; } current_time = qemu_clock_get_ms(QEMU_CLOCK_REALTIME); migration_update_counters(s, current_time); if (qemu_file_rate_limit(s->to_dst_file)) { /* usleep expects microseconds */ g_usleep((s->iteration_start_time + BUFFER_DELAY - current_time) * 1000); } } trace_migration_thread_after_loop(); /* 对应 Stage 3 */ migration_iteration_finish(s); rcu_unregister_thread(); return NULL;}
migration_thread
主要就是用来完成热迁移的三个步骤。
首先来看第一个步骤,qemu_savevm_state_setup
将标记所有的RAM
为dirty
:
void qemu_savevm_state_setup() /* savevm.c */ -> SaveVMHandlers.save_setup /* block-dirty-bitmap.c */ -> static int dirty_bitmap_save_setup() /* block-dirty-bitmap.c */ -> static int init_dirty_bitmap_migration() /* block-dirty-bitmap.c */ -> static void send_bitmap_start() /* block-dirty-bitmap.c */ -> static void qemu_put_bitmap_flags() /* block-dirty-bitmap.c */
未完待续…
KVM 目前支持savevm/loadvm
即快照、静态迁移、动态迁移,可通过快捷键Ctrl+Alt+2
调出qemu-monitor
,并在其中通过migrate
相关命令进行迁移操作。迁移成功完成后,VM 就可以在目标主机上继续运行。
注意:支持在
AMD
和Intel
主机之间进行迁移。通常情况下,64 位的 VM 仅可以被迁移至 64 位的 Host 运行,而 32 位的 VM 则可以迁移至 32 位或 64 位的 Host
未完待续…
]]>
- QEMU
- QEMU Wiki
- KVM
- kvm/kvm.git
- KVM Documents
- Virt Tools | Blogging about open source virtualization
- Migration | KVM
- QEMU Detailed Study | PDF
- QEMU vl.c 源码学习 | CSDN
- QEMU 参数解析 | CSDN
- qemu-kvm 部分流程/源代码分析 | CSDN
- qemu 学习(一)———— qemu 整体流程解读 | CSDN
- qemu 学习(二)———— qemu 中对处理器大小端的设置 | CSDN
- qemu 学习(三)———— qemu 中反汇编操作解析 | CSDN
- QEMU 源码架构 | ChinaUnix
- QEMU 源码分析系列(二) | ChinaUnix
- QEMU-KVM 虚机动态迁移原理 | 51CTO
- 虚拟化在线迁移优化实践(一):KVM虚拟化跨机迁移原理 - UCloud 云计算 | 知乎
- QEMU 热迁移简介 | 不忘初心,方得始终
- Live Migrating QEMU-KVM Virtual Machines | Red Hat Developer
- 虚拟机迁移之热迁移(live_migrate) | 随便写写
- 上面文章博主关于虚拟化的文章列表 | 随便写写
- KVM 虚拟机静态和动态迁移 | bbsmax
- 虚拟机活迁移揭秘 | 博客园
- qemu-kvm-1.1.0 源码中关于迁移的代码分析 | CSDN
- QEMU 源码分析系列(一)| CSDN
- qemu-kvm 虚拟机 live 迁移源代码解读 | CSDN
- QEMU live migration 代码简单剖析 | CSDN
- qemu-kvm savevm/loadvm 流程 | CSDN
- KVM/QEMU 2.3.0 虚拟机动态迁移分析(一)| CSDN
- KVM/QEMU 2.3.0 虚拟机动态迁移分析(二)| CSDN
- KVM/QEMU 2.3.0 虚拟机动态迁移分析(三)| CSDN
- Qemu-KVM 虚拟机初始化及创建过程源码简要分析(一)| CSDN
- Qemu-KVM 虚拟机初始化及创建过程源码简要分析(二)| CSDN
- qemu-kvm 虚拟机live迁移源代码解读 | CSDN
- 北方南方的文章列表 | CSDN
- qemu 迁移代码分析 | 随便写写
- QEMU live migration 代码简单剖析 | ChinaUnix
- qemu-kvm 虚拟机 live 迁移源代码解读 | ChinaUnix
- qemu-kvm 磁盘读写的缓冲(cache)的五种模式 | jusonalien
- 关于追踪 qemu 源码函数路径的一个方法 | jusonalien
- QEMU main 流程分析 | CSDN
- QEMU 翻译块(TB)分析 | CSDN
- 我见过最全的剖析 QEMU 原理的文章 | CSDN
摘自 《SQL 必知必会》
database
:保存有组织的数据的容器。数据库软件通常称为数据库管理系统DBMS
数据库是通过
DBMS
创建和操纵的容器
table
:某种特定类型数据的结构化清单schema
:用来描述数据在表中如何存储,包含存储什么样的数据,数据如何分解,各部分信息如何命名等信息模式可以用来描述数据库中特定的表,也可以用来描述整个数据库(和其中表的关系)
column
:表中的一个字段。表由列组成,列存储表中的某部分信息datatype
:定义了列可以存储那些数据种类row
:表中的一个记录。表中的数据是按行存储的,每条记录存储在自己的行内也可称为数据库记录
record
primary key
:一列(或一组列),其值能够唯一标识表中每一行表中的任何列只要满足以下条件,都可以作为主键:
- 任意一行的主键值唯一
- 主键列不允许
NULL
值- 主键列中的值不允许修改或更新
- 主键值不能重用,即被删除行的主键值不能赋给以后的新行
SQL 即结构化查询语言(Structured Query Language)。与常见的过程式编程语言相比,SQL 更加类似于一种声明式语言。
标准 SQL 由 ANSI 标准委员会管理,从而称为ANSI SQL
。所有主要的 DBMS ,即使有自己的扩展,也都支持ANSI SQL
。
各个 DBMS 关于 SQL 的实现有自己的名称,例如 Oracle 的
PL/SQL
、微软的Transact-SQL
等
SQL 支持以下三种注释风格:
# 注释SELECT *FROM mytable; -- 注释/* 注释 1 注释 2 */
摘自 Sams Teach Yourself SQL in 10 Minutes (Fourth Edition) | Ben Forta
首先创建数据库:
CREATE DATABASE sql_10mins;USE sql_10mins;
之后创建以下五张样例表:
Vendors
表存储供应商信息,每个供应商有唯一的vendor_id
作为主键,用于匹配产品与供应商:
-- ---------------------- Create Vendors table-- --------------------CREATE TABLE Vendors( vend_id char(10) NOT NULL, -- 供应商 ID,主键 vend_name char(50) NOT NULL, -- 供应商名 vend_address char(50) NULL, -- 供应商地址 vend_city char(50) NULL, -- 供应商所在城市 vend_state char(5) NULL, -- 供应商所在州 vend_zip char(10) NULL, -- 供应商邮编 vend_country char(50) NULL -- 供应商所在国家);
Products
表包含产品目录,每行一个产品。每个产品有唯一的prod_id
作为主键,并借助vend_id
作为外键与Vendors
表相关联:
-- ----------------------- Create Products table-- ---------------------CREATE TABLE Products( prod_id char(10) NOT NULL, -- 产品 ID,主键 vend_id char(10) NOT NULL, -- 供应商 ID 外键 prod_name char(255) NOT NULL, -- 产品名 prod_price decimal(8, 2) NOT NULL, -- 产品价格 prod_desc text NULL -- 产品描述);
Customers
表存储所有顾客信息。每个顾客有唯一的cust_id
作为主键:
-- ------------------------ Create Customers table-- ----------------------CREATE TABLE Customers( cust_id char(10) NOT NULL, -- 顾客 ID,主键 cust_name char(50) NOT NULL, -- 顾客名 cust_address char(50) NULL, -- 顾客地址 cust_city char(50) NULL, -- 顾客所在城市 cust_state char(5) NULL, -- 顾客所在州 cust_zip char(10) NULL, -- 顾客邮编 cust_country char(50) NULL, -- 顾客所在国家 cust_contact char(50) NULL, -- 顾客联系名 cust_email char(255) NULL -- 顾客邮件地址);
Orders
表存储顾客订单,每个订单有唯一的编号order_num
作为主键,按cust_id
作为外键关联到Customers
表的相应顾客:
-- --------------------- Create Orders table-- -------------------CREATE TABLE Orders( order_num int NOT NULL, -- 订单号,主键 order_date datetime NOT NULL, -- 订单日期 cust_id char(10) NOT NULL -- 订单顾客 ID,外键);
OrderItems
表存储每个订单中的实际物品,每个订单的每个物品一行。对于Orders
表的每一行,在OrderItems
表中有一行或多行。
每个OrderItem
由订单号order_num
加订单物品号order_item
作为唯一标识的主键,并用order_num
作为外键关联到Orders
表。另外,prod_id
列也作为外键关联到Products
表:
-- ------------------------- Create OrderItems table-- -----------------------CREATE TABLE OrderItems( order_num int NOT NULL, -- 订单号,主键,外键 order_item int NOT NULL, -- 订单物品号 prod_id char(10) NOT NULL, -- 产品 ID,外键 quantity int NOT NULL, -- 物品数量 item_price decimal(8, 2) NOT NULL -- 物品价格);
刚才只是创建了五张表及其各个字段,现在添加主键:
-- --------------------- Define primary keys-- -------------------ALTER TABLE Customers ADD PRIMARY KEY (cust_id);ALTER TABLE OrderItems ADD PRIMARY KEY (order_num, order_item);ALTER TABLE Orders ADD PRIMARY KEY (order_num);ALTER TABLE Products ADD PRIMARY KEY (prod_id);ALTER TABLE Vendors ADD PRIMARY KEY (vend_id);
之后添加外键:
-- --------------------- Define foreign keys-- -------------------ALTER TABLE OrderItems ADD CONSTRAINT FK_OrderItems_Orders FOREIGN KEY (order_num) REFERENCES Orders (order_num);ALTER TABLE OrderItems ADD CONSTRAINT FK_OrderItems_Products FOREIGN KEY (prod_id) REFERENCES Products (prod_id);ALTER TABLE Orders ADD CONSTRAINT FK_Orders_Customers FOREIGN KEY (cust_id) REFERENCES Customers (cust_id);ALTER TABLE Products ADD CONSTRAINT FK_Products_Vendors FOREIGN KEY (vend_id) REFERENCES Vendors (vend_id);
样例数据库sql_10mins
的ER 图如下所示:
Customers
表:
-- -------------------------- Populate Customers table-- ------------------------INSERT INTO Customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)VALUES('1000000001', 'Village Toys', '200 Maple Lane', 'Detroit', 'MI', '44444', 'USA', 'John Smith', 'sales@villagetoys.com');INSERT INTO Customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact)VALUES('1000000002', 'Kids Place', '333 South Lake Drive', 'Columbus', 'OH', '43333', 'USA', 'Michelle Green');INSERT INTO Customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)VALUES('1000000003', 'Fun4All', '1 Sunny Place', 'Muncie', 'IN', '42222', 'USA', 'Jim Jones', 'jjones@fun4all.com');INSERT INTO Customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)VALUES('1000000004', 'Fun4All', '829 Riverside Drive', 'Phoenix', 'AZ', '88888', 'USA', 'Denise L. Stephens', 'dstephens@fun4all.com');INSERT INTO Customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact)VALUES('1000000005', 'The Toy Store', '4545 53rd Street', 'Chicago', 'IL', '54545', 'USA', 'Kim Howard');
Vendors
表:
-- ------------------------ Populate Vendors table-- ----------------------INSERT INTO Vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)VALUES('BRS01','Bears R Us','123 Main Street','Bear Town','MI','44444', 'USA');INSERT INTO Vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)VALUES('BRE02','Bear Emporium','500 Park Street','Anytown','OH','44333', 'USA');INSERT INTO Vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)VALUES('DLL01','Doll House Inc.','555 High Street','Dollsville','CA','99999', 'USA');INSERT INTO Vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)VALUES('FRB01','Furball Inc.','1000 5th Avenue','New York','NY','11111', 'USA');INSERT INTO Vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)VALUES('FNG01','Fun and Games','42 Galaxy Road','London', NULL,'N16 6PS', 'England');INSERT INTO Vendors(vend_id, vend_name, vend_address, vend_city, vend_state, vend_zip, vend_country)VALUES('JTS01','Jouets et ours','1 Rue Amusement','Paris', NULL,'45678', 'France');
Products
表:
-- ------------------------- Populate Products table-- -----------------------INSERT INTO Products (prod_id, vend_id, prod_name, prod_price, prod_desc)VALUES ('BR01', 'BRS01', '8 inch teddy bear', 5.99, '8 inch teddy bear, comes with cap and jacket');INSERT INTO Products (prod_id, vend_id, prod_name, prod_price, prod_desc)VALUES ('BR02', 'BRS01', '12 inch teddy bear', 8.99, '12 inch teddy bear, comes with cap and jacket');INSERT INTO Products (prod_id, vend_id, prod_name, prod_price, prod_desc)VALUES ('BR03', 'BRS01', '18 inch teddy bear', 11.99, '18 inch teddy bear, comes with cap and jacket');INSERT INTO Products (prod_id, vend_id, prod_name, prod_price, prod_desc)VALUES ('BNBG01', 'DLL01', 'Fish bean bag toy', 3.49, 'Fish bean bag toy, complete with bean bag worms with which to feed it');INSERT INTO Products (prod_id, vend_id, prod_name, prod_price, prod_desc)VALUES ('BNBG02', 'DLL01', 'Bird bean bag toy', 3.49, 'Bird bean bag toy, eggs are not included');INSERT INTO Products (prod_id, vend_id, prod_name, prod_price, prod_desc)VALUES ('BNBG03', 'DLL01', 'Rabbit bean bag toy', 3.49, 'Rabbit bean bag toy, comes with bean bag carrots');INSERT INTO Products (prod_id, vend_id, prod_name, prod_price, prod_desc)VALUES ('RGAN01', 'DLL01', 'Raggedy Ann', 4.99, '18 inch Raggedy Ann doll');INSERT INTO Products (prod_id, vend_id, prod_name, prod_price, prod_desc)VALUES ('RYL01', 'FNG01', 'King doll', 9.49, '12 inch king doll with royal garments and crown');INSERT INTO Products (prod_id, vend_id, prod_name, prod_price, prod_desc)VALUES ('RYL02', 'FNG01', 'Queen doll', 9.49, '12 inch queen doll with royal garments and crown');
Orders
表:
-- ----------------------- Populate Orders table-- ---------------------INSERT INTO Orders (order_num, order_date, cust_id)VALUES (20005, '2012-05-01', '1000000001');INSERT INTO Orders (order_num, order_date, cust_id)VALUES (20006, '2012-01-12', '1000000003');INSERT INTO Orders (order_num, order_date, cust_id)VALUES (20007, '2012-01-30', '1000000004');INSERT INTO Orders (order_num, order_date, cust_id)VALUES (20008, '2012-02-03', '1000000005');INSERT INTO Orders (order_num, order_date, cust_id)VALUES (20009, '2012-02-08', '1000000001');
OrderItems
表:
-- --------------------------- Populate OrderItems table-- -------------------------INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20005, 1, 'BR01', 100, 5.49);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20005, 2, 'BR03', 100, 10.99);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20006, 1, 'BR01', 20, 5.99);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20006, 2, 'BR02', 10, 8.99);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20006, 3, 'BR03', 10, 11.99);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20007, 1, 'BR03', 50, 11.49);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20007, 2, 'BNBG01', 100, 2.99);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20007, 3, 'BNBG02', 100, 2.99);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20007, 4, 'BNBG03', 100, 2.99);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20007, 5, 'RGAN01', 50, 4.49);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20008, 1, 'RGAN01', 5, 4.99);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20008, 2, 'BR03', 5, 11.99);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20008, 3, 'BNBG01', 10, 3.49);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20008, 4, 'BNBG02', 10, 3.49);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20008, 5, 'BNBG03', 10, 3.49);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20009, 1, 'BNBG01', 250, 2.49);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20009, 2, 'BNBG02', 250, 2.49);INSERT INTO OrderItems (order_num, order_item, prod_id, quantity, item_price)VALUES (20009, 3, 'BNBG03', 250, 2.49);
Vendors
表:
mysql> describe Vendors;+--------------+----------+------+-----+---------+-------+| Field | Type | Null | Key | Default | Extra |+--------------+----------+------+-----+---------+-------+| vend_id | char(10) | NO | PRI | NULL | || vend_name | char(50) | NO | | NULL | || vend_address | char(50) | YES | | NULL | || vend_city | char(50) | YES | | NULL | || vend_state | char(5) | YES | | NULL | || vend_zip | char(10) | YES | | NULL | || vend_country | char(50) | YES | | NULL | |+--------------+----------+------+-----+---------+-------+7 rows in set (0.00 sec)mysql> select * from Vendors;+---------+-----------------+-----------------+------------+------------+----------+--------------+| vend_id | vend_name | vend_address | vend_city | vend_state | vend_zip | vend_country |+---------+-----------------+-----------------+------------+------------+----------+--------------+| BRE02 | Bear Emporium | 500 Park Street | Anytown | OH | 44333 | USA || BRS01 | Bears R Us | 123 Main Street | Bear Town | MI | 44444 | USA || DLL01 | Doll House Inc. | 555 High Street | Dollsville | CA | 99999 | USA || FNG01 | Fun and Games | 42 Galaxy Road | London | NULL | N16 6PS | England || FRB01 | Furball Inc. | 1000 5th Avenue | New York | NY | 11111 | USA || JTS01 | Jouets et ours | 1 Rue Amusement | Paris | NULL | 45678 | France |+---------+-----------------+-----------------+------------+------------+----------+--------------+6 rows in set (0.00 sec)
Products
表:
mysql> describe Products;+------------+--------------+------+-----+---------+-------+| Field | Type | Null | Key | Default | Extra |+------------+--------------+------+-----+---------+-------+| prod_id | char(10) | NO | PRI | NULL | || vend_id | char(10) | NO | MUL | NULL | || prod_name | char(255) | NO | | NULL | || prod_price | decimal(8,2) | NO | | NULL | || prod_desc | text | YES | | NULL | |+------------+--------------+------+-----+---------+-------+5 rows in set (0.00 sec)mysql> select * from Products;+---------+---------+---------------------+------------+-----------------------------------------------------------------------+| prod_id | vend_id | prod_name | prod_price | prod_desc |+---------+---------+---------------------+------------+-----------------------------------------------------------------------+| BNBG01 | DLL01 | Fish bean bag toy | 3.49 | Fish bean bag toy, complete with bean bag worms with which to feed it || BNBG02 | DLL01 | Bird bean bag toy | 3.49 | Bird bean bag toy, eggs are not included || BNBG03 | DLL01 | Rabbit bean bag toy | 3.49 | Rabbit bean bag toy, comes with bean bag carrots || BR01 | BRS01 | 8 inch teddy bear | 5.99 | 8 inch teddy bear, comes with cap and jacket || BR02 | BRS01 | 12 inch teddy bear | 8.99 | 12 inch teddy bear, comes with cap and jacket || BR03 | BRS01 | 18 inch teddy bear | 11.99 | 18 inch teddy bear, comes with cap and jacket || RGAN01 | DLL01 | Raggedy Ann | 4.99 | 18 inch Raggedy Ann doll || RYL01 | FNG01 | King doll | 9.49 | 12 inch king doll with royal garments and crown || RYL02 | FNG01 | Queen doll | 9.49 | 12 inch queen doll with royal garments and crown |+---------+---------+---------------------+------------+-----------------------------------------------------------------------+9 rows in set (0.00 sec)
Customers
表:
mysql> describe Customers;+--------------+-----------+------+-----+---------+-------+| Field | Type | Null | Key | Default | Extra |+--------------+-----------+------+-----+---------+-------+| cust_id | char(10) | NO | PRI | NULL | || cust_name | char(50) | NO | | NULL | || cust_address | char(50) | YES | | NULL | || cust_city | char(50) | YES | | NULL | || cust_state | char(5) | YES | | NULL | || cust_zip | char(10) | YES | | NULL | || cust_country | char(50) | YES | | NULL | || cust_contact | char(50) | YES | | NULL | || cust_email | char(255) | YES | | NULL | |+--------------+-----------+------+-----+---------+-------+9 rows in set (0.00 sec)mysql> select * from Customers;+------------+---------------+----------------------+-----------+------------+----------+--------------+--------------------+-----------------------+| cust_id | cust_name | cust_address | cust_city | cust_state | cust_zip | cust_country | cust_contact | cust_email |+------------+---------------+----------------------+-----------+------------+----------+--------------+--------------------+-----------------------+| 1000000001 | Village Toys | 200 Maple Lane | Detroit | MI | 44444 | USA | John Smith | sales@villagetoys.com || 1000000002 | Kids Place | 333 South Lake Drive | Columbus | OH | 43333 | USA | Michelle Green | NULL || 1000000003 | Fun4All | 1 Sunny Place | Muncie | IN | 42222 | USA | Jim Jones | jjones@fun4all.com || 1000000004 | Fun4All | 829 Riverside Drive | Phoenix | AZ | 88888 | USA | Denise L. Stephens | dstephens@fun4all.com || 1000000005 | The Toy Store | 4545 53rd Street | Chicago | IL | 54545 | USA | Kim Howard | NULL |+------------+---------------+----------------------+-----------+------------+----------+--------------+--------------------+-----------------------+5 rows in set (0.00 sec)
Orders
表:
mysql> describe Orders;+------------+----------+------+-----+---------+-------+| Field | Type | Null | Key | Default | Extra |+------------+----------+------+-----+---------+-------+| order_num | int(11) | NO | PRI | NULL | || order_date | datetime | NO | | NULL | || cust_id | char(10) | NO | MUL | NULL | |+------------+----------+------+-----+---------+-------+3 rows in set (0.00 sec)mysql> select * from Orders;+-----------+---------------------+------------+| order_num | order_date | cust_id |+-----------+---------------------+------------+| 20005 | 2012-05-01 00:00:00 | 1000000001 || 20006 | 2012-01-12 00:00:00 | 1000000003 || 20007 | 2012-01-30 00:00:00 | 1000000004 || 20008 | 2012-02-03 00:00:00 | 1000000005 || 20009 | 2012-02-08 00:00:00 | 1000000001 |+-----------+---------------------+------------+5 rows in set (0.00 sec)
OrderItems
表:
mysql> describe OrderItems;+------------+--------------+------+-----+---------+-------+| Field | Type | Null | Key | Default | Extra |+------------+--------------+------+-----+---------+-------+| order_num | int(11) | NO | PRI | NULL | || order_item | int(11) | NO | PRI | NULL | || prod_id | char(10) | NO | MUL | NULL | || quantity | int(11) | NO | | NULL | || item_price | decimal(8,2) | NO | | NULL | |+------------+--------------+------+-----+---------+-------+5 rows in set (0.00 sec)mysql> select * from OrderItems;+-----------+------------+---------+----------+------------+| order_num | order_item | prod_id | quantity | item_price |+-----------+------------+---------+----------+------------+| 20005 | 1 | BR01 | 100 | 5.49 || 20005 | 2 | BR03 | 100 | 10.99 || 20006 | 1 | BR01 | 20 | 5.99 || 20006 | 2 | BR02 | 10 | 8.99 || 20006 | 3 | BR03 | 10 | 11.99 || 20007 | 1 | BR03 | 50 | 11.49 || 20007 | 2 | BNBG01 | 100 | 2.99 || 20007 | 3 | BNBG02 | 100 | 2.99 || 20007 | 4 | BNBG03 | 100 | 2.99 || 20007 | 5 | RGAN01 | 50 | 4.49 || 20008 | 1 | RGAN01 | 5 | 4.99 || 20008 | 2 | BR03 | 5 | 11.99 || 20008 | 3 | BNBG01 | 10 | 3.49 || 20008 | 4 | BNBG02 | 10 | 3.49 || 20008 | 5 | BNBG03 | 10 | 3.49 || 20009 | 1 | BNBG01 | 250 | 2.49 || 20009 | 2 | BNBG02 | 250 | 2.49 || 20009 | 3 | BNBG03 | 250 | 2.49 |+-----------+------------+---------+----------+------------+18 rows in set (0.00 sec)
# 检索单个列SELECT prod_name FROM Products;# 检索多个列SELECT prod_id, prod_name, prod_price FROM Products;# 检索所有列SELECT *FROM Products;
mysql> SELECT vend_id -> FROM Products;+---------+| vend_id |+---------+| BRS01 || BRS01 || BRS01 || DLL01 || DLL01 || DLL01 || DLL01 || FNG01 || FNG01 |+---------+9 rows in set (0.00 sec)mysql> SELECT DISTINCT vend_id -> FROM Products;+---------+| vend_id |+---------+| BRS01 || DLL01 || FNG01 |+---------+3 rows in set (0.00 sec)
mysql> SELECT prod_name -> FROM Products;+---------------------+| prod_name |+---------------------+| Fish bean bag toy || Bird bean bag toy || Rabbit bean bag toy || 8 inch teddy bear || 12 inch teddy bear || 18 inch teddy bear || Raggedy Ann || King doll || Queen doll |+---------------------+# 返回前 5 行mysql> SELECT prod_name -> FROM Products -> LIMIT 5;+---------------------+| prod_name |+---------------------+| Fish bean bag toy || Bird bean bag toy || Rabbit bean bag toy || 8 inch teddy bear || 12 inch teddy bear |+---------------------+# 返回从第 3 行 (1+2) 开始,不超过 4 行的数据mysql> SELECT prod_name -> FROM Products -> LIMIT 4 OFFSET 2;+---------------------+| prod_name |+---------------------+| Rabbit bean bag toy || 8 inch teddy bear || 12 inch teddy bear || 18 inch teddy bear |+---------------------+# 返回第 3~6 行mysql> SELECT prod_name -> FROM Products -> LIMIT 2, 4;+---------------------+| prod_name |+---------------------+| Rabbit bean bag toy || 8 inch teddy bear || 12 inch teddy bear || 18 inch teddy bear |+---------------------+4 rows in set (0.00 sec)
注意:确保
ORDER BY
是SELECT
语句中最后一条子句,否则会报错
# 通过选择的列进行排序SELECT prod_nameFROM ProductsORDER BY prod_name;# 通过非选择列进行排序SELECT prod_nameFROM ProductsORDER BY prod_id;# 按多个列排序SELECT prod_id, prod_price, prod_nameFROM ProductsORDER BY prod_price, prod_name;-- 或者ORDER BY 2, 3;
默认升序
ASC
,使用DESC
关键字降序排序
SELECT prod_id, prod_price, prod_nameFROM ProductsORDER BY prod_price DESC, prod_name;
mysql> SELECT prod_name, prod_price -> FROM Products -> WHERE prod_price = 3.49 -> ORDER BY prod_name;+---------------------+------------+| prod_name | prod_price |+---------------------+------------+| Bird bean bag toy | 3.49 || Fish bean bag toy | 3.49 || Rabbit bean bag toy | 3.49 |+---------------------+------------+
操作符 | 说明 |
---|---|
= | 等于 |
<> | 不等于 |
!= | 不等于 |
< | 小于 |
<= | 小于等于 |
!< | 不小于 |
> | 大于 |
>= | 大于等于 |
!> | 不大于 |
BETWEEN | 在两个值之间 |
IS NULL | 为 NULL 值 |
# 范围值检查SELECT prod_name, prod_priceFROM ProductsWHERE prod_price BETWEEN 5 AND 10;# 空值检查SELECT cust_nameFROM CustomersWHERE cust_email IS NULL;
SELECT prod_id, prod_price, prod_nameFROM ProductsWHERE vend_id = 'DLL01' AND prod_price <= 4;
事实上,许多 DBMS 在OR WHERE
子句的第一个条件得到满足的情况下,就不再计算第二个条件了:
SELECT prod_name, prod_priceFROM ProductsWHERE vend_id = 'DLL01' OR vend_id = 'BRS01';
在 SQL 中,AND
的优先级比OR
高,因此尽量使用圆括号明确分组操作符:
SELECT prod_name, prod_priceFROM ProductsWHERE (vend_id = 'DLL01' OR vend_id = 'BRS01') AND prod_price >= 10;
IN
操作符用来指定条件范围:
SELECT prod_name, prod_priceFROM ProductsWHERE vend_id IN ('DLL01', 'BRS01')ORDER BY prod_name;
NOT
关键字用来在WHERE
子句中否定其后条件:
SELECT prod_nameFROM ProductsWHERE NOT vend_id = 'DLL01'ORDER BY prod_name;
这与使用<>
操作符效果相同:
SELECT prod_nameFROM ProductsWHERE vend_id <> 'DLL01'ORDER BY prod_name;
MariaDB 支持使用
NOT
否定IN
、BETWEEN
、EXISTS
子句。大多数 DBMS 允许使用NOT
否定任何条件
关于
MySQL
中通配符和正则表达式的使用,参见 MySQL-通配符与正则表达式的使用 | CSDN
最常用的通配符是百分号%
。在搜索串中,%
表示任意字符出现任意次数:
SELECT prod_id, prod_nameFROM ProductsWHERE prod_name LIKE '%bean bag%';
注意:通配符
%
不会匹配NULL
下划线_
只匹配单个字符,而不是多个字符:
SELECT prod_id, prod_nameFROM ProductsWHERE prod_name LIKE '__ inch teddy bear';------+---------+--------------------+| prod_id | prod_name |+---------+--------------------+| BR02 | 12 inch teddy bear || BR03 | 18 inch teddy bear |+---------+--------------------+
注意:仅在
Access
及SQL Server
中有效
方括号通配符[]
用来指定一个字符集,它会匹配指定位置上的一个字符:
SELECT cust_contactFROM CustomersWHERE cust_contact LIKE '[JM]%'ORDER BY cust_contact;
使用前缀字符^
表示否定:
SELECT cust_contactFROM CustomersWHERE cust_contact LIKE '[^JM]%'ORDER BY cust_contact;
更新中…
]]>
libvirt 是目前使用最为广泛的对 KVM 虚拟机进行管理的工具和 API,主要作为连接底层 Hypervisor 和上层应用程序的一个中间适配层。
libvirt 主要包含三个模块:
libvirtd
:接收并处理 API 请求virt-manager
virsh
是经常会使用到的 KVM 命令行管理工具libvirt 总体上可分为三个层次:
另外,在libvirt 中涉及到以下几个重要概念:
Node
:是一个物理机器,上面可能运行着多个虚拟客户机。Hypervisor
和Domain
都运行在节点上Hypervisor
:也称虚拟机监控器(VMM),如 KVM、Xen、VMWare、Hyper-V 等,是虚拟化中的一个底层软件层,它可以虚拟化一个节点使其运行多个虚拟客户机Domain
:是在Hypervisor
上运行的一个客户机操作系统实例。域也被称为实例(instance)、客户机操作系统(Guest OS)或虚拟机(VM),它们都是同一个概念libvirt 的管理功能主要包含以下五个部分:
libvirtd
守护进程,远程的管理程序就可以连接到该节点进行管理操作。libvirt 支持多种网络远程传输类型,如 SSH、TCP 套接字、Unix domain socket、TLS 的加密传输等。例如,可通过virsh -c qemu+ssh://root@example.com/system
连接到example.com
上,从而管理其上的域libvirtd
守护进程的主机,都可以通过 libvirt 来管理不同类型的存储,如创建不同格式的客户机镜像、挂载 NFS、查看现有的 LVM 卷组、创建新的 LVM 卷组和逻辑卷、对磁盘设备分区、挂载 iSCSI 共享存储、使用 Ceph 系统支持的 RBD 远程存储等在CentOS 7.5
中,部分 libvirt 相关的包如下所示:
> yum install libvirt> rpm -qa | grep libvirtlibvirt-daemon-driver-network-4.5.0-10.el7_6.6.x86_64libvirt-daemon-driver-storage-mpath-4.5.0-10.el7_6.6.x86_64libvirt-libs-4.5.0-10.el7_6.6.x86_64libvirt-daemon-config-network-4.5.0-10.el7_6.6.x86_64libvirt-python-4.5.0-1.el7.x86_64libvirt-daemon-driver-storage-rbd-4.5.0-10.el7_6.6.x86_64libvirt-daemon-kvm-4.5.0-10.el7_6.6.x86_64libvirt-gconfig-1.0.0-1.el7.x86_64libvirt-bash-completion-4.5.0-10.el7_6.6.x86_64libvirt-glib-1.0.0-1.el7.x86_64libvirt-daemon-driver-secret-4.5.0-10.el7_6.6.x86_64libvirt-daemon-driver-lxc-4.5.0-10.el7_6.6.x86_64libvirt-daemon-driver-storage-4.5.0-10.el7_6.6.x86_64libvirt-daemon-driver-nwfilter-4.5.0-10.el7_6.6.x86_64libvirt-daemon-driver-storage-gluster-4.5.0-10.el7_6.6.x86_64libvirt-4.5.0-10.el7_6.6.x86_64libvirt-daemon-4.5.0-10.el7_6.6.x86_64libvirt-daemon-driver-storage-iscsi-4.5.0-10.el7_6.6.x86_64libvirt-daemon-driver-interface-4.5.0-10.el7_6.6.x86_64libvirt-daemon-config-nwfilter-4.5.0-10.el7_6.6.x86_64libvirt-daemon-driver-storage-scsi-4.5.0-10.el7_6.6.x86_64libvirt-devel-4.5.0-10.el7_6.6.x86_64libvirt-client-4.5.0-10.el7_6.6.x86_64libvirt-gobject-1.0.0-1.el7.x86_64libvirt-daemon-driver-nodedev-4.5.0-10.el7_6.6.x86_64libvirt-daemon-driver-qemu-4.5.0-10.el7_6.6.x86_64libvirt-daemon-driver-storage-disk-4.5.0-10.el7_6.6.x86_64libvirt-daemon-driver-storage-core-4.5.0-10.el7_6.6.x86_64libvirt-daemon-driver-storage-logical-4.5.0-10.el7_6.6.x86_64
首先查看libvirtd
的使用说明:
> libvirtd --helpUsage: libvirtd [options]Options: -h | --help Display program help: -v | --verbose Verbose messages. -d | --daemon Run as a daemon & write PID file. -l | --listen Listen for TCP/IP connections. -t | --timeout <secs> Exit after timeout period. -f | --config <file> Configuration file. -V | --version Display version information. -p | --pid-file <file> Change name of PID file.libvirt management daemon: Default paths: Configuration file (unless overridden by -f): /etc/libvirt/libvirtd.conf Sockets: /var/run/libvirt/libvirt-sock /var/run/libvirt/libvirt-sock-ro TLS: CA certificate: /etc/pki/CA/cacert.pem Server certificate: /etc/pki/libvirt/servercert.pem Server private key: /etc/pki/libvirt/private/serverkey.pem PID file (unless overridden by -p): /var/run/libvirtd.pid
以CentOS 7.5
为例,libvirt 的相关配置文件都在/etc/libvirt/
目录中:
/etc/libvirt > ls -hltotal 80K-rw-r--r-- 1 root root 450 Mar 14 18:25 libvirt-admin.conf-rw-r--r-- 1 root root 547 Mar 14 18:25 libvirt.conf-rw-r--r-- 1 root root 17K Mar 14 18:25 libvirtd.conf-rw-r--r-- 1 root root 1.2K Mar 14 18:25 lxc.confdrwx------. 2 root root 4.0K Apr 24 16:29 nwfilterdrwx------. 3 root root 55 May 30 11:18 qemu-rw-r--r-- 1 root root 30K Mar 14 18:25 qemu.conf-rw-r--r-- 1 root root 2.2K Mar 14 18:25 qemu-lockd.confdrwx------. 2 root root 6 Nov 13 2018 secretsdrwxr-xr-x 3 root root 116 Apr 25 09:22 storage-rw-r--r-- 1 root root 3.2K Mar 14 18:25 virtlockd.conf-rw-r--r-- 1 root root 3.2K Mar 14 18:25 virtlogd.conf/etc/libvirt > cd qemu/etc/libvirt/qemu > ls -hltotal 16Kdrwx------. 3 root root 42 Apr 25 10:23 networks-rw------- 1 root root 4.1K May 30 11:18 win10.xml-rw------- 1 root root 4.5K Apr 25 10:21 win7.xml
其中几个重要的配置文件和目录介绍如下:
libvirt.conf
文件用于配置本地默认的URI
连接以及一些常用libvirt
远程连接的别名:
## This can be used to setup URI aliases for frequently# used connection URIs. Aliases may contain only the# characters a-Z, 0-9, _, -.## Following the '=' may be any valid libvirt connection# URI, including arbitrary parameters#uri_aliases = [# "hail=qemu+ssh://root@hail.cloud.example.com/system",# "sleet=qemu+ssh://root@sleet.cloud.example.com/system",#]## These can be used in cases when no URI is supplied by the application# (@uri_default also prevents probing of the hypervisor driver).##uri_default = "qemu:///system"uri_aliases = [ "abelsu7-ubuntu=qemu+ssh://root@abelsu7-ubuntu/system", "centos-1=qemu+ssh://root@centos-1/system"]
配置别名后,即可使用abelsu7-ubuntu
来替代qemu+ssh://root@abelsu7-ubuntu/system
远程的libvirt
连接:
> systemctl reload libvirtd # 重启 libvirtd> virsh -c abelsu7-ubuntu # 使用别名连接至远程 libvirtWelcome to virsh, the virtualization interactive terminal.Type: 'help' for help with commands 'quit' to quitvirsh # hostnameabelsu7-ubuntuvirsh #
当然在代码中也可以使用这个别名,例如以下 Go 代码:
package mainimport ( libvirt "github.com/libvirt/libvirt-go")func main() { // conn, err := libvirt.NewConnect("qemu+ssh://root@abelsu7-ubuntu/system") conn, err := libvirt.NewConnect("abelsu7-ubuntu") ... ...}
libvirtd.conf
是 libvirt 的守护进程libvirtd
的配置文件,被修改后需要让 libvirtd 重新加载配置文件或重启 libvirtd 才会生效。
在
libvirtd.conf
中配置了libvirtd
启动时的许多设置,包括是否建立 TCP、UNIX domain socket 等连接方式及其最大连接数,以及这些连接的认证机制,设置libvirtd
的日志级别等
例如修改libvirtd.conf
中的以下配置:
listen_tls = 0 # 关闭 TLS 安全认证的连接(默认是打开的)listen_tcp = 1 # 打开 TCP 连接(默认是关闭的)tcp_port = "16666" # 设置 TCP 监听的端口unix_sock_dir = "/var/run/libvirt" # 设置 UNIX domain socket 的保存目录auth_tcp = "none" # TCP 连接不使用认证授权方式
要让 TCP、TLS 等连接生效,需要在启动
libvirtd
时加上--listen
参数
修改完成后,使用systemctl
命令重启libvirtd
服务:
> systemctl daemon-reload> systemctl restart libvirtd> systemctl status libvirtd● libvirtd.service - Virtualization daemon Loaded: loaded (/usr/lib/systemd/system/libvirtd.service; enabled; vendor preset: enabled) Active: active (running) since Mon 2019-06-03 11:08:57 CST; 8s ago Docs: man:libvirtd(8) https://libvirt.org Main PID: 12192 (libvirtd) Tasks: 19 (limit: 32768) Memory: 13.6M CGroup: /system.slice/libvirtd.service ├─ 1827 /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/libexec/libvirt_leaseshelper ├─ 1828 /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/libexec/libvirt_leaseshelper └─12192 /usr/sbin/libvirtd --listenJun 03 11:08:57 centos-2 systemd[1]: Starting Virtualization daemon...Jun 03 11:08:57 centos-2 systemd[1]: Started Virtualization daemon.Jun 03 11:08:57 centos-2 dnsmasq[1827]: read /etc/hosts - 5 addressesJun 03 11:08:57 centos-2 dnsmasq[1827]: read /var/lib/libvirt/dnsmasq/default.addnhosts - 0 addressesJun 03 11:08:57 centos-2 dnsmasq-dhcp[1827]: read /var/lib/libvirt/dnsmasq/default.hostsfile
可以看到libvirtd
的启动命令已经添加了--listen
参数。测试一下 TCP 连接是否可用:
> virsh -c qemu+tcp://localhost:16666/systemWelcome to virsh, the virtualization interactive terminal.Type: 'help' for help with commands 'quit' to quitvirsh # exit> ll /var/run/libvirt/total 0drwxr-xr-x 2 root root 100 Jun 3 11:08 networksrwx------ 1 root root 0 Jun 3 11:08 libvirt-admin-socksrwxrwxrwx 1 root root 0 Jun 3 11:08 libvirt-socksrwxrwxrwx 1 root root 0 Jun 3 11:08 libvirt-sock-rodrwxr-xr-x 2 root root 40 May 30 11:32 qemusrw-rw-rw- 1 root root 0 May 30 11:19 virtlogd-admin-sockdrwxr-xr-x 2 root root 40 May 29 09:47 lxcdrwxr-xr-x 2 root root 40 May 29 09:47 hostdevmgrdrwx------ 2 root root 40 May 29 09:47 nwfilter-bindingdrwxr-xr-x 2 root root 40 May 29 09:47 storagesrw-rw-rw- 1 root root 0 May 29 09:47 virtlockd-socksrw-rw-rw- 1 root root 0 May 29 09:47 virtlogd-sock
qemu.conf
是 libvirt 对 QEMU 的驱动配置文件,包括 VNC、SPICE 等,以及连接它们时采用的权限认证方式的配置,也包括内存大页、SELinux、Cgroups 等相关配置。
在qemu
目录下存放的是使用 QEMU 驱动的域的配置文件,查看qemu
目录如下:
> ls -ltotal 16drwx------. 3 root root 42 Apr 25 10:23 networks-rw------- 1 root root 4165 May 30 11:18 win10.xml-rw------- 1 root root 4560 Apr 25 10:21 win7.xml
其中包括了两个域的 XML 配置文件(win10.xml
和win7.xml
),这是使用virt-manager
工具创建的两个域,默认会将其保存到/etc/libvirt/qemu/
目录下。
另一个networks
目录则保存了创建一个域时默认使用的网络配置。
libvirtd
是虚拟化管理系统中服务端的守护进程。要想让某个节点能够利用 libvirt 进行管理,就需要在这个节点上运行libvirtd
守护进程,以便让其他上层管理工具可以连接到该节点。
在RHEL 7.3
和CentOS 7.5
中,libvirtd
是作为一个服务配置在系统中的:
> systemctl list-unit-files | grep libvirtdlibvirtd.service enabled> systemctl is-enabled libvirtdenabled> systemctl is-active libvirtdactive> systemctl status libvirtd● libvirtd.service - Virtualization daemon Loaded: loaded (/usr/lib/systemd/system/libvirtd.service; enabled; vendor preset: enabled) Active: active (running) since Mon 2019-06-03 11:08:57 CST; 22h ago Docs: man:libvirtd(8) https://libvirt.org Main PID: 12192 (libvirtd) Tasks: 20 (limit: 32768) Memory: 23.2M CGroup: /system.slice/libvirtd.service ├─ 1827 /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/libexec/libvirt_leaseshelper ├─ 1828 /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/libexec/libvirt_leaseshelper └─12192 /usr/sbin/libvirtd --listenJun 03 17:19:33 centos-2 libvirtd[12192]: 2019-06-03 09:19:33.679+0000: 12195: warning : qemuProcessVerifyHypervFeatures:3936 : host doesn't support hyper...d' featureJun 03 17:19:33 centos-2 libvirtd[12192]: 2019-06-03 09:19:33.679+0000: 12195: warning : qemuProcessVerifyHypervFeatures:3936 : host doesn't support hyper...c' featureJun 03 17:19:33 centos-2 libvirtd[12192]: 2019-06-03 09:19:33.679+0000: 12195: warning : qemuProcessVerifyHypervFeatures:3936 : host doesn't support hyper...s' featureJun 03 17:20:41 centos-2 dnsmasq-dhcp[1827]: DHCPDISCOVER(virbr0) 192.168.122.176 52:54:00:b1:88:3bJun 03 17:20:41 centos-2 dnsmasq-dhcp[1827]: DHCPOFFER(virbr0) 192.168.122.176 52:54:00:b1:88:3bJun 03 17:20:41 centos-2 dnsmasq-dhcp[1827]: DHCPREQUEST(virbr0) 192.168.122.176 52:54:00:b1:88:3bJun 03 17:20:41 centos-2 dnsmasq-dhcp[1827]: DHCPACK(virbr0) 192.168.122.176 52:54:00:b1:88:3b DESKTOP-RKPPR8AJun 03 17:20:46 centos-2 dnsmasq-dhcp[1827]: DHCPREQUEST(virbr0) 192.168.122.176 52:54:00:b1:88:3bJun 03 17:20:46 centos-2 dnsmasq-dhcp[1827]: DHCPACK(virbr0) 192.168.122.176 52:54:00:b1:88:3b DESKTOP-RKPPR8AJun 03 17:31:35 centos-2 libvirtd[12192]: 2019-06-03 09:31:35.081+0000: 12192: error : qemuMonitorIO:718 : internal error: End of file from qemu monitorHint: Some lines were ellipsized, use -l to show in full.
-l
或者--listen
的命令行参数来开启对libvirtd.conf
配置文件中TCP/IP socket
的监听libvirtd
守护进程的启动或停止,并不会直接影响正在运行中的客户机libvirtd
在启动或重启完成时,只要客户机的 XML 配置文件是存在的,libvirtd
就会自动加载这些客户机的配置,获取它们的信息libvirt
格式的 XML 文件来运行(例如直接通过qemu
命令行启动的客户机),libvirtd
就不会自动发现它们libvirtd
常用的命令行参数如下:
-d
或--daemon
:作为守护进程在后台运行-f
或--config FILE
:指定配置文件为FILE
,默认为/etc/libvirt/libvirtd.conf
-l
或--listen
:监听配置文件中的 TCP Socket-p
或--pid-file FILE
:将libvirtd
进程的 PID 写入FILE
文件中,默认为/var/run/libvirtd.pid
-t
或--timeout SECONDS
:设置超时时间为SECONDS
秒-v
或--verbose
:调整日志级别为Verbose
-V
或--version
:版本号> libvirtd --helpUsage: libvirtd [options]Options: -h | --help Display program help: -v | --verbose Verbose messages. -d | --daemon Run as a daemon & write PID file. -l | --listen Listen for TCP/IP connections. -t | --timeout <secs> Exit after timeout period. -f | --config <file> Configuration file. -V | --version Display version information. -p | --pid-file <file> Change name of PID file.libvirt management daemon: Default paths: Configuration file (unless overridden by -f): /etc/libvirt/libvirtd.conf Sockets: /var/run/libvirt/libvirt-sock /var/run/libvirt/libvirt-sock-ro TLS: CA certificate: /etc/pki/CA/cacert.pem Server certificate: /etc/pki/libvirt/servercert.pem Server private key: /etc/pki/libvirt/private/serverkey.pem PID file (unless overridden by -p): /var/run/libvirtd.pid
在 libvirt 中,客户机(即域)的配置是采用 XML 格式来描述的。例如下面是我使用virt-manager
创建的客户机配置文件fedora-30.xml
:
<!--WARNING: THIS IS AN AUTO-GENERATED FILE. CHANGES TO IT ARE LIKELY TO BEOVERWRITTEN AND LOST. Changes to this xml configuration should be made using: virsh edit fedora-30or other application using the libvirt API.--><domain type='kvm'> <name>fedora-30</name> <uuid>70b8f58a-1589-4b83-bfd1-90dfd0cc8a56</uuid> <memory unit='KiB'>16777216</memory> <currentMemory unit='KiB'>16777216</currentMemory> <vcpu placement='static'>4</vcpu> <os> <type arch='x86_64' machine='pc-i440fx-2.11'>hvm</type> <boot dev='hd'/> </os> <features> <acpi/> <apic/> <vmport state='off'/> </features> <cpu mode='host-model' check='partial'> <model fallback='allow'/> </cpu> <clock offset='utc'> <timer name='rtc' tickpolicy='catchup'/> <timer name='pit' tickpolicy='delay'/> <timer name='hpet' present='no'/> </clock> <on_poweroff>destroy</on_poweroff> <on_reboot>restart</on_reboot> <on_crash>destroy</on_crash> <pm> <suspend-to-mem enabled='no'/> <suspend-to-disk enabled='no'/> </pm> <devices> <emulator>/usr/bin/kvm-spice</emulator> <disk type='file' device='disk'> <driver name='qemu' type='qcow2'/> <source file='/var/lib/libvirt/images/generic.qcow2'/> <target dev='hda' bus='ide'/> <address type='drive' controller='0' bus='0' target='0' unit='0'/> </disk> <disk type='file' device='cdrom'> <driver name='qemu' type='raw'/> <target dev='hdb' bus='ide'/> <readonly/> <address type='drive' controller='0' bus='0' target='0' unit='1'/> </disk> <controller type='usb' index='0' model='ich9-ehci1'> <address type='pci' domain='0x0000' bus='0x00' slot='0x05' function='0x7'/> </controller> <controller type='usb' index='0' model='ich9-uhci1'> <master startport='0'/> <address type='pci' domain='0x0000' bus='0x00' slot='0x05' function='0x0' multifunction='on'/> </controller> <controller type='usb' index='0' model='ich9-uhci2'> <master startport='2'/> <address type='pci' domain='0x0000' bus='0x00' slot='0x05' function='0x1'/> </controller> <controller type='usb' index='0' model='ich9-uhci3'> <master startport='4'/> <address type='pci' domain='0x0000' bus='0x00' slot='0x05' function='0x2'/> </controller> <controller type='pci' index='0' model='pci-root'/> <controller type='ide' index='0'> <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x1'/> </controller> <controller type='virtio-serial' index='0'> <address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/> </controller> <interface type='network'> <mac address='52:54:00:36:59:c3'/> <source network='default'/> <model type='rtl8139'/> <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> </interface> <serial type='pty'> <target type='isa-serial' port='0'> <model name='isa-serial'/> </target> </serial> <console type='pty'> <target type='serial' port='0'/> </console> <channel type='spicevmc'> <target type='virtio' name='com.redhat.spice.0'/> <address type='virtio-serial' controller='0' bus='0' port='1'/> </channel> <input type='mouse' bus='ps2'/> <input type='keyboard' bus='ps2'/> <graphics type='spice' autoport='yes' listen='0.0.0.0'> <listen type='address' address='0.0.0.0'/> <image compression='off'/> </graphics> <sound model='ich6'> <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/> </sound> <video> <model type='qxl' ram='65536' vram='65536' vgamem='16384' heads='1' primary='yes'/> <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/> </video> <redirdev bus='usb' type='spicevmc'> <address type='usb' bus='0' port='1'/> </redirdev> <redirdev bus='usb' type='spicevmc'> <address type='usb' bus='0' port='2'/> </redirdev> <memballoon model='virtio'> <address type='pci' domain='0x0000' bus='0x00' slot='0x07' function='0x0'/> </memballoon> </devices></domain>
如需编辑
fedora-30.xml
,请使用virsh edit fedora-30
命令
在该域的 XML 文件中,所有的有效配置都在<domain>
和</domain>
标签之间,这表明该配置文件是一个域的配置。
通过 libvirt 启动客户机,经过文件解析和命令参数的转换,最终也会调用qemu
命令行工具来实际完成客户机的创建。该命令行参数非常详尽冗长,通过ps
命令查询到的创建命令如下:
> ps -ef | grep qemu | grep fedora-30libvirt+ 20817 1 0 10:23 ? 00:02:39 qemu-system-x86_64 -enable-kvm -name guest=fedora-30,debug-threads=on -S -object secret,id=masterKey0,format=raw,file=/var/lib/libvirt/qemu/domain-35-fedora-30/master-key.aes -machine pc-i440fx-2.11,accel=kvm,usb=off,vmport=off,dump-guest-core=off -cpu Skylake-Client-IBRS,ss=on,vmx=on,hypervisor=on,tsc_adjust=on,clflushopt=on,ssbd=on,xsaves=on,pdpe1gb=on -m 16384 -realtime mlock=off -smp 4,sockets=4,cores=1,threads=1 -uuid 70b8f58a-1589-4b83-bfd1-90dfd0cc8a56 -no-user-config -nodefaults -chardev socket,id=charmonitor,path=/var/lib/libvirt/qemu/domain-35-fedora-30/monitor.sock,server,nowait -mon chardev=charmonitor,id=monitor,mode=control -rtc base=utc,driftfix=slew -global kvm-pit.lost_tick_policy=delay -no-hpet -no-shutdown -global PIIX4_PM.disable_s3=1 -global PIIX4_PM.disable_s4=1 -boot strict=on -device ich9-usb-ehci1,id=usb,bus=pci.0,addr=0x5.0x7 -device ich9-usb-uhci1,masterbus=usb.0,firstport=0,bus=pci.0,multifunction=on,addr=0x5 -device ich9-usb-uhci2,masterbus=usb.0,firstport=2,bus=pci.0,addr=0x5.0x1 -device ich9-usb-uhci3,masterbus=usb.0,firstport=4,bus=pci.0,addr=0x5.0x2 -device virtio-serial-pci,id=virtio-serial0,bus=pci.0,addr=0x6 -drive file=/var/lib/libvirt/images/generic.qcow2,format=qcow2,if=none,id=drive-ide0-0-0 -device ide-hd,bus=ide.0,unit=0,drive=drive-ide0-0-0,id=ide0-0-0,bootindex=1 -drive if=none,id=drive-ide0-0-1,readonly=on -device ide-cd,bus=ide.0,unit=1,drive=drive-ide0-0-1,id=ide0-0-1 -netdev tap,fd=27,id=hostnet0 -device rtl8139,netdev=hostnet0,id=net0,mac=52:54:00:36:59:c3,bus=pci.0,addr=0x3 -chardev pty,id=charserial0 -device isa-serial,chardev=charserial0,id=serial0 -chardev spicevmc,id=charchannel0,name=vdagent -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=com.redhat.spice.0 -spice port=5900,addr=0.0.0.0,disable-ticketing,image-compression=off,seamless-migration=on -device qxl-vga,id=video0,ram_size=67108864,vram_size=67108864,vram64_size_mb=0,vgamem_mb=16,max_outputs=1,bus=pci.0,addr=0x2 -device intel-hda,id=sound0,bus=pci.0,addr=0x4 -device hda-duplex,id=sound0-codec0,bus=sound0.0,cad=0 -chardev spicevmc,id=charredir0,name=usbredir -device usb-redir,chardev=charredir0,id=redir0,bus=usb.0,port=1 -chardev spicevmc,id=charredir1,name=usbredir -device usb-redir,chardev=charredir1,id=redir1,bus=usb.0,port=2 -device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x7 -msg timestamp=on
关于 CPU 的配置如下:
<vcpu placement='static'>4</vcpu><features> <acpi/> <apic/></features><cpu mode='custom' match='exact'> <model fallback='allow'>Haswell-noTSX</model></cpu>
关于内存的配置如下:
<domain> ... <memory unit='KiB'>16777216</memory> <currentMemory unit='KiB'>16777216</currentMemory> ...</domain>
关于客户机类型和启动顺序配置如下:
<os> <type arch='x86_64' machine='pc-i440fx-2.11'>hvm</type> <boot dev='hd'/> <boot dev='cdrom'/></os>
更新中…
]]>
Python 基础速查笔记,摘自 《Python 编程快速上手》
操作符 | 操作 | 例子 | 求值 |
---|---|---|---|
** | 指数 | 2 ** 3 | 8 |
% | 取模 | 22 % 8 | 6 |
// | 整除 | 22 // 8 | 2 |
/ | 除法 | 22 / 8 | 2.75 |
* | 乘法 | 3 * 5 | 15 |
- | 减法 | 5 - 2 | 3 |
+ | 加法 | 2 + 2 | 4 |
数据类型 | 例子 |
---|---|
整型 int | 86 |
浮点型 float | 3.14159 |
字符串 str | 'Abel Su' |
>>> 'Abel' + 'Su''AbelSu'>>> 'abel' * 5'abelabelabelabelabel'
>>> name = 'Abel'>>> name'Abel'>>> name = 'Yuki''Yuki'
#!/usr/local/bin/python3print('Hello Python!')print("What's your name?") # waiting for inputname = input()print('The length of your name is: ' + str(len(name)))
print()
:打印input()
:读取输入len()
:返回长度str()
:转换为字符串int()
:截断取整float()
:转换为浮点数首字母大写:
True
False
# 比较操作符==!=<><=>=# 布尔操作符andornot
#!/usr/local/bin/python3if name == 'Alice': print('Hi, Alice.')elif age < 12: print('You are not Alice, kiddo.')elif age > 2000: print('Unlike you, Alice is not an undead, immortal vampire.')elif age > 100: print('You are not Alice, grannie.')else: print('You are neither Alice nor a little kid')
#!/usr/local/bin/python3name = ''while name != 'your name': print('Please type your name') name = input()print('Thank you!')
也可以用break
跳出当前循环:
#!/usr/local/bin/python3name = ''while True: print('Please type your name') name = input() if name == 'your name': breakprint('Thank you!')
还可以用continue
跳过之后的语句,进入下一次循环:
#!/usr/local/bin/python3while True: print('Who are you?') name = input() if name != 'Joe': continue print('Hello, Joe. What is the password?') password = input() if password == 'swordfish': breakprint('Access granted.')
range()
的三个参数分别为:起始、停止、步长
#!/usr/local/bin/python3for i in range(0, 10, 2): print(i)
#!/usr/local/bin/python3import randomfor i in range(5): print(random.randint(1, 10))
或者使用from import
语句,此时调用randint
函数不需要random.
前缀:
#!/usr/local/bin/python3from random import *for i in range(5): print(randint(1, 10))
调用sys.exit()
函数,可以让程序提前终止:
#!/usr/local/bin/python3import syswhile True: print('Type exit to exit') response = input() if response == 'exit': sys.exit() print('You typed ' + response + '.')
#!/usr/local/bin/python3def hello(name): print('Hello ' + name)hello('Alice')hello('Bob')
#!/usr/local/bin/python3import randomdef getAnswer(answerNumber): if answerNumber == 1: return 'It is certain' elif answerNumber == 2: return 'It is decidedly so' elif answerNumber == 3: return 'Yes' elif answerNumber == 4: return 'Reply hazy try again' elif answerNumber == 5: return 'Ask again later' elif answerNumber == 6: return 'Concentrate and ask again' elif answerNumber == 7: return 'My reply is no' elif answerNumber == 8: return 'Outlook not so good' elif answerNumber == 9: return 'Very doubtful'r = random.randint(1, 9)fortune = getAnswer(r)print(fortune)
None
值是NoneType
数据类型的唯一值。
return
语句的函数定义,Python 都会在末尾加上return None
return
语句不带返回值,也会默认返回None
这类似于
while
或for
循环隐式的以continue
语句结尾
>>> spam = print('Hello')Hello>>> None == spamTrue
print()
函数默认在行末打印换行,可以设置end
关键字参数替换行末的换行符:
>>> print('Hello', end=' ')Hello >>>
如果向print()
传入多个字符串值,则该函数会自动的用一个空格来分隔它们:
>>> print('Arsenal', 'Chelsea', 'Liverpool')Arsenal Chelsea Liverpool
传入sep
关键字参数,替换默认的分隔字符串:
>>> print('Arsenal', 'Chelsea', 'Liverpool', sep=',')Arsenal,Chelsea,Liverpool
如果需要在一个函数内修改全局变量,则使用global
语句:
#!/usr/local/bin/python3def spam(): global eggs eggs = 'spam'eggs = 'global'spam()print(eggs)
#!/usr/local/bin/python3def spam(divideBy): try: return 42 / divideBy except ZeroDivisionError: print('Error: Invalid argument')print(spam(2))print(spam(12))print(spam(0))print(spam(1))------21.03.5Error: Invalid argumentNone42.0
更新中…
]]>
C++ 基础速查笔记
更新中…
1979
年在贝尔实验室开发1998
年,第一个 C++ 标准获得了 ISO 标准委员会的批准,俗称C++98
C++11
、C++14
、C++17
#include <iostream>int main(){ std::cout<< "Hello World!" << std::endl; return 0;}
#include
:预处理器编译指令,在编译前运行< >
:包含标准头文件" "
:包含自定义头文件main()
:程序的主体std::count
:命名空间,即namespace
int main(int argc, char* argv[])
int first = 0, second = 0, third = 0;
C++14
新增了用'
表示的组块分隔符chunking separator
,可以提高数字的可读性
int totalCash = 0;bool isLampOn = false;char userInput = 'Y';short int gradesInMath = -5;int moneyInBank = -7'0000;long populationChange = -8'5000;long long countryGDPChange = -700'0000;unsigned short int numColorsInRainbow = 7;unsigned int unmEggsInBasket = 24;unsigned long numCarsInNewYork = 70'0000;unsigned long long countryMedicareExpense = 7'0000'0000;float pi = 3.14;double morePrecisePi = 22.0 / 7;
cout << "Size of an int: " << sizeof(int);
C++11
引入了列表初始化来禁止缩窄转换:
int largeNum = 700'0000;short smallNum{largeNum};
C++11
或更高版本可不显式指定变量的类型,而使用关键字auto
:
auto coinFlippedHeads = true;
注意:如果将变量类型声明为
auto
,但不对其进行初始化,将出现编译错误
使用关键字typedef
定义变量类型:
typedef unsigned int STRICTLY_POSITIVE_INTEGER;STRICTLY_POSITIVE_INTEGER numEggsInBasket = 4532;
在C++
中,常量可以是:
const
声明的常量constexpr
声明的表达式(C++11
新增)enum
声明的枚举常量#define
定义的常量(已废弃)字面常量可以是任何类型。从C++14
开始,还可以使用二进制字面常量:
int someNumber = 10;int someNumber = 012; // 八进制int someNumber = 0b1010; // 二进制
使用关键字const
定义常量,常量定义后不可修改:
const double pi = 22.0 / 7;
常量表达式提供了编译阶段优化的可能性。
只要编译器能够从常量表达式计算出常量,就会在编译阶段将其替换为常量,避免了在代码运行时进行计算。
const double GetPi(){ return 22.0 / 7;}const double TwicePi(){ return 2 * GetPi();}
可使用关键字enum
声明枚举,枚举由一组称为枚举量emumerator
的常量组成:
enum RainbowColors{ Violet, // 0 Indigo, // 1 Blue, // 2 Green = 4, // 4, 显式初始化 Yellow, // 5 Orange = 1, // 1, 显式初始化 Red // 2};
1
0
静态数组的长度在编译阶段就已确定,因此其占据的内存也是固定的。
编译器为静态数组预留的内存量为
sizeof(element-type)*length
const int ARRAY_LENGTH = 5;int myNumbers[ARRAY_LENGTH] = {34, 21, -56, 5002, 1314};int myNumbers[ARRAY_LENGTH] = {}; // {0, 0, 0, 0, 0}int myNumbers[ARRAY_LENGTH] = {34, 21}; // {34, 21, 0, 0, 0,}
int solarPanels[2][3] = {{0, 1, 2}, {3, 4, 5}};
需要包含头文件
#include <vector>
#include <iostream>#include <vector>using namespace std;int main(){ vector<int> dynArray(3); // dynamic array of int dynArray[0] = 365; dynArray[1] = -421; dynArray[2] = 789; cout << "Number of integers in array: " << dynArray.size() << endl; cout << "Enter another element to insert: "; int newValue = 0; cin >> newValue; dynArray.push_back(newValue); cout << "Number of integers in array: " << dynArray.size() << endl; cout << "Last element in array: "; cout << dynArray[dynArray.size() - 1] << endl; return 0;}------Number of integers in array: 3Enter another element to insert: 4Number of integers in array: 4Last element in array: 4
\0
,即终止空字符\0
不会改变数组的长度,只会导致字符串处理到这个位置结束#include <iostream>using namespace std;int main(){ char sayHello[] = {'H', 'e', 'l', 'l', 'o', '\0'}; cout << sayHello << endl; cout << "Size of sayHello: " << sizeof(sayHello); return 0;}------HelloSize of sayHello: 6
不同于字符数组(C 风格字符串实现),std::string
是动态的,在需要存储更多数据时其容量会增大。
#include <iostream>#include <string>using namespace std;int main(){ string greetStr("Hello std::string!"); cout << greetStr << endl; cout << "Enter a line of text:" << endl; string firstLine; getline(cin, firstLine); cout << "Enter another:" << endl; string secondLine; getline(cin, secondLine); cout << "Result of concatenation:" << endl; string concatStr = firstLine + " " + secondLine; cout << concatStr << endl; cout << "Copy of concatenated string:" << endl; string aCopy; aCopy = concatStr; cout << aCopy << endl; cout << "Length of concat string: " << concatStr.length() << endl; return 0;}------Hello std::string!Enter a line of text:this is the first lineEnter another:this is the second lineResult of concatenation:this is the first line this is the second lineCopy of concatenated string:this is the first line this is the second lineLength of concat string: 46
当编译器注意到有两个相邻的字符串字面量后,会将其拼接成一个:
cout << "The first line of content" "The second line of content" "The third line of content" << endl;
num1++
先将右值赋给左值,再将右值递增或递减++num1
先将右值递增或递减,再将结果赋给左值++startValue
优于startValue++
,因为使用后缀运算符时,编译器需要临时存储初始值,以防需要将其赋给其他变量int num1 = 101;int num2 = num1++; // num1 = 102, num2 = 101int num3 = ++num1; // num1 = 103, num3 = 103
!
&&
||
~
&
|
^
<<
,相当于乘以2^n
>>
,相当于除以2^n
if (condition){ do something...}else{ do something else...}
switch (expression){case /* constant-expression */: /* code */ break;default: break;}
int max = (num1 > num2) ? num1 : num2;
while (/* condition */){ /* code */}
do{ /* code */} while (/* condition */);
for (size_t i = 0; i < count; i++){ /* code */}
for (VarType varName : sequence){ do something...}
可以使用关键字auto
自动推断变量的类型:
for (auto anElement : elements)
continue
:跳转到循环开头,跳过本次循环之后的代码,进入下一次循环break
:退出循环块,结束当前循环#include <iostream>using namespace std;const double Pi = 3.14159265;// Function Declarations (Prototypes)double Area(double radius);double Circumference(double radius);int main(int argc, char const *argv[]){ cout << "Enter radius: "; double radius = 0; cin >> radius; // Call function "Area" cout << "Area is: " << Area(radius) << endl; // Call function "Circumference" cout << "Circumference is: " << Circumference(radius) << endl; return 0;}// Function definitions (Implementations)double Area(double radius){ return Pi * radius * radius;}double Circumference(double radius){ return 2 * Pi * radius;}------Enter radius: 10Area is: 314.159Circumference is: 62.8319
在函数原型中可以添加函数参数的默认值:
double Area(double radius, double Pi = 3.14);
// for circledouble Area(double radius){ return Pi * radius * radius;}// overloaded for cylinderdouble Area(double radius, double height){ // reuse the area of circle return 2 * Area(radius) + 2 * Pi * radius * height;}
当希望函数修改的变量在其外部中也可用时,可将形参的类型声明为引用:
#include <iostream>using namespace std;const double Pi = 3.14159265;// output parameter result by referencevoid Area(double radius, double &result){ result = Pi * radius * radius;}int main(int argc, char const *argv[]){ cout << "Enter radius: "; double radius = 0; cin >> radius; double areaFetched = 0; Area(radius, areaFetched); cout << "The area is: " << areaFetched << endl; return 0;}------Enter radius: 10The area is: 314.159
函数调用意味着:
CALL
指令RET
语句常规函数调用被转换为CALL
指令,这会导致栈操作、微处理器跳转到函数处执行等。但如果函数非常简单:
double GetPi(){ return 3.14159;}
相对于实际执行GetPi()
的时间,执行函数调用的开销可能非常高。使用关键字inline
可以让函数在被调用时就地展开:
inline double GetPi(){ return 3.14159;}
仅当函数非常简单,需要降低开销时,才应使用
inline
关键字
从C++14
起,auto
也适用于函数返回类型的自动推断:
auto Area(double radius){ return Pi * radius * radius;}
语法如下,后续会详细介绍:
[optional parameters](parameter list){ statements; }
0x60fe80
&
获取变量的地址*
访问指针指向的数据32
位系统,指针变量为4
字节,64
位系统为8
字节#include <iostream>using namespace std;int main(int argc, char const *argv[]){ int dogsAge = 30; cout << "Initialize dogsAge = " << dogsAge << endl; int *pointsToAnAge = &dogsAge; cout << "pointsToAnAge points to dogsAge" << endl; cout << "Enter an age for your dog: "; // store input at the memory pointed to by pointsToAnAge cin >> *pointsToAnAge; // Displaying the address where age is stored cout << "Integer stored at " << hex << pointsToAnAge << endl; cout << "Integer dogsAge = " << dec << dogsAge << endl; return 0;}------Initialize dogsAge = 30pointsToAnAge points to dogsAgeEnter an age for your dog: 14Integer stored at 0x60fe88Integer dogsAge = 14
使用new
来分配新的内存块,最终都需使用对应的delete
进行释放:
int *pNum = new int; // get a pointer to an integerint *pNums = new int[10]; // pointer to a block of 10 integersdelete pNum;delete[] pNums;
对于使用
new[...]
分配的内存块,需要使用delete[]
进行释放
不再使用分配的内存后,如果不释放它们,这些内存仍被预留并分配给应用程序,这将减少系统内存量,甚至降低应用程序的执行速度,即内存泄露。
#include <iostream>using namespace std;int main(int argc, char const *argv[]){ // Request for memory space for an int int *pointsToAnAge = new int; // Use the allocated memory to store a number cout << "Enter your dog's age: "; cin >> *pointsToAnAge; // Use indirection operator* to access value cout << "Age " << *pointsToAnAge << " is stored at " << hex << pointsToAnAge << endl; // Release memory delete pointsToAnAge; return 0;}------Enter your dog's age: 14Age 14 is stored at 0xf518c0
而对于使用new[...]
分配的内存,应使用delete[]
来释放:
int *myNumbers = new int[numEntries];...// de-allocate before existingdelete[] myNumbers;
将指针递增或递减时,其包含的地址将增加或减少sizeof(Type)
。
例如声明了如下指针:
Type *pType = Address;
则执行++pType
后,pType
将指向Address + sizeof(Type)
。
将
++
用于该指针,相当于告诉编译器,希望它指向下一个int
#include <iostream>using namespace std;int main(int argc, char const *argv[]){ cout << "How many integers you wish to enter? "; int numEntries = 0; cin >> numEntries; int *pointsToInts = new int[numEntries]; cout << "Allocated for " << numEntries << " integers" << endl; for (int counter = 0; counter < numEntries; ++counter) { cout << "Enter number " << counter << ": "; cin >> *(pointsToInts + counter); } cout << "Displaying all numbers entered: " << endl; for (int counter = 0; counter < numEntries; ++counter) { cout << *(pointsToInts++) << " "; } cout << endl; // return pointer to initial position pointsToInts -= numEntries; cout << *pointsToInts; // done with using memory? release delete[] pointsToInts; return 0;}------How many integers you wish to enter? 5Allocated for 5 integersEnter number 0: 10Enter number 1: 29Enter number 2: 35Enter number 3: -3Enter number 4: 918Displaying all numbers entered:10 29 35 -3 91810
注:Golang 从1.13
开始默认使用Go Mod
,请切换至Go Mod
并配置goproxy
国内使用go get
命令时,Google 相关的域名例如golang.org
经常被墙。
注:Gopm 目前已停止维护
使用gopm get
命令替代go get
:
> go get -u github.com/gpmgo/gopm> gopmNAME: Gopm - Go Package ManagerUSAGE: Gopm [global options] command [command options] [arguments...]VERSION: 0.8.8.0307 BetaCOMMANDS: list list all dependencies of current project gen generate a gopmfile for current Go project get fetch remote package(s) and dependencies bin download and link dependencies and build binary config configure gopm settings run link dependencies and go run test link dependencies and go test build link dependencies and go build install link dependencies and go install clean clean all temporary files update check and update gopm resources including itself help, h Shows a list of commands or help for one commandGLOBAL OPTIONS: --noterm, -n disable color output --strict, -s strict mode --debug, -d debug mode --help, -h show help --version, -v print the version
阅读get.go
源码会发现,go get
命令通过git clone
命令将远程仓库的代码拉取到本地。
根据官方 golang/go - GoGetProxyConfig | Github 的说明,需要设置git
的代理:
> git config [--global] http.proxy http://proxy.example.com:port
然而并没有起作用。。
我在
Linux
和Windows
的机器上都开启了Shadowsocks
代理,本地端口为1080
搜索了一圈之后发现,需要设置http_proxy
和https_proxy
这两个环境变量。
我在CentOS 7.5
的机器上已经使用ProxyChains-NG
作为终端命令的代理程序,这样可以很方便的在需要代理的时候在命令前加上pc
前缀(之前设置了alias pc='proxychains4'
)。
而添加http_proxy
环境变量后,所有终端命令的 HTTP 连接都会走代理,这并非我所期望的。因此不能直接在~/.zshrc
中添加环境变量。
我的解决方案是:将设置http_proxy
与https_proxy
环境变量的export
命令单独写在 Shell 脚本中,需要走代理时直接source
即可。
首先以下内容另存为export-http-proxy.sh
:
#!/bin/bashexport http_proxy=socks5://127.0.0.1:1080 # 代理地址export https_proxy=$http_proxyexport | grep http
之后添加执行权限
> chmod +x export-http-proxy.sh
最后
> source export-http-proxy.shhttp_proxy=socks5://127.0.0.1:1080https_proxy=socks5://127.0.0.1:1080
这样就可以直接go get
被墙的包了。
Windows 就非常简单了,直接设置以下环境变量:
http_proxy socks5://127.0.0.1:1080 # 代理地址https_proxy socks5://127.0.0.1:1080
要想临时添加代理,只需将以下内容保存为http-proxy.bat
,需要时执行即可:
set http_proxy=socks5://127.0.0.1:1080set https_proxy=%http_proxy%
]]>
众所周知,技术文章越读越多,永远没个完
升级一时爽,一直升级一直爽
A
,依赖于B
,但是已经安装的C
也依赖于B
,且A
和C
依赖的B
版本不一致> apt-get update> apt-get -f install # 即 --fix-broken,会针对当前不满足的依赖关系,下载正确版本的依赖库> apt-get install [YOUR_PACKAGE_NAME]
> apt show aptitude # 或 apt-cache show aptitudePackage: aptitudeVersion: 0.8.10-6ubuntu1Priority: optionalSection: adminOrigin: UbuntuMaintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>Original-Maintainer: Aptitude Development Team <aptitude-devel@lists.alioth.debian.org>Bugs: https://bugs.launchpad.net/ubuntu/+filebugInstalled-Size: 4,414 kBDepends: aptitude-common (= 0.8.10-6ubuntu1), libapt-pkg5.0 (>= 1.1), libboost-filesystem1.65.1, libboost-iostreams1.65.1, libboost-system1.65.1, libc6 (>= 2.14), libcwidget3v5, libgcc1 (>= 1:3.0), libncursesw5 (>= 6), libsigc++-2.0-0v5 (>= 2.8.0), libsqlite3-0 (>= 3.6.5), libstdc++6 (>= 5.2), libtinfo5 (>= 6), libxapian30Recommends: libparse-debianchangelog-perl, sensible-utilsSuggests: aptitude-doc-en | aptitude-doc, apt-xapian-index, debtags, taskselHomepage: https://aptitude.alioth.debian.org/Supported: 5yDownload-Size: 1,269 kBAPT-Manual-Installed: yesAPT-Sources: http://cn.archive.ubuntu.com/ubuntu bionic/main amd64 PackagesDescription: 基于终端的软件包管理器 aptitude 是一个功能丰富的包管理器,包括:使用类似 mutt 的语法灵活地 检索软件包,类似 dselect 的持续用户操作,获取并显示大多数软件包的 Debian changelog 的功能,一个类似 apt-get 的命令行模式。 . aptitude 还是个 Y2K 兼容,轻便,自清洁以及友好的程序> apt install aptitude
运行后,不接受未安装方案,选择降级方案:
> aptitude install [YOUR_PACKAGE_NAME]
apt
命令的引入就是为了解决命令过于分散的问题,它包括了apt-get
命令出现以来使用最广泛的功能选项,以及apt-cache
和apt-config
命令中很少用到的功能。
简单来说就是:
apt
=apt-get
、apt-cache
、apt-config
中最常用命令选项的集合
> apt list --insalled> apt list --upgradable> apt search htop> apt show htop
可以使用apt
替换部分apt-get
命令:
apt 命令 | 取代的命令 | 命令的功能 |
---|---|---|
apt install | apt-get install | 安装软件包 |
apt remove | apt-get remove | 移除软件包 |
apt purge | apt-get purge | 移除软件包及配置文件 |
apt update | apt-get update | 刷新数据库索引 |
apt upgrade | apt-get upgrade | 升级所有可升级的软件包 |
apt autoremove | apt-get autoremove | 自动删除不需要的包 |
apt full-upgrade | apt-get dist-upgrade | 升级软件包时自动处理依赖关系 |
apt search | apt-cache search | 搜索软件包 |
apt show | apt-cache show | 显示软件包详情 |
另外还有一些apt
自己的命令:
新的 apt 命令 | 命令的功能 |
---|---|
apt list | 根据条件列出软件包(已安装、可升级等) |
apt edit-sources | 编辑源列表 |
apt list --installed
apt list --upgradeable
apt list --all-versions
]]>
The only thing you need is just a termimal.
待更新… (立了好多 Flag 溜了溜了
<Space>
<Space>+1/2/3...
:切换 TabCtrl+W
]]>
摘自 通过 RSSHub 订阅不支持 RSS 的网站 | 少数派
待更新… (立了好多 Flag 溜了溜了
Terminal 快捷键速查
以下组合键不区分大小写
快捷键 | 功能 |
---|---|
Alt+F | 光标向前移动一个单词 |
Alt+B | 光标向后移动一个单词 |
Ctrl+F | 光标向前移动一格 |
Ctrl+B | 光标向后移动一格 |
Ctrl+A | 光标移动到行首 |
Ctrl+E | 光标移动至行尾 |
Ctrl+C | 另起新行 |
Ctrl+U | 清空当前行 |
Ctrl+D | 删除当前字符 |
Ctrl+L | 清屏 |
Esc+W | 删除光标之前的所有字符 |
Ctrl+K | 删除从光标到行尾的左右字符 |
Ctrl+Y | 粘贴刚才删除的字符 |
Ctrl+(X U) | 撤销刚才的操作 |
快捷键 | 功能 |
---|---|
!! | 上一条命令 |
!pre | 执行以pre 为开头的最新命令 |
!n | 执行历史 |
Alt+< | 历史第一项 |
Alt+> | 历史最后一项,即当前输入的命令 |
Ctrl+R | 查询历史 |
Ctrl+G | 从历史搜索模式退出 |
快捷键 | 功能 |
---|---|
Ctrl+Shift+T | 新建标签页 |
Ctrl+Shift+W | 关闭标签页 |
Ctrl+PageUp | 前一标签页 |
Ctrl+PageDown | 后一标签页 |
Ctrl+Shift+PageUp | 标签页左移 |
Ctrl+Shift+PageDown | 标签页右移 |
Alt+2 | 切换到标签 2 |
Ctrl+Shift+N | 新建窗口 |
Ctrl+Shift+Q | 关闭终端 |
快捷键 | 功能 |
---|---|
Alt+F1 | 打开主菜单 |
Alt+F2 | 运行命令 |
Alt+F10 | 窗口最大化 |
快捷键 | 功能 |
---|---|
Ctrl+H | 显示隐藏文件 |
Ctrl+T | 新建标签 |
Ctrl+W | 关闭标签 |
]]>
> yum info imagemagickInstalled PackagesName : ImageMagickArch : x86_64Version : 6.7.8.9Release : 16.el7_6Size : 7.6 MRepo : installedFrom repo : updatesSummary : An X application for displaying and manipulating imagesURL : http://www.imagemagick.org/License : ImageMagickDescription : ImageMagick is an image display and manipulation tool for the X : Window System. ImageMagick can read and write JPEG, TIFF, PNM, GIF, : and Photo CD image formats. It can resize, rotate, sharpen, color : reduce, or add special effects to an image, and when finished you can : either save the completed work in the original format or a different : one. ImageMagick also includes command line programs for creating : animated or transparent .gifs, creating composite images, creating : thumbnail images, and more. : : ImageMagick is one of your choices if you need a program to manipulate : and display images. If you want to develop your own applications : which use ImageMagick code or APIs, you need to install : ImageMagick-devel as well.> yum info imagemagick-develAvailable PackagesName : ImageMagick-develArch : i686Version : 6.7.8.9Release : 16.el7_6Size : 100 kRepo : updates/7/x86_64Summary : Library links and header files for ImageMagick app developmentURL : http://www.imagemagick.org/License : ImageMagickDescription : ImageMagick-devel contains the library links and header files you'll : need to develop ImageMagick applications. ImageMagick is an image : manipulation program. : : If you want to create applications that will use ImageMagick code or : APIs, you need to install ImageMagick-devel as well as ImageMagick. : You do not need to install it if you just want to use ImageMagick, : however.> yum install imagemagick imagemagick-devel
> man imagemagick # 查看使用手册NAME ImageMagick - is a free software suite for the creation, modification and display of bitmap images.SYNOPSIS convert input-file [options] output-fileOVERVIEW convert identify mogrify composite montage compare stream display animate import clojure> man convert # 查看各命令对应的使用手册
> identify bg.jpgbg.jpg JPEG 1900x870 1900x870+0+0 8-bit DirectClass 104KB 0.000u 0:00.009
> convert bg.jpg bg.png
> mogrify -format png ~/.Wallpapers/*.jpg
]]>
需要 Node.js 版本在
4.0.0
以上
> git clone https://github.com/hakimel/reveal.js.git> cd reveal.jsreveal.js > npm install # 安装依赖
安装puppeteer@1.12.2
时报错:
> puppeteer@1.12.2 install C:\Users\abel1\GithubProjects\reveal.js\node_modules\puppeteer> node install.jsERROR: Failed to download Chromium r624492! Set "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" env variable to skip download.
参见 ERROR: Failed to download Chromium | 简书,使用淘宝的npm
源:
> npm config set puppeteer_download_host=https://npm.taobao.org/mirrors> npm i puppeteer
或者使用淘宝的 cnpm,自动使用国内源:
> npm install -g cnpm --registry=https://registry.npm.taobao.org> cnpm i puppeteer
启动Server
:
> cnpm start
再次报错,这次是node-sass
:
> reveal.js@3.8.0 start C:\Users\abel1\GithubProjects\reveal.js> grunt serveLoading "Gruntfile.js" tasks...ERROR>> Error: ENOENT: no such file or directory, scandir 'C:\Users\abel1\GithubProjects\reveal.js\node_modules\node-sass\vendor'
重新构建node-sass
:
> cnpm rebuild node-sass
再次启动Server
,成功:
$ cnpm start> reveal.js@3.8.0 start C:\Users\abel1\GithubProjects\reveal.js> grunt serveRunning "connect:server" (connect) taskStarted connect web server on http://localhost:8000Running "watch" task
未完待续…
]]>
摘自 A Beginners Guide To Cron Jobs | OSTechNix
Minute(0-59) Hour(0-24) Day_of_month(1-31) Month(1-12) Day_of_week(0-6) Command_to_execute
待更新…
]]>
摘自 Ping Multiple Servers And Show The Output In Top-like Text UI | OSTechNix
使用pip
安装pingtop
,确保已经在系统中安装过Python 3.7.x
以及pip
:
> pip3 search pingtoppingtop (0.2.8) - Ping multiple servers and show the result in a top like terminal UI. INSTALLED: 0.2.8 (latest)> pip3 install pingtop
> pingtop abelsu7.top github.com
]]>
摘自 从并发模型看 Go 的语言设计 | 腾讯技术工程,更新中…
Go 语言中
channel
的<-
:左读右写
package mainimport "fmt"const limit = 5func main() { fact := MakeFactFunc() for i := 0; i < limit; i++ { fmt.Println(fact(i)) }}// 阶乘计算的实体func FactCalc(in <-chan int, out chan<- int) { var subIn, subOut chan int for { n := <-in if n == 0 { out <- 1 } else { if subIn == nil { subIn, subOut = make(chan int), make(chan int) go FactCalc(subIn, subOut) } subIn <- n - 1 r := <-subOut out <- n * r } }}// 包装一个阶乘计算函数func MakeFactFunc() func(int) int { in, out := make(chan int), make(chan int) go FactCalc(in, out) return func(x int) int { in <- x return <-out }}------112624Process finished with exit code 0
package mainimport "fmt"func main() { primes := make(chan int) go PrimeSieve(primes) for i := 0; i < 5; i++ { fmt.Println(<-primes) }}func Counter(out chan<- int) { for i := 2; ; i++ { out <- i }}func PrimeFilter(prime int, in <-chan int, out chan<- int) { for { i := <-in if i%prime != 0 { out <- i } }}func PrimeSieve(out chan<- int) { c := make(chan int) go Counter(c) for { prime := <-c out <- prime newC := make(chan int) go PrimeFilter(prime, c, newC) c = newC }}------235711Process finished with exit code 0
]]>
摘自 解决 fedora 28 桌面图标问题 | cnblog
在 Fedora 30 中,Gnome Desktop 默认是没有桌面图标的,只会显示背景,然而这样并不是很方便,我们可以根据需要手动开启桌面图标。
> dnf info nemoAvailable PackagesName : nemoVersion : 4.0.6Release : 2.fc30Architecture : i686Size : 1.5 MSource : nemo-4.0.6-2.fc30.src.rpmRepository : fedoraSummary : File manager for CinnamonURL : https://github.com/linuxmint/nemoLicense : GPLv2+ and LGPLv2+Description : Nemo is the file manager and graphical shell for the Cinnamon desktop : that makes it easy to manage your files and the rest of your system. : It allows to browse directories on local and remote filesystems, preview : files and launch applications associated with them. : It is also responsible for handling the icons on the Cinnamon desktop.> dnf install nemo
创建~/.config/autostart/nemo-autostart-with-gnome.desktop
,并在文件中保存以下内容:
> vim ~/.config/autostart/nemo-autostart-with-gnome.desktop[Desktop Entry]Type=ApplicationName=NemoComment=Start Nemo desktop at log inExec=nemo-desktopOnlyShowIn=GNOME;AutostartCondition=GSettings org.nemo.desktop show-desktop-iconsX-GNOME-AutoRestart=trueNoDisplay=true
此时已经配置完成,这时只需要注销后重新登录,或者直接按Alt+F2
,并输入nemo-desktop
,就可以看到熟悉的图标出现在桌面上:
这时虽然桌面已经出现了图标,但是无法进行拖拽移动等操作,需要在终端内输入如下命令:
> gsettings set org.nemo.desktop use-desktop-grid false
例如以abelsu
用户的身份登录系统,之后在终端执行sudo su
切换至root
用户后,在终端启动应用会报该错误。
切换回abelsu
用户,执行如下命令即可解决:
> xhost +access control disabled, clients can connect from any host
]]>
ikbc DC-87 终于重新用蓝牙连上 XPS-13 了,我先去哭会儿!QAQ
系统环境
Windows 10-1803
去年用 ikbc DC-87 的蓝牙模式连接到我的 XPS 13 上,后来有一天突然就不能正常使用了。
尝试了先在系统设置中删除设备,打算再重新配对,然而 Amazing 的是,删除设备之后,HM KB3
这个蓝牙设备又很快出现在列表中,并显示已配对(╯‵□′)╯︵┻━┻
:
然而设备无法删除就不能被识别为待配对的新设备,这样一来就没办法重新配对。。
科学上网 Google 了一圈,重启、飞行模式、卸载设备、升级蓝牙驱动统统试过,全都不管用。。
那叫一个心塞啊,只好先用有线模式连着,凑合过吧,还能离咋的。。
本来最近都忘了这个事儿,今天突发奇想再次搜索一下,发现了这篇救命博客:
这位 CSDN 博主也是看到一篇国外友人的文章,想必大家都是有缘人:
How to completely remove a Bluetooth device from Win 10? | Windows Ten Forums
简单来说,彻底删除冥顽不化的蓝牙设备,总共分以下几步:
cmd
或powershell
,命令行输入btpair -u
,回车执行并等待一小会儿,这将取消所有蓝牙设备的配对]]>
摘自 ubuntu 18.04 (bionic) 配置 opsx 安装源 | OPSX
> sudo vim /etc/apt/sources.list.d/aliyun.list
deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiversedeb-src http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiversedeb-src http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiversedeb-src http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiversedeb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiversedeb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiversedeb-src http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
sudo apt-get update
]]>
编译新版本内核时nvidia.ko
总是报错,卸载原驱动340.107
,安装新驱动
To be updated…
]]>
记一次莫名其妙的 CPU 100% Bug 排查
系统环境
CentOS 7.5.1804
用htop
查看系统负载,发现其中一个 CPU 长时间处于100%
状态,排查后发现罪魁祸首就是/usr/libexec/tracker-extract
这个进程。
直接kill
或kill -9
后,进程tracker-extract
会自动重启,并再次达到CPU 100%
。
> yum info trackerInstalled PackagesName : trackerArch : x86_64Version : 1.10.5Release : 6.el7Size : 5.6 MRepo : installedFrom repo : anacondaSummary : Desktop-neutral search tool and indexerURL : https://wiki.gnome.org/Projects/TrackerLicense : GPLv2+Description : Tracker is a powerful desktop-neutral first class object database, : tag/metadata database, search tool and indexer. : : It consists of a common object database that allows entities to have an : almost infinite number of properties, metadata (both embedded/harvested as : well as user definable), a comprehensive database of keywords/tags and : links to other entities. : : It provides additional features for file based objects including context : linking and audit trails for a file object. : : It has the ability to index, store, harvest metadata. retrieve and search : all types of files and other first class objects
tracker-extract
属于tracker
包,主要用于桌面索引,下面介绍几种解决办法。
最直接的办法,卸载tracker
包,不过会同时卸载nautilus
、gnome-classic-session
等相关依赖包,不推荐这种方法。
> yum info trackerInstalled PackagesName : trackerArch : x86_64Version : 1.10.5Release : 6.el7Size : 5.6 MRepo : installedFrom repo : anacondaSummary : Desktop-neutral search tool and indexerURL : https://wiki.gnome.org/Projects/TrackerLicense : GPLv2+Description : Tracker is a powerful desktop-neutral first class object database, : tag/metadata database, search tool and indexer. : : It consists of a common object database that allows entities to have an : almost infinite number of properties, metadata (both embedded/harvested as : well as user definable), a comprehensive database of keywords/tags and : links to other entities. : : It provides additional features for file based objects including context : linking and audit trails for a file object. : : It has the ability to index, store, harvest metadata. retrieve and search : all types of files and other first class objects> yum remove trackerDependencies Resolved================================================================================= Package Arch Version Repository Size=================================================================================Removing: tracker x86_64 1.10.5-6.el7 @anaconda 5.6 MRemoving for dependencies: brasero x86_64 3.12.1-2.el7 @anaconda 11 M brasero-nautilus x86_64 3.12.1-2.el7 @anaconda 47 k evince-nautilus x86_64 3.22.1-7.el7 @anaconda 19 k gnome-boxes x86_64 3.22.4-4.el7 @anaconda 5.0 M gnome-classic-session noarch 3.26.2-3.el7 @anaconda 199 k grilo-plugins x86_64 0.3.4-3.el7 @anaconda 2.1 M nautilus x86_64 3.22.3-5.el7 @anaconda 15 M totem x86_64 1:3.22.1-1.el7 @anaconda 6.3 M totem-nautilus x86_64 1:3.22.1-1.el7 @anaconda 36 k tracker-preferences x86_64 1.10.5-6.el7 @base 248 kTransaction Summary=================================================================================Remove 1 Package (+10 Dependent packages)Installed size: 45 MIs this ok [y/N]: n
暂时性的方法,调用tracker daemon -k
杀死所有tracker
相关进程:
> trackerusage: tracker [--version] [--help] <command> [<args>]Available tracker commands are: daemon Start, stop, pause and list processes responsible for indexing content extract Extract information from a file info Show information known about local files or items indexed index Backup, restore, import and (re)index by MIME type or file name reset Reset or remove index and revert configurations to defaults search Search for content indexed or show content by type sparql Query and update the index using SPARQL or search, list and tree the ontology sql Query the database at the lowest level using SQL status Show the indexing progress, content statistics and index state tag Create, list or delete tags for indexed contentSee 'tracker help <command>' to read about a specific subcommand.> tracker daemon -k allFound 10 PIDs… Killed process 2390 - 'tracker-miner-user-guides' Killed process 2395 - 'tracker-store' Killed process 2656 - 'tracker-miner-apps' Killed process 2705 - 'tracker-miner-fs' Killed process 2952 - 'tracker-miner-user-guides' Killed process 2962 - 'tracker-extract' Killed process 2963 - 'tracker-miner-apps' Killed process 2964 - 'tracker-miner-fs' Killed process 2987 - 'tracker-store' Killed process 13666 - 'tracker-extract'
可通过
tracker daemon -s
重新启动tracker
相关进程
在/etc/xdg/autostart/tracker*.desktop
文件的末尾添加以下内容:
Hidden=true
注销后重新登录生效。
> yum info tracker-preferencesAvailable PackagesName : tracker-preferencesArch : x86_64Version : 1.10.5Release : 6.el7Size : 58 kRepo : base/7/x86_64Summary : Tracker preferencesURL : https://wiki.gnome.org/Projects/TrackerLicense : GPLv2+Description : Graphical frontend to tracker configuration.> yum install tracker-preferences
打勾的选项全部取消,注销后重新登录生效。
]]>
- tracker-extract high cpu usage | StackExchange
- tracker-extract process with 100% cpu usage | CentOS
- [Solved] How to disable tracker-store processes that eat 100% CPU | openSUSE Forum
- Ubuntu Gnome 14.04 tracker-extract 占用内存太高 | CSDN
- 解决 Linux 中 tracker 大量占用 CPU 的问题 | Linux 公社
- Ubuntu 16.04 莫名其妙占用 CPU 的 tracker | Paulus.Chen
摘自 How to Setup Passwordless SSH Login | Linuxize
> ls -l ~/.ssh/total 12-rw------- 1 root root 1679 Apr 11 10:11 id_rsa-rw-r--r-- 1 root root 398 Apr 11 10:11 id_rsa.pub-rw-r--r--. 1 root root 1736 Apr 11 10:21 known_hosts
若可看到id_rsa
、id_rsa.pub
存在,则说明该机器上之前已经生成了 SSH 密钥,可以选择继续使用该密钥或重新生成新密钥。
若选择重新生成密钥,则先备份旧密钥(如有需要),再使用以下命令:
> ssh-keygen -t rsa -b 4096 -C "your_email@domain.com"
之后连按 4 次回车,表示采用默认设置,生成密钥:
最后确认已经生成密钥文件id_rsa
、id_rsa.pub
:
> ls ~/.ssh/id_*/root/.ssh/id_rsa /root/.ssh/id_rsa.pub
使用ssh-copy-id
命令将本机的公钥复制到指定主机的authorized_keys
文件中:
> ssh-copy-id remote_username@server_ip_address
例如现在我有三台 Linux 主机,均已生成 SSH 密钥,主机名如下所示:
abelsu7-ubuntu
centos-1
centos-2
以abelsu7-ubuntu
为例,执行以下命令:
> ssh-copy-id root@centos-1/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/root/.ssh/id_rsa.pub"/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keysroot@centos-1 password: Number of key(s) added: 1Now try logging into the machine, with: "ssh 'root@centos-1'"and check to make sure that only the key(s) you wanted were added.> ssh-copy-id root@centos-2/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/root/.ssh/id_rsa.pub"/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keysroot@centos-2 password: Number of key(s) added: 1Now try logging into the machine, with: "ssh 'root@centos-2'"and check to make sure that only the key(s) you wanted were added.
之后就可以在abelsu7-ubuntu
上直接通过 SSH 免密登录centos-1
、centos-2
:
> ssh root@centos-1> ssh root@centos-2
在其他两台主机
centos-1
、centos-2
上重复以上操作,即可在三台 Linux 主机上互相 SSH 免密登录
另外,如果ssh-copy-id
不可用,则可使用以下命令作为替代:
> cat ~/.ssh/id_rsa.pub | ssh remote_username@server_ip_address "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
关于
sshd_config
的更多配置,可参考 Using the SSH Config File | Linuxize
若要禁用 SSH 密码登录,则需修改sshd_config
配置文件:
> sudo vim /etc/ssh/sshd_config...# 修改如下PasswordAuthentication noChallengeResponseAuthentication noUsePAM no...> sudo systemctl restart sshd # 重启服务后生效
]]>
译自 How to Check Disk Space in Linux Using the df Command,补充整理来源于网络
> dfFilesystem 1K-blocks Used Available Use% Mounted onudev 948204 0 948204 0% /devtmpfs 193132 19896 173236 11% /run/dev/vda1 51474044 2331696 46520964 5% /tmpfs 965652 24 965628 1% /dev/shmtmpfs 5120 0 5120 0% /run/locktmpfs 965652 0 965652 0% /sys/fs/cgrouptmpfs 100 0 100 0% /run/lxcfs/controllerstmpfs 193132 0 193132 0% /run/user/0
指定挂载路径/
:
> df /Filesystem 1K-blocks Used Available Use% Mounted on/dev/vda1 51474044 2332576 46520084 5% /
> df -hFilesystem Size Used Avail Use% Mounted onudev 926M 0 926M 0% /devtmpfs 189M 20M 170M 11% /run/dev/vda1 50G 2.3G 45G 5% /tmpfs 944M 24K 943M 1% /dev/shmtmpfs 5.0M 0 5.0M 0% /run/locktmpfs 944M 0 944M 0% /sys/fs/cgrouptmpfs 100K 0 100K 0% /run/lxcfs/controllerstmpfs 189M 0 189M 0% /run/user/0
> df -hTFilesystem Type Size Used Avail Use% Mounted onudev devtmpfs 926M 0 926M 0% /devtmpfs tmpfs 189M 20M 170M 11% /run/dev/vda1 ext3 50G 2.3G 45G 5% /tmpfs tmpfs 944M 24K 943M 1% /dev/shmtmpfs tmpfs 5.0M 0 5.0M 0% /run/locktmpfs tmpfs 944M 0 944M 0% /sys/fs/cgrouptmpfs tmpfs 100K 0 100K 0% /run/lxcfs/controllerstmpfs tmpfs 189M 0 189M 0% /run/user/0
指定文件系统类型ext3
:
> df -t ext3Filesystem 1K-blocks Used Available Use% Mounted on/dev/vda1 51474044 2332712 46519948 5% /> df -x ext3 # 除 ext3 以外的其他类型Filesystem 1K-blocks Used Available Use% Mounted onudev 948204 0 948204 0% /devtmpfs 193132 19896 173236 11% /runtmpfs 965652 24 965628 1% /dev/shmtmpfs 5120 0 5120 0% /run/locktmpfs 965652 0 965652 0% /sys/fs/cgrouptmpfs 100 0 100 0% /run/lxcfs/controllerstmpfs 193132 0 193132 0% /run/user/0
> df -ih /Filesystem Inodes IUsed IFree IUse% Mounted on/dev/vda1 3.2M 93K 3.1M 3% /
还可以在df
命令中指定打印的字段,可以添加--output[=FIELD_LIST]
选项,FIELD_LIST
中各个字段用,
隔开:
source
:文件系统源地址fstype
:文件系统类型itotal
:文件系统的 inodes 总量iused
:已使用的 inodesiavail
:可使用的 inodesipcent
:已使用的 inodes 百分比size
:磁盘空间总量used
:已使用的磁盘空间大小avail
:可用的磁盘空间大小pcent
:已使用的磁盘空间百分比file
:命令行中指定的文件名target
:文件系统挂载点> df -h -t tmpfs --output=source,size,pcent,targetFilesystem Size Use% Mounted ontmpfs 189M 11% /runtmpfs 944M 1% /dev/shmtmpfs 5.0M 0% /run/locktmpfs 944M 0% /sys/fs/cgrouptmpfs 100K 0% /run/lxcfs/controllerstmpfs 189M 0% /run/user/0
]]>
一入 Vim 深似海,从此 IDE 是路人
更新中…
^ k Hint: The h key is at the left and moves left. < h l > The l key is at the right and moves right. j The j key looks like a down arrow. v
待补充
:w
:wq
:q!
:qa
x
:删除光标选中的字符i
:进入编辑模式A
:可在当前光标行末添加新字符Many commands that change text are made from an operator and a motion.The format for a delete command with the d delete operator is as follows: d motionWhere: d - is the delete operator. motion - is what the operator will operate on (listed below).A short list of motions: w - until the start of the next word, EXCLUDING its first character. e - to the end of the current word, INCLUDING the last character. $ - to the end of the line, INCLUDING the last character.
dw
:删除光标后的单词,光标停留在下一个单词的开头de
:删除光标后的单词,光标停留在下一个单词开头的前一个字符d$
:删除至行末w
:移动至下一个单词的开头,可与数字连用,例如2w
会移动至后数第二个单词的开头e
:移动至下一个单词的末尾,可与数字连用,例如3e
会移动至后数第三个单词的末尾0
:移动至光标所在行的开头(类似Home
)d2w
:删除光标后数的两个单词,光标停留在第三个单词的首字符d2e
:删除光标后数的两个单词,光标停留在第三个单词首字符的前一个字符dd
:删除当前行2dd
:删除包括当前行之后的 2 行u
:撤销最近的一次更改U
:撤销整行的更改Ctrl+R
:Redop
:将上一次删除的内容插入到光标之后r
:替换光标选中的字符与d
命令类似,满足以下格式:
> c [number] motion
ce
:编辑光标之后的单词直至其末尾c2e
:编辑光标之后的 2 个单词直至其末尾c$
:编辑光标至行末的内容Ctrl+G
:显示当前位置及行号G
:移动至文件末尾gg
:移动至文件开头313 G
:移动至第313
行/pattern
:向后查找包含pattern
的字符串?pattern
:向前查找包含pattern
的字符串n
:下一个匹配N
:上一个匹配Ctrl+O
:跳转至光标上一次所在位置Ctrl+I
:跳转至光标下一次所在位置:s/old/new
:将该行第一个出现的old
替换为new
:s/old/new/g
:将该行所有出现的old
替换为new
:#,#s/old/new/g
:将两行之间所有出现的old
替换为new
:%s/old/new/g
:将文件中所有出现的old
替换为new
:%s/old/new/g
:查找文件中所有出现的old
,并提示用户是否用new
进行替换%
:查找与光标后最近的左括号(
、[
、{
匹配的右括号:!command
:例如:!pwd
将打印当前工作目录路径:w FILENAME
:将更改保存至FILENAME
v
进入可视化选择模式:
,屏幕下方会出现:'<,'>
w TEST
,即屏幕下方显示:'<,'>w TEST
,将选中内容保存至TEST
:r TEST
:在光标后插入文件TEST
的内容:r !pwd
:在光标后插入pwd
命令输出的内容:o
:在光标下方插入新行,并进入编辑模式:O
:在光标上方插入新行,并进入编辑模式:a
:在当前光标之后插入新内容,并进入编辑模式:R
:进入编辑模式,并用输入的字符替换当前光标选中的字符y
:复制选中内容y2w
:复制光标之后的两个单词p
:在光标之后粘贴内容:set ic
:Ignore Case,忽略大小写:set noic
:开启大小写:set hls
:Highlight Search,高亮搜索:nohlsearch
:取消当前的高亮,可简写为:nohl
或:noh
:set is
:Increasing Search,递进搜索:set nois
:取消递进搜索:help
:打开帮助文档Ctrl+D
:显示所有匹配开头的命令Tab
:自动补全至下一项在~/.vimrc
中加入以下内容:
我用的是
nvim
,所以配置文件在~/.config/nvim/
目录下
" 快捷键设置nmap <F2> :NERDTreeToggle<cr>nmap <F3> :TagbarToggle<cr>nmap <F6> :GoFmt<cr>nmap <C-s> :w<cr>
]]>
- Learning Vim: What I Wish I Knew | Hacker Noon
- Introduction To Vim Customization | Linode
- The Ultimate vimrc | Github
- Vim Dracula Theme | Github
- Vundle.vim | Github
- lexVim - lexkong | Github
- 138 条 Vim 命令、操作、快捷键全集 | 马哥 Linux 运维
- 练了一年再来总结的 vim 使用技巧 | CU 技术社区
- 哈哈:180万程序员不知如何退出Vim编辑器 | 实验楼
- 精通 VIM ,此文就够了 | zempty 笔记
- 超酷的 Vim 搜索技巧 | Linux 中国
- Vim 系列教程 | 卡瓦邦噶
译自 How to Get the Size of a Directory in Linux,补充整理来源于网络
du
命令为disk usage
的缩写,是一个计算磁盘上目录或文件占用空间的工具,它可以用来显示文件系统上的目录、单个/多个文件所占用的磁盘空间。
这与
df
命令有所不同,df
命令用来显示每个文件系统的磁盘使用量以及可用量的信息
> du -csh /var /kvm5.3G /var7.5G /kvm13G total
参数释义:
-c
、--total
:最后打印所有参数目录的空间占用大小总和-s
、--summarize
:仅打印各参数目录的空间占用大小总和,不打印其子目录-h
、--human-readable
:以K
、M
、G
为单位显示空间占用大小> du -shc /var/*0 /var/account0 /var/adm2.0G /var/cache0 /var/crash8.0K /var/db0 /var/empty0 /var/games0 /var/gopher0 /var/kerberos3.1G /var/lib0 /var/local0 /var/lock180M /var/log0 /var/mail0 /var/nis0 /var/opt0 /var/preserve0 /var/run45M /var/spool0 /var/target32K /var/tmp0 /var/yp5.3G total
或者:
> du -h --max-depth=1 /var32K /var/tmp3.1G /var/lib180M /var/log0 /var/adm2.0G /var/cache8.0K /var/db0 /var/empty0 /var/games0 /var/gopher0 /var/local0 /var/nis0 /var/opt0 /var/preserve45M /var/spool0 /var/yp0 /var/kerberos0 /var/crash0 /var/target0 /var/account5.3G /var
添加--apparent-size
参数:
> du -sh /var5.3G /var> du -sh --apparent-size /var5.2G /var
du
命令还可以通过管道与其他命令结合使用,例如以下命令将打印/var
目录下占用空间最大的前 5 个目录:
> du -h /var/ | sort -rh | head -55.3G /var/3.1G /var/lib2.8G /var/lib/docker/overlay22.8G /var/lib/docker2.0G /var/cache/yum/x86_64/7
]]>
文章内容收集整理于网络,详见参考文章
To be updated
- 什么是 CI/CD?| Linux 中国
- 什么是 CI/CD?| RedHat DevOps
- 借助 Ansible 实现持续集成和交付 | RedHat
- 如何理解持续集成、持续交付、持续部署?| CSDN
- 一文帮你秒懂CI, CD AND CD | 知乎
- 如何理解持续集成、持续交付、持续部署?| 知乎
- The Product Managers’ Guide to Continuous Delivery and DevOps | mind the Product
- 一张图带你了解持续交付和 DevOps 的前世今生 - 乔梁 | GitChat
- 持续集成是什么?| 阮一峰
- 什么是持续集成(CI)/持续部署(CD)?| 知乎
- CI/CD Hello World in OpenShift | 陈沙克日志
一文搞定 CentOS 7 VNC 配置
> yum install tigervnc-server
切换至通过 VNC 连接的用户,并使用vncpasswd
命令设置密码,长度至少为 6 位:
> su - your_user # If you want to configure VNC server to run under this user directly from CLI without switching users from GUI> vncpasswd
> cp /lib/systemd/system/vncserver@.service /etc/systemd/system/vncserver@:1.service> vim /etc/systemd/system/vncserver@:1.service
编辑配置文件/etc/systemd/system/vncserver@:1.service
,注意修改高亮部分:
[Unit]Description=Remote desktop service (VNC)After=syslog.target network.target[Service]Type=forkingExecStartPre=/bin/sh -c '/usr/bin/vncserver -kill %i > /dev/null 2>&1 || :'ExecStart=/sbin/runuser -l my_user -c "/usr/bin/vncserver %i -geometry 1280x1024"PIDFile=/home/my_user/.vnc/%H%i.pidExecStop=/bin/sh -c '/usr/bin/vncserver -kill %i > /dev/null 2>&1 || :'[Install]WantedBy=multi-user.target
> systemctl daemon-reload> systemctl start vncserver@:1> systemctl status vncserver@:1> systemctl enable vncserver@:1
使用以下命令查看服务状态:
> systemctl status vncserver@:1.service# 或> service vncserver@:1 status
可以看到vncserver
已正常启动:
> ss -tulpn | grep vnctcp LISTEN 0 5 *:5901 *:* users:(("Xvnc",pid=1356,fd=9))tcp LISTEN 0 128 *:6001 *:* users:(("Xvnc",pid=1356,fd=6))tcp LISTEN 0 5 :::5901 :::* users:(("Xvnc",pid=1356,fd=10))tcp LISTEN 0 128 :::6001 :::* users:(("Xvnc",pid=1356,fd=5))
或者:
> vncserver -listTigerVNC server sessions:X DISPLAY # PROCESS ID:1 1344:2 4112
> firewall-cmd --add-port=5901/tcp> firewall-cmd --add-port=5901/tcp --permanent
手动清除残余文件后,重新启动服务:
> rm ~/.vnc/*.pid> rm -rf /tmp/.X11-unix> service vncserver@:1 start
可以看到服务已正常启动:
]]>
KVM 从诞生之初就需要硬件虚拟化扩展的支持,其最初的开发是基于x86
和x86-64
处理器架构上的 Linux 系统进行的。
目前,KVM 被移植到多种不同处理器架构上,但在x86-64
架构上的支持是最完善的。
本文默认基于
x86-64
的 Linux 系统进行相关操作
在x86-64
架构的处理器中,KVM 需要的硬件虚拟化扩展分别为:
VT
即Intel(R) Virtualization Technology
VT-d
即Intel(R) VT for Directed I/O
设置好VT
和VT-d
的相关选项,保存 BIOS 设置并退出,系统重启后生效。
可通过检查/proc/cpuinfo
文件中的 CPU 特性标志(flags)来查看 CPU 目前是否支持硬件虚拟化:
Intel CPU 支持虚拟化的标志为
vmx
AMD CPU 支持虚拟化的标志为svm
> grep -E "vmx|svm" /proc/cpuinfoflags : ... vmx ... ...
Virtualization Host
,因为后续会自行编译 KVM 及 QEMU 源码Server with GUI
、GNOME Desktop
以及Development Tools
本文实验环境:OS
Ubuntu 18.04 LTS
,CPUi7-6700 * 8
,内存32G
> git clone git://git.kernel.org/pub/scm/virt/kvm/kvm.git# or> git clone https://git.kernel.org/pub/scm/virt/kvm/kvm.git
内核版本:本文使用的是
kvm.git
的4.21.1
版本,编译后显示的内核版本为4.20.0-rc6
源码地址: index: kvm/kvm.git。
KVM 是作为 Linux 内核中的一个 module 而存在的,而kvm.git
是一个包含了最新的 KVM 模块开发中代码的完整的 Linux 内核源码仓库。它的配置方式与普通的 Linux 内核配置完全一样,只是需要注意将 KVM 相关的配置选择为编译进内核或者编译为模块。
在kvm.git
目录下,运行make help
查看关于如何配置和编译 kernel 的帮助说明:
> make helpCleaning targets: clean - Remove most generated files but keep the config and enough build support to build external modules mrproper - Remove all generated files + config + various backup files distclean - mrproper + remove editor backup and patch filesConfiguration targets: config - Update current config utilising a line-oriented program nconfig - Update current config utilising a ncurses menu based program menuconfig - Update current config utilising a menu based program xconfig - Update current config utilising a Qt based front-end gconfig - Update current config utilising a GTK+ based front-end oldconfig - Update current config utilising a provided .config as base localmodconfig - Update current config disabling modules not loaded localyesconfig - Update current config converting local mods to core defconfig - New config with default from ARCH supplied defconfig savedefconfig - Save current config as ./defconfig (minimal config) allnoconfig - New config where all options are answered with no allyesconfig - New config where all options are accepted with yes allmodconfig - New config selecting modules when possible alldefconfig - New config with all symbols set to default randconfig - New config with random answer to all options listnewconfig - List new options olddefconfig - Same as oldconfig but sets new symbols to their default value without prompting kvmconfig - Enable additional options for kvm guest kernel support xenconfig - Enable additional options for xen dom0 and guest kernel support tinyconfig - Configure the tiniest possible kernel testconfig - Run Kconfig unit tests (requires python3 and pytest)Other generic targets: all - Build all targets marked with [*]* vmlinux - Build the bare kernel* modules - Build all modules modules_install - Install all modules to INSTALL_MOD_PATH (default: /) dir/ - Build all files in dir and below dir/file.[ois] - Build specified target only dir/file.ll - Build the LLVM assembly file (requires compiler support for LLVM assembly generation) dir/file.lst - Build specified mixed source/assembly target only (requires a recent binutils and recent build (System.map)) dir/file.ko - Build module including final link modules_prepare - Set up for building external modules tags/TAGS - Generate tags file for editors cscope - Generate cscope index gtags - Generate GNU GLOBAL index kernelrelease - Output the release version string (use with make -s) kernelversion - Output the version stored in Makefile (use with make -s) image_name - Output the image name (use with make -s) headers_install - Install sanitised kernel headers to INSTALL_HDR_PATH (default: ./usr)Static analysers: checkstack - Generate a list of stack hogs namespacecheck - Name space analysis on compiled kernel versioncheck - Sanity check on version.h usage includecheck - Check for duplicate included header files export_report - List the usages of all exported symbols headers_check - Sanity check on exported headers headerdep - Detect inclusion cycles in headers coccicheck - Check with CoccinelleKernel selftest: kselftest - Build and run kernel selftest (run as root) Build, install, and boot kernel before running kselftest on it kselftest-clean - Remove all generated kselftest files kselftest-merge - Merge all the config dependencies of kselftest to existing .config.Userspace tools targets: use "make tools/help" or "cd tools; make help"Kernel packaging: rpm-pkg - Build both source and binary RPM kernel packages binrpm-pkg - Build only the binary kernel RPM package deb-pkg - Build both source and binary deb kernel packages bindeb-pkg - Build only the binary kernel deb package snap-pkg - Build only the binary kernel snap package (will connect to external hosts) tar-pkg - Build the kernel as an uncompressed tarball targz-pkg - Build the kernel as a gzip compressed tarball tarbz2-pkg - Build the kernel as a bzip2 compressed tarball tarxz-pkg - Build the kernel as a xz compressed tarball perf-tar-src-pkg - Build perf-4.20.0-rc6.tar source tarball perf-targz-src-pkg - Build perf-4.20.0-rc6.tar.gz source tarball perf-tarbz2-src-pkg - Build perf-4.20.0-rc6.tar.bz2 source tarball perf-tarxz-src-pkg - Build perf-4.20.0-rc6.tar.xz source tarballDocumentation targets: Linux kernel internal documentation in different formats from ReST: htmldocs - HTML latexdocs - LaTeX pdfdocs - PDF epubdocs - EPUB xmldocs - XML linkcheckdocs - check for broken external links (will connect to external hosts) refcheckdocs - check for references to non-existing files under Documentation cleandocs - clean all generated files make SPHINXDIRS="s1 s2" [target] Generate only docs of folder s1, s2 valid values for SPHINXDIRS are: driver-api networking input core-api userspace-api media gpu process sound crypto vm maintainer sh dev-tools doc-guide filesystems kernel-hacking admin-guide make SPHINX_CONF={conf-file} [target] use *additional* sphinx-build configuration. This is e.g. useful to build with nit-picking config. Default location for the generated documents is Documentation/outputArchitecture specific targets (x86):* bzImage - Compressed kernel image (arch/x86/boot/bzImage) install - Install kernel using (your) ~/bin/installkernel or (distribution) /sbin/installkernel or install to $(INSTALL_PATH) and run lilo fdimage - Create 1.4MB boot floppy image (arch/x86/boot/fdimage) fdimage144 - Create 1.4MB boot floppy image (arch/x86/boot/fdimage) fdimage288 - Create 2.8MB boot floppy image (arch/x86/boot/fdimage) isoimage - Create a boot CD-ROM image (arch/x86/boot/image.iso) bzdisk/fdimage*/isoimage also accept: FDARGS="..." arguments for the booted kernel FDINITRD=file initrd for the booted kernel i386_defconfig - Build for i386 x86_64_defconfig - Build for x86_64 make V=0|1 [targets] 0 => quiet build (default), 1 => verbose build make V=2 [targets] 2 => give reason for rebuild of target make O=dir [targets] Locate all output files in "dir", including .config make C=1 [targets] Check re-compiled c source with $CHECK (sparse by default) make C=2 [targets] Force check of all c source with $CHECK make RECORDMCOUNT_WARN=1 [targets] Warn about ignored mcount sections make W=n [targets] Enable extra gcc checks, n=1,2,3 where 1: warnings which may be relevant and do not occur too often 2: warnings which occur quite often but may still be relevant 3: more obscure warnings, can most likely be ignored Multiple levels can be combined with W=12 or W=123Execute "make" or "make all" to build all targets marked with [*] For further info see the ./README file
对 KVM 进行内核配置常用的一些配置命令如下:
make config
:基于文本的最为传统也是最为枯燥的一种配置方式,适用于任何情况make oldconfig
:在现有的内核设置文件基础上建立一个新的设置文件,只会向用户提供有关新内核特性的问题make silentoldconfig
:和上面的make oldconfig
一样,只是额外会静默更新选项的依赖关系make olddefconfig
:和上面的make silentoldconfig
一样,但不需要手动交互,而是对新选项以其默认值配置make menuconfig
:基于终端的一种配置方式,提供了文本模式的图形用户界面,用户可以通过移动光标来浏览所支持的各种特性,要求系统中安装ncurses
库make xconfig
:基于 X Window 的一种配置方式,只能在 X Server 上运行 X 桌面应用程序时使用,并且依赖于 QT 库make gconfig
:与make xconfig
类似,不同的是它依赖于 GTK 库makedefconfig
:按照内核代码中提供的默认配置文件对内核进行配置。例如在Intel x86_64
平台上,默认配置为arch/x86/configs/x86_64_defconfig
,生成.config
文件可以用作初始化配置,然后再使用make menuconfig
进行定制化配置make allmodconfig
:尽可能多的使用y
输入设置内核选项值,生成的配置中包含了全部的内核特性make allnoconfig
:除必需的选项外,其他选项一律不选(常用于嵌入式系统的编译)make allmodconfig
:尽可能多的使用m
输入设置内核选项值来生成配置文件make localmodconfig
:会执行lsmod
命令查看当前系统中加载了哪些模块,并最终将原来的.config
中不需要的模块去掉为了确保生成的.config
文件生成的 Kernel 是实际可以工作的(直接make defconfig
生成的.config
文件编译出来的 Kernel 常常是不能工作的),最佳实践是以你当前使用的 config 为基础,将它复制到当前编译目录下,重命名为.config
,然后再通过make olddefconfig
更新补充这个设置文件:
> cp /boot/config-3.10.0-862.14.4.el7.x86_64 .configcp: overwrite ‘.config’? y> make olddefconfig HOSTCC scripts/basic/fixdep HOSTCC scripts/kconfig/conf.o HOSTCC scripts/kconfig/confdata.o HOSTCC scripts/kconfig/expr.o LEX scripts/kconfig/lexer.lex.c YACC scripts/kconfig/parser.tab.h HOSTCC scripts/kconfig/lexer.lex.o YACC scripts/kconfig/parser.tab.c HOSTCC scripts/kconfig/parser.tab.o HOSTCC scripts/kconfig/preprocess.o HOSTCC scripts/kconfig/symbol.o HOSTLD scripts/kconfig/confscripts/kconfig/conf --olddefconfig Kconfig.config:676:warning: symbol value 'm' invalid for CPU_FREQ_STAT.config:755:warning: symbol value 'm' invalid for HOTPLUG_PCI_SHPC.config:918:warning: symbol value 'm' invalid for NF_CT_PROTO_GRE.config:946:warning: symbol value 'm' invalid for NF_NAT_REDIRECT.config:949:warning: symbol value 'm' invalid for NF_TABLES_INET.config:1111:warning: symbol value 'm' invalid for NF_TABLES_IPV4.config:1115:warning: symbol value 'm' invalid for NF_TABLES_ARP.config:1156:warning: symbol value 'm' invalid for NF_TABLES_IPV6.config:1188:warning: symbol value 'm' invalid for NF_TABLES_BRIDGE.config:1532:warning: symbol value 'm' invalid for NET_DEVLINK.config:2958:warning: symbol value 'm' invalid for HW_RANDOM_TPM.config:3554:warning: symbol value 'm' invalid for LIRC.config:4104:warning: symbol value 'm' invalid for HSA_AMD.config:4460:warning: symbol value 'm' invalid for SND_X86## configuration written to .config#> cat .config | grep KVMCONFIG_KVM_GUEST=yCONFIG_KVM_DEBUG_FS=yCONFIG_HAVE_KVM=yCONFIG_HAVE_KVM_IRQCHIP=yCONFIG_HAVE_KVM_IRQFD=yCONFIG_HAVE_KVM_IRQ_ROUTING=yCONFIG_HAVE_KVM_EVENTFD=yCONFIG_KVM_MMIO=yCONFIG_KVM_ASYNC_PF=yCONFIG_HAVE_KVM_MSI=yCONFIG_HAVE_KVM_CPU_RELAX_INTERCEPT=yCONFIG_KVM_VFIO=yCONFIG_KVM_GENERIC_DIRTYLOG_READ_PROTECT=yCONFIG_KVM_COMPAT=yCONFIG_HAVE_KVM_IRQ_BYPASS=yCONFIG_KVM=mCONFIG_KVM_INTEL=mCONFIG_KVM_AMD=mCONFIG_KVM_AMD_SEV=yCONFIG_KVM_MMU_AUDIT=yCONFIG_PTP_1588_CLOCK_KVM=mCONFIG_DRM_I915_GVT_KVMGT=m
之后使用make menuconfig
进行定制化配置,首先需要安装以下依赖项:
> yum install -y ncurses-devel flex bison
之后启动make menuconfig
:
> make menuconfig HOSTCC scripts/kconfig/mconf.o HOSTCC scripts/kconfig/lxdialog/checklist.o HOSTCC scripts/kconfig/lxdialog/inputbox.o HOSTCC scripts/kconfig/lxdialog/menubox.o HOSTCC scripts/kconfig/lxdialog/textbox.o HOSTCC scripts/kconfig/lxdialog/util.o HOSTCC scripts/kconfig/lxdialog/yesno.o HOSTLD scripts/kconfig/mconfscripts/kconfig/mconf Kconfig*** End of the configuration.*** Execute 'make' to start the build or try 'make help'.
修改完成后保存Save
并退出Exit
,可以看到.config
中CONFIG_KVM_AMD
已被修改:
> cat .config | grep KVMCONFIG_KVM_GUEST=yCONFIG_KVM_DEBUG_FS=yCONFIG_HAVE_KVM=yCONFIG_HAVE_KVM_IRQCHIP=yCONFIG_HAVE_KVM_IRQFD=yCONFIG_HAVE_KVM_IRQ_ROUTING=yCONFIG_HAVE_KVM_EVENTFD=yCONFIG_KVM_MMIO=yCONFIG_KVM_ASYNC_PF=yCONFIG_HAVE_KVM_MSI=yCONFIG_HAVE_KVM_CPU_RELAX_INTERCEPT=yCONFIG_KVM_VFIO=yCONFIG_KVM_GENERIC_DIRTYLOG_READ_PROTECT=yCONFIG_KVM_COMPAT=yCONFIG_HAVE_KVM_IRQ_BYPASS=yCONFIG_KVM=mCONFIG_KVM_INTEL=m# CONFIG_KVM_AMD is not setCONFIG_KVM_MMU_AUDIT=yCONFIG_PTP_1588_CLOCK_KVM=mCONFIG_DRM_I915_GVT_KVMGT=m
在对 KVM 源代码进行配置之后,编译 KVM 就比较容易了。它的编译过程就是一个普通的 Linux 内核编译过程,包括以下三个步骤:
> make vmlinux -j 10error: Cannot generate ORC metadata for CONFIG_UNWINDER_ORC=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel> yum install -y elfutils-libelf-devel> make vmlinux -j 10# 此处省略部分编译时的输出信息 GEN .version CHK include/generated/compile.h UPD include/generated/compile.h CC init/version.o AR init/built-in.a LD vmlinux.o MODPOST vmlinux.o KSYM .tmp_kallsyms1.o KSYM .tmp_kallsyms2.o LD vmlinux # 这里就是编译、链接后生成的启动所需的 Linux Kernel 文件 SORTEX vmlinux SYSMAP System.map
其中,编译命令中的-j
参数不是必需的,他是允许 make 工具用多任务(job)来进行编译。例如上面的-j 10
,表示 make 工具最多可以创建 20 个 GCC 进程,同时来进行编译任务。
在一个比较空闲的系统上,-j
参数的推荐值大约为 2 倍于系统上的 CPU core。
如果
-j
后面不跟数字,则 make 会根据现在系统中的 CPU core 的数量自动安排任务数,通常比 core 的数量略多一点
编译完成后,可看到当前目录下生成了我们所需的vmlinux
内核文件:
> ls -hl vmlinux-rwxr-xr-x 1 root root 446M Apr 21 18:14 vmlinux
> make bzImage CALL scripts/checksyscalls.sh CALL scripts/atomic/check-atomics.sh DESCEND objtool CHK include/generated/compile.h HOSTCC arch/x86/tools/insn_decoder_test HOSTCC arch/x86/tools/insn_sanity TEST posttestarch/x86/tools/insn_decoder_test: success: Decoded and checked 6299855 instructions TEST posttestarch/x86/tools/insn_sanity: Success: decoded and checked 1000000 random instructions with 0 errors (seed:0x7ffcb19) CC arch/x86/boot/a20.o AS arch/x86/boot/bioscall.o CC arch/x86/boot/cmdline.o AS arch/x86/boot/copy.o HOSTCC arch/x86/boot/mkcpustr CPUSTR arch/x86/boot/cpustr.h CC arch/x86/boot/cpu.o CC arch/x86/boot/cpuflags.o CC arch/x86/boot/cpucheck.o CC arch/x86/boot/early_serial_console.o CC arch/x86/boot/edd.o LDS arch/x86/boot/compressed/vmlinux.lds AS arch/x86/boot/compressed/head_64.o VOFFSET arch/x86/boot/compressed/../voffset.h CC arch/x86/boot/compressed/misc.o CC arch/x86/boot/compressed/string.o CC arch/x86/boot/compressed/cmdline.o CC arch/x86/boot/compressed/error.o OBJCOPY arch/x86/boot/compressed/vmlinux.bin RELOCS arch/x86/boot/compressed/vmlinux.relocs GZIP arch/x86/boot/compressed/vmlinux.bin.gz HOSTCC arch/x86/boot/compressed/mkpiggy MKPIGGY arch/x86/boot/compressed/piggy.S AS arch/x86/boot/compressed/piggy.o CC arch/x86/boot/compressed/cpuflags.o CC arch/x86/boot/compressed/early_serial_console.o CC arch/x86/boot/compressed/kaslr.o CC arch/x86/boot/compressed/kaslr_64.o AS arch/x86/boot/compressed/mem_encrypt.o CC arch/x86/boot/compressed/pgtable_64.o CC arch/x86/boot/compressed/acpi.o CC arch/x86/boot/compressed/eboot.o AS arch/x86/boot/compressed/efi_stub_64.o AS arch/x86/boot/compressed/efi_thunk_64.o LD arch/x86/boot/compressed/vmlinux ZOFFSET arch/x86/boot/zoffset.h AS arch/x86/boot/header.o CC arch/x86/boot/main.o CC arch/x86/boot/memory.o CC arch/x86/boot/pm.o AS arch/x86/boot/pmjump.o CC arch/x86/boot/printf.o CC arch/x86/boot/regs.o CC arch/x86/boot/string.o CC arch/x86/boot/tty.o CC arch/x86/boot/video.o CC arch/x86/boot/video-mode.o CC arch/x86/boot/version.o CC arch/x86/boot/video-vga.o CC arch/x86/boot/video-vesa.o CC arch/x86/boot/video-bios.o LD arch/x86/boot/setup.elf OBJCOPY arch/x86/boot/setup.bin OBJCOPY arch/x86/boot/vmlinux.bin HOSTCC arch/x86/boot/tools/build BUILD arch/x86/boot/bzImageSetup is 17340 bytes (padded to 17408 bytes).System is 7661 kBCRC 93bcb672Kernel: arch/x86/boot/bzImage is ready (#2)
可以看到arch/x86/boot/bzImage
已经生成:
> ls -hl arch/x86/boot/bzImage-rw-r--r-- 1 root root 7.5M Apr 21 19:03 arch/x86/boot/bzImage> ls -hl arch/x86_64/boot/bzImagelrwxrwxrwx 1 root root 22 Apr 21 19:03 arch/x86_64/boot/bzImage -> ../../x86/boot/bzImage
编译 Kernel 和 bzImage 之后编译内核的模块:
> make modules -j 10# 此处省略部分编译时的输出信息 LD [M] sound/soc/snd-soc-acpi.ko LD [M] sound/soc/snd-soc-core.ko LD [M] sound/soundcore.ko LD [M] sound/synth/emux/snd-emux-synth.ko LD [M] sound/synth/snd-util-mem.ko LD [M] sound/usb/6fire/snd-usb-6fire.ko LD [M] sound/usb/bcd2000/snd-bcd2000.ko LD [M] sound/usb/caiaq/snd-usb-caiaq.ko LD [M] sound/usb/hiface/snd-usb-hiface.ko LD [M] sound/usb/line6/snd-usb-line6.ko LD [M] sound/usb/line6/snd-usb-pod.ko LD [M] sound/usb/line6/snd-usb-podhd.ko LD [M] sound/usb/line6/snd-usb-toneport.ko LD [M] sound/usb/line6/snd-usb-variax.ko LD [M] sound/usb/misc/snd-ua101.ko LD [M] sound/usb/snd-usb-audio.ko LD [M] sound/usb/snd-usbmidi-lib.ko LD [M] sound/usb/usx2y/snd-usb-us122l.ko LD [M] sound/usb/usx2y/snd-usb-usx2y.ko LD [M] sound/x86/snd-hdmi-lpe-audio.ko LD [M] virt/lib/irqbypass.ko
KVM 的安装包括两个步骤:
通过make modules_install
命令可以将编译好的 module 安装到相应的目录中:
> make modules_install# 此处省略部分编译时的输出信息 INSTALL sound/usb/snd-usbmidi-lib.ko INSTALL sound/usb/usx2y/snd-usb-us122l.ko INSTALL sound/usb/usx2y/snd-usb-usx2y.ko INSTALL sound/x86/snd-hdmi-lpe-audio.ko INSTALL virt/lib/irqbypass.ko DEPMOD 4.20.0-rc6
默认情况下,module 会被安装到
/lib/modules/$kernel_version/kernel
目录中
安装完成后可以查看对应的安装路径,且kvm.ko
、kvm-intel.ko
两个模块也已经安装:
> ls -hl /lib/modules/4.20.0-rc6/kernel total 16Kdrwxr-xr-x 3 root root 17 Apr 23 16:35 archdrwxr-xr-x 3 root root 4.0K Apr 23 16:35 cryptodrwxr-xr-x 68 root root 4.0K Apr 23 16:36 driversdrwxr-xr-x 25 root root 4.0K Apr 23 16:36 fsdrwxr-xr-x 3 root root 19 Apr 23 16:36 kerneldrwxr-xr-x 5 root root 207 Apr 23 16:36 libdrwxr-xr-x 2 root root 32 Apr 23 16:36 mmdrwxr-xr-x 35 root root 4.0K Apr 23 16:37 netdrwxr-xr-x 12 root root 167 Apr 23 16:37 sounddrwxr-xr-x 3 root root 17 Apr 23 16:37 virt> ls -hl /lib/modules/4.20.0-rc6/kernel/arch/x86/kvmtotal 15M-rw-r--r-- 1 root root 3.6M Apr 23 16:35 kvm-intel.ko-rw-r--r-- 1 root root 11M Apr 23 16:35 kvm.ko
通过make install
命令安装 kernel 和 initramfs,命令行输出如下:
> make installsh ./arch/x86/boot/install.sh 4.20.0-rc6 arch/x86/boot/bzImage \ System.map "/boot"> ls -hl /boot -ttotal 300M-rw------- 1 root root 108M Apr 23 21:37 initramfs-4.20.0-rc6.imglrwxrwxrwx 1 root root 27 Apr 23 21:34 System.map -> /boot/System.map-4.20.0-rc6lrwxrwxrwx 1 root root 24 Apr 23 21:34 vmlinuz -> /boot/vmlinuz-4.20.0-rc6-rw-r--r-- 1 root root 3.5M Apr 23 21:34 System.map-4.20.0-rc6-rw-r--r-- 1 root root 7.5M Apr 23 21:34 vmlinuz-4.20.0-rc6drwx------. 2 root root 21 Jan 9 16:48 grub2drwxr-xr-x. 2 root root 27 Nov 13 11:28 grubdrwx------ 3 root root 16K Jan 1 1970 efi
可以看到在/boot
目录下生成了内核文件vmlinuz
和initramfs
等内核启动所需的文件。
另外在运行make install
之后,/boot/efi/EFI/centos/grub.cfg
配置文件中也自动添加了一个 grub 选项,如下所示:
注:在下面的
menuentry
中还配置了KVMGT
的相关选项,之后会另写一篇文章加以说明
menuentry 'Ubuntu,Linux 4.20.0-rc6 KVMGT' --class kvmgt --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option'gnulinux-4.20.0-rc6-advanced-26d36e85-367a-4200-87fb-0505c5837078' { recordfail load_video gfxmode $linux_gfx_mode insmod gzio if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi insmod part_gpt insmod ext2 set root='hd0,gpt8' if [ x$feature_platform_search_hint = xy ]; then search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt8 --hint-efi=hd0,gpt8 --hint-baremetal=ahci0,gpt8 26d36e85-367a-4200-87fb-0505c5837078 else search --no-floppy --fs-uuid --set=root 26d36e85-367a-4200-87fb-0505c5837078 fi echo '载入 Linux 4.20.0-rc6 ...' linux /boot/vmlinuz-4.20.0-rc6 root=UUID=26d36e85-367a-4200-87fb-0505c5837078 ro quiet splash $vt_handoff ignore_loglevel log_buf_len=128M console=ttyS0,115200,8n1 i915.enable_gvt=1 kvm.ignore_msrs=1 intel_iommu=on drm.debug=0 echo '载入初始化内存盘...' initrd /boot/initrd.img-4.20.0-rc6}
联想扬天 T4900 开机进入 BIOS 快捷键 F12
检查grub.cfg
配置无误后,重新启动系统,选择刚才为 KVM 而编译、安装的内核启动。
进入系统后,使用uname -r
查看内核版本:
> uname -r4.20.0-rc6
通常情况下,系统启动时已经默认加载了kvm
和kvm_intel
这两个模块,如果没有加载,则需要手动使用modprobe
命令依次加载这两个模块:
> modprobe kvm> modprobe kvm_intel> lsmod | grep kvmkvm_intel 245760 0kvmgt 28672 1mdev 24576 2 kvmgt,vfio_mdevvfio 32768 3 kvmgt,vfio_mdev,vfio_iommu_type1kvm 634880 2 kvmgt,kvm_intelirqbypass 16384 1 kvm
确认 KVM 相关模块加载成功后,检查/dev/kvm
文件是否存在:
> ls -l /dev/kvmcrw-rw---- 1 root kvm 10, 232 Apr 29 09:36 /dev/kvm
/dev/kvm
是 KVM 内核模块提供给用户空间 QEMU 程序使用的一个控制接口,提供了Guest OS
运行所需要的模拟和实际的硬件设备环境。
crw-rw----
以c
为开头,表示/dev/kvm
是一个字符设备
除了在内核空间的kvm.ko
模块之外,在用户空间还需要 QEMU 来模拟 VM 所需要的 I/O 设备,并启动客户机进程。
在早期版本中,支持 KVM 的qemu-kvm
是由 kernel 社区维护的专门用于 KVM 虚拟化的 QEMU 分支。2012 年末,这个分支并入了主流的 QEMU 仓库,从此就不再需要特殊的qemu-kvm
,而只需在通用的 QEMU 命令后添加--enable-kvm
选项,即可创建 KVM Guest。
直接下载源代码归档包:
~ > wget https://download.qemu.org/qemu-4.0.0.tar.xz~ > tar -xvJf qemu-4.0.0.tar.xz~ > cd qemu-4.0.0qemu-4.0.0 > lsaccel capstone device_tree.c hmp-commands.hx MAINTAINERS pc-bios qemu-keymap.c qtest.c tpm.carch_init.c Changelog disas hmp-commands-info.hx Makefile po qemu-nbd.c README traceaudio chardev disas.c hmp.h Makefile.objs python qemu-nbd.texi replay trace-eventsauthz CODING_STYLE dma-helpers.c hw Makefile.target qapi qemu.nsi replication.c uibackends config.log docs include memory.c qdev-monitor.c qemu-options.h replication.h utilballoon.c config-temp dtc io memory_ldst.inc.c qemu-bridge-helper.c qemu-options.hx roms VERSIONblock configure dump.c ioport.c memory_mapping.c qemu-deprecated.texi qemu-options-wrapper.h rules.mak version.rcblock.c contrib exec.c iothread.c migration qemu-doc.texi qemu-option-trace.texi scripts vl.cblockdev.c COPYING fpu job.c module-common.c qemu-edid.c qemu.sasl scsi win_dump.cblockdev-nbd.c COPYING.LIB fsdev job-qmp.c monitor.c qemu-ga.texi qemu-seccomp.c slirp win_dump.hblockjob.c cpus.c gdbstub.c Kconfig.host nbd qemu-img.c qemu-tech.texi stubsbootdevice.c cpus-common.c gdb-xml libdecnumber net qemu-img-cmds.hx qga targetbsd-user crypto gitdm.config LICENSE numa.c qemu-img.texi qmp.c tcgbt-host.c default-configs HACKING linux-headers os-posix.c qemu-io.c qobject testsbt-vhci.c device-hotplug.c hmp.c linux-user os-win32.c qemu-io-cmds.c qom thunk.cqemu-4.0.0 > cat VERSION4.0.0
或者使用 Git 拉取 QEMU 源代码:
~ > git clone https://git.qemu.org/git/qemu.git~ > cd qemuqemu > git submodule initqemu > git submodule update --recursive
本文使用的 QEMU 版本为
4.0.50
首先运行./configure --help
查看配置 QEMU 的选项及帮助信息:
qemu-4.0.50 > ./configure --helpUsage: configure [options]Options: [defaults in brackets after descriptions]Standard options: --help print this message --prefix=PREFIX install in PREFIX [/usr/local] --interp-prefix=PREFIX where to find shared libraries, etc. use %M for cpu name [/usr/gnemul/qemu-%M] --target-list=LIST set target list (default: build everything) Available targets: aarch64-softmmu alpha-softmmu arm-softmmu cris-softmmu hppa-softmmu i386-softmmu lm32-softmmu m68k-softmmu microblaze-softmmu microblazeel-softmmu mips-softmmu mips64-softmmu mips64el-softmmu mipsel-softmmu moxie-softmmu nios2-softmmu or1k-softmmu ppc-softmmu ppc64-softmmu riscv32-softmmu riscv64-softmmu s390x-softmmu sh4-softmmu sh4eb-softmmu sparc-softmmu sparc64-softmmu tricore-softmmu unicore32-softmmu x86_64-softmmu xtensa-softmmu xtensaeb-softmmu aarch64-linux-user aarch64_be-linux-user alpha-linux-user arm-linux-user armeb-linux-user cris-linux-user hppa-linux-user i386-linux-user m68k-linux-user microblaze-linux-user microblazeel-linux-user mips-linux-user mips64-linux-user mips64el-linux-user mipsel-linux-user mipsn32-linux-user mipsn32el-linux-user nios2-linux-user or1k-linux-user ppc-linux-user ppc64-linux-user ppc64abi32-linux-user ppc64le-linux-user riscv32-linux-user riscv64-linux-user s390x-linux-user sh4-linux-user sh4eb-linux-user sparc-linux-user sparc32plus-linux-user sparc64-linux-user tilegx-linux-user x86_64-linux-user xtensa-linux-user xtensaeb-linux-user --target-list-exclude=LIST exclude a set of targets from the default target-listAdvanced options (experts only): --source-path=PATH path of source code [/kvm/qemu_src/qemu-4.0.0] --cross-prefix=PREFIX use PREFIX for compile tools [] --cc=CC use C compiler CC [cc] --iasl=IASL use ACPI compiler IASL [iasl] --host-cc=CC use C compiler CC [cc] for code run at build time --cxx=CXX use C++ compiler CXX [c++] --objcc=OBJCC use Objective-C compiler OBJCC [cc] --extra-cflags=CFLAGS append extra C compiler flags QEMU_CFLAGS --extra-cxxflags=CXXFLAGS append extra C++ compiler flags QEMU_CXXFLAGS --extra-ldflags=LDFLAGS append extra linker flags LDFLAGS --cross-cc-ARCH=CC use compiler when building ARCH guest test cases --cross-cc-flags-ARCH= use compiler flags when building ARCH guest tests --make=MAKE use specified make [make] --install=INSTALL use specified install [install] --python=PYTHON use specified python [python] --smbd=SMBD use specified smbd [/usr/sbin/smbd] --with-git=GIT use specified git [git] --static enable static build [no] --mandir=PATH install man pages in PATH --datadir=PATH install firmware in PATH/qemu --docdir=PATH install documentation in PATH/qemu --bindir=PATH install binaries in PATH --libdir=PATH install libraries in PATH --libexecdir=PATH install helper binaries in PATH --sysconfdir=PATH install config in PATH/qemu --localstatedir=PATH install local state in PATH (set at runtime on win32) --firmwarepath=PATH search PATH for firmware files --with-confsuffix=SUFFIX suffix for QEMU data inside datadir/libdir/sysconfdir [/qemu] --with-pkgversion=VERS use specified string as sub-version of the package --enable-debug enable common debug build options --enable-sanitizers enable default sanitizers --disable-strip disable stripping binaries --disable-werror disable compilation abort on warning --disable-stack-protector disable compiler-provided stack protection --audio-drv-list=LIST set audio drivers list: Available drivers: oss alsa sdl pa --block-drv-whitelist=L Same as --block-drv-rw-whitelist=L --block-drv-rw-whitelist=L set block driver read-write whitelist (affects only QEMU, not qemu-img) --block-drv-ro-whitelist=L set block driver read-only whitelist (affects only QEMU, not qemu-img) --enable-trace-backends=B Set trace backend Available backends: dtrace ftrace log simple syslog ust --with-trace-file=NAME Full PATH,NAME of file to store traces Default:trace- --disable-slirp disable SLIRP userspace network connectivity --enable-tcg-interpreter enable TCG with bytecode interpreter (TCI) --enable-malloc-trim enable libc malloc_trim() for memory optimization --oss-lib path to OSS library --cpu=CPU Build for host CPU [x86_64] --with-coroutine=BACKEND coroutine backend. Supported options: ucontext, sigaltstack, windows --enable-gcov enable test coverage analysis with gcov --gcov=GCOV use specified gcov [gcov] --disable-blobs disable installing provided firmware blobs --with-vss-sdk=SDK-path enable Windows VSS support in QEMU Guest Agent --with-win-sdk=SDK-path path to Windows Platform SDK (to build VSS .tlb) --tls-priority default TLS protocol/cipher priority string --enable-gprof QEMU profiling with gprof --enable-profiler profiler support --enable-debug-stack-usage track the maximum stack usage of stacks created by qemu_alloc_stackOptional features, enabled with --enable-FEATURE anddisabled with --disable-FEATURE, default is enabled if available: system all system emulation targets user supported user emulation targets linux-user all linux usermode emulation targets bsd-user all BSD usermode emulation targets docs build documentation guest-agent build the QEMU Guest Agent guest-agent-msi build guest agent Windows MSI installation package pie Position Independent Executables modules modules support debug-tcg TCG debugging (default is disabled) debug-info debugging information sparse sparse checker gnutls GNUTLS cryptography support nettle nettle cryptography support gcrypt libgcrypt cryptography support auth-pam PAM access control sdl SDL UI sdl_image SDL Image support for icons gtk gtk UI vte vte support for the gtk UI curses curses UI iconv font glyph conversion support vnc VNC UI support vnc-sasl SASL encryption for VNC server vnc-jpeg JPEG lossy compression for VNC server vnc-png PNG compression for VNC server cocoa Cocoa UI (Mac OS X only) virtfs VirtFS mpath Multipath persistent reservation passthrough xen xen backend driver support xen-pci-passthrough PCI passthrough support for Xen brlapi BrlAPI (Braile) curl curl connectivity membarrier membarrier system call (for Linux 4.14+ or Windows) fdt fdt device tree bluez bluez stack connectivity kvm KVM acceleration support hax HAX acceleration support hvf Hypervisor.framework acceleration support whpx Windows Hypervisor Platform acceleration support rdma Enable RDMA-based migration pvrdma Enable PVRDMA support vde support for vde network netmap support for netmap network linux-aio Linux AIO support cap-ng libcap-ng support attr attr and xattr support vhost-net vhost-net kernel acceleration support vhost-vsock virtio sockets device support vhost-scsi vhost-scsi kernel target support vhost-crypto vhost-user-crypto backend support vhost-kernel vhost kernel backend support vhost-user vhost-user backend support spice spice rbd rados block device (rbd) libiscsi iscsi support libnfs nfs support smartcard smartcard support (libcacard) libusb libusb (for usb passthrough) live-block-migration Block migration in the main migration stream usb-redir usb network redirection support lzo support of lzo compression library snappy support of snappy compression library bzip2 support of bzip2 compression library (for reading bzip2-compressed dmg images) lzfse support of lzfse compression library (for reading lzfse-compressed dmg images) seccomp seccomp support coroutine-pool coroutine freelist (better performance) glusterfs GlusterFS backend tpm TPM support libssh2 ssh block device support numa libnuma support libxml2 for Parallels image format tcmalloc tcmalloc support jemalloc jemalloc support avx2 AVX2 optimization support replication replication support opengl opengl support virglrenderer virgl rendering support xfsctl xfsctl support qom-cast-debug cast debugging support tools build qemu-io, qemu-nbd and qemu-img tools vxhs Veritas HyperScale vDisk backend support bochs bochs image format support cloop cloop image format support dmg dmg image format support qcow1 qcow v1 image format support vdi vdi image format support vvfat vvfat image format support qed qed image format support parallels parallels image format support sheepdog sheepdog block driver support crypto-afalg Linux AF_ALG crypto backend driver capstone capstone disassembler support debug-mutex mutex debugging support libpmem libpmem supportNOTE: The object files are built at the place where configure is launched
根据实际需求启用或禁用相关配置项,配置命令如下:
./configure --prefix=/usr \ --enable-kvm \ --enable-libusb \ --enable-usb-redir \ --enable-debug \ --enable-debug-info \ --enable-curl \ --enable-sdl \ --enable-vhost-net \ --enable-spice \ --enable-vnc \ --enable-opengl \ --enable-gtk \ --target-list=x86_64-softmmu
可能需要根据提示信息安装相应的依赖包,参见 3.3.1 Build Qemu for KVMGT | gvt-linux
CentOS 7 的 yum 包:
yum install SDL2-devel libcurl-devel
Ubuntu 18.04 的 apt 包:
apt-get install libsdl2-dev libcurl4-openssl-dev libusbredirhost-dev
若相关依赖均已安装,则可成功配置,终端输出如下所示:
Install prefix /usrBIOS directory /usr/share/qemufirmware path /usr/share/qemu-firmwarebinary directory /usr/binlibrary directory /usr/libmodule directory /usr/lib/qemulibexec directory /usr/libexecinclude directory /usr/includeconfig directory /usr/etclocal state directory /usr/varManual directory /usr/share/manELF interp prefix /usr/gnemul/qemu-%MSource path /kvm/qemu_src/qemu-4.0.50GIT binary gitGIT submodules ui/keycodemapdb tests/fp/berkeley-testfloat-3 tests/fp/berkeley-softfloat-3 dtc capstoneC compiler ccHost C compiler ccC++ compiler c++Objective-C compiler ccARFLAGS rvCFLAGS -g QEMU_CFLAGS -I/usr/include/pixman-1 -I$(SRC_PATH)/dtc/libfdt -Werror -pthread -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -fPIE -DPIE -m64 -mcx16 -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -Wstrict-prototypes -Wredundant-decls -Wall -Wundef -Wwrite-strings -Wmissing-prototypes -fno-strict-aliasing -fno-common -fwrapv -std=gnu99 -Wexpansion-to-defined -Wendif-labels -Wno-shift-negative-value -Wno-missing-include-dirs -Wempty-body -Wnested-externs -Wformat-security -Wformat-y2k -Winit-self -Wignored-qualifiers -Wold-style-declaration -Wold-style-definition -Wtype-limits -fstack-protector-strong -I/usr/include/libpng16 -I/usr/include/spice-server -I/usr/include/spice-1 -I$(SRC_PATH)/capstone/includeLDFLAGS -Wl,--warn-common -Wl,-z,relro -Wl,-z,now -pie -m64 -g QEMU_LDFLAGS -L$(BUILD_DIR)/dtc/libfdt make makeinstall installpython python -B (2.7.15rc1)slirp support internal smbd /usr/sbin/smbdmodule support nohost CPU x86_64host big endian notarget list x86_64-softmmugprof enabled nosparse enabled nostrip binaries noprofiler nostatic build noSDL support yes (2.0.8)SDL image support noGTK support yes (3.22.30)GTK GL support yesVTE support no TLS priority NORMALGNUTLS support nolibgcrypt nonettle no libtasn1 noPAM noiconv support yescurses support novirgl support no curl support yesmingw32 support noAudio drivers pa ossBlock whitelist (rw) Block whitelist (ro) VirtFS support noMultipath support noVNC support yesVNC SASL support noVNC JPEG support noVNC PNG support yesxen support nobrlapi support nobluez support noDocumentation noPIE yesvde support nonetmap support noLinux AIO support yesATTR/XATTR support yesInstall blobs yesKVM support yesHAX support noHVF support noWHPX support noTCG support yesTCG debug enabled yesTCG interpreter nomalloc trim support yesRDMA support noPVRDMA support nofdt support gitmembarrier nopreadv support yesfdatasync yesmadvise yesposix_madvise yesposix_memalign yeslibcap-ng support novhost-net support yesvhost-crypto support yesvhost-scsi support yesvhost-vsock support yesvhost-user support yesTrace backends logspice support yes (0.12.13/0.14.0)rbd support noxfsctl support nosmartcard support nolibusb yesusb net redir yesOpenGL support yesOpenGL dmabufs yeslibiscsi support nolibnfs support nobuild guest agent yesQGA VSS support noQGA w32 disk info noQGA MSI support noseccomp support nocoroutine backend ucontextcoroutine pool yesdebug stack usage nomutex debugging yescrypto afalg noGlusterFS support nogcov gcovgcov enabled noTPM support yeslibssh2 support noTPM passthrough TPM emulator QOM debugging yesLive block migration yeslzo support nosnappy support nobzip2 support nolzfse support noNUMA host support nolibxml2 yestcmalloc support nojemalloc support noavx2 optimization yesreplication support yesVxHS block device nobochs support yescloop support yesdmg support yesqcow v1 support yesvdi support yesvvfat support yesqed support yesparallels support yessheepdog support yescapstone gitdocker yeslibpmem support nolibudev yesdefault devices yesNOTE: cross-compilers enabled: 'cc'
在配置完以后,当前目录
qemu-4.0.50
下会生成config-host.mak
和config.status
文件:
config-host.mak
:可查看上述./configure
之后的配置结果,会在后续make
中被引用config.status
:便于后续要重新configure
时,只要执行./config.status
,就可以恢复上一次的配置经过配置之后,编译就很简单了,直接执行make
命令:
qemu-4.0.50 > make -j 16qemu-4.0.50 > cd x86_64-softmmux86_64-softmmu > lsaccel config-devices.mak cpus.d dump.o gdbstub.o hmp-commands-info.h memory.d monitor.d qemu-system-x86_64 tracearch_init.d config-devices.mak.old cpus.o exec.d gdbstub-xml.c hw memory_mapping.d monitor.o qtest.d win_dump.darch_init.o config-target.h disas.d exec.o gdbstub-xml.d ioport.d memory_mapping.o numa.d qtest.o win_dump.oballoon.d config-target.h-timestamp disas.o fpu gdbstub-xml.o ioport.o memory.o numa.o targetballoon.o config-target.mak dump.d gdbstub.d hmp-commands.h Makefile migration qapi tcgx86_64-softmmu > ./qemu-system-x86_64 --versionQEMU emulator version 4.0.50 (v4.0.0-142-ge0fb2c3d89-dirty)Copyright (c) 2003-2019 Fabrice Bellard and the QEMU Project developers
为了支持KVMGT
,还需单独编译seabios
,之后会在out
目录下生成bios.bin
文件:
qemu-4.0.50 > cd roms/seabiosseabios > make -j 16seabios > ls ./outasm-offsets.h bios.bin.prep ccode16.o.tmp.c code16.o code32seg.d include rom32seg.strip.o romlayout.d scripts vgasrcautoconf.h bios.bin.raw ccode32flat.d code16.o.objdump code32seg.o rom16.o romlayout16.lds romlayout.o srcautoversion.h ccode16.d ccode32flat.o code32flat.o code32seg.o.objdump rom16.strip.o romlayout32flat.lds rom.o version.dbios.bin ccode16.o ccode32flat.o.tmp.c code32flat.o.objdump code32seg.o.tmp.c rom32seg.o romlayout32seg.lds rom.o.objdump version.o
编译完成后,运行make install
命令即可安装 QEMU:
qemu-4.0.50 > make installqemu-4.0.50 > ls /usr/share/qemuacpi-dsdt.aml edk2-x86_64-code.fd init ppc_rom.bin qemu-icon.bmp trace-eventsbamboo.dtb edk2-x86_64-secure-code.fd keymaps pvh.bin qemu_logo_no_text.svg trace-events-allbios-256k.bin efi-e1000e.rom kvmvapic.bin pxe-e1000.rom QEMU,tcx.bin u-boot.e500bios.bin efi-e1000.rom linuxboot.bin pxe-eepro100.rom QEMU,VGA.bin u-boot-sam460-20100605.bincanyonlands.dtb efi-eepro100.rom linuxboot_dma.bin pxe-ne2k_isa.rom qemu_vga.ndrv vgabios.binedk2-aarch64-code.fd efi-ne2k_pci.rom multiboot.bin pxe-ne2k_pci.rom s390-ccw.img vgabios-bochs-display.binedk2-arm-code.fd efi-pcnet.rom openbios-ppc pxe-pcnet32.rom s390-netboot.img vgabios-cirrus.binedk2-arm-vars.fd efi-rtl8139.rom openbios-sparc32 pxe-pcnet.rom s390-zipl.rom vgabios-qxl.binedk2-i386-code.fd efi-virtio.rom openbios-sparc64 pxe-rtl8139.rom sgabios.bin vgabios-ramfb.binedk2-i386-secure-code.fd efi-vmxnet3.rom palcode-clipper pxe-virtio.rom skiboot.lid vgabios-stdvga.binedk2-i386-vars.fd firmware petalogix-ml605.dtb q35-acpi-dsdt.aml slof.bin vgabios-virtio.binedk2-licenses.txt hppa-firmware.img petalogix-s3adsp1800.dtb QEMU,cgthree.bin spapr-rtas.bin vgabios-vmware.binqemu-4.0.50 > ls /usr/share/qemu/firmware50-edk2-i386-secure.json 50-edk2-x86_64-secure.json 60-edk2-aarch64.json 60-edk2-arm.json 60-edk2-i386.json 60-edk2-x86_64.jsonqemu-4.0.50 > ls /usr/share/qemu/keymapsar common da de-ch en-us et fo fr-be fr-ch hu it lt mk nl no pt ru sv trbepo cz de en-gb es fi fr fr-ca hr is ja lv modifiers nl-be pl pt-br sl th
另外还需将编译后的bios.bin
拷贝至/usr/bin
目录:
qemu-4.0.50 > cp roms/seabios/out/bios.bin /usr/bin/bios.bin
QEMU 的安装过程主要有以下几个任务:
sgabios.bin
、kvmvapic.bin
)到目录下,以便 QEMU 命令行启动时可以找到对应的固件供客户机使用keymaps
到相应的目录下,以便在客户机中支持各种所需的键盘类型qemu-system-x86_64
、qemu-img
等可执行程序到对应的目录下 > ls /usr/share/qemuacpi-dsdt.aml edk2-x86_64-code.fd init ppc_rom.bin qemu-icon.bmp trace-eventsbamboo.dtb edk2-x86_64-secure-code.fd keymaps pvh.bin qemu_logo_no_text.svg trace-events-allbios-256k.bin efi-e1000e.rom kvmvapic.bin pxe-e1000.rom QEMU,tcx.bin u-boot.e500bios.bin efi-e1000.rom linuxboot.bin pxe-eepro100.rom QEMU,VGA.bin u-boot-sam460-20100605.bincanyonlands.dtb efi-eepro100.rom linuxboot_dma.bin pxe-ne2k_isa.rom qemu_vga.ndrv vgabios.binedk2-aarch64-code.fd efi-ne2k_pci.rom multiboot.bin pxe-ne2k_pci.rom s390-ccw.img vgabios-bochs-display.binedk2-arm-code.fd efi-pcnet.rom openbios-ppc pxe-pcnet32.rom s390-netboot.img vgabios-cirrus.binedk2-arm-vars.fd efi-rtl8139.rom openbios-sparc32 pxe-pcnet.rom s390-zipl.rom vgabios-qxl.binedk2-i386-code.fd efi-virtio.rom openbios-sparc64 pxe-rtl8139.rom sgabios.bin vgabios-ramfb.binedk2-i386-secure-code.fd efi-vmxnet3.rom palcode-clipper pxe-virtio.rom skiboot.lid vgabios-stdvga.binedk2-i386-vars.fd firmware petalogix-ml605.dtb q35-acpi-dsdt.aml slof.bin vgabios-virtio.binedk2-licenses.txt hppa-firmware.img petalogix-s3adsp1800.dtb QEMU,cgthree.bin spapr-rtas.bin vgabios-vmware.bin> ls /usr/share/qemu/keymapsar common da de-ch en-us et fo fr-be fr-ch hu it lt mk nl no pt ru sv trbepo cz de en-gb es fi fr fr-ca hr is ja lv modifiers nl-be pl pt-br sl th
由于 QEMU 是用户空间的程序,所以安装之后不需要重启系统,直接使用
qemu-system-x86_64
、qemu-img
这样的命令行工具就可以了
安装客户机前,需要先创建一个镜像文件来存储客户机中的系统和文件:
> qemu-img create -f raw fedora30.raw 20G # 指定为 raw 格式Formatting 'fedora30.raw', fmt=raw size=21474836480> qemu-img info fedora30.rawimage: fedora30.rawfile format: rawvirtual size: 20G (21474836480 bytes)disk size: 0
可以看到目前fedora30.raw
并不占用任何磁盘空间,这是因为qemu-img
默认的方式是按需分配,镜像文件的大小会随着实际的使用而增大。
可在qemu-img
命令中添加-o preallocation=full
选项,使得镜像在创建时分配全部的空间,不过这样的话格式化过程就会比较耗时:
> qemu-img create -f raw fedora30-full.raw 10G -o preallocation=fullFormatting 'fedora30-full.raw', fmt=raw size=10737418240 preallocation='full'> qemu-img info fedora30-full.rawimage: fedora30-full.rawfile format: rawvirtual size: 10G (10737418240 bytes)disk size: 10G
最后即可使用以下命令启动 KVM 客户机,并安装系统:
> qemu-system-x86_64 -accel kvm \ -m 2G -smp 2 -boot once=d \ -cdrom /kvm/iso/Fedora-Server-dvd-x86_64-30-1.2.iso \ -drive file=/kvm/image/fedora30.raw,format=raw
]]>
Your terminal never felt this good before.
> cat /etc/shells/bin/sh/bin/bash/sbin/nologin/usr/bin/sh/usr/bin/bash/usr/sbin/nologin/bin/tcsh/bin/csh/usr/bin/tmux> chsh -l/bin/sh/bin/bash/sbin/nologin/usr/bin/sh/usr/bin/bash/usr/sbin/nologin/bin/tcsh/bin/csh/usr/bin/tmux> echo $SHELL/bin/bash
可以看到 CentOS 7 的默认终端是/bin/bash
。
> yum info zshAvailable PackagesName : zshArch : x86_64Version : 5.0.2Release : 31.el7Size : 2.4 MRepo : base/7/x86_64Summary : Powerful interactive shellURL : http://zsh.sourceforge.net/License : MITDescription : The zsh shell is a command interpreter usable as an interactive : login shell and as a shell script command processor. Zsh resembles : the ksh shell (the Korn shell), but includes many enhancements. : Zsh supports command line editing, built-in spelling correction, : programmable command completion, shell functions (with : autoloading), a history mechanism, and more.> yum install -y zsh> cat /etc/shells/bin/sh/bin/bash/sbin/nologin/usr/bin/sh/usr/bin/bash/usr/sbin/nologin/bin/tcsh/bin/csh/usr/bin/tmux/bin/zsh
请在
root
用户下切换 Shell
> chsh -s /bin/zshChanging shell for root.Shell changed.
提示终端切换完成,但还需重启方能生效。之后继续安装oh-my-zsh
。
sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"
sh -c "$(wget https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh -O -)"
提示oh-my-zsh
安装成功:
__ __ ____ / /_ ____ ___ __ __ ____ _____/ /_ / __ \/ __ \ / __ `__ \/ / / / /_ / / ___/ __ \ / /_/ / / / / / / / / / / /_/ / / /_(__ ) / / / \____/_/ /_/ /_/ /_/ /_/\__, / /___/____/_/ /_/ /____/ ....is now installed!Please look over the ~/.zshrc file to select plugins, themes, and options.p.s. Follow us at https://twitter.com/ohmyzsh.p.p.s. Get stickers, shirts, and coffee mugs at https://shop.planetargon.com/collections/oh-my-zsh.
oh-my-zsh 的默认主题是robbyrussell
,可以在~/.zshrc
中修改ZSH_THEME
主题字段,主题清单参见 Themes | oh-my-zsh。
# Set name of the theme to load --- if set to "random", it will# load a random theme each time oh-my-zsh is loaded, in which case,# to know which specific one was loaded, run: echo $RANDOM_THEME# See https://github.com/robbyrussell/oh-my-zsh/wiki/ThemesZSH_THEME="agnoster"
另外还可以在~/.zshrc
中设置PATH
路径或添加alias
命令别名,使用方法与~/.bashrc
相同:
> vim ~/.zshrc# If you come from bash you might have to change your $PATH.# export PATH=$HOME/bin:/usr/local/bin:$PATHexport GOLANDPATH=$HOME/GoLand-2018.3.5/export GOPATH=$HOME/goexport PATH="$PATH:$GOPATH/bin:$GOLANDPATH:bin"# Path to your oh-my-zsh installation.export ZSH="/root/.oh-my-zsh"# Set name of the theme to load --- if set to "random", it will# load a random theme each time oh-my-zsh is loaded, in which case,ZSH_THEME="agnoster"......# Set list of themes to pick from when loading at random# If set to an empty array, this variable will have no effect.# ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" )# Set personal aliases, overriding those provided by oh-my-zsh libs,# plugins, and themes. Aliases can be placed here, though oh-my-zsh# users are encouraged to define aliases within the ZSH_CUSTOM folder.# For a full list of active aliases, run `alias`.## Example aliases# alias zshconfig="mate ~/.zshrc"# alias ohmyzsh="mate ~/.oh-my-zsh"alias rm='rm -i'alias cp='cp -i'alias mv='mv -i'alias vi='vim'alias pc='proxychains4'
保存之后更新配置:
source ~/.zshrc
例如下图所示即为agnoster-zsh-theme
主题效果:
注意:在 CentOS 7 下使用agnoster
主题,部分符号在终端无法正常显示,还需安装 Powerline fonts 字体:
# clonegit clone https://github.com/powerline/fonts.git --depth=1# installcd fonts./install.sh# clean-up a bitcd ..rm -rf fonts
之后在终端输入以下命令测试Powerline font
字体是否成功安装:
echo "\ue0b0 \u00b1 \ue0a0 \u27a6 \u2718 \u26a1 \u2699"
输入d
命令,即可查看在这个终端会话中访问过的目录,输入目录对应的序号即可跳转:
~ > d0 ~1 ~/.oh-my-zsh/plugins/git2 /opt/google3 ~/.oh-my-zsh4 ~/.oh-my-zsh/plugins5 ~/GolandProjects/go-rest-api-server6 ~/GolandProjects~ > 6~/GolandProjects~/GolandProjects >
另外还可以忽略cd
命令,输入..
、...
或目录名都可以跳转。
]]>
云计算开发,2019.4.5
package mainimport ( "fmt" "strings")func main() { var n int var s string fmt.Scanln(&n) fmt.Scanln(&s) bitsOf1 := strings.Count(s, "1") bitsOf0 := n - bitsOf1 if bitsOf1 >= bitsOf0 { fmt.Println(bitsOf1 - bitsOf0) } else { fmt.Println(bitsOf0 - bitsOf1) }}------1010010001012Process finished with exit code 0
package mainimport ( "fmt" "sort")func main() { var m, n int fmt.Scanf("%d %d", &m, &n) coinValues := make([]int, n) for i := 0; i < n; i++ { fmt.Scanln(&coinValues[i]) } sort.Slice(coinValues, func(i, j int) bool { return coinValues[i] > coinValues[j] }) fmt.Println(minCoins(m, n, coinValues))}func minCoins(target, n int, coinValues []int) int { if coinValues[n-1] != 1 { // 若没有 1 元硬币,则此题无解 return -1 } coinNum := 0 // 当前硬币总数 maxValue := 0 // 当前硬币总金额 coinNums := make([]int, target+1) // 记录每个硬币的数量 coinNums[0] = 0 for i := 1; i <= target; i++ { if i > maxValue { // 目标金额比 maxValue 大 for _, coinValue := range coinValues { if i >= coinValue { // 取可凑成该金额的最大硬币 coinNum++ maxValue += coinValue break } } coinNums[i] = coinNum } else { coinNums[i] = coinNums[i-1] } } return coinNums[target]}------20 4152105Process finished with exit code 0
package mainimport "fmt"func main() { var n int // 怪兽数量 fmt.Scanln(&n) boss := make([]int, n) coins := make([]int, n) for i := 0; i < n; i++ { fmt.Scan(&boss[i]) } for i := 0; i < n; i++ { fmt.Scan(&coins[i]) } curValue := boss[0] curCoins := coins[0] fmt.Println(challenge(boss, coins, 1, n, curValue, curCoins))}func challenge(boss []int, coins []int, curIndex, n int, curValue, curCoins int) int { if curIndex == n { return curCoins } if boss[curIndex] > curValue { return challenge(boss, coins, curIndex+1, n, curValue+boss[curIndex], curCoins+coins[curIndex]) } else { return min(challenge(boss, coins, curIndex+1, n, curValue, curCoins), challenge(boss, coins, curIndex+1, n, curValue+boss[curIndex], curCoins+coins[curIndex])) }}func min(a, b int) int { if a < b { return a } else { return b }}------58 1 1 10 132 1 1 3 45Process finished with exit code 0
]]>Leetcode、牛客刷题之旅,不定期更新中
原题传送门👉二维数组中的查找 | 牛客
func find(target int, array [][]int) bool { if array == nil || len(array) == 0 || len(array[0]) == 0 { return false } rows := len(array) cols := len(array[0]) // 从右上角开始 curRow := 0 curCol := cols - 1 for curRow <= rows-1 && curCol >= 0 { switch { case target == array[curRow][curCol]: return true case target < array[curRow][curCol]: curCol-- case target > array[curRow][curCol]: curRow++ default: break } } return false}
原题传送门👉数组中重复的数字 | 牛客
// Parameters:// numbers: an array of integers// length: the length of array numbers// duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation;// Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++// 这里要特别注意~返回任意重复的一个,赋值duplication[0]// Return value: true if the input is valid, and there are some duplications in the array number// otherwise falsefunc duplicate(numbers []int, length int, duplication []int) bool { if numbers == nil || length <= 0 { return false } for i := 0; i < length; i++ { for numbers[i] != i { if numbers[i] == numbers[numbers[i]] { duplication[0] = numbers[i] return true } numbers[i], numbers[numbers[i]] = numbers[numbers[i]], numbers[i] } } return false}
原题传送门👉构建乘积数组 | 牛客
func multiply(A []int) []int { if A == nil || len(A) == 0 { return []int{} } n := len(A) B := make([]int, n) B[0] = 1 for i := 1; i < n; i++ { // 计算下三角连乘 B[i] = B[i-1] * A[i-1] } product := 1 for j := n - 2; j >= 0; j-- { // 计算上三角连乘 product *= A[j+1] B[j] *= product } return B}
原题传送门👉替换空格 | 牛客
func replaceSpace(str string) string { return strings.Replace(str, " ", "%20", -1)}
原题传送门👉斐波那契数列 | 牛客
func Fibonacci(n int) int { if n == 0 || n == 1 { return n } fn1, fn2, cur := 0, 1, 0 for i := 2; i <= n; i++ { cur = fn1 + fn2 fn1, fn2 = fn2, cur } return cur}
原题传送门👉二进制中 1 的个数 | 牛客
func numberOf1(n int) int { num := 0 for n != 0 { num++ n &= n - 1 } return num}
原题传送门👉调整数组顺序使奇数位于偶数前面 | 牛客
func reOrderArray(nums []int) { oddNum := 0 for _, val := range nums { if val&0x1 == 1 { oddNum++ } } copyNums := make([]int, len(nums)) copy(copyNums, nums) i, j := 0, oddNum for _, val := range copyNums { if val&0x1 == 1 { nums[i] = val i++ } else { nums[j] = val j++ } }}
原题传送门👉179. 最大数 | Leetcode
给定一组非负整数,重新排列它们的顺序使之组成一个最大的整数。
输出结果可能非常大,所以你需要返回一个字符串而不是整数
输入: [10, 2]输出: 210输入: [3, 30, 34, 5, 9]输出: 9534330
需要注意输入全为
0
的特殊情况
func largestNumber(nums []int) string { length := len(nums) if length < 1 { return "0" } strs := make([]string, length) for i := 0; i < length; i++ { strs[i] = strconv.Itoa(nums[i]) } sort.Slice(strs, func(i, j int) bool { return (strs[i] + strs[j]) > (strs[j] + strs[i]) }) numsStr := strings.Join(strs, "") numsStr = strings.TrimLeft(numsStr, "0") if numsStr == "" { return "0" } return numsStr}
package mainimport ( "fmt" "sort" "strconv" "strings")func main() { strings1 := []int{3, 30, 34, 5, 9} strings2 := []int{10, 2} strings3 := []int{0, 0, 0, 0} strings4 := make([]int, 10) fmt.Println(largestNumber(strings1)) fmt.Println(largestNumber(strings2)) fmt.Println(largestNumber(strings3)) fmt.Println(largestNumber(strings4))}func largestNumber(nums []int) string { length := len(nums) if length < 1 { return "0" } strs := make([]string, length) for i := 0; i < length; i++ { strs[i] = strconv.Itoa(nums[i]) } sort.Slice(strs, func(i, j int) bool { return (strs[i] + strs[j]) > (strs[j] + strs[i]) }) numsStr := strings.Join(strs, "") numsStr = strings.TrimLeft(numsStr, "0") if numsStr == "" { return "0" } return numsStr}------953433021000Process finished with exit code 0
上面的解法是在 Leetcode 评论区看到的,比较好奇sort.Slice
是用什么算法对切片进行排序。翻了一下sort/slice.go
的源码,如下所示:
// Copyright 2017 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.// +build !compiler_bootstrap go1.8package sortimport "reflect"// Slice sorts the provided slice given the provided less function.//// The sort is not guaranteed to be stable. For a stable sort, use// SliceStable.//// The function panics if the provided interface is not a slice.func Slice(slice interface{}, less func(i, j int) bool) { rv := reflect.ValueOf(slice) swap := reflect.Swapper(slice) length := rv.Len() quickSort_func(lessSwap{less, swap}, 0, length, maxDepth(length))}// SliceStable sorts the provided slice given the provided less// function while keeping the original order of equal elements.//// The function panics if the provided interface is not a slice.func SliceStable(slice interface{}, less func(i, j int) bool) { rv := reflect.ValueOf(slice) swap := reflect.Swapper(slice) stable_func(lessSwap{less, swap}, rv.Len())}// SliceIsSorted tests whether a slice is sorted.//// The function panics if the provided interface is not a slice.func SliceIsSorted(slice interface{}, less func(i, j int) bool) bool { rv := reflect.ValueOf(slice) n := rv.Len() for i := n - 1; i > 0; i-- { if less(i, i-1) { return false } } return true}
可以看到默认的sort.Slice()
方法是用快排对切片进行排序的。
我们知道,快排是不稳定的,所以如果希望使用稳定排序算法对切片进行排序,则可以使用SliceStable()
方法。
而SliceStable()
所使用的stable_func()
定义在sort/zfuncversion.go
中,而实际上该函数在sort/sort.go
中定义:
func stable(data Interface, n int) { blockSize := 20 // must be > 0 a, b := 0, blockSize for b <= n { insertionSort(data, a, b) a = b b += blockSize } insertionSort(data, a, n) for blockSize < n { a, b = 0, 2*blockSize for b <= n { symMerge(data, a, a+blockSize, b) a = b b += 2 * blockSize } if m := a + blockSize; m < n { symMerge(data, a, m, n) } blockSize *= 2 }}
可以看到SliceStable()
中定义了变量blocksize := 20
,用来控制插入排序的区间大小。当待排序的切片长度<=20
时,相当于直接对该切片进行插入排序:
// Insertion sortfunc insertionSort(data Interface, a, b int) { for i := a + 1; i < b; i++ { for j := i; j > a && data.Less(j, j-1); j-- { data.Swap(j, j-1) } }}
而当b <= n
即切片长度超过20
时,SliceStable()
会先调用insertionSort_func()
,对切片按照blockSize
分段进行插入排序。之后,调用symMerge_func()
,对每一段有序的切片元素进行归并排序,逐步翻倍blockSize
大小直至归并排序完成:
// SymMerge merges the two sorted subsequences data[a:m] and data[m:b] using// the SymMerge algorithm from Pok-Son Kim and Arne Kutzner, "Stable Minimum// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz// Radzik, editors, Algorithms - ESA 2004, volume 3221 of Lecture Notes in// Computer Science, pages 714-723. Springer, 2004.//// Let M = m-a and N = b-n. Wolog M < N.// The recursion depth is bound by ceil(log(N+M)).// The algorithm needs O(M*log(N/M + 1)) calls to data.Less.// The algorithm needs O((M+N)*log(M)) calls to data.Swap.//// The paper gives O((M+N)*log(M)) as the number of assignments assuming a// rotation algorithm which uses O(M+N+gcd(M+N)) assignments. The argumentation// in the paper carries through for Swap operations, especially as the block// swapping rotate uses only O(M+N) Swaps.//// symMerge assumes non-degenerate arguments: a < m && m < b.// Having the caller check this condition eliminates many leaf recursion calls,// which improves performance.func symMerge(data Interface, a, m, b int) { // Avoid unnecessary recursions of symMerge // by direct insertion of data[a] into data[m:b] // if data[a:m] only contains one element. if m-a == 1 { // Use binary search to find the lowest index i // such that data[i] >= data[a] for m <= i < b. // Exit the search loop with i == b in case no such index exists. i := m j := b for i < j { h := int(uint(i+j) >> 1) if data.Less(h, a) { i = h + 1 } else { j = h } } // Swap values until data[a] reaches the position before i. for k := a; k < i-1; k++ { data.Swap(k, k+1) } return } // Avoid unnecessary recursions of symMerge // by direct insertion of data[m] into data[a:m] // if data[m:b] only contains one element. if b-m == 1 { // Use binary search to find the lowest index i // such that data[i] > data[m] for a <= i < m. // Exit the search loop with i == m in case no such index exists. i := a j := m for i < j { h := int(uint(i+j) >> 1) if !data.Less(m, h) { i = h + 1 } else { j = h } } // Swap values until data[m] reaches the position i. for k := m; k > i; k-- { data.Swap(k, k-1) } return } mid := int(uint(a+b) >> 1) n := mid + m var start, r int if m > mid { start = n - b r = mid } else { start = a r = m } p := n - 1 for start < r { c := int(uint(start+r) >> 1) if !data.Less(p-c, c) { start = c + 1 } else { r = c } } end := n - start if start < m && m < end { rotate(data, start, m, end) } if a < start && start < mid { symMerge(data, a, start, mid) } if mid < end && end < b { symMerge(data, mid, end, b) }}// Rotate two consecutive blocks u = data[a:m] and v = data[m:b] in data:// Data of the form 'x u v y' is changed to 'x v u y'.// Rotate performs at most b-a many calls to data.Swap.// Rotate assumes non-degenerate arguments: a < m && m < b.func rotate(data Interface, a, m, b int) { i := m - a j := b - m for i != j { if i > j { swapRange(data, m-i, m, j) i -= j } else { swapRange(data, m-i, m+j-i, i) j -= i } } // i == j swapRange(data, m-i, m, i)}/*Complexity of Stable SortingComplexity of block swapping rotationEach Swap puts one new element into its correct, final position.Elements which reach their final position are no longer moved.Thus block swapping rotation needs |u|+|v| calls to Swaps.This is best possible as each element might need a move.Pay attention when comparing to other optimal algorithms whichtypically count the number of assignments instead of swaps:E.g. the optimal algorithm of Dudzinski and Dydek for in-placerotations uses O(u + v + gcd(u,v)) assignments which isbetter than our O(3 * (u+v)) as gcd(u,v) <= u.Stable sorting by SymMerge and BlockSwap rotationsSymMerg complexity for same size input M = N:Calls to Less: O(M*log(N/M+1)) = O(N*log(2)) = O(N)Calls to Swap: O((M+N)*log(M)) = O(2*N*log(N)) = O(N*log(N))(The following argument does not fuzz over a missing -1 orother stuff which does not impact the final result).Let n = data.Len(). Assume n = 2^k.Plain merge sort performs log(n) = k iterations.On iteration i the algorithm merges 2^(k-i) blocks, each of size 2^i.Thus iteration i of merge sort performs:Calls to Less O(2^(k-i) * 2^i) = O(2^k) = O(2^log(n)) = O(n)Calls to Swap O(2^(k-i) * 2^i * log(2^i)) = O(2^k * i) = O(n*i)In total k = log(n) iterations are performed; so in total:Calls to Less O(log(n) * n)Calls to Swap O(n + 2*n + 3*n + ... + (k-1)*n + k*n) = O((k/2) * k * n) = O(n * k^2) = O(n * log^2(n))Above results should generalize to arbitrary n = 2^k + pand should not be influenced by the initial insertion sort phase:Insertion sort is O(n^2) on Swap and Less, thus O(bs^2) per block ofsize bs at n/bs blocks: O(bs*n) Swaps and Less during insertion sort.Merge sort iterations start at i = log(bs). With t = log(bs) constant:Calls to Less O((log(n)-t) * n + bs*n) = O(log(n)*n + (bs-t)*n) = O(n * log(n))Calls to Swap O(n * log^2(n) - (t^2+t)/2*n) = O(n * log^2(n))*/
另外,如果想判断一个切片是否已经有序,则可以使用SliceIsSorted()
方法,返回一个bool
值。
原题传送门👉分数序列和(百度2017秋招真题)| 赛码
package mainimport ( "fmt")func main() { n := 0 // total number of test cases part := 0 // how many elements to calculate fmt.Scan(&n) for i := 0; i < n; i++ { fmt.Scan(&part) fmt.Printf("%.4f", partSum(part)) }}func partSum(n int) float64 { sum := 0.0 for i := 1; i <= n; i++ { sum += float64(fibonacci(i+1)) / float64(fibonacci(i)) } return sum}func fibonacci(n int) int { f1, f2 := 1, 2 var f3 int switch n { case 1: return f1 case 2: return f2 default: for i := 3; i <= n; i++ { f3 = f1 + f2 f1, f2 = f2, f3 } return f3 }}
表 1:Person
+-------------+---------+| 列名 | 类型 |+-------------+---------+| PersonId | int || FirstName | varchar || LastName | varchar |+-------------+---------+PersonId 是上表主键
表 2:Address
+-------------+---------+| 列名 | 类型 |+-------------+---------+| AddressId | int || PersonId | int || City | varchar || State | varchar |+-------------+---------+AddressId 是上表主键
编写一个 SQL 查询,满足条件:无论Person
是否有地址信息,都需要基于上述两表提供Person
的以下信息:
FirstName, LastName, City, State
使用左外连接以及
on
过滤条件,参见 SQL中on条件与where条件的区别 | 博客园
SELECT p.FirstName AS FirstName, p.LastName AS LastName, a.City AS City, a.State AS StateFROM Person p LEFT JOIN Address aON p.PersonId = a.PersonId;
]]>
整理自书籍、博客、网络,更新中…
肖光荣:KVM 是 Kernel-based Virtual Machine 的简称,KVM 要求 CPU 支持硬件虚拟化技术(例如Intel VT
或AMD-V
),是 Linux 下的全虚拟化解决方案。KVM 由处于内核态的 KVM 模块kvm.ko
和用户态的 QEMU 两部分构成。内核模块kvm.ko
实现了 CPU 和内存虚拟化等决定关键性能和核心安全的功能,并向用户空间提供了使用这些功能的接口,QEMU 利用 KVM 模块提供的接口来实现设备模拟、I/O 虚拟化和网络虚拟化等。单个虚拟机是宿主机上的一个普通 QEMU 进程,虚拟机中的 CPU 核(vCPU)是 QEMU 的一个线程,VM 的物理地址空间是 QEMU 的虚拟地址空间(图 1)。
虚拟化是云计算的基础。简单来说,虚拟化使得一台物理的服务器上可以跑多台虚拟机,虚拟机共享物理机的 CPU、内存、I/O 硬件资源,但逻辑上虚拟机之间是相互隔离的。
Host
:宿主机,即物理机Guest
:客户机,即虚拟机Host
将自己的硬件资源虚拟化,并提供给Guest
使用,主要是通过Hypervisor
来实现的。根据Hypervisor
的实现方式和所处的位置,虚拟化又可以分为两种:1 型虚拟化和 2 型虚拟化。
Hypervisor
直接安装在物理机上,多个虚拟机在Hypervisor
上运行。Hypervisor
的实现方式一般是一个特殊定制的 Linux 系统,Xen 和 VMWare ESXi 都属于这个类型。
物理机上首先安装常规的操作系统,而Hypervisor
作为 OS 上的一个程序模块运行,并对虚拟机进行管理。KVM、VirtualBox、VMWare Workstation 都属于这个类型。
由于 KVM 目前已经是 Linux 内核中的一个模块,因此也有人将其视为 1 型虚拟化
理论上讲:
kvm.ko
,它只会负责提供vCPU
以及对虚拟内存进行管理和调度kvm.ko
模块提供的系统调用进入内核,由 KVM 负责将虚拟机置于特殊模式运行/dev/kvm
是 KVM 内核模块提供给用户空间的一个接口,这个接口被qemu-kvm
调用,通过ioctl
系统调用就可以给用户提供一个工具,用以创建、删除、管理虚拟机qemu-kvm
就是通过open()
、close()
、ioctl()
等方法去打开、关闭和调用这个接口,从而实现与 KVM 的互动open("/dev/kvm")ioctl(KVM_CREATE_VM)ioctl(KVM_CREATE_VCPU)for (;;) { ioctl(KVM_RUN) switch (exit_reason) { case KVM_EXIT_IO: /* ... */ case KVM_EXIT_HLT: /* ... */ }}
qemu-kvm
通过/dev/kvm
接口来调用 KVM:
/dev/kvm
设备KVM_CREATE_VM
创建一个虚拟机对象KVM_CREATE_VCPU
为虚拟机创建vcpu
对象KVM_RUN
设置vcpu
为运行状态虚拟机只有vCPU
和vMEM
是无法运行的,还需要外设的支持。真实的外设需要利用 Linux 系统内核进行管理,而通常来说我们使用的还是虚拟外设。VM 要和虚拟外设进行交互的话,就需要利用 QEMU 模拟的虚拟外设。
KVM 实现主要基于Intel-V
或者AMD-V
提供的虚拟化平台,利用普通的 Linux 进程运行虚拟态的指令集,模拟虚拟机监视器和 CPU。KVM 本身并不提供硬件虚拟化操作,其 I/O 操作都借助 QEMU 完成。
KVM 全称为Kernel-based Virtual Machine
,也就是说 KVM 是基于 Linux 内核实现的。KVM 有一个核心的内核模块kvm.ko
,它只用于管理 vCPU 和内存。
作为一个
Hypervisor
,KVM 本身只关注虚拟机调度和内存管理这两个方面,I/O 外设的任务就交给了 Linux 内核和 QEMU
libvirt 主要包含三个模块:后台daemon
程序libvirtd
、API 库以及命令行工具virsh
。
libvirtd
:守护进程,接收并处理 API 请求virt-manager
virsh
是经常会使用到的 KVM 命令行工具Guest
作为一个普通进程运行于宿主机Guest
的 CPU(vCPU)作为进程的线程存在,并受到宿主机内核的调度Guest
继承了宿主机内核的一些属性,例如Huge Pages
Guest
的磁盘 I/O 和网络 I/O 会受到宿主机设置的影响Guest
通过宿主机上的虚拟网桥与外部相连每一个虚拟机Guest
在Host
上都被模拟为一个 QEMU 进程,即emulation
进程。创建虚拟机后,使用virsh
命令即可查看:
> virsh list --all Id Name State---------------------------------------------------- 1 kvm-01 running> ps aux | grep qemulibvirt+ 20308 15.1 7.5 5023928 595884 ? Sl 17:29 0:10 /usr/bin/qemu-system-x86_64 -name kvm-01 -S -machine pc-i440fx-wily,accel=kvm,usb=off -m 2048 -realtime mlock=off -smp 2 qemu ....
可以看到,此虚拟机就是一个普通的 Linux 进程,有自己的PID
,并且有四个线程。线程数量不是固定的,但是至少会有三个(vCPU、I/O、Signal)。其中有两个是vCPU
线程,一个I/O
线程,还有一个Signal
信号处理线程。
> pstree -p 20308qemu-system-x86(20308)-+-{qemu-system-x86}(20353) |-{qemu-system-x86}(20408) |-{qemu-system-x86}(20409) |-{qemu-system-x86}(20412)
Guest
的所有用户级别的指令集,都会直接由宿主机线程执行,此线程会调用 KVM 的ioctl
方式提供的接口加载Guest
的指令,并在特殊的 CPU 模式下运行。这样就不需要经过 CPU 指令集的软件模拟转换,降低了虚拟化的成本,这也是 KVM 优于其他虚拟化方式的原因之一。
KVM 对外提供了一个虚拟设备/dev/kvm
,可以通过ioctl
(I/O 设备带外管理接口)来对 KVM 进行操作。
Guest
虽然作为一个进程存在,但其内核的所有驱动都依然存在。只是硬件设备由 QEMU 模拟。Guest
的所有硬件操作都会由 QEMU 来接管,QEMU 负责与真实的宿主机硬件打交道。
Guest
的内存在Host
上由 Emulator 提供(即 QEMU)。对于 QEMU 来说,Guest
访问的内存就是 QEMU 的虚拟地址空间。Guest
上需要经过一次虚拟地址到物理地址的转换,转换得到的Guest
的物理地址其实也就是 QEMU 的虚拟地址。这时 QEMU 再做一次转换,将虚拟地址转换为Host
的物理地址。
第一步,获取到kvm句柄kvmfd = open("/dev/kvm", O_RDWR);第二步,创建虚拟机,获取到虚拟机句柄。vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0);第三步,为虚拟机映射内存,还有其他的PCI,信号处理的初始化。ioctl(kvmfd, KVM_SET_USER_MEMORY_REGION, &mem);第四步,将虚拟机镜像映射到内存,相当于物理机的boot过程,把镜像映射到内存。第五步,创建vCPU,并为vCPU分配内存空间。ioctl(kvmfd, KVM_CREATE_VCPU, vcpuid);vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);第五步,创建vCPU个数的线程并运行虚拟机。ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0);第六步,线程进入循环,并捕获虚拟机退出原因,做相应的处理。这里的退出并不一定是虚拟机关机,虚拟机如果遇到IO操作,访问硬件设备,缺页中断等都会退出执行,退出执行可以理解为将CPU执行上下文返回到QEMU。
虚拟机的启动过程总结如下:
KVM
句柄VM
KVM_RUN
open("/dev/kvm")ioctl(KVM_CREATE_VM)ioctl(KVM_CREATE_VCPU)for (;;) { ioctl(KVM_RUN) switch (exit_reason) { case KVM_EXIT_IO: /* ... */ case KVM_EXIT_HLT: /* ... */ }}
为了实现内存虚拟化,让Guest
使用一个隔离的、从零开始且连续的内存空间,KVM 引入了一层新的地址空间,即客户机物理地址空间(Guest Physical Address,GPA)。
这个地址空间并不是真正的物理地址空间,它只是Host
虚拟地址空间在Guest
地址空间的一个映射。对客户机来说,客户机物理地址空间都是从零开始的连续地址空间。但对宿主机来说,客户机的物理地址空间并不一定是连续的,客户机物理地址空间有可能映射在若干个不连续的宿主机地址区间,如下图所示:
为了将客户机物理地址转换成宿主机虚拟地址(Host Virtual Address,HVA),KVM 用一个kvm_memory_slot
数据结构来记录每一个地址区间的映射关系,此数据结构包含了对应此映射区间的起始客户机页帧号(Guest Frame Number,GFN)、映射的内存页数目以及起始宿主机虚拟地址,从而实现 GPA 到 HPA 的转换。
实现内存虚拟化,最主要的是实现客户机虚拟地址GVA
到宿主机物理地址HPA
之间的转换。如果通过之前提到的两步映射的方式,客户机的每次内存访问都需要 KVM 介入,并由软件进行多次地址转换,其效率是非常低的。
因此,为了提高GVA
到HPA
的转换效率,KVM 提供了两种实现方式来进行GVA
到HPA
之间的直接转换:
由于宿主机 MMU 不能直接装载客户机的页表来进行内存访问,所以当客户机访问宿主机物理内存时,需要经过多次地址转换。而通过影子页表,则可以实现客户机虚拟地址到宿主机物理地址的直接转换:
影子页表简化了地址转换过程,实现了客户机虚拟地址空间到宿主机物理地址空间的直接映射。
由于客户机中每个进程都有自己的虚拟地址空间,所以 KVM 需要为客户机中的每个进程页表都维护一套相应的影子页表。在客户机访问内存时,真正被装入宿主机 MMU 的是客户机当前页表所对应的影子页表
在使用影子页表的情况下,客户机的大多数内存访问都可以在没有 KVM 介入的情况下正常执行,没有额外的地址转换开销,也就大大提高了客户机运行的效率。
但是影子页表的引入也意味着 KVM 需要为每个客户机的每个进程页表都要维护一套相应的影子页表,这会带来较大的内存开销。此外,客户机页表和影子页表之间的同步也较为复杂。因此,Intel 的 EPT(Extent Page Table)技术和 AMD 的 NPT(Nest Page Table)技术都为内存虚拟化提供了硬件支持,在硬件层面上实现了GVA
到HPA
之间的转换。
EPT(Extent Page Table) 技术在原有客户机页表对GVA
到GPA
映射的基础上,又引入了 EPT 页表来实现GPA
到HPA
的另一次映射,这两次地址映射都是由硬件自动完成。
EPT 页表相对于影子页表,其实现方式大大简化。而且,由于客户机内部的缺页异常不会导致客户机退出,因此也提高了客户机性能。此外,KVM 只需为每个客户机维护一套 EPT 页表,从而大大减少了内存的额外开销。
Guest
运行于物理机的Hypervisor
之上,Guest
操作系统并不知道它已被虚拟化,而且不需要任何更改就可以在该配置下工作Guest
操作系统不仅知道它运行在Hypervisor
之上,还包含了让Guest
操作系统更高效的过渡到Hypervisor
的代码在传统的全虚拟化环境中,Hypervisor
必须捕捉Guest
的 I/O 请求,然后模拟物理硬件的行为,虽然比较灵活,但是效率较低。
而在半虚拟化环境中,Guest
知道自己运行在Hypervisor
之上,并且包含了充当前端程序的驱动程序。Hypervisor
为特定的设备模拟实现后端驱动程序。通过前端和后端驱动程序中的virtio
,为模拟设备提供标准化接口,从而提高了代码的跨平台重用率与运行效率。
总结来看,virtio
是对半虚拟化Hypervisor
中的一组通用模拟设备的抽象。有了半虚拟化Hypervisor
之后,Guest
操作系统就能够实现一组通用的接口,针对不同的后端驱动程序采用特定的设备模拟。后端驱动程序不需要是通用的,因为它们只需要实现前端所需的行为。
virtio API
依赖一个简单的缓冲抽象来封装Guest
操作系统所需要的命令和数据。在前端和后端驱动程序之外,virtio
还定义了两个层来支持Guest
到Hypervisor
的通信。
]]>
- 【必看】KVM 虚拟化原理探究 - Overview | CSDN
- CloudMan
- KVM 内存虚拟化及其实现 | IBM Developer
- 【必看】虚拟化技术之KVM,搭建KVM | CSDN
- Virtio:针对 Linux 的 I/O 虚拟化框架 | IBM Developer
- Linux 虚拟化技术和 PCI 透传技术 | IBM Developer
- CPU 和内存虚拟化原理 - CloudMan | CSDN
- 热迁移、RTC 计时与安全增强 - 腾讯云 KVM 性能优化实践经验谈 | InfoQ
- qemu+kvm的IO路径分析 | CSDN
- QEMU-KVM 原理综述 | FlyFlyPeng
- 阿里云郑晓:浅谈GPU虚拟化技术(一)| 阿里云栖社区
- 阿里云郑晓:浅谈GPU虚拟化技术(二)| 阿里云栖社区
- 阿里云郑晓:浅谈GPU虚拟化技术(三)| 阿里云栖社区
- 浅谈GPU虚拟化技术(四)- GPU分片虚拟化 | 阿里云栖社区
- 浅谈GPU虚拟化技术(五)- VDI 的用户体验
- 一文带你领略虚拟化领域顶级技术会议 KVM Forum 2018 | 阿里云栖社区
- 【必看】KVM 虚拟化原理探究—启动过程及各部分虚拟化原理 | CSDN
- KVM 计算虚拟化原理,偏基础 | 博客园
- Qemu虚拟机QCOW2格式镜像文件的组成部分及关键算法分析 | 开源中国
- KVM-QEMU, QCOW2, QEMU-IMG and Snapshots | 开源中国
- KVM 磁盘扩容 | 51CTO
- RAW(裸) 与 QCOW2(写时复制) 的区别 | CSDN
- QCOW2实现原理的一般性思考 | CSDN
- QEMU 使用的镜像文件:qcow2 与 raw | CSDN
- KVM虚拟化原理 | 开源中国
- virtio基本原理(kvm半虚拟化驱动) | 开源中国
- qemu,kvm,qemu-kvm,xen,libvir 区别 | 开源中国
Keep calm and use Go.
//冒泡排序,a是数组,n表示数组大小func BubbleSort(a []int, n int) { if n <= 1 { return } for i := 0; i < n; i++ { // 提前退出标志 flag := false for j := 0; j < n-i-1; j++ { if a[j] > a[j+1] { a[j], a[j+1] = a[j+1], a[j] //此次冒泡有数据交换 flag = true } } // 如果没有交换数据,提前退出 if !flag { break } }}
// 插入排序,a表示数组,n表示数组大小func InsertionSort(a []int, n int) { if n <= 1 { return } for i := 1; i < n; i++ { value := a[i] j := i - 1 //查找要插入的位置并移动数据 for ; j >= 0; j-- { if a[j] > value { a[j+1] = a[j] } else { break } } a[j+1] = value }}
// 选择排序,a表示数组,n表示数组大小func SelectionSort(a []int, n int) { if n <= 1 { return } for i := 0; i < n; i++ { // 查找最小值 minIndex := i for j := i + 1; j < n; j++ { if a[j] < a[minIndex] { minIndex = j } } // 交换 a[i], a[minIndex] = a[minIndex], a[i] }}
// 归并排序func MergeSort(a []int, n int) { if n <= 1 { return } mergeSort(a, 0, n-1)}func mergeSort(a []int, start, end int) { if start >= end { return } mid := (start + end) / 2 mergeSort(a, start, mid) mergeSort(a, mid+1, end) merge(a, start, mid, end)}func merge(a []int, start, mid, end int) { tmpArr := make([]int, end-start+1) i := start j := mid + 1 k := 0 for ; i <= mid && j <= end; k++ { if a[i] < a[j] { tmpArr[k] = a[i] i++ } else { tmpArr[k] = a[j] j++ } } for ; i <= mid; i++ { tmpArr[k] = a[i] k++ } for ; j <= end; j++ { tmpArr[k] = a[j] j++ } copy(a[start:end+1], tmpArr)}
func QuickSort(a []int, n int) { separateSort(a, 0, n-1)}func separateSort(a []int, start, end int) { if start >= end { return } i := partition(a, start, end) separateSort(a, start, i-1) separateSort(a, i+1, end)}func partition(a []int, start, end int) int { // 选取最后一位当对比数字 pivot := a[end] i := start for j := start; j < end; j++ { if a[j] < pivot { if !(i == j) { // 交换位置 a[i], a[j] = a[j], a[i] } i++ } } a[i], a[end] = a[end], a[i] return i}
func CountingSort(a []int, n int) { if n <= 1 { return } var max = math.MinInt32 for i := range a { if a[i] > max { max = a[i] } } c := make([]int, max+1) for i := range a { c[a[i]]++ } for i := 1; i <= max; i++ { c[i] += c[i-1] } r := make([]int, n) for i := range a { index := c[a[i]] - 1 r[index] = a[i] c[a[i]]-- } copy(a, r)}
(x-1)/2
2x+1
2x+2
(n/2)-1
func HeapSort(arr []int, n int) { // 1. 建立一个大顶堆 buildMaxHeap(arr, n) length := n // 2. 交换堆顶元素与堆尾,并对剩余的元素重新建堆 for i := n - 1; i > 0; i-- { swap(arr, 0, i) length-- heapify(arr, 0, length) } // 3. 返回堆排序后的数组 return}func buildMaxHeap(arr []int, n int) { for i := n/2 - 1; i >= 0; i-- { heapify(arr, i, n) }}// 从上至下堆化func heapify(arr []int, i int, n int) { left := 2*i + 1 right := 2*i + 2 largest := i if left < n && arr[left] > arr[largest] { largest = left } if right < n && arr[right] > arr[largest] { largest = right } if largest != i { swap(arr, i, largest) heapify(arr, largest, n) }}func swap(arr []int, i int, j int) { arr[i], arr[j] = arr[j], arr[i]}
func BinarySearch(a []int, v int) int { n := len(a) if n == 0 { return -1 } low := 0 high := n - 1 for low <= high { mid := (low + high) / 2 if a[mid] == v { return mid } else if a[mid] > v { high = mid - 1 } else { low = mid + 1 } } return -1}// 递归写法func BinarySearchRecursive(a []int, v int) int { n := len(a) if n == 0 { return -1 } return bs(a, v, 0, n-1)}func bs(a []int, v int, low, high int) int { if low > high { return -1 } mid := (low + high) / 2 if a[mid] == v { return mid } else if a[mid] > v { return bs(a, v, low, mid-1) } else { return bs(a, v, mid+1, high) }}// 查找第一个等于给定值的元素func BinarySearchFirst(a []int, v int) int { n := len(a) if n == 0 { return -1 } low := 0 high := n - 1 for low <= high { mid := low + (high-low)>>1 if a[mid] > v { high = mid - 1 } else if a[mid] < v { low = mid + 1 } else { if mid == 0 || a[mid-1] != v { return mid } else { high = mid - 1 } } } return -1}// 查找最后一个值等于给定值的元素func BinarySearchLast(a []int, v int) int { n := len(a) if n == 0 { return -1 } low := 0 high := n - 1 for low <= high { mid := low + (high-low)>>1 if a[mid] > v { high = mid - 1 } else if a[mid] < v { low = mid + 1 } else { if mid == n-1 || a[mid+1] != v { return mid } else { low = mid + 1 } } } return -1}// 查找第一个大于等于给定值的元素func BinarySearchFirstGT(a []int, v int) int { n := len(a) if n == 0 { return -1 } low := 0 high := n - 1 for low <= high { mid := (high + low) >> 1 if a[mid] > v { high = mid - 1 } else if a[mid] < v { low = mid + 1 } else { if mid != n-1 && a[mid+1] > v { return mid + 1 } else { low = mid + 1 } } } return -1}// 查找最后一个小于等于给定值的元素func BinarySearchLastLT(a []int, v int) int { n := len(a) if n == 0 { return -1 } low := 0 high := n - 1 for low <= high { mid := (low + high) >> 1 if a[mid] > v { high = mid - 1 } else if a[mid] < v { low = mid + 1 } else { if mid == 0 || a[mid-1] < v { return mid - 1 } else { high = mid - 1 } } } return -1}
type Node struct { data interface{} left *Node right *Node}func NewNode(data interface{}) *Node { return &Node{data: data}}func (this *Node) String() string { return fmt.Sprintf("v:%+v, left:%+v, right:%+v", this.data, this.left, this.right)}
package _6_linkedlistimport "fmt"/*单链表基本操作author:leo*/type ListNode struct { next *ListNode value interface{}}type LinkedList struct { head *ListNode length uint}func NewListNode(v interface{}) *ListNode { return &ListNode{nil, v}}func (this *ListNode) GetNext() *ListNode { return this.next}func (this *ListNode) GetValue() interface{} { return this.value}func NewLinkedList() *LinkedList { return &LinkedList{NewListNode(0), 0}}//在某个节点后面插入节点func (this *LinkedList) InsertAfter(p *ListNode, v interface{}) bool { if nil == p { return false } newNode := NewListNode(v) oldNext := p.next p.next = newNode newNode.next = oldNext this.length++ return true}//在某个节点前面插入节点func (this *LinkedList) InsertBefore(p *ListNode, v interface{}) bool { if nil == p || p == this.head { return false } cur := this.head.next pre := this.head for nil != cur { if cur == p { break } pre = cur cur = cur.next } if nil == cur { return false } newNode := NewListNode(v) pre.next = newNode newNode.next = cur this.length++ return true}//在链表头部插入节点func (this *LinkedList) InsertToHead(v interface{}) bool { return this.InsertAfter(this.head, v)}//在链表尾部插入节点func (this *LinkedList) InsertToTail(v interface{}) bool { cur := this.head for nil != cur.next { cur = cur.next } return this.InsertAfter(cur, v)}//通过索引查找节点func (this *LinkedList) FindByIndex(index uint) *ListNode { if index >= this.length { return nil } cur := this.head.next var i uint = 0 for ; i < index; i++ { cur = cur.next } return cur}//删除传入的节点func (this *LinkedList) DeleteNode(p *ListNode) bool { if nil == p { return false } cur := this.head.next pre := this.head for nil != cur { if cur == p { break } pre = cur cur = cur.next } if nil == cur { return false } pre.next = p.next p = nil this.length-- return true}//打印链表func (this *LinkedList) Print() { cur := this.head.next format := "" for nil != cur { format += fmt.Sprintf("%+v", cur.GetValue()) cur = cur.next if nil != cur { format += "->" } } fmt.Println(format)}
package _7_linkedlistimport "fmt"//单链表节点type ListNode struct { next *ListNode value interface{}}//单链表type LinkedList struct { head *ListNode}//打印链表func (this *LinkedList) Print() { cur := this.head.next format := "" for nil != cur { format += fmt.Sprintf("%+v", cur.value) cur = cur.next if nil != cur { format += "->" } } fmt.Println(format)}/*单链表反转时间复杂度:O(N)*/func (this *LinkedList) Reverse() { if nil == this.head || nil == this.head.next || nil == this.head.next.next { return } var pre *ListNode = nil cur := this.head.next for nil != cur { tmp := cur.next cur.next = pre pre = cur cur = tmp } this.head.next = pre}/*判断单链表是否有环*/func (this *LinkedList) HasCycle() bool { if nil != this.head { slow := this.head fast := this.head for nil != fast && nil != fast.next { slow = slow.next fast = fast.next.next if slow == fast { return true } } } return false}/*两个有序单链表合并*/func MergeSortedList(l1, l2 *LinkedList) *LinkedList { if nil == l1 || nil == l1.head || nil == l1.head.next { return l2 } if nil == l2 || nil == l2.head || nil == l2.head.next { return l1 } l := &LinkedList{head: &ListNode{}} cur := l.head curl1 := l1.head.next curl2 := l2.head.next for nil != curl1 && nil != curl2 { if curl1.value.(int) > curl2.value.(int) { cur.next = curl2 curl2 = curl2.next } else { cur.next = curl1 curl1 = curl1.next } cur = cur.next } if nil != curl1 { cur.next = curl1 } else if nil != curl2 { cur.next = curl2 } return l}/*删除倒数第N个节点*/func (this *LinkedList) DeleteBottomN(n int) { if n <= 0 || nil == this.head || nil == this.head.next { return } fast := this.head for i := 1; i <= n && fast != nil; i++ { fast = fast.next } if nil == fast { return } slow := this.head for nil != fast.next { slow = slow.next fast = fast.next } slow.next = slow.next.next}/*获取中间节点*/func (this *LinkedList) FindMiddleNode() *ListNode { if nil == this.head || nil == this.head.next { return nil } if nil == this.head.next.next { return this.head.next } slow, fast := this.head, this.head for nil != fast && nil != fast.next { slow = slow.next fast = fast.next.next } return slow}
type Stack interface { Push(v interface{}) Pop(v interface{}) IsEmpty() bool Top() interface{} Flush()}
type ArrayStack struct { // 数据 data []interface{} // 栈顶指针 top int}func NewArrayStack() *ArrayStack { return &ArrayStack{ data: make([]interface{}, 0, 32), top: -1, }}func (this *ArrayStack) IsEmpty() bool { if this.top < 0 { return true } return false}func (this *ArrayStack) Push(v interface{}) { if this.top < 0 { this.top = 0 } else { this.top += 1 } if this.top > len(this.data)-1 { this.data = append(this.data, v) } else { this.data[this.top] = v }}func (this *ArrayStack) Pop() interface{} { if this.IsEmpty() { return nil } v := this.data[this.top] this.top -= 1 return v}func (this *ArrayStack) Top() interface{} { if this.IsEmpty() { return nil } return this.data[this.top]}func (this *ArrayStack) Flush() { this.top = -1}func (this *ArrayStack) Print() { if this.IsEmpty() { fmt.Println("empty stack") } else { for i:= this.top; i>=0; i-- { fmt.Println(this.data[i]) } }}
package _8_stackimport "fmt"/*基于链表实现的栈*/type node struct { next *node val interface{}}type LinkedListStack struct { topNode *node}func NewLinkedListStack() *LinkedListStack { return &LinkedListStack{nil}}func (this *LinkedListStack) IsEmpty() bool { if this.topNode == nil { return true } return false}func (this *LinkedListStack) Push(v interface{}) { this.topNode = &node{next: this.topNode, val: v}}func (this *LinkedListStack) Pop() interface{} { if this.IsEmpty() { return nil } v := this.topNode.val this.topNode = this.topNode.next return v}func (this *LinkedListStack) Top() interface{} { if this.IsEmpty() { return nil } return this.topNode.val}func (this *LinkedListStack) Flush() { this.topNode = nil}func (this *LinkedListStack) Print() { if this.IsEmpty() { fmt.Println("empty stack") } else { cur := this.topNode for nil != cur { fmt.Println(cur.val) cur = cur.next } }}
package queueimport "fmt"type ArrayQueue struct { q []interface{} capacity int head int tail int}func NewArrayQueue(n int) *ArrayQueue { return &ArrayQueue{make([]interface{}, n), n, 0, 0}}func (this *ArrayQueue) EnQueue(v interface{}) bool { if this.tail == this.capacity { // head == 0 && tail == capacity 表示整个队列都占满了 if this.head == 0 { return false } // 数据搬移 for i := this.head; i < this.tail; i++ { this.q[i-this.head] = this.q[this.tail] } // 搬移完之后,更新 head 和 tail this.tail -= this.head this.head = 0 } this.q[this.tail] = v this.tail++ return true}func (this *ArrayQueue) DeQueue() interface{} { if this.head == this.tail { return nil } v := this.q[this.head] this.head++ return v}func (this *ArrayQueue) String() string { if this.head == this.tail { return "empty queue" } result := "head" for i := this.head; i <= this.tail-1; i++ { result += fmt.Sprintf("<-%+v", this.q[i]) } result += "<-tail" return result}
type Heap struct { a []int n int count int}
]]>事务指满足 ACID 特性的一组操作,可以通过Commit
提交一个事务,也可以通过Rollback
进行回滚。
事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。
使用回滚日志
Undo Log
保证原子性
数据库在事务执行前后都保持一致性状态,所有事务对一个数据的读取结果都是相同的。
一个事务所做的修改在最终提交以前,对其他事务不可见。
一旦事务提交,则其所做的修改将会永远保存到数据库中,即使系统发生崩溃,事务的执行结果也不能丢失。
使用重做日志
Redo Log
保证持久性
MySQL 默认采用自动提交模式,如果不显式使用START TRANSACTION
语句来开始一个事务,那么每个查询都会被当作一个事务自动提交。
在并发环境下,事务的隔离性很难保证,有可能出现很多并发一致性问题。
T2
的修改覆盖了T1
的修改:
T1
修改一个数据,T2
随后读取这个数据,如果T1
撤销了这次修改,那么T2
读取的数据就是脏数据:
T2
读取一个数据,T1
对该数据进行了修改。如果T2
再次读取这个数据,结果会和第一次读取的结果不同:
T1
读取某个范围的数据,T2
在这个范围内插入新的数据。T1
再次读取这个范围的数据,结果会和第一次读不同:
MySQL
中提供了两种封锁粒度:行级锁以及表级锁。
应该尽量只锁定需要修改的那部分数据。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。
但是加锁需要消耗资源,锁的各种操作(包括获取锁、释放锁、检查锁状态)都会增加系统开销。因此封锁粒度越小,系统开销就越大。
在选择锁粒度时,需要在锁开销和并发程度之间做一个权衡。
X
锁,又称为写锁S
锁,又称为读锁使用意向锁(Intention Locks)可以更容易的支持多粒度封锁。
意向锁在原来的X/S
锁之上引入了IX/IS
,都是表级锁,用来表示一个事务想要在表中的某个数据行上加X
锁或S
**锁。有以下两个规定:
S
锁之前,必须获得表的IS
锁或更强的锁X
锁之前,必须先获得表的IX
锁一级封锁协议
事务T
要修改数据A
时必须加X
锁,直到T
结束才释放锁.
可以解决丢失修改的问题,因为不能同时有两个事务对同一个数据进行修改,那么事务就不会被覆盖
二级封锁协议
在一级的基础上,要求读取数据A
时必须加S
锁,读取完马上释放S
锁。
可以解决读脏数据的问题,因为如果一个事务在对数据
A
进行修改,根据一级封锁协议,会加X
锁,那么就不能再加S
锁了,也就不会读入脏数据
三级封锁协议
在二级的基础上,要求读取数据A
时必须加S
锁,直到事务结束了才能释放S
锁。
可以解决不可重复读的问题,因为读
A
时,其他事务不能对A
加X
锁,从而避免了在读的期间数据发生改变
加锁和解锁分为两个阶段进行。
可串行化调度是指,通过并发控制,使得并发执行的事务结果串行执行的事务结果相同**。
事务遵循两段锁协议是保证可串行化调度的充分条件。例如以下操作满足两段锁协议,是可串行化调度:
lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B)
但不是必要条件,例如以下操作不满足两段锁协议,但它仍是可串行化调度:
lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C)
MySQL
的InnoDB
存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。
InnoDB
也支持使用特定的语句进行显式锁定:
SELECT ... LOCK In SHARE MODE;SELECT ... FOR UPDATE;
事务中的修改,即使没有提交,对其他事务也是可见的
一个事务只能读取已经提交事务所做的修改。也就是说,一个事务所做的修改在提交之前对其他事务是不可见的
保证在同一个事务中多次读取同样数据的结果是一样的
强制事务串行执行
多版本并发控制(Multi-Version Concurrency Control,MVCC)是MySQL
的InnoDB
存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。
MVVC 在每行记录后都保存着两个隐藏的列,用来存储两个版本号:
MVCC 使用到的快照存储在Undo
日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。
更新中…
]]>
Hold on just a little while longer…
legendtkl
所谓逃逸分析(Escape Analysis)是指编译器决定内存分配的位置,不需要程序员指定。在函数中申请一个新的对象:
参考文章:
整数
x
按位取反:-(x+1)
判断奇偶:i&0x1 == 1
每个包可以包含任意多个
init
函数,这些函数都会在程序执行开始的时候被调用。所有被编译器发现的init
函数都会安排在main
函数之前执行。init
函数用在设置包、初始化变量或其他要在程序运行前优先完成的引导工作。——《Go 语言实战》
当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的
- 负载均衡(Load Balance)是集群技术(Cluster)的一种应用
- 负载均衡可以将工作任务分摊到多个处理单元,从而提高并发处理能力
- 目前最常见的负载均衡应用是 Web 负载均衡
- 常见的 Web 负载均衡技术包括:DNS 轮询、IP 负载均衡和 CDN
- 其中 IP 负载均衡可以使用硬件设备或软件方式来实现
Douglas McIlroy 认为的 UNIX 三条哲学:
KISS: Keep it simple, stupid
一个系列:
# 综合tophtop glancesdstat & sarmpstat# 性能分析perf# 进程pspstree -ppgreppkillpidofCtrl+z & jobs & fg# 网络ipifconfigdigpingtracerouteiftop pingtop nloadnetstatvnstatslurmscptcpdump# 磁盘 I/Oiotop iostat# 虚拟机virt-top# 用户wwhoami# 运行时间uptime# 磁盘dudflsblk# 权限chownchmod# 服务systemctl list-unit-files# 定位findlocate
Linux 也是一种类 Unix(Unix-like
)操作系统,基于 GNU GPL 协议开源。与其他一些著名的商用 Unix 内核相比,Linux 具有以下特点:
Linux 是一个do-it-yourself
自我完善的程序,由几个逻辑上独立的成分构成。大多数商用 Unix 变体也是单块结构。
大部分现代操作系统可以动态的装载或卸载部分内核代码(例如设备驱动程序)。Linux 对模块的支持很完备,可以自动按需装载或卸载模块。
内核线程(kernel thread)是一个能被独立调度的执行环境,通常在同一个地址空间执行,因此内核线程之间的上下文切换比普通进程之间的上下文切换花费的代价要少很多。
一个多线程用户程序由很多轻量级进程(lightweight process,LWP)组成,这些进程可能对共同的地址空间、物理内存页、打开文件等进行操作。Linux 定义了自己的 LWP 版本,并把 LWP 当作基本的执行上下文,通过非标准的clone()
系统调用来处理它们。
当采用“可抢占的内核”选项CONFIG_PREEMPT
来编译内核时,Linux 2.6
可以随意交错执行处于特权模式的执行流。
几种 Unix 内核变体都利用了多处理器系统。Linux 2.6
支持不同存储模式的对称多处理(SMP),包括 NUMA:系统不仅可以使用多处理器,而且每个处理器可以无差别的处理任何一个任务。
Ext2/3/4
、XFS
、JFS
等。
任何计算机系统都包含一个名为操作系统(Operating System,OS)的基本程序集合。在这个集合里,最重要的程序称为内核(kernel)。
当操作系统启动时,内核被装入到 RAM 中,内核中包含了系统运行所必需的核心过程(procudre)。
操作系统必须完成两个主要目标:
- 一些操作系统允许所有的用户程序都直接与硬件部分进行交互(例如 MS-DOS)。与此相反,类 Unix 操作系统把与计算机硬件相关的所有底层细节都对用户程序隐藏起来。
- 当程序想使用硬件资源时,必须向操作系统发出请求,由内核对这个请求进行评估。
- 如果允许,则内核将代表应用程序与相关的硬件部分进行交互。
为了实现这种机制,硬件为 CPU 引入了至少两种不同的执行模式:
多用户系统(multiuser system)就是一台能并发和独立的执行分别属于两个或多个用户应用程序的计算机:
在多用户系统中,每个用户在机器上都有一个私有空间。操作系统必须保证用户空间的私有部分仅仅对其拥有者可见。
所有的用户由一个唯一的用户标识符(User ID,UID)来标识。而为了和其他用户有选择的共享资料,每个用户都是一个或多个用户组的一名成员,用户组由唯一的用户组标识符(user group ID)标识,每个文件也恰好与一个用户组相对应。
任何类 Unix 操作系统都有一个超级用户
root
,系统管理员必须以root
的身份登录
一个进程(process)可以定义为:程序执行时的一个实例,或一个运行程序的“执行上下文”。
在传统的操作系统中,一个进程在地址空间(address space)中执行一个单独的指令序列。地址空间是允许进程引用的内存地址集合。
现代操作系统允许具有多个执行流的进程,即在相同的地址空间可执行多个指令序列
几个进程能并发的执行同一程序,而同一进程能顺序的执行几个程序。允许进程并发活动的系统称为多道程序系统(multiprogramming)或多处理系统(multiprocessing)。
Unix 是具有抢占式进程的多处理操作系统,即使没有用户登录或者程序运行,也会一直有一些系统进程在监视外围设备。例如有几个进程在监听系统终端等待用户登录。
类 Unix 操作系统采用进程/内核模式。每个进程都自认为它是系统中唯一的进程,可以独占操作系统所提供的服务。只要进程发出系统调用,硬件就会把特权模式由用户态变成内核态,然后进程以非常有限的目的开始一个内核过程的执行。一旦这个请求得到满足,内核将迫使硬件返回到用户态,而进程将从系统调用的下一条指令继续执行。
如前所述,大部分 Unix 内核是单块结构:每一个内核层都被集成到整个内核程序中,并代表当前进程在内核态下运行。
相反,微内核(microkernel)操作系统只需要内核有一个很小的函数集,通常包括几个同步原语、一个简单的调度程序和进程间通信机制
为了达到微内核理论上的诸多优点而不影响性能,Linux 内核提供了模块(module)。模块是一个目标文件,其代码可以在运行时链接到内核或从内核解除链接。
这种目标代码通常由一组函数组成,用来实现文件系统、驱动程序或其他内核上层功能
与微内核操作系统的外层不同,模块不是作为一个特殊的进程执行的。相反,与任何其他静态链接的内核函数一样,模块代表当前进程在内核态下执行。
使用模块(module)的主要优点如下:
更新中…
]]>
Node
:工作节点,可以是物理机或虚拟机,当状态满足要求后达到Ready
状态Deployment
:部署,一种对期望状态的描述Pod
:集群中可调度的最小调度单元,可包含多个容器Container Runtime
:容器运行时,这里默认为 Docker从宏观上看,K8S 遵循 C/S 架构,可以用下面的图来表示:
+-------------+ | | | | +---------------+ | | +-----> | Node 1 | | Kubernetes | | +---------------++-----------------+ | Server | | | CLI | | | | +---------------+| (Kubectl) |----------->| ( Master ) |<------+-----> | Node 2 || | | | | +---------------++-----------------+ | | | | | | +---------------+ | | +-----> | Node 3 | | | +---------------+ +-------------+
Master
是整个 K8S 集群的大脑,他有几个重要的功能:
上述功能通过一些组件来共同完成,我们称其为Control Plane
:
+----------------------------------------------------------+ | Master | | +-------------------------+ | | +------->| API Server |<--------+ | | | | | | | | v +-------------------------+ v | | +----------------+ ^ +--------------------+ | | | | | | | | | | Scheduler | | | Controller Manager | | | | | | | | | | +----------------+ v +--------------------+ | | +------------------------------------------------------+ | | | | | | | Cluster state store | | | | | | | +------------------------------------------------------+ | +----------------------------------------------------------+
Master
主要包含以下几个重要的组成部分:
用来存储集群所有需要持久化的状态,并且提供watch
的功能支持,可以快速的通知各组件的变更等操作。
目前 Kubernetes 的存储层选择是etcd
,所以一般情况下,我们直接以etcd
来代表集群状态存储服务,即将所有状态存储到etcd
实例中。
这是整个集群的入口,类似于人体的感官,接收外部的信号和请求,并将相应的信息写入到etcd
中。
为了保证安全,API Server 还提供了认证相关的功能,用于判断客户端是否有权限进行操作。API Server 支持多种认证方法,不过一般情况下,我们使用x509
证书来进行认证。
API Server 的目标是成为一个极简的 Server,只提供
REST
操作,更新etcd
,并充当着集群的网关。至于其他的业务逻辑,则通过插件或者其他组件来实现
Controller Manager 大概是 K8S 集群中最繁忙的部分,它在后台运行着许多不同的控制器进程,用来调节集群的状态。
当集群的配置发生改变时,控制器就会朝着预期的状态开始工作。
Scheduler 是集群的调度器,它会持续关注集群中未被调度的 Pod,并根据资源可用性、节点亲和性或是其他一些限制条件,通过绑定的 API 将 Pod 调度/绑定到 Node 上。
在这个过程中,调度程序一般只考虑调度开始时 Node 的状态,而不考虑在调度过程中 Node 的状态变化
与Master
相对应,可以将Node
简单理解为加入集群中的机器,它有以下几个核心组件:
+--------------------------------------------------------+ | +---------------------+ +---------------------+ | | | kubelet | | kube-proxy | | | | | | | | | +---------------------+ +---------------------+ | | +----------------------------------------------------+ | | | Container Runtime (Docker) | | | | +---------------------+ +---------------------+ | | | | |Pod | |Pod | | | | | | +-----+ +-----+ | |+-----++-----++-----+| | | | | | |C1 | |C2 | | ||C1 ||C2 ||C3 || | | | | | | | | | | || || || || | | | | | +-----+ +-----+ | |+-----++-----++-----+| | | | | +---------------------+ +---------------------+ | | | +----------------------------------------------------+ | +--------------------------------------------------------+
Kubelet
实现了集群中最重要的关于 Node 和 Pod 的控制功能。
K8S 原生的执行模式是操作应用程序的容器,基于这种模式,可以让应用程序之间相互隔离,互不影响。
此外,由于是操作容器,所以应用程序和主机之间也是相互隔离的,在任何容器运行时(比如 Docker)上都可以部署和运行。
K8S 将Pod
作为可调度的基本单位,Pod
可以是一组容器(也可以包含存储卷),它分离开了构建时和部署时的关注点:
这种隔离的模式,可以很方便的将应用程序与底层的基础设施解耦,极大提高了集群扩/缩容、迁移的灵活性
之前提到Master
的Scheduler
组件,它会调度未绑定的 Pod 到符合条件的 Node 上。至于最终该 Pod 是否能运行于 Node 上,则是由kubelet
来决定。
容器运行时最主要的功能是下载镜像和运行容器,最常见的实现是Docker
,目前还有其他一些实现,例如rkt
、cri-o
等。
K8S 提供了一套通用的容器运行时接口 CRI (Container Runtime Interface),凡是符合这套标准的容器运行时实现,都可以在 K8S 上使用。
要想访问某个服务,要么通过域名,要么通过 IP 地址。而每个 Pod 在创建后都有一个虚拟 IP,在 K8S 中有一个抽象的概念叫做Service
。kube-proxy
提供的便是代理的服务,让我们可以通过Service
访问到 Pod。
实际的工作原理是:K8S 会在每个 Node 上启动一个
kube-proxy
进程,通过编排iptables
规则来达到上述效果
首先在终端下执行kubectl
:
> kubectlkubectl controls the Kubernetes cluster manager. Find more information at: https://kubernetes.io/docs/reference/kubectl/overview/Basic Commands (Beginner): create Create a resource from a file or from stdin. expose Take a replication controller, service, deployment or pod and expose it as a new Kubernetes Service run Run a particular image on the cluster set Set specific features on objectsBasic Commands (Intermediate): explain Documentation of resources get Display one or many resources edit Edit a resource on the server delete Delete resources by filenames, stdin, resources and names, or by resources and label selectorDeploy Commands: rollout Manage the rollout of a resource scale Set a new size for a Deployment, ReplicaSet, Replication Controller, or Job autoscale Auto-scale a Deployment, ReplicaSet, or ReplicationControllerCluster Management Commands: certificate Modify certificate resources. cluster-info Display cluster info top Display Resource (CPU/Memory/Storage) usage. cordon Mark node as unschedulable uncordon Mark node as schedulable drain Drain node in preparation for maintenance taint Update the taints on one or more nodesTroubleshooting and Debugging Commands: describe Show details of a specific resource or group of resources logs Print the logs for a container in a pod attach Attach to a running container exec Execute a command in a container port-forward Forward one or more local ports to a pod proxy Run a proxy to the Kubernetes API server cp Copy files and directories to and from containers. auth Inspect authorizationAdvanced Commands: diff Diff live version against would-be applied version apply Apply a configuration to a resource by filename or stdin patch Update field(s) of a resource using strategic merge patch replace Replace a resource by filename or stdin wait Experimental: Wait for a specific condition on one or many resources. convert Convert config files between different API versionsSettings Commands: label Update the labels on a resource annotate Update the annotations on a resource completion Output shell completion code for the specified shell (bash or zsh)Other Commands: api-resources Print the supported API resources on the server api-versions Print the supported API versions on the server, in the form of "group/version" config Modify kubeconfig files plugin Provides utilities for interacting with plugins. version Print the client and server version informationUsage: kubectl [flags] [options]Use "kubectl <command> --help" for more information about a given command.Use "kubectl options" for a list of global command-line options (applies to all commands).
首先来看~/.kube/config
配置文件的内容(以minikube
为例):
> ls $HOME/.kube/config/home/tao/.kube/config> cat $HOME/.kube/configapiVersion: v1clusters:- cluster: certificate-authority: /home/tao/.minikube/ca.crt server: https://192.168.99.101:8443 name: minikubecontexts:- context: cluster: minikube user: minikube name: minikubecurrent-context: minikubekind: Configpreferences: {}users:- name: minikube user: client-certificate: /home/tao/.minikube/client.crt client-key: /home/tao/.minikube/client.key
可以看出,$HOME/.kube/config
中主要包含:
如果想指定配置文件路径,可以使用--kubeconfig
或者环境变量KUBECONFIG
来传递。
另外如果你并不想使用配置文件的话,也可以在命令行直接传递相关参数来使用:
> kubectl --client-key='/home/tao/.minikube/client.key' --client-certificate='/home/tao/.minikube/client.crt' --server='https://192.168.99.101:8443' get nodesNAME STATUS ROLES AGE VERSIONminikube Ready master 2d v1.11.3
使用kubectl get nodes
命令查看集群中的所有节点:
> kubectl get nodesNAME STATUS ROLES AGE VERSIONabelsu7-ubuntu Ready master 20d v1.13.3centos-1 Ready <none> 20d v1.13.3centos-2 Ready <none> 20d v1.13.3
传递-o wide/yaml/json
可以得到不同格式的输出:
> kubectl get nodes -o wideNAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIMEabelsu7-ubuntu Ready master 20d v1.13.3 xxx.xxx.xxx.xxx <none> Ubuntu 18.04.1 LTS 4.15.0-38-generic docker://18.6.1centos-1 Ready <none> 20d v1.13.3 xxx.xxx.xxx.xxx <none> CentOS Linux 7 (Core) 3.10.0-862.14.4.el7.x86_64 docker://18.9.2centos-2 Ready <none> 20d v1.13.3 xxx.xxx.xxx.xxx <none> CentOS Linux 7 (Core) 3.10.0-862.14.4.el7.x86_64 docker://18.9.1
当使用-o json
将内容以 JSON 格式输出时,可以配合jq
进行内容提取,例如:
> kubectl get nodes -o json | jq ".items[] | {name: .metadata.name} + .status.nodeInfo"{ "name": "abelsu7-ubuntu", "architecture": "amd64", "bootID": "efeb7bb0-9c57-40d1-b592-47c0974db9c5", "containerRuntimeVersion": "docker://18.6.1", "kernelVersion": "4.15.0-38-generic", "kubeProxyVersion": "v1.13.3", "kubeletVersion": "v1.13.3", "machineID": "c7eeb3f409394ad79a08c27afcc8958c", "operatingSystem": "linux", "osImage": "Ubuntu 18.04.1 LTS", "systemUUID": "314AB8CC-38BC-11E6-9C43-BC0000500000"}{ "name": "centos-1", "architecture": "amd64", "bootID": "f109d499-2dea-45e8-834a-907f78d267bc", "containerRuntimeVersion": "docker://18.9.2", "kernelVersion": "3.10.0-862.14.4.el7.x86_64", "kubeProxyVersion": "v1.13.3", "kubeletVersion": "v1.13.3", "machineID": "b9d5ec44cf284913b48d1ca1a7662c83", "operatingSystem": "linux", "osImage": "CentOS Linux 7 (Core)", "systemUUID": "E364DF48-5F20-11E6-8BF7-57717FCC0F00"}{ "name": "centos-2", "architecture": "amd64", "bootID": "8fadfee7-e09b-4ee9-81c2-d5464f60a4c0", "containerRuntimeVersion": "docker://18.9.1", "kernelVersion": "3.10.0-862.14.4.el7.x86_64", "kubeProxyVersion": "v1.13.3", "kubeletVersion": "v1.13.3", "machineID": "bbe91187ab474caebff29ffc64bcd487", "operatingSystem": "linux", "osImage": "CentOS Linux 7 (Core)", "systemUUID": "A1C63AE4-5F04-11E6-88F2-108D9A211300"}
此方法可以得到Node
的基础信息。
可以使用kubectl api-resources
查看服务端支持的 API 资源及其别名、描述等信息:
> kubectl api-resourcesNAME SHORTNAMES APIGROUP NAMESPACED KINDbindings true Bindingcomponentstatuses cs false ComponentStatusconfigmaps cm true ConfigMapendpoints ep true Endpointsevents ev true Eventlimitranges limits true LimitRangenamespaces ns false Namespacenodes no false Nodepersistentvolumeclaims pvc true PersistentVolumeClaimpersistentvolumes pv false PersistentVolumepods po true Podpodtemplates true PodTemplatereplicationcontrollers rc true ReplicationControllerresourcequotas quota true ResourceQuotasecrets true Secretserviceaccounts sa true ServiceAccountservices svc true Servicemutatingwebhookconfigurations admissionregistration.k8s.io false MutatingWebhookConfigurationvalidatingwebhookconfigurations admissionregistration.k8s.io false ValidatingWebhookConfigurationcustomresourcedefinitions crd,crds apiextensions.k8s.io false CustomResourceDefinitionapiservices apiregistration.k8s.io false APIServicecontrollerrevisions apps true ControllerRevisiondaemonsets ds apps true DaemonSetdeployments deploy apps true Deploymentreplicasets rs apps true ReplicaSetstatefulsets sts apps true StatefulSettokenreviews authentication.k8s.io false TokenReviewlocalsubjectaccessreviews authorization.k8s.io true LocalSubjectAccessReviewselfsubjectaccessreviews authorization.k8s.io false SelfSubjectAccessReviewselfsubjectrulesreviews authorization.k8s.io false SelfSubjectRulesReviewsubjectaccessreviews authorization.k8s.io false SubjectAccessReviewhorizontalpodautoscalers hpa autoscaling true HorizontalPodAutoscalercronjobs cj batch true CronJobjobs batch true Jobcertificatesigningrequests csr certificates.k8s.io false CertificateSigningRequestleases coordination.k8s.io true Leaseevents ev events.k8s.io true Eventdaemonsets ds extensions true DaemonSetdeployments deploy extensions true Deploymentingresses ing extensions true Ingressnetworkpolicies netpol extensions true NetworkPolicypodsecuritypolicies psp extensions false PodSecurityPolicyreplicasets rs extensions true ReplicaSetnetworkpolicies netpol networking.k8s.io true NetworkPolicypoddisruptionbudgets pdb policy true PodDisruptionBudgetpodsecuritypolicies psp policy false PodSecurityPolicyclusterrolebindings rbac.authorization.k8s.io false ClusterRoleBindingclusterroles rbac.authorization.k8s.io false ClusterRolerolebindings rbac.authorization.k8s.io true RoleBindingroles rbac.authorization.k8s.io true Rolepriorityclasses pc scheduling.k8s.io false PriorityClassstorageclasses sc storage.k8s.io false StorageClassvolumeattachments storage.k8s.io false VolumeAttachment
可以使用kubectl explain <API>
来查看 API 的相应说明:
> kubectl explain nodesKIND: NodeVERSION: v1DESCRIPTION: Node is a worker node in Kubernetes. Each node will have a unique identifier in the cache (i.e. in etcd).FIELDS: apiVersion <string> APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources kind <string> Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds metadata <Object> Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata spec <Object> Spec defines the behavior of a node. https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status status <Object> Most recently observed status of the node. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status
之前提到,Pod 是 K8s 中最小的调度单元,所以我们无法直接在 K8s 中运行一个 container,但是可以运行一个只包含一个 container 的 Pod
kubectl run
的基础用法如下:
Usage: kubectl run NAME --image=image [--env="key=value"] [--port=port] [--replicas=replicas] [--dry-run=bool] [--overrides=inline-json] [--command] -- [COMMAND] [args...] [options]
NAME
和--image
是必须项,分别代表此次部署的名字及所使用的镜像。而在实际使用时,推荐编写配置文件并通过kubectl create
进行部署。
例如部署一个Redis
实例:
> kubectl run redis --image='redis:alpine'deployment.apps/redis created
可以看到已创建部署deployment.apps/redis created
。使用kubectl get all
查看发生了什么:
> kubectl get allNAME READY STATUS RESTARTS AGEpod/redis-7c7545cbcb-2m6rp 1/1 Running 0 30sNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEservice/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 32sNAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGEdeployment.apps/redis 1 1 1 1 30sNAME DESIRED CURRENT READY AGEreplicaset.apps/redis-7c7545cbcb 1 1 1 30s
使用
kubectl get all
输出内容的格式,/
前代表类型,/
后代表名称
Deployment
是一种高级别的抽象,允许我们进行扩容、滚动更新及降级等操作。我们使用kubectl run redis --image='redis:alpine'
命令便创建了一个名为redis
的Deployment
,并指向了其使用的镜像为redis:alpine
。
同时 K8S 会默认为其增加一些标签Label
,可以添加-o wide
选项进行查看:
> kubectl get deployment.apps/redis -o wideNAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTORredis 1 1 1 1 40s redis redis:alpine run=redis> kubectl get deploy redis -o wideNAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTORredis 1 1 1 1 40s redis redis:alpine run=redis
可以将这些Label
作为选择条件使用:
> kubectl get deploy -l run=redis -o wideNAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTORredis 1 1 1 1 11h redis redis:alpine run=redis
Deployment
的创建除了使用上述方式之外,更推荐的方式是使用yaml
格式的配置文件。在配置文件中主要是声名一种预期的状态,而其他组件则负责协同调度并最终达成这种预期的状态。最后,Deployment
会将Pod
托管给下面将要介绍的ReplicaSet
。
ReplicaSet
是一种较低级别的结构,允许进行扩容。
之前提到了Deployment
主要是声明一种预期的状态,并且会将Pod
托管给ReplicaSet
,而ReplicaSet
则会检查当前的Pod
数量及状态是否符合预期,并尽量满足这一预期。
ReplicaSet
可简写为rs
,通过以下命令查看:
> kubectl get rs -o wideNAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR redis-7c7545cbcb 1 1 1 11h redis redis:alpine pod-template-hash=3731017676,run=redis
简单来说,Service
就是提供稳定访问入口的一组Pod
,通过Service
可以很方便的实现服务发现和负载均衡。
> kubectl get service -o wideNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTORkubernetes ClusterIP 10.96.0.1 <none> 443/TCP 16m <none>
Service
目前有 4 种类型:
ClusterIP
:目前 K8s 默认的Service
类型,将Service
暴露于一个仅集群内可访问的虚拟 IP 上NodePort
:通过在集群内所有Node
上都绑定固定端口的方式将服务暴露出来LoadBalancer
:是通过Cloud Provider
创建一个外部的负载均衡器,将服务暴露出来,并且会自动创建外部负载均衡器路由请求所需的NodePort
或ClusterIP
ExternalName
:将服务由DNS CNAME
的方式转发到指定的域名上将服务暴露出来> kubectl expose deploy/redis --port=6379 --protocol=TCP --target-port=6379 --name=redis-serverservice/redis-server exposed> kubectl get svc -o wideNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTORkubernetes ClusterIP 10.96.0.1 <none> 443/TCP 49m <none>redis-server ClusterIP 10.108.105.63 <none> 6379/TCP 4s run=redis
现在redis-server
这个Service
使用的是默认类型ClusterIP
,所以并不能直接从外部进行访问。需要使用port-forward
的方式让它可以在集群外部访问到:
> kubectl port-forward svc/redis-server 6379:6379Forwarding from 127.0.0.1:6379 -> 6379Forwarding from [::1]:6379 -> 6379Handling connection for 6379
这样在另一个本地终端上就可以通过redis-cli
工具进行连接:
> redis-cli -h 127.0.0.1 -p 6379127.0.0.1:6379> pingPONG
当然,也可以使用NodePort
方式对外暴露服务:
> kubectl expose deploy/redis --port=6379 --protocol=TCP --target-port=6379 --name=redis-server-nodeport --type=NodePortservice/redis-server-nodeport exposed> kubectl get service/redis-server-nodeport -o wideNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTORredis-server-nodeport NodePort 10.109.248.204 <none> 6379:31913/TCP 11s run=redis
这样就可以通过任意Node
上的31913
端口访问到redis
服务。
更新中…
]]>
Docker 的实现,主要归结于三大技术:命名空间 (Namespaces)、控制组 (Control Groups) 和联合文件系统 (Union File System)。
命名空间是 Linux 内核在2.4
版本之后逐渐引入的一项用于进程运行隔离的模块。
和很多编程语言中命名空间的概念类似,Linux Kernel 中的 Namespace 能够将计算机资源进行切割划分,形成各自独立的空间。
就实现而言,命名空间可以分为很多具体的子系统,如User Namespace
、Net Namespace
、PID Namespace
、Mount Namespace
等等。
利用PID Namespace
,Docker 就实现了容器中运行进程相互隔离这一目标。
资源控制组(常缩写为CGroups
)是 Linux 内核在2.6
版本后逐渐引入的一项对计算机资源进行控制的模块。
顾名思义,CGroups 的作用就是控制计算机资源。它与 Namespace 的对比如下:
Namespace
:以隔离进程、网络、文件系统等虚拟资源为目的CGroups
:主要做的是硬件资源的隔离虚拟化除了制造出虚拟的环境以隔离统一物理平台运行的不同程序之外,另一大作用就是控制硬件资源的分配。CGroups
的使用正是为了这样的目的。
CGroups 除了隔离硬件资源,还有控制资源分配这个关键性作用。通过 CGroups,我们可以指定任意一个隔离环境对任意资源的占用值或占用率,在很多分布式场景下会很有帮助
联合文件系统(Union File System)是一种能够同时挂载不同实际文件或文件夹到同一目录,形成一种联合文件结构的文件系统。Docker 创新性的将其引入到容器实现中,用它解决虚拟环境对文件系统占用过量、实现虚拟环境快速启停等问题。
在 Docker 中,提供了一种对 UnionFS 的改进实现,也就是 AUFS(Advanced Union File System)。
AUFS 将文件的更新挂载到旧的文件上,而不去修改那些不更新的内容(类似差量更新的概念)。这样一来,Docker 就大幅减少了虚拟文件系统对物理存储空间的占用。
先来看一张 Docker 官方提供的容器结构设计架构图:
与其他虚拟化实现甚至其他容器引擎不同的是,Docker 推崇一种轻量级容器结构,即一个应用一个容器。
Docker 的轻量级容器实现和虚拟机的相关参数对比如下:
属性 | Docker | 虚拟机 |
---|---|---|
启动速度 | 秒级 | 分钟级 |
硬盘使用 | MB 级 | GB 级 |
性能 | 接近原生 | 较低 |
普通机器支撑量 | 数百个 | 几个 |
之前提到了 Docker 实现容器引擎的一些技术,但都是相对底层的原理实现。在 Docker 将它们进行封装后,我们并不会直接去操作它们。在 Docker 中,还另外提供了一些软件层面的概念,这才是我们操作 Docker 所针对的对象
在 Docker 的体系中,有四大基本组件(Object):
镜像(Image)也是其他虚拟化技术中常常使用的一个概念。所谓镜像,可以理解为一个只读的文件包,其中包含了虚拟环境运行最原始文件系统的内容。
Docker 的镜像与虚拟机中的镜像还是存在一定区别的。首先,Docker 利用 AUFS 作为底层文件系统实现。通过这种方式,Docker 实现了一种增量式的镜像结构:
每次对镜像内容的修改,Docker 都会将这些修改写入一个新镜像层。因此,Docker 镜像实质上是无法修改的,因为所有对镜像的修改只会产生新的镜像,而不是更新原有的镜像。
在容器技术中,容器(Container)就是用来隔离虚拟环境的基础设施。而在 Docker 里,它也被引申为隔离出来的虚拟环境。
可以将镜像理解为编程中的类,那么容器就是类的一个实例。镜像内存放的是不可变化的东西,而当以它们为基础的容器启动后,容器内也就成为了一个“活”的空间
Docker 实现了强大的网络功能,我们不但能够轻松的对每个容器的网络进行配置,还可以在容器间建立虚拟网络,将多个容器包裹其中,同时与其他网络环境隔离。
另外,Docker 还可以在容器中构建独立的域名解析环境,这使得我们可以在不修改代码和配置的前提下直接迁移容器,而 Docker 会为我们完成新环境的网络适配。
对于这个功能,甚至可以在不同的物理服务器之间实现,让处在两台物理机上的两个 Docker 容器,加入到同一个虚拟网络中,形成完全屏蔽硬件的效果
得益于 Docker 底层 UnionFS 技术的支持,我们除了能够从宿主机操作系统中挂载目录之外,还可以建立独立的目录以持久化存放数据,或者在容器之间共享数据。
在 Docker 中,通过这几种方式进行数据共享或持久化的文件或目录,我们都称之为数据卷(Volume)。
目前这款实现容器化的工具是由 Docker 官方进行维护的,Docker 官方将其命名为 Docker Engine,同时定义其为工业级的容器引擎(Industry-standard Container Engine)。在 Docker Engine 中,实现了 Docker 技术最核心的部分——容器引擎。
深究 Docker Engine,会发现它其实是由多个独立软件所组成的软件包。在这些程序中,最核心的就是 docker daemon 和 docker CLI。
Docker 所提供的容器管理、应用编排、镜像分发等功能,都集中在了 docker daemon 中。而我们之前所提到的镜像模块、容器模块、数据卷模块和网络模块也都实现在其中。
在操作系统中,docker daemon 通常以服务的形式运行以便静默的提供这些功能,所以我们通常称之为 Docker 服务
在 docker daemon 管理容器等相关资源的同时,它也向外暴露了一套 RESTful API,我们能够通过这套接口对 docker daemon 中运行的容器和其他资源进行管理。
为了方便我们通过控制台对 docker daemon 进行管理,Docker Engine 直接附带了 docker CLI 这个控制台程序。
容易看出,docker daemon 和 docker CLI 组成了一个标准的 C/S 结构应用程序。而衔接这两者的,正是 docker daemon 所提供的 RESTful API
> docker versionClient: Version: 18.06.1-ce API version: 1.38 Go version: go1.10.3 Git commit: e68fc7a Built: Tue Aug 21 17:24:56 2018 OS/Arch: linux/amd64 Experimental: falseServer: Engine: Version: 18.06.1-ce API version: 1.38 (minimum version 1.12) Go version: go1.10.3 Git commit: e68fc7a Built: Tue Aug 21 17:23:21 2018 OS/Arch: linux/amd64 Experimental: false
> docker infoContainers: 32 Running: 16 Paused: 0 Stopped: 16Images: 33Server Version: 18.06.1-ceStorage Driver: overlay2 Backing Filesystem: extfs Supports d_type: true Native Overlay Diff: trueLogging Driver: json-fileCgroup Driver: cgroupfsPlugins: Volume: local Network: bridge host macvlan null overlay Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslogSwarm: inactiveRuntimes: runcDefault Runtime: runcInit Binary: docker-initcontainerd version: 468a545b9edcd5932818eb9de8e72413e616e86erunc version: 69663f0bd4b60df09991c08812a60108003fa340init version: fec3683Security Options: apparmor seccomp Profile: defaultKernel Version: 4.15.0-38-genericOperating System: Ubuntu 18.04.1 LTSOSType: linuxArchitecture: x86_64CPUs: 8Total Memory: 31.21GiBName: abelsu7-ubuntuID: RT3B:UYYD:MO4K:IMYS:3TG6:ZKGT:PUUK:DZBO:4FF5:KUA5:2OH7:YTDLDocker Root Dir: /var/lib/dockerDebug Mode (client): falseDebug Mode (server): falseRegistry: https://index.docker.io/v1/Labels:Experimental: falseInsecure Registries: 127.0.0.0/8Live Restore Enabled: false
修改/etc/docker/daemon.json
(若文件不存在则直接新建)这个 Docker 服务的配置文件:
{ "registry-mirrors": [ "https://registry.docker-cn.com" ]}
之后重启 Docker 使配置生效:
> sudo systemctl restart docker
可通过docker info
来查阅当前注册的镜像源列表:
> docker info## ......Registry Mirrors: https://registry.docker-cn.com/## ......
可以将 Docker 镜像理解为包含应用程序及其相关依赖的一个基础文件系统,在 Docker 容器启动的过程中,它会以只读的方式被用于创建容器的运行环境。
与其他虚拟机的镜像管理不同,Docker 将镜像管理纳入到了自身设计中,也就是说,所有的 Docker 镜像都是按照 Docker 所设定的逻辑打包的,也是受到 Docker Engine 所控制的。
对于每一个记录文件系统修改的镜像层来说,Docker 都会根据它们的信息生成一个 Hash 码,这是一个长度为 64 位的字符串,足以保证全球唯一性
由于镜像层都拥有唯一的编码,我们就能够区分不同的镜像层并保证它们的内容与编码是一致的,从而允许我们在镜像之间共享镜像层:
使用docker images
命令查看当前连接的 docker daemon 中存放和管理了哪些镜像:
> docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEredis alpine a5cff96d7b8f 5 weeks ago 50.8MBk8s.gcr.io/kube-controller-manager v1.13.3 0482f6400933 5 weeks ago 146MBk8s.gcr.io/kube-proxy v1.13.3 98db19758ad4 5 weeks ago 80.3MBk8s.gcr.io/kube-apiserver v1.13.3 fe242e556a99 5 weeks ago 181MBk8s.gcr.io/kube-scheduler v1.13.3 3a6f709e97a0 5 weeks ago 79.6MBquay.io/coreos/flannel v0.11.0-amd64 ff281650a721 6 weeks ago 52.6MBubuntu 16.04 b0ef3016420a 2 months ago 117MBinfluxdb latest 623f651910b3 3 months ago 238MBmemcached latest 8230c836a4b3 3 months ago 62.2MBmongo 3.2 fb885d89ea5c 3 months ago 300MBmist/mailmock latest 95c29bda552f 3 months ago 299MBmist/docker-socat latest f00ed0eed13f 3 months ago 7.8MBmistce/logstash v3-3-1 0f90a36d12c8 4 months ago 730MBmistce/api v3-3-1 4a21b676352f 4 months ago 705MBmistce/nginx v3-3-1 4f55dd9b39e0 4 months ago 109MBmistce/gocky v3-3-1 ee93caf66f70 4 months ago 440MBmistce/elasticsearch-manage v3-3-1 10a48b9ea0e1 4 months ago 65.8MBmistce/ui v3-3-1 b8fdbe0ccb23 4 months ago 626MBubuntu-with-vi-dockerfile latest 74ba87f80b96 4 months ago 169MBubuntu-with-vi latest 9d2fac08719d 4 months ago 169MBk8s.gcr.io/coredns 1.2.6 f59dcacceff4 4 months ago 40MBubuntu latest ea4c82dcd15a 4 months ago 85.8MBcentos latest 75835a67d134 5 months ago 200MBk8s.gcr.io/etcd 3.2.24 3cab8e1b9802 5 months ago 220MBhello-world latest 4ab4c602aa5e 6 months ago 1.84kBelasticsearch 5.6.10 73e6fdf8bd4f 7 months ago 486MBmistce/landing v3-3-1 b0e433749aa9 7 months ago 532MBkibana 5.6.10 bc661616b61c 8 months ago 389MBhello-world <none> 2cb0d9787c4d 8 months ago 1.85kBtraefik v1.5 fde722950ccf 12 months ago 49.7MBmist/swagger-ui latest 0b5230f1b6c4 12 months ago 24.8MBk8s.gcr.io/pause 3.1 da86e6ba6ca1 14 months ago 742kBrabbitmq 3.6.6-management c74093aa9895 2 years ago 179MB
在docker images
命令打印出来的内容中,我们还能看到两个与镜像命名有关的数据:REPOSITORY
和TAG
,这两者共同组成了 Docker 镜像的命名规则:
准确来说,Docker 镜像的命名可以分成三个部分:username
、repository
和tag
:
username
:主要用于识别上传镜像的不同用户,与 Github 中的用户空间类似repository
:主要用于识别镜像的内容,形成对镜像的表意描述tag
:主要用于标记镜像的版本,方便区分镜像内容的不同细节有的镜像没有
username
这个部分,表示这个镜像是由 Docker 官方所维护和提供的,就不再单独标记用户了
另外,Docker 中还有一个约定,当我们在操作中没有具体给出镜像的tag
时,Docker 会采用latest
作为缺省tag
。
下面是一张容器运行的状态流转图:
上图展示了几种常见的对 Docker 容器的操作命令,以及执行它们之后容器运行状态的变化。重点关注容器以下几个核心状态:
Created
:容器已创建,但尚未运行Running
:容器运行中Paused
:容器暂停运行Stopped
:容器停止运行(注意与Create
的区别)Deleted
:容器被删除在 Docker 的设计中,容器的生命周期其实与容器中PID
为1
的进程有着密切的关系。容器的启动,本质上可以理解为这个进程的启动,而容器的停止也意味着这个进程的停止,反之亦然。
当我们启动容器时,Docker 会按照镜像中的定义,启动对应的程序,并将这个程序的主进程作为容器的主进程(也就是
PID
为1
的进程)。而当我们控制容器停止时,Docker 会向主进程发送结束信号,通知程序退出
Docker 的写时复制(Copy on Write)与编程中的相类似,也就是在通过镜像运行容器时,并不是马上就把镜像里的所有内容拷贝到容器所运行的沙盒文件系统中,而是利用 UnionFS 将镜像以只读的方式挂载到沙盒文件系统中。只有在容器中发生对文件的修改时,修改才会体现到沙盒环境上。
换言之,容器在创建和启动的过程中,不需要进行任何的文件系统复制操作,也不需要为容器单独开辟大量的硬盘空间,Docker 容器的启动速度也由此得到了保障
Docker 官网关于容器与镜像关系的说明
A container is launched by running an image. An image is an executable package that includes everything needed to run an application—the code, a runtime, libraries, environment variables, and configuration files.
A container is a runtime instance of an image—what the image becomes in memory when executed (that is, an image with state, or a user process). You can see a list of your running containers with the command, docker ps, just as you would in Linux.
如果说我们把镜像的结构用 Git 项目的结构做类比,那么镜像仓库就可以看作是 Gitlab、Github 等代码托管平台,只不过 Docker 的镜像仓库托管的不是代码项目,而是镜像
借助镜像仓库这个中转站,Docker 实现了镜像的分发功能。我们可以将开发环境上所使用的镜像推送至镜像仓库,并在测试或生产环境上拉取它们,而这个过程仅需要几个命令,甚至可以自动化的完成。
可以使用docker pull
命令拉取镜像:
> docker pull ubuntuUsing default tag: latestlatest: Pulling from library/ubuntu124c757242f8: Downloading [===============================================> ] 30.19MB/31.76MB9d866f8bde2a: Download complete fa3f2f277e67: Download complete 398d32b153e8: Download complete afde35469481: Download complete
当没有显式指定镜像的标签时,Docker 将默认使用latest
。当然,也可以使用完整的镜像名来拉取镜像:
> docker pull openresty/openresty:1.13.6.2-alpine1.13.6.2-alpine: Pulling from openresty/openrestyff3a5c916c92: Pull complete ede0a2a1012b: Pull complete 0e0a11843023: Pull complete 246b2c6f4992: Pull complete Digest: sha256:23ff32a1e7d5a10824ab44b24a0daf86c2df1426defe8b162d8376079a548bf2Status: Downloaded newer image for openresty/openresty:1.13.6.2-alpine
Docker Hub 是 Docker 官方建立的中央镜像仓库,同时也是 Docker Engine 的默认镜像仓库。
使用docker search
命令搜索 Docker Hub 中的镜像:
> docker search ubuntuNAME DESCRIPTION STARS OFFICIAL AUTOMATEDubuntu Ubuntu is a Debian-based Linux operating sys… 9312 [OK] dorowu/ubuntu-desktop-lxde-vnc Docker image to provide HTML5 VNC interface … 281 [OK]rastasheep/ubuntu-sshd Dockerized SSH service, built on top of offi… 208 [OK]consol/ubuntu-xfce-vnc Ubuntu container with "headless" VNC session… 161 [OK]ubuntu-upstart Upstart is an event-based replacement for th… 96 [OK] ansible/ubuntu14.04-ansible Ubuntu 14.04 LTS with ansible 96 [OK]neurodebian NeuroDebian provides neuroscience research s… 56 [OK] 1and1internet/ubuntu-16-nginx-php-phpmyadmin-mysql-5 ubuntu-16-nginx-php-phpmyadmin-mysql-5 49 [OK]ubuntu-debootstrap debootstrap --variant=minbase --components=m… 40 [OK] nuagebec/ubuntu Simple always updated Ubuntu docker images w… 23 [OK]tutum/ubuntu Simple Ubuntu docker images with SSH access 19 i386/ubuntu Ubuntu is a Debian-based Linux operating sys… 17 1and1internet/ubuntu-16-apache-php-7.0 ubuntu-16-apache-php-7.0 13 [OK]ppc64le/ubuntu Ubuntu is a Debian-based Linux operating sys… 12 eclipse/ubuntu_jdk8 Ubuntu, JDK8, Maven 3, git, curl, nmap, mc, … 8 [OK]codenvy/ubuntu_jdk8 Ubuntu, JDK8, Maven 3, git, curl, nmap, mc, … 5 [OK]darksheer/ubuntu Base Ubuntu Image -- Updated hourly 5 [OK]pivotaldata/ubuntu A quick freshening-up of the base Ubuntu doc… 2 smartentry/ubuntu ubuntu with smartentry 1 [OK]1and1internet/ubuntu-16-sshd ubuntu-16-sshd 1 [OK]paasmule/bosh-tools-ubuntu Ubuntu based bosh-cli 1 [OK]pivotaldata/ubuntu-gpdb-dev Ubuntu images for GPDB development 0 1and1internet/ubuntu-16-healthcheck ubuntu-16-healthcheck 0 [OK]ossobv/ubuntu Custom ubuntu image from scratch (based on o… 0 1and1internet/ubuntu-16-rspec ubuntu-16-rspec 0 [OK]
要想获得镜像更详细的信息,可以使用docker inspect
命令:
> docker inspect mongo:3.2[ { "Id": "sha256:fb885d89ea5c35ac02acf79a398b793555cbb3216900f03f4b5f7dc31e595e31", "RepoTags": [ "mongo:3.2" ], "RepoDigests": [ "mongo@sha256:9e09fe9e747fb0ee1e64b572818e7397eb9a73e36a2b08bcc7846e9acf0a587f" ], "Parent": "", "Comment": "", "Created": "2018-11-16T00:55:06.547559408Z", "Container": "16a23b0d45ef66220aec0a2e542ff527da9da07889d4d862087630912d9ad86f", "ContainerConfig": { "Hostname": "16a23b0d45ef", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "ExposedPorts": { "27017/tcp": {} }, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "GOSU_VERSION=1.10", "JSYAML_VERSION=3.10.0", "GPG_KEYS=DFFA3DCF326E302C4787673A01C4E7FAAAB2461C \t42F3E95A2C4F08279C4960ADD68FA50FEA312927", "MONGO_PACKAGE=mongodb-org", "MONGO_REPO=repo.mongodb.org", "MONGO_MAJOR=3.2", "MONGO_VERSION=3.2.21" ], "Cmd": [ "/bin/sh", "-c", "#(nop) ", "CMD [\"mongod\"]" ], "ArgsEscaped": true, "Image": "sha256:d7430950b72ba7ecb5986396f9a3404b5b0d88c2ba39eb7f2d4b51b002db00ea", "Volumes": { "/data/configdb": {}, "/data/db": {} }, "WorkingDir": "", "Entrypoint": [ "docker-entrypoint.sh" ], "OnBuild": [], "Labels": {} }, "DockerVersion": "17.06.2-ce", "Author": "", "Config": { "Hostname": "", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "ExposedPorts": { "27017/tcp": {} }, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "GOSU_VERSION=1.10", "JSYAML_VERSION=3.10.0", "GPG_KEYS=DFFA3DCF326E302C4787673A01C4E7FAAAB2461C \t42F3E95A2C4F08279C4960ADD68FA50FEA312927", "MONGO_PACKAGE=mongodb-org", "MONGO_REPO=repo.mongodb.org", "MONGO_MAJOR=3.2", "MONGO_VERSION=3.2.21" ], "Cmd": [ "mongod" ], "ArgsEscaped": true, "Image": "sha256:d7430950b72ba7ecb5986396f9a3404b5b0d88c2ba39eb7f2d4b51b002db00ea", "Volumes": { "/data/configdb": {}, "/data/db": {} }, "WorkingDir": "", "Entrypoint": [ "docker-entrypoint.sh" ], "OnBuild": [], "Labels": null }, "Architecture": "amd64", "Os": "linux", "Size": 300019217, "VirtualSize": 300019217, "GraphDriver": { "Data": { "LowerDir": "/var/lib/docker/overlay2/de4bf7c9580fda62420fd6a4e529783aeb161b44457ec6636bfaa97e94084ab0/diff:/var/lib/docker/overlay2/31a2f54b5cf142ae50d5ff530fd9159cd61129a47ab76b6b32656b6db42b765b/diff:/var/lib/docker/overlay2/f495f57ba2b9e665444151dc913bd1b8952a2e3d416d546b6722e44a038900c0/diff:/var/lib/docker/overlay2/51696b913195f45c1ce36c76240a0cf9836b593a16b0853238a5515bd9178322/diff:/var/lib/docker/overlay2/bcb73a5809c820e1eeb3c7cf4acc04c89b9e4d17be7c5ce9e3962580d14f2446/diff:/var/lib/docker/overlay2/d84695101463e67d0a4c901285a557cb0f4fc84a56840ce6433a225b799e2fc4/diff:/var/lib/docker/overlay2/d86783053f0a3e71f89c7b05328b2021a75bcf833911f7dc5fdad50e166a3d39/diff:/var/lib/docker/overlay2/a7a9a982dc727d527a3af4d04a19e359062c2d74bdd8fb497a057ca09ffcf290/diff:/var/lib/docker/overlay2/cbd9ce2cce4a6e2ec032e6cf25281016715a57f11ede109097c796383d13aac2/diff:/var/lib/docker/overlay2/339f33b0ff9703a7e50cce8459b89f5fb932ccc9460c8489a0f5d2cf65114033/diff", "MergedDir": "/var/lib/docker/overlay2/6ba7ad9bf4a5e344d1edd12b87fe42d9abd828d480385a2637a47947c9a4af7f/merged", "UpperDir": "/var/lib/docker/overlay2/6ba7ad9bf4a5e344d1edd12b87fe42d9abd828d480385a2637a47947c9a4af7f/diff", "WorkDir": "/var/lib/docker/overlay2/6ba7ad9bf4a5e344d1edd12b87fe42d9abd828d480385a2637a47947c9a4af7f/work" }, "Name": "overlay2" }, "RootFS": { "Type": "layers", "Layers": [ "sha256:337a2e6463ae008c12681f29c50edde52ea5be2cc2f46d09b8254fd835b1f5a9", "sha256:9d3049f87bb2ba7ac0469cad7ee11f871ff4fc735cc4dfdc1be6a1fe877861a5", "sha256:75c2031620755be658ab335a6abb72376804e533e91fecb52315682b21aeeeca", "sha256:ed81bb40beffca626f698965d5ec236b394ad8db229bb6dbfeb7be7a61b32768", "sha256:38ccb1166c8a15aedf5a9d7f12b81436b9812175c3ce6c50fac39246a3ffc935", "sha256:1f5a9fb2648f17bd23ab13f9e70f8631d233f33f73329302144da1aa2e4a5b0f", "sha256:fcd5eec06559827da59d45500626b2dbf5673d03bba7aea9c9b9b786e8a10b54", "sha256:2bcf250f248858339faf2dc746c44197c9eecc34999d485c60d636c7fcbc4d20", "sha256:f6a5611931ed6ed6db65ad6a87abd7774f267c68c6f6d84cae65e0760c8a47b0", "sha256:b436f480c034edfc426e1fcadbebaf50c72c0ce92c66924b6cf6ba344e455560", "sha256:7eaf69109a2207f735d6423fe61a05200e3431ba9cdeafd6a27fa3c067c9f0ae" ] }, "Metadata": { "LastTagTime": "0001-01-01T00:00:00Z" } }]
可以使用docker rmi
命令删除镜像,参数是镜像名或 ID,可以同时删除多个镜像。需要注意的是,需要先通过docker rm
删除依赖该镜像的容器之后,该镜像才可以被删除:
> docker rmi redis:3.2 redis:4.0Untagged: redis:3.2Untagged: redis@sha256:745bdd82bad441a666ee4c23adb7a4c8fac4b564a1c7ac4454aa81e91057d977Deleted: sha256:2fef532eadb328740479f93b4a1b7595d412b9105ca8face42d3245485c39ddc## ......Untagged: redis:4.0Untagged: redis@sha256:b77926b30ca2f126431e4c2055efcf2891ebd4b4c4a86a53cf85ec3d4c98a4c9Deleted: sha256:e1a73233e3beffea70442fc2cfae2c2bab0f657c3eebb3bdec1e84b6cc778b75## ......
先来回顾一下容器的状态转换图:
可以看到,Docker 容器的生命周期共分为以下五种状态:
Created
:容器已经被创建,容器所需的相关资源已经准备就绪,但容器中的程序还未处于运行状态Running
:容器正在运行,其中的应用程序也正在运行Paused
:容器已经暂停,其中的所有程序都处于暂停状态Stopped
:容器处于停止状态,占用的资源和沙盒环境依然存在,只是容器中的应用程序均已停止运行Deleted
:容器已删除,相关占用的资源及存储在 Docker 中的管理信息也都被释放和移除> docker create --name=nginx nginx:1.1234f277e22be252b51d204acbb32ce21181df86520de0c337a835de6932ca06c3
> docker start nginx
Docker 还允许我们通过docker run
这个命令将docker create
和docker start
这两步操作合成为一步:
> docker run --name nginx -d nginx:1.12
通过-d
或--detach
选项告诉 Docker 在启动后将程序与控制台分离,使其在后台运行。
使用docker ps
查看正在运行中的 Docker 容器:
> docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES89f2b769498a nginx:1.12 "nginx -g 'daemon of…" About an hour ago Up About an hour 80/tcp nginx
添加-a
或--al
选项查看所有状态下的容器:
> docker ps -aCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES425a0d3cd18b redis:3.2 "docker-entrypoint.s…" 2 minutes ago Created redis89f2b769498a nginx:1.12 "nginx -g 'daemon of…" About an hour ago Up About an hour 80/tcp nginx
使用docker stop
命令停止正在运行中的容器:
> docker stop nginx
容器停止后,其维持的文件系统沙盒环境还是存在的,内部被修改的内容也都会保留,我们可以通过docker start
命令再次启动这个容器。
当需要完全删除容器时,可以使用docker rm
命令:
> docker rm nginx
正在运行中的容器默认情况下是不能被删除的,可以增加
-f
或--force
选项来强制停止并删除容器,不过这样做并不妥当
我们知道,容器是一个隔离运行环境的东西,它里面除了镜像所规定的主进程之外,其他进程也是能够运行的。Docker 为我们提供了docker exec
命令来让容器运行我们所给出的命令:
> docker exec nginx more /etc/hostname::::::::::::::/etc/hostname::::::::::::::83821ea220ed
通过下列命令可以在容器中另外启动一个bash
终端,并利用-it
参数启用一个伪终端,方便我们与容器中的bash
进行交互:
> docker exec -it nginx bashroot@83821ea220ed:/>
-i
(--interactive
)表示保持我们的输入流,只有使用它才能保证控制台程序能够正确识别我们的命令-t
(--tty
)表示启用一个伪终端,形成我们与bash
的交互。如果没有它,我们就无法看到bash
内部的执行结果Docker 为我们提供了一个docker attach
命令,用于将当前的输入输出流连接到指定的容器上:
> docker attach nginx
可以理解为:将容器中的主程序转为了前台运行
由于我们的输入输出流连接到了容器的主程序上,我们的输入输出操作也就直接针对了这个程序,而我们发送的 Linux 信号也会转移到这个程序上。例如我们可以通过Ctrl^C
来向程序发送停止信号,这样一来容器也会停止运行。
容器网络实质上也是由 Docker 为应用程序所创造的虚拟环境的一部分,它能让应用从宿主机操作系统的网络环境中独立出来,形成容器自有的网络设备、IP 协议栈、端口套接字、IP 路由表、防火墙等等与网络相关的模块。
在 Docker 网络中,有三个比较核心的概念,沙盒(Sandbox)、网络(Network)、端点(Endpoint):
这三者一起构成了 Docker 网络的核心模型,即容器网络模型(Container Network Model)
容器网络模型为容器引擎提供了一套标准的网络对接范式,而在 Docker 中,实现这套范式的是 Docker 所封装的libnetwork
模块。
目前 Docker 官方提供了五种网络驱动:Bridge
、Host
、Overlay
、MacLan
、None
。
其中,Bridge
网络是 Docker 容器的默认网络驱动,而Overlay
网络则是借助 Docker Swarm 来搭建的跨 Docker Daemon 网络,我们可以通过它搭建跨物理主机的虚拟网络,进而让不同物理机中运行的容器感知不到多个物理机的存在。
要让一个容器连接到另外一个容器,我们可以在容器通过docker create
或docker run
创建时通过--link
选项进行配置:
> docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes mysql> docker run -d --name webapp --link mysql webapp:latest
要想在 Web 应用中连接到 MySQL 数据库,只需要将容器的网络命名填入到连接地址中。例如下面的代码,连接地址中的mysql
就类似我们常见的域名解析,Docker 会将其指向 MySQL 容器的 IP 地址:
String url = "jdbc:mysql://mysql:3306/webapp"
虽然容器间的网络打通了,但并不意味着我们可以任意访问被连接容器中的任何服务。Docker 为容器网络增加了一套安全机制,只有容器自身允许的端口,才能被其他容器所访问。
在docker ps
的结果中可以看到容器暴露给其他容器访问的端口:
> docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES95507bc88082 mysql:5.7 "docker-entrypoint.s…" 17 seconds ago Up 16 seconds 3306/tcp, 33060/tcp mysql
暴露端口可以通过 Docker 镜像定义,也可以在容器创建时通过--expose
选项进行定义:
> docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --expose 13306 --expose 23306 mysql:5.7
可以看到13306
和23306
这两个端口已经成功的打开:
> docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES3c4e645f21d7 mysql:5.7 "docker-entrypoint.s…" 4 seconds ago Up 3 seconds 3306/tcp, 13306/tcp, 23306/tcp, 33060/tcp mysql
Docker 还支持连接时使用别名来摆脱容器名的限制:
> docker run -d -name webapp --link mysql:database webapp:latest
这里使用了--link <name>:<alias>
的形式连接到 MySQL 容器,并设置它的别名为database
。这样当我们要在 Web 应用中使用 MySQL 连接时,就可以用database
替代连接地址:
String url = "jdbc:mysql://database:3306/webapp";
容器能够互相连接的前提是两者同处于一个网络中,这里的网络可以理解为 Docker 所虚拟的子网,而容器网络沙盒可以看作是虚拟的主机。只有当多个主机在同一个子网时,才能互相看到并进行网络数据交换。
当我们启动 Docker 服务时,他会为我们创建一个默认的bridge
网络。而我们创建的容器在不专门指定网络的情况下,都会连接到这个网络上。
通过docker inspect
命令查看容器,可在Network
部分看到容器网络的相关信息:
> docker inspect mysql[ {## ...... "NetworkSettings": {## ...... "Networks": { "bridge": { "IPAMConfig": null, "Links": null, "Aliases": null, "NetworkID": "bc14eb1da66b67c7d155d6c78cb5389d4ffa6c719c8be3280628b7b54617441b", "EndpointID": "1e201db6858341d326be4510971b2f81f0f85ebd09b9b168e1df61bab18a6f22", "Gateway": "172.17.0.1", "IPAddress": "172.17.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "MacAddress": "02:42:ac:11:00:02", "DriverOpts": null } }## ...... }## ...... }]
使用docker network create
命令创建网络,通过-d
选项指定驱动类型,默认为bridge
:
> docker network create -d bridge individual
通过docker network ls
或者docker network list
查看 Docker 中已经存在的网络:
> docker network lsNETWORK ID NAME DRIVER SCOPEbc14eb1da66b bridge bridge local35c3ef1cc27d individual bridge local
在之后创建容器时,可以通过--network
来指定容器所要加入的网络:
> docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --network individual mysql:5.7
通过docker inspect mysql
观察一下此时的容器网络:
> docker inspect mysql[ {## ...... "NetworkSettings": {## ...... "Networks": { "individual": { "IPAMConfig": null, "Links": null, "Aliases": [ "2ad678e6d110" ], "NetworkID": "35c3ef1cc27d24e15a2b22bdd606dc28e58f0593ead6a57da34a8ed989b1b15d", "EndpointID": "41a2345b913a45c3c5aae258776fcd1be03b812403e249f96b161e50d66595ab", "Gateway": "172.18.0.1", "IPAddress": "172.18.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "MacAddress": "02:42:ac:12:00:02", "DriverOpts": null } }## ...... }## ...... }]
可以看到容器所加入的网络已经变成为individual
。
当两个容器处于不同的网络时,之间是不能互相连接引用的
Docker 提供了端口映射的功能来允许我们从容器外部通过网络访问容器中的应用:
要映射端口,我们可以在创建容器时使用-p
或--publish
选项**,格式为-p <ip>:<host-port>:<container-port>
:
> docker run -d --name nginx -p 80:80 -p 443:443 nginx:1.12
之后就可以在容器列表里看到端口映射的配置:
> docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESbc79fc5d42a6 nginx:1.12 "nginx -g 'daemon of…" 4 seconds ago Up 2 seconds 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp nginx
Docker 中的沙盒文件系统虽然说有很多优势,但也存在弊端:
为了解决这些问题,UnionFS 支持挂载不同类型的文件系统到统一的目录结构中。
基于底层存储实现,Docker 提供了三种适用于不同场景的文件系统挂载方式:Bind Mount、Volume 和 Tmpfs Mount。
Bind Mount
:能够直接将宿主机操作系统中的目录和文件挂载到容器内的文件系统中,需要同时指定容器内、外的路径。在容器内外对文件读写,都是相互可见的Volume
:也是从宿主机操作系统中挂载目录到容器,只不过这个挂载的目录由 Docker 进行管理,我们只需要指定容器内的目录即可Tmpfs Mount
:支持挂载系统内存的中的一部分到容器的文件系统里,不过存储并不是持久的,其中的内容会随着容器的停止而消失在创建容器时通过传递-v
或--volume
选项来指定挂载对应的目录或文件,格式为-v <host-path>:<container-path>
:
> docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html nginx:1.12
容器启动后,就可以看到挂载的目录或文件已经出现在容器中:
> docker exec nginx ls /usr/share/nginx/htmlindex.html
可以通过docker inspect
查看容器数据挂载的相关信息:
> docker inspect nginx[ {## ...... "Mounts": [ { "Type": "bind", "Source": "/webapp/html", "Destination": "/usr/share/nginx/html", "Mode": "", "RW": true, "Propagation": "rprivate" } ],## ...... }]
可以看到有一个RW
字段,表示挂载目录或文件具有读写性(Read and Write)。
Docker 还支持以只读的方式挂载,这样目录或文件只能被容器中的程序读取,而无法修改。只需要在挂载选项后添加:ro
:
> docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html:ro nginx:1.12
Tmpfs Mount
是一种特殊的挂载方式,它主要利用内存来存储数据,因此其特征就是高读写速度、临时性挂载。
在创建容器时,通过--tmpfs
传递挂载到容器内的目录即可,不需要指定内存的具体位置:
> docker run -d --name webapp --tmpfs /webapp/cache webapp:latest
也可以通过docker inspect
命令进行查看:
> docker inspect webapp[ {## ...... "Tmpfs": { "/webapp/cache": "" },## ...... }]
Tmpfs Mount
有以下几种常见的使用场景:
数据卷(Volume)本质上仍然是宿主机操作系统上的一个目录,只不过它存放在 Docker 内部,接受 Docker 的管理。
在使用Volume
进行挂载时,我们不需要知道数据具体存储在了宿主机操作系统的何处,只需要给定容器中的哪个目录会被挂载即可:
> docker run -d --name webapp -v /webapp/storage webapp:latest
数据卷挂载到容器后,可以通过docker inspect
命令查看容器中数据卷的挂载信息:
> docker inspect webapp[ {## ...... "Mounts": [ { "Type": "volume", "Name": "2bbd2719b81fbe030e6f446243386d763ef25879ec82bb60c9be7ef7f3a25336", "Source": "/var/lib/docker/volumes/2bbd2719b81fbe030e6f446243386d763ef25879ec82bb60c9be7ef7f3a25336/_data", "Destination": "/webapp/storage", "Driver": "local", "Mode": "", "RW": true, "Propagation": "" } ],## ...... }]
为了方便识别数据卷,可以通过-v <name>:<container-path>
的形式来命名数据卷:
> docker run -d --name webapp -v appdata:/webapp/storage webapp:latest
由于数据卷的命名在 Docker 中是唯一的,因此可以很方便的让多个容器挂载同一个数据卷:
> docker run -d --name webapp -v html:/webapp/html webapp:latest> docker run -d --name nginx -v html:/usr/share/nginx/html:ro nginx:1.12
使用
-v
选项来挂载数据卷时,如果数据卷不存在,Docker 就会自动创建和分配宿主机操作系统的目录。如果同名数据卷已经存在,则会直接引用
可以直接通过docker volume rm
命令来删除指定的数据卷:
> docker volume rm appdata
在删除数据卷之前,我们必须保证数据卷没有被任何容器所使用,否则 Docker 会报错
在docker rm
删除容器的命令中,还可以添加-v
选项来删除容器关联的数据卷:
docker rm -v webapp
如果没有随容器删除这些数据卷,Docker 在创建新的容器时也不会启用它们。这时可以通过docker volume prune
命令删除那些没有被容器引用的数据卷:
> docker volume pruneDeleted Volumes:af6459286b5ce42bb5f205d0d323ac11ce8b8d9df4c65909ddc2feea7c3d1d530783665df434533f6b53afe3d9decfa791929570913c7aff10f302c17ed1a38965b822e27d0be93d149304afb1515f8111344da9ea18adc3b3a34bddd2b243c7## ......
所谓数据卷容器(Volume Container),就是一个没有具体指定应用,甚至不需要运行的容器。我们使用它的目的,是为了定义一个或多个数据卷并持有它们的引用:
可通过以下命令创建一个数据卷容器:
> docker create --name appdata -v /webapp/storage ubuntu
数据卷容器可以看作是容器间的文件系统桥梁,可以像加入网络一样引用数据卷容器,添加--volumes-from
参数即可:
> docker run -d -name webapp --volumes-from appdata webapp:latest
利用数据卷容器,可以很方便的对数据卷中的数据进行迁移。
数据备份、迁移、恢复的过程可以理解为对数据进行打包,移动到其他位置,在需要的地方解压的过程
首先建立一个用来存放打包文件的目录/backup
。要备份数据,我们还需要建立一个临时容器,将用于备份的目录和要备份的数据卷都挂载上去:
> docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar cvf /backup/backup.tar /webapp/storage
--rm
选项用来让容器在停止后自动删除
备份后,就可以在/backup
目录下找到数据卷的备份文件backup.tar
了。
要恢复数据卷中的数据,也可以借助临时容器来完成:
> docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar xvf /backup/backup.tar -C /webapp/storage --strip
Docker 还为我们提供了一个支持相对丰富的挂载方式,也就是通过--mount
选项来配置挂载:
> docker run -d --name webapp webapp:latest --mount 'type=volume,src=appdata,dst=/webapp/storage,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>' webapp:latest
Docker 将容器内沙盒文件系统记录成镜像层的时候,会先暂停容器的运行,保证容器内的文件系统处于一个相对稳定的状态,以确保数据的一致性。
> docker commit -m "Configured" webappsha256:0bc42f7ff218029c6c4199ab5c75ab83aeaaed3b5c731f715a3e807dda61d19e> docker imagesREPOSITORY TAG IMAGE ID CREATED SIZE<none> <none> 0bc42f7ff218 3 seconds ago 372MB## ......
使用docker tag
能够为未命名的镜像指定镜像名:
> docker tag 0bc42f7ff218 webapp:1.0
也可以为已有的镜像创建一个新的命名:
> docker tag webapp:1.0 webapp:2.0
当我们对未命名的镜像进行命名后,Docker 就不会在镜像列表里继续显示这个镜像,取而代之的是我们新的命名。而如果我们对已有镜像使用docker tag
时,旧的镜像依然会存在于镜像列表中:
> docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEwebapp 1.0 0bc42f7ff218 29 minutes ago 372MBwebapp latest 0bc42f7ff218 29 minutes ago 372MB## ......
还可以直接在提交镜像更改时指定新的镜像名:
> docker commit -m "Upgrade" webapp webapp:2.0
可以使用管道:
> docker save webapp:1.0 > webapp-1.0.tar
或者可以使用docker save
命令,并添加-o
选项,用来指定输出文件:
> docker save -o ./webapp-1.0.tar webapp:1.0
可以使用管道:
> docker load < webapp-1.0.tar
或者添加-i
选项指定输入文件:
> docker load -i webapp-1.0.tar
通过docker save
和docker load
命令还可以批量迁移镜像,只要在docker save
中传入多个镜像名作为参数,就可以将这些镜像都打成一个包,方便我们一次性迁移多个镜像:
> docker save -o ./images.tar webapp:1.0 nginx:1.12 mysql:5.7
使用docker export
命令可以直接导出容器,可以简单的将其理解为docker commit
和docker save
命令的结合体:
> docker export -o ./webapp.tar webapp
相对的,使用docker export
导出的容器包,我们可以使用docker import
导入。使用docker import
并非直接将容器导入,而是将容器运行时的内容以镜像的形式导入,所以导入的结果还是一个镜像,而不是容器:
> docker import ./webapp.tar webapp:1.0
Dockerfile 是 Docker 中用于定义镜像自动化构建流程的配置文件,在 Dockerfile 中,包含了构建镜像过程中需要执行的命令和其他操作。
Dockerfile 的内容很简单,主要以两种形式呈现:一种是注释行,另一种是指令行。
首先来看一个完整的 Dockerfile 例子,这是用于构建 Docker 官方所提供的Redis
镜像的 Dockerfile 文件:
FROM debian:stretch-slim# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get addedRUN groupadd -r redis && useradd -r -g redis redis# grab gosu for easy step-down from root# https://github.com/tianon/gosu/releasesENV GOSU_VERSION 1.10RUN set -ex; \ \ fetchDeps=" \ ca-certificates \ dirmngr \ gnupg \ wget \ "; \ apt-get update; \ apt-get install -y --no-install-recommends $fetchDeps; \ rm -rf /var/lib/apt/lists/*; \ \ dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \ wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \ wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \ export GNUPGHOME="$(mktemp -d)"; \ gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \ gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \ gpgconf --kill all; \ rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \ chmod +x /usr/local/bin/gosu; \ gosu nobody true; \ \ apt-get purge -y --auto-remove $fetchDepsENV REDIS_VERSION 3.2.12ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-3.2.12.tar.gzENV REDIS_DOWNLOAD_SHA 98c4254ae1be4e452aa7884245471501c9aa657993e0318d88f048093e7f88fd# for redis-sentinel see: http://redis.io/topics/sentinelRUN set -ex; \ \ buildDeps=' \ wget \ \ gcc \ libc6-dev \ make \ '; \ apt-get update; \ apt-get install -y $buildDeps --no-install-recommends; \ rm -rf /var/lib/apt/lists/*; \ \ wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \ echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \ mkdir -p /usr/src/redis; \ tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \ rm redis.tar.gz; \ \# disable Redis protected mode [1] as it is unnecessary in context of Docker# (ports are not automatically exposed when running inside Docker, but rather explicitly by specifying -p / -P)# [1]: https://github.com/antirez/redis/commit/edd4d555df57dc84265fdfb4ef59a4678832f6da grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \ sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr/src/redis/src/server.h; \ grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \# for future reference, we modify this directly in the source instead of just supplying a default configuration flag because apparently "if you specify any argument to redis-server, [it assumes] you are going to specify everything"# see also https://github.com/docker-library/redis/issues/4#issuecomment-50780840# (more exactly, this makes sure the default behavior of "save on SIGTERM" stays functional by default) \ make -C /usr/src/redis -j "$(nproc)"; \ make -C /usr/src/redis install; \ \ rm -r /usr/src/redis; \ \ apt-get purge -y --auto-remove $buildDepsRUN mkdir /data && chown redis:redis /dataVOLUME /dataWORKDIR /dataCOPY docker-entrypoint.sh /usr/local/bin/ENTRYPOINT ["docker-entrypoint.sh"]EXPOSE 6379CMD ["redis-server"]
总体上来看,可以将 Dockerfile 理解为一个由上往下执行指令的脚本文件。可以将 Dockerfile 的指令简单的分为五大类:
在镜像构建的过程中,可以通过FROM
指令指定一个基础镜像。Docker 会先获取到这个基础镜像,再在这个镜像的基础上进行构建操作
FROM <image> [AS <name>]FROM <image>[:<tag>] [AS <name>]FROM <image>[@<digest>] [AS <name>]
在RUN
指令之后,我们直接拼接上需要执行的命令。在构建时,Docker 就会执行这些命令,并将它们对文件系统的修改记录下来,形成镜像的变化:
RUN <command>RUN ["executable", "param1", "param2"]
RUN
指令支持以\
换行,如果单行的长度过大,建议对内容进行切割,方便阅读
基于镜像启动的容器,在容器启动时会根据镜像所定义的一条命令来启动容器中进程号为1
的进程。而这个命令的定义,就是通过 Dockerfile 中的ENTRYPOINT
和CMD
实现的。
ENTRYPOINT ["executable", "param1", "param2"]ENTRYPOINT command param1 param2CMD ["executable","param1","param2"]CMD ["param1","param2"]CMD command param1 param2
ENTRYPOINT
和CMD
指令用法近似,都是给出需要执行的指令,并且它们都可以为空ENTRYPOINT
和CMD
同时给出时,CMD
中的内容会作为ENTRYPOINT
定义命令的参数,最终执行容器启动的还是ENTRYPOINT
所给出的命令通过EXPOSE
指令可以为镜像指定要暴露的端口:
EXPOSE <port> [<port>/<protocol>...]
当我们通过EXPOSE
指令配置了镜像的端口暴露定义,那么基于这个镜像所创建的容器,在被其他容器通过--link
选项连接时,就能够直接允许来自其他容器对这些端口的访问。
在一些程序里,我们需要持久化一些数据。可以通过VOLUME
指令来定义基于此镜像的容器所自动建立的数据卷,这样就无需单独使用-v
选项进行配置:
VOLUME ["/data"]
在制作新镜像时,我们可能需要将一些软件配置、程序代码、执行脚本等直接导入到镜像内的文件系统里。使用COPY
或ADD
指令能够帮助我们直接从宿主机的文件系统里拷贝内容到镜像里的文件系统中:
COPY [--chown=<user>:<group>] <src>... <dest>ADD [--chown=<user>:<group>] <src>... <dest>COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
COPY
与ADD
指令的定义完全一样,主要区别在于ADD
能够支持使用网络端的URL
地址作为src
源,并且在源文件被识别为压缩包时,自动进行解压,而COPY
则没有这两个能力。
在编写好Dockerfile
之后,我们就可以使用docker build
命令构建我们所定义的镜像:
> docker build ./webapp
docker build
可以接收一个参数,这个参数为一个目录路径(本地路径或 URL 路径)。Docker 会将这个目录作为构建的环境目录,默认情况下,也会从这个目录下寻找名为Dockerfile
的文件。
如果我们的Dockerfile
文件路径不在这个目录下,则可以通过-f
选项单独给出Dockerfile
文件的路径:
> docker build -t webapp:latest -f ./webapp/a.Dockerfile ./webapp
最好在构建镜像时添加-t
选项,用来指定新生成的镜像的名称:
> docker build -t webapp:latest ./webapp
在 Dockerfile 里,可以使用ARG
指令建立一个参数变量。我们可以在构建时通过构建指令传入这个参数变量,并且在 Dockerfile 里使用它:
FROM debian:stretch-slim## ......ARG TOMCAT_MAJORARG TOMCAT_VERSION## ......RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"## ......
如果我们需要通过这个 Dockerfile 文件构建 Tomcat 镜像,可以在构建时通过docker build
的--build-arg
选项来设置参数变量:
> docker build --build-arg TOMCAT_MAJOR=8 --build-arg TOMCAT_VERSION=8.0.53 -t tomcat:8.0 ./tomcat
环境变量通过ENV
指令定义:
FROM debian:stretch-slim## ......ENV TOMCAT_MAJOR 8ENV TOMCAT_VERSION 8.0.53## ......RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"
与参数变量只能影响构建过程不同,环境变量不仅能够影响构建,还能够影响基于此镜像创建的容器。
环境变量设置的实质,其实就是定义操作系统环境变量,所以在运行的容器里,一样拥有这些变量,而容器中运行的程序也能够得到这些变量的值
由于环境变量在容器运行时依然有效,所以运行容器时我们还可以对其进行覆盖。
在创建容器时使用-e
或--env
选项,可以对环境变量的值进行修改或定义新的环境变量:
> docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7
ENV
指令所定义的变量,永远会覆盖ARG
所定义的变量,即使它们定义时的顺序是相反的
在构建镜像时,RUN
指令有两种写法:
RUN apt-get update; \ apt-get install -y --no-install-recommends $fetchDeps; \ rm -rf /var/lib/apt/lists/*;# 或RUN apt-get updateRUN apt-get install -y --no-install-recommends $fetchDepsRUN rm -rf /var/lib/apt/lists/*
而我们更常见的是第一种形式,这就要从镜像构建的过程说起了。
看似连续的镜像构建过程,其实是由多个小段组成的。每当一条能够形成对文件系统改动的指令在被执行前,Docker 先会基于上条命令的结果启动一个容器,在容器中运行这条指令的内容,之后将结果打包成一个镜像层,如此反复,最终形成镜像
所以,构建而来的镜像是由多个镜像层叠加而得的,而这些镜像层其实就是在我们 Dockerfile 中每条指令所生成的。
因此,绝大多数镜像会将命令合并到一条指令中,因为这样不但减少了镜像层的数量,也减少了镜像构建过程中反复创建容器的次数,提高了镜像构建的速度。
Docker 在镜像构建的过程中,还支持一种缓存策略来提高镜像的构建速度。
由于镜像是多个指令所创建的镜像层组合而得,那么如果我们判断新编译的镜像层与已经存在的镜像层未发生变化,那么我们完全可以直接利用之前构建的结果,而不需要再执行这条构建指令,这就是镜像构建缓存的原理
基于这个原则,我们在条件允许的前提下,更建议将不容易发生变化的搭建过程放到 Dockerfile 的前部,充分利用构建缓存提高镜像构建的速度。
另外,指令的合并也不宜过度,而是将易变和不易变的过程拆分,分别放到不同的指令里。
当不希望 Docker 在构建镜像中使用构建缓存时,可以通过--no-cache
选项禁用:
> docker build --no-cache ./webapp
ENTRYPOINT
和CMD
两个命令都是用来指定基于此镜像所创建容器里主进程的启动命令,而它们的区别在于,ENTRYPOINT
指令的优先级高于CMD
指令。当ENTRYPOINT
和CMD
同时在镜像中被指定时,CMD
里的内容会作为ENTRYPOINT
的参数,两者拼接之后,才是最终执行的命令。
之所以ENTRYPOINT
和CMD
要分成两个不同的命令,是因为它们的设计目的是不同的:
ENTRYPOINT
:主要用于对容器进行一些初始化CMD
:用于真正定义容器中主程序的启动命令以Redis
镜像为例:
## ......COPY docker-entrypoint.sh /usr/local/bin/ENTRYPOINT ["docker-entrypoint.sh"]## ......CMD ["redis-server"]
可以看到,CMD
指令定义的正是启动Redis
的服务程序,而ENTRYPOINT
使用的则是外部引入的脚本文件docker-entrypoint.sh
,内容如下:
#!/bin/shset -e# first arg is `-f` or `--some-option`# or first arg is `something.conf`if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then set -- redis-server "$@"fi# allow the container to be started with `--user`if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then find . \! -user redis -exec chown redis '{}' + exec gosu redis "$0" "$@"fiexec "$@"
脚本的最后一条命令exec "$@"
其作用是运行一个程序,而运行命令就是ENTRYPOINT
脚本的参数,所以实际执行的就是CMD
里的命令。
在 Docker 开发中最常使用的多容器定义和运行软件就是 Docker Compose。
如果说 Dockerfile 是将容器内运行环境的搭建固化下来,那么 Docker Compose 就可以理解为将多个容器运行的方式和配置固化下来。
在 Docker Compose 里,我们通过一个docker-compose.yml
配置文件,将所有与应用系统相关的软件及它们对应的容器进行配置,之后使用 Docker Compose 提供的命令进行启动,就能让 Docker Compose 将刚才我们所提到的那些复杂问题解决掉。
Docker Compose 是一个由 Python 编写的软件。通过下面的命令下载 Docker Compose 到应用执行目录,并附上运行权限,这样 Docker Compose 就可以在机器中使用了:
> sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose> sudo chmod +x /usr/local/bin/docker-compose> sudo docker-compose versiondocker-compose version 1.21.2, build a133471docker-py version: 3.3.0CPython version: 3.6.5OpenSSL version: OpenSSL 1.0.1t 3 May 2016
也可以通过pip
安装:
> sudo pip install docker-compose
简单来说,使用 Docker Compose 的步骤共分为三步:
Dockerfile
(也可以使用现有镜像)docker-compose.yml
docker-compose
命令启动应用栈一个简单的例子:
version: '3'services: webapp: build: ./image/webapp ports: - "5000:5000" volumes: - ./code:/code - logvolume:/var/log links: - mysql - redis redis: image: redis:3.2 mysql: image: mysql:5.7 environment: - MYSQL_ROOT_PASSWORD=my-secret-pwvolumes: logvolume: {}
对与开发而言,最常使用的就是docker-compose up
和docker-compose down
命令:
docker-compose up
docker-compose up
命令类似于 Docker Engine 中的docker run
。它会根据docker-compose.yml
中配置的内容,创建所有的容器、网络、数据卷等内容,并将它们启动。
默认情况下,docker-compose up
会在前台运行,可以使用-d
选项使其在后台运行:
> sudo docker-compose up -d
需要注意的是,docker-compose
命令默认会识别当前控制台所在目录内的docker-compose.yml
文件,而且会以当前目录的名字作为组装的应用项目的名称。可以通过-f
选项来指定配置文件名,通过-p
选项来定义项目名:
> sudo docker-compose -f ./compose/docker-compose.yml -p myapp up -d
docker-compose down
docker-compose down
命令用于停止所有的容器,并将它们删除,同时清除网络等配置内容:
> sudo docker-compose down
除了启动和停止命令之外,Docker Compose 还为我们提供了很多直接操作服务的命令,服务可以看成是一组相同容器的集合。
可以使用docker-compose logs
命令查看容器中主进程的输出内容:
> sudo docker-compose logs nginx
通过docker-compose create/start/stop
可以实现与docker create/start/stop
相似的效果,只不过操作的对象由 Docker Engine 中的容器变为了 Docker Compose 中的服务:
> sudo docker-compose create webapp> sudo docker-compose start webapp> sudo docker-compose stop webapp
更新中…
]]>问题:你知道设置
GOPATH
有什么意义吗?
可以把GOPATH
简单理解成 Go 语言的工作目录,它的值是一个目录的路径,也可以是多个目录路径,每个目录都代表 Go 语言的一个工作区(workspace)。
我们需要利用这些工作区,来放置 Go 语言的源码文件(source file),以及安装后的归档文件(archieve file,以.a
为扩展名的文件)和可执行文件(executable file)。
Go 语言中的源码文件可分为三种:
问题:命令源码文件的用途是什么,怎样编写它?
命令源码文件是程序的运行入口,是每个可独立运行的程序必须拥有的。我们可以通过构建go build
或安装go install
,生成与其对应的可执行文件,后者一般会与该命令源码文件的直接父目录同名。
如果一个源码文件声明属于main
包,并且包含一个无参数声明且无结果声明的main
函数,那么它就是命令源码文件:
package mainimport "fmt"func main() { fmt.Println("Hello, Golang!")}
对于一个独立的程序来说,命令源码文件永远只会也只能有一个。如果有与命令源码文件同包的源码文件,那么它们也应该声明属于
main
包
Go 语言标准库中的flag
包专门用于接收和解析命令参数,可使用如下语句:
flag.StringVar(&name, "name", "everyone", "The greeting object.")// 或var name = flag.String("name", "everyone", "The greeting object.")
函数flag.stringVar
接受 4 个参数:
&name
:用于存储该命令参数值的地址"name"
:指定该命令参数的名称"everyone"
:指定在为追加该命令参数时的默认值"The greeting object."
:该命令参数的简短说明,打印命令说明时会用到package mainimport ( "flag" "fmt")var name stringfunc init() { flag.StringVar(&name, "name", "everyone", "The greeting object")}func main() { flag.Parse() // 解析命令参数,并把它们的值赋给相应的变量 fmt.Printf("Hello, %s!\n", name)}
将上述代码保存为demo1.go
,运行以下命令:
go run demo1.go -name="abelsu7"------Hello, abelsu7!
查看参数说明:
go run demo1.go --help------Usage of C:\Users\abel1\AppData\Local\Temp\go-build617189518\b001\exe\demo1.exe: -name string The greeting object (default "everyone")exit status 2
有多种方式,最简单的就是对变量flag.Usage
重新赋值。flag.Usage
的类型是func()
,无参数声明也无结果声明。
在flag.Parse()
之前加入如下语句:
flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question") flag.PrintDefaults()}
这样调用go run demo1.go --help
后就会输出:
Usage of question: -name string The greeting object (default "everyone")exit status 2
再深入来看,当我们调用flag
包中的一些函数时(比如stringVar
、Parse
等),实际上是在调用flag.CommandLine
变量的对应方法。
flag.CommandLine
相当于默认情况下的命令参数容器,所以,通过对flag.CommandLine
重新赋值,就可以更深层次的定制当前命令源码文件的参数使用说明。
将程序修改为:
package mainimport ( "flag" "fmt" "os")var name stringfunc init() { flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError) flag.CommandLine.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question") flag.PrintDefaults() } flag.StringVar(&name, "name", "everyone", "The greeting object")}func main() { //flag.Usage = func() { // fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question") // flag.PrintDefaults() //} flag.Parse() fmt.Printf("Hello, %s!\n", name)}------> go run demo1.go --helpUsage of question: -name string The greeting object (default "everyone")exit status 2
就会得到一样的输出。而当我们把flag.CommandLine
赋值的那条语句改为:
flag.CommandLine = flag.NewFlagSet("", flag.PanicOnError)
再次运行得到:
> go run demo1.go --helpUsage of question: -name string The greeting object (default "everyone")panic: flag: help requestedgoroutine 1 [running]:flag.(*FlagSet).Parse(0xc000084060, 0xc0000443f0, 0x1, 0x1, 0x4, 0x4d0ab5) C:/Go/src/flag/flag.go:938 +0x107flag.Parse() C:/Go/src/flag/flag.go:953 +0x76main.main() C:/Users/abel1/go/src/github.com/abelsu7/hello/demo1.go:25 +0x2dexit status 2
flag.ExitOnError
:告诉命令参数容器,当命令后跟--help
或者参数设置不正确的时候,在打印命令参数使用说明以后,以exit status 2
结束当前程序flag.PanicOnError
:区别在于最后会抛出运行时恐慌panic
另外,还可以创建一个私有的命令参数容器,这样就不会影响到全局变量flag.CommandLine
:
package mainimport ( "flag" "fmt" "os")var name stringvar cmdLine = flag.NewFlagSet("question", flag.ExitOnError)func init() { cmdLine.StringVar(&name, "name", "everyone", "The greeting object")}func main() { cmdLine.Parse(os.Args[1:]) fmt.Printf("Hello, %s!\n", name)}------> go run demo1.go --helpUsage of question: -name string The greeting object (default "everyone")exit status 2
关于
flag
包的更多用法可以参考 Package flag | golang.google.cn
库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用。
在 Go 语言中,程序实体是变量、常量、函数、结构体和接口的统称
程序实体的名字被统称为标识符,它可以是任何 Unicode 编码可以表示的字母字符、数字以及下划线_
,但其首字母不能是数字。
首先新建一个_03_demo
的包,在该路径下创建命令源码文件demo4.go
:
package mainimport ( "flag")var name stringfunc init() { flag.StringVar(&name, "name", "everyone", "The greeting object.")}func main() { flag.Parse() hello(name)}
然后在相同路径下,新建demo4_lib.go
:
package mainimport "fmt"func hello(name string) { fmt.Printf("Hello, %s!\n", name)}
在{project_path}/_03_demo/
路径下,使用以下命令运行程序:
> go run demo4.go demo4_lib.goHello, every one!// 或者> go build> _03_demo.exeHello, every one!
代码包声明的基本规则
package
声明语句要一致。如果目录中有命令源码文件,那么其他种类的源码文件也应该声明属于main
**包,这样才能成功构建和运行go build
时,生成的结果文件的主名称与其父目录的名称一致src
目录的相对路径,就是它的代码包导入路径,而实际使用其程序实体时给定的限定符要与它声明所属的代码包名称对应internal
代码包让一些程序实体仅仅能被当前模块中的其他代码引用。具体规则是,internal
代码包中声明的公开程序实体仅能被该代码包的直接父包及其子包中的代码引用。当然,引用前需要先导入internal
包。对于其他代码包,导入都是非法的,无法通过编译Go 语言中的程序实体包括变量、常量、函数、结构体和接口。
Go 语言是静态类型的编程语言,所以在声明变量或常量时,需要指定它们的类型,或者给予足够的信息,这样才能让 Go 语言推导出变量的类型
package mainimport "fmt"func main() { var s1 int = 42 // 显式定义,可读性最强 var s2 = 42 // 编译器自动推导变量类型 s3 := 42 // 自动推导类型 + 赋值 fmt.Println(s1, s2, s3)}-------------42 42 42
var
无法直接写进循环条件的初始化语句中问题:Go 语言的类型推断可以带来哪些好处?
除了写代码时可以省略变量类型之外,真正的好处体现在代码重构。
例如下面的代码:
package mainimport ( "flag" "fmt")func main() { var name = getTheFlag() flag.Parse() fmt.Printf("Hello, %v!\n", *name)}func getTheFlag() *string { return flag.String("name", "everyone", "The greeting object.")}
这样一来,我们可以随意改变getTheFlag
函数的内部实现,及其返回结果的类型,而不用修改main
函数中的任何代码,这个命令源码文件依然可以通过编译,并成功构建、运行。
通过这种类型推断,可以初步体验动态类型编程语言所带来的一部分优势,即以程序的可维护性和运行效率换来程序灵活性的明显提升。
事实上,Go 语言是静态类型的,所以一旦在初始化变量时确定了它的类型,之后就不可能再改变。这种类型的确定是在编译器完成的,因此不会对程序的运行效率产生任何影响
var err errorn, err := io.WriteString(os.Stdout, "Hello, everyone!\n")
这里使用短变量声明对新变量n
和旧变量err
进行了声明并赋值,同时也是对旧变量err
的重声明。
package mainimport "fmt"func main() { // 有符号整数,可以表示正负 var a int8 = 1 // 1 字节 var b int16 = 2 // 2 字节 var c int32 = 3 // 4 字节 var d int64 = 4 // 8 字节 fmt.Println(a, b, c, d) // 无符号整数,只能表示非负数 var ua uint8 = 1 var ub uint16 = 2 var uc uint32 = 3 var ud uint64 = 4 fmt.Println(ua, ub, uc, ud) // int 类型,在32位机器上占4个字节,在64位机器上占8个字节 var e int = 5 var ue uint = 5 fmt.Println(e, ue) // bool 类型 var f bool = true fmt.Println(f) // 字节类型 var j byte = 'a' fmt.Println(j) // 字符串类型 var g string = "abcdefg" fmt.Println(g) // 浮点数 var h float32 = 3.14 var i float64 = 3.141592653 fmt.Println(h, i)}-------------1 2 3 41 2 3 45 5trueabcdefg3.14 3.14159265397
以下面的代码为例:
package mainimport "fmt"var container = []string{"zero", "one", "two"}func main() { container := map[int]string{0: "zero", 1: "one", 2: "two"} fmt.Printf("The element is %q.\n", container[1])}
要想在打印其中元素之前,正确判断变量container
的类型,则可以使用「类型断言」表达式x.(T)
:
package mainimport "fmt"var container = []string{"zero", "one", "two"}func main() { container := map[int]string{0: "zero", 1: "one", 2: "two"} value, ok := interface{}(container).(map[int]string) if ok { fmt.Println(value[1]) } fmt.Printf("The element is %q.\n", container[1])}------oneThe element is "one".Process finished with exit code 0
需要注意的是,在类型断言表达式x.(T)
中,x
代表要被判断类型的值,这个值当下的类型必须是接口类型,所以当container
变量类型不是任何的接口类型时,就需要先把它转成某个接口类型的值。
问题:类型转换规则中有哪些值得注意的地方?
在类型转换表达式T(x)
中,x
可以是一个变量,也可以是一个代表值的字面量(例如1.23
和struct{}
),还可以是一个表达式。
如果是表达式,那么该表达式的结果只能是一个值,而不能是多个值。如果从源类型到目标类型的转换是不合法的,就会引发一个编译错误。
string
类型是可行的,但如果被转换的整数值不是一个有效的 Unicode 代码点,则结果会是�
问题:什么是别名类型?什么是潜在类型?
可以用关键字type
声明自定义的各种类型。其中有一种「别名类型」,可以像下面一样声明:
type MyString = string
别名类型与其源类型除了名称不同,其他是完全相同的。例如 Go 语言内建的基本类型中就存在两个别名类型:byte
是uint8
的别名类型,rune
是uint32
的别名类型。
而下面没有=
的语法被称为对类型的再定义:
type MyString2 string // MyString2 是一个新的类型
对于这里的类型再定义来说,string
可以被称为MyString2
的潜在类型:即某个类型在本质上是哪个类型,或者是哪个类型的集合。
MyString2
与string
类型的值,就可以互相转换[]MyString2
与[]string
来说,这样做是不合法的,因为它们的潜在类型不同,分别是MyString2
和string
另外,即使两个类型的潜在类型相同,它们的值之间也不能进行判等或比较,它们的变量之间也不能赋值
Go 语言的数组(array)和切片(slice)类型:
数组的长度在声明它时就必须给定,并且之后不会再改变。可以说,数组的长度是其类型的一部分。例如,[1]string
和[2]string
就是两个不同的数组类型。
而切片的类型字面量中只有元素的类型,没有长度。切片的长度可以自动的随着其中元素数量的增长而增长,但不会随之减少。
可以把切片看作是对数组的一层简单的封装,因为每个切片都会有一个底层数组,而切片也可以被看作是对数组的某个连续片段的引用。
Go 语言中不存在所谓的“传值还是传引用”的问题。只要看被传递的值的类型就可判断:如果是引用类型,则可看作“传引用”。如果是值类型,则可看作“传值”。从传递成本的角度看,引用类型的值往往比值类型的值低很多
来看一个例子:
s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}s4 := s3[3:6]fmt.Printf("The length of s4: %d\n", len(s4))fmt.Printf("The capacity of s4: %d\n", cap(s4))fmt.Printf("The value of s4: %d\n", s4)------The length of s4: 3The capacity of s4: 5The value of s4: [4 5 6]
s3[3:6]
可看作[3,6)
,这里的3
被称为起始索引,6
被称为结束索引s4[0:cap(s4)]
的结果值即为把切片的窗口向右扩展到最大问题:怎样估算切片容量的增长?
一旦一个切片无法容纳更多的元素,Go 语言就会生成一个容量更大的切片(一般情况下容量扩为 2 倍),并将原切片的元素和新元素一并拷贝到新切片中。
但是,当原切片的长度>=1024
时候,Go 语言将会以原容量的1.25
倍作为新容量的基准,新容量基准会被调整(不断与1.25
相乘),直到结果不低于新长度。
另外,如果我们一次追加的元素过多,以至于新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准。最终的新容量在很多时候都要比新容量基准更大一些。
更多细节可以查看
runtime
包中slice.go
文件里的growslice
及相关函数的具体实现
package mainimport "fmt"func main() { // 示例1。 s6 := make([]int, 0) fmt.Printf("The capacity of s6: %d\n", cap(s6)) for i := 1; i <= 5; i++ { s6 = append(s6, i) fmt.Printf("s6(%d): len: %d, cap: %d\n", i, len(s6), cap(s6)) } fmt.Println() // 示例2。 s7 := make([]int, 1024) fmt.Printf("The capacity of s7: %d\n", cap(s7)) s7e1 := append(s7, make([]int, 200)...) fmt.Printf("s7e1: len: %d, cap: %d\n", len(s7e1), cap(s7e1)) s7e2 := append(s7, make([]int, 400)...) fmt.Printf("s7e2: len: %d, cap: %d\n", len(s7e2), cap(s7e2)) s7e3 := append(s7, make([]int, 600)...) fmt.Printf("s7e3: len: %d, cap: %d\n", len(s7e3), cap(s7e3)) fmt.Println() // 示例3。 s8 := make([]int, 10) fmt.Printf("The capacity of s8: %d\n", cap(s8)) s8a := append(s8, make([]int, 11)...) fmt.Printf("s8a: len: %d, cap: %d\n", len(s8a), cap(s8a)) s8b := append(s8a, make([]int, 23)...) fmt.Printf("s8b: len: %d, cap: %d\n", len(s8b), cap(s8b)) s8c := append(s8b, make([]int, 45)...) fmt.Printf("s8c: len: %d, cap: %d\n", len(s8c), cap(s8c))}------The capacity of s6: 0s6(1): len: 1, cap: 1s6(2): len: 2, cap: 2s6(3): len: 3, cap: 4s6(4): len: 4, cap: 4s6(5): len: 5, cap: 8The capacity of s7: 1024s7e1: len: 1224, cap: 1280s7e2: len: 1424, cap: 1696s7e3: len: 1624, cap: 2048The capacity of s8: 10s8a: len: 21, cap: 22s8b: len: 44, cap: 44s8c: len: 89, cap: 96Process finished with exit code 0
问题:切片的底层数组什么时候会被替换?
确切地说,一个切片的底层数组永远也不会被替换。虽然在扩容的时候 Go 语言也会生成新的底层数组,但同时也生成了新的切片。它只是把新的切片作为了新底层数组的窗口,而没有对原切片及其底层数组做任何改动。
在无需扩容时,append
函数返回的是指向原底层数组的新切片。而在需要扩容时,append
函数返回的是指向新底层数组的新切片。
Go 语言的链表实现在标准库container/list
中,有两个公开的程序实体:List
和Element
,List
实现了一个双向链表,而Element
则代表了链表中元素的结构。
package mainimport ( "container/list" "fmt")func main() { link := list.New() // 循环插入到头部 for i := 0; i <= 10; i++ { link.PushBack(i) } // 遍历链表 for p := link.Front(); p != link.Back(); p = p.Next() { fmt.Println("Number", p.Value) }}------Number 0Number 1Number 2Number 3Number 4Number 5Number 6Number 7Number 8Number 9Process finished with exit code 0
参考:
标准库contianer/ring
包中的Ring
类型实现的是一个循环链表,也就是我们俗称的环。其实List
在内部就是一个循环链表,它的根元素永远不会持有任何实际的元素值,而该元素的存在就是为了连接这个循环链表的首尾两端。
List
可以作为Queue
和Stack
的基础数据结构Ring
可以用来保存固定数量的元素,例如保存最近 100 万条日志,用户最近 10 次操作等Heap
可以用来排序,可用于构造优先级队列package main;import ( "container/ring" "fmt")func printRing(r *ring.Ring) { r.Do(func(v interface{}) { fmt.Print(v.(int), " ") }) fmt.Println()}func main() { //创建环形链表 r := ring.New(5) //循环赋值 for i := 0; i < 5; i++ { r.Value = i //取得下一个元素 r = r.Next() } printRing(r) //环的长度 fmt.Println(r.Len()) //移动环的指针 r.Move(2) //从当前指针删除n个元素 r.Unlink(2) printRing(r) //连接两个环 r2 := ring.New(3) for i := 0; i < 3; i++ { r2.Value = i + 10 //取得下一个元素 r2 = r2.Next() } printRing(r2) r.Link(r2) printRing(r)}------0 1 2 3 4 50 3 4 10 11 12 0 10 11 12 3 4 Process finished with exit code 0
package mainimport ( "container/heap" "fmt")type IntHeap []int//我们自定义一个堆需要实现5个接口//Len(),Less(),Swap()这是继承自sort.Interface//Push()和Pop()是堆自已的接口//返回长度func (h *IntHeap) Len() int { return len(*h)}//比较大小(实现最小堆)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 interface{}) { //将数据追加到h中 *h = append(*h, x.(int))}//弹出数据func (h *IntHeap) Pop() interface{} { old := *h n := len(old) x := old[n-1] //让h指向新的slice *h = old[0 : n-1] //返回最后一个元素 return x}//打印堆func (h *IntHeap) PrintHeap() { //元素的索引号 i := 0 //层级的元素个数 levelCount := 1 for i+1 <= h.Len() { fmt.Println((*h)[i : i+levelCount]) i += levelCount if (i + levelCount*2) <= h.Len() { levelCount *= 2 } else { levelCount = h.Len() - i } }}func main() { a := IntHeap{6, 2, 3, 1, 5, 4} //初始化堆 heap.Init(&a) a.PrintHeap() //弹出数据,保证每次操作都是规范的堆结构 fmt.Println(heap.Pop(&a)) a.PrintHeap() fmt.Println(heap.Pop(&a)) a.PrintHeap() heap.Push(&a, 0) heap.Push(&a, 8) a.PrintHeap()}------[1][2 3][6 5 4]1[2][4 3][6 5]2[3][4 5][6][0][3 5][6 4 8]Process finished with exit code 0
参考:
Go 语言中的字典(map)用来存储键值对的集合,它其实是一个哈希表(Hash Table)的特定实现。Go 语言字典的键类型不可以是函数类型、字典类型和切片类型,键类型的值必须支持判等操作。
Go 语言的通道(channel)类型的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类型。
略
在 Go 语言中,函数是一等(first class)公民,函数类型也是一等数据类型。也就是说,函数本身也可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等:
package mainimport "fmt"type Printer func(contents string) (n int, err error)func printToStd(contents string) (byteNum int, err error) { return fmt.Println(contents)}func main() { var p Printer p = printToStd p("something")}------somethingProcess finished with exit code 0
函数签名其实就是函数的参数列表和结果列表的统称,它定义了可用来鉴别不同函数的那些特征,同时也定义了我们与函数交互的方式
只要两个函数的参数列表和结果列表中的元素顺序及其类型是一致的,就可以说它们是实现了同一个函数类型的函数。
因此,在上面的代码中,函数printToStd
是函数类型Printer
的一个具体实现。
只要满足下面的任意一个条件,就可以说这个函数是一个高阶函数:
首先声明一个operate
函数类型:
type operate func(x, y int) int
注意:函数类型属于引用类型,它的值可以为
nil
然后编写calculate
函数:
func calculate(x int, y int, op operate) (int, error) { if op == nil { return 0, errors.New("invalid operation") } return op(x, y), nil}
完整代码如下:
package mainimport ( "errors" "fmt")type operate func(x, y int) intfunc sum(x, y int) int { return x + y}func calculate(x int, y int, op operate) (int, error) { if op == nil { return 0, errors.New("invalid operation") } return op(x, y), nil}func main() { sumResult, _ := calculate(4, 5, sum) fmt.Println(sumResult)}------9Process finished with exit code 0
package mainimport ( "errors" "fmt")type operate func(x, y int) int// 方案 1func calculate(x int, y int, op operate) (int, error) { if op == nil { return 0, errors.New("invalid operation") } return op(x, y), nil}// 方案 2type calculateFunc func(x int, y int) (int, error)func genCalculator(op operate) calculateFunc { return func(x int, y int) (int, error) { if op == nil { return 0, errors.New("invalid operation") } return op(x, y), nil }}func main() { // 方案 1 x, y := 12, 23 op := func(x, y int) int { return x + y } result, err := calculate(x, y, op) fmt.Printf("The result: %d (error: %v)\n", result, err) result, err = calculate(x, y, nil) fmt.Printf("The result: %d (error: %v)\n", result, err) // 方案 2 x, y = 56, 78 add := genCalculator(op) result, err = add(x, y) fmt.Printf("The result: %d (error: %v)\n", result, err)}------The result: 35 (error: <nil>)The result: 0 (error: invalid operation)The result: 134 (error: <nil>)Process finished with exit code 0
package mainimport "fmt"func main() { array1 := [3]string{"a", "b", "c"} fmt.Printf("The array: %v\n", array1) array2 := modifyArray(array1) fmt.Printf("The modified array: %v\n", array2) fmt.Printf("The original array: %v\n", array1)}func modifyArray(a [3]string) [3]string { a[1] = "x" return a}------The array: [a b c]The modified array: [a x c]The original array: [a b c]Process finished with exit code 0
在 Go 语言的语境中,当我们谈论“接口”的时候,一定是指接口类型,因为接口类型与其他数据类型不同,是没法被实例化的。
具体来讲,就是说我们既不能通过调用new
或make
函数创建出一个接口类型的值,也无法用字面量来表示一个接口类型的值。
对于任何数据类型,只要它的方法集合中完全包含了一个接口的全部特征(即实现了全部方法),那么它就是这个接口的实现类型。
goroutine
代表着并发编程模型中的用户级线程。
进程,描述的是程序的执行过程,是运行着的程序代表,也是资源分配的基本单位。
线程,总是在进程之内,可以被视为进程中运行着的控制流,是调度的基本单位。
Go 语言的运行时(runtime)系统会帮助我们自动的创建和销毁系统级的线程,而用户级的线程需要用户自己手动创建和销毁
Go 语言不但有独特的并发编程模型,以及用户级线程goroutine
,还有提供了一个用于调度goroutine
、对接系统级线程的调度器。这个调度器是 Go 语言 runtime 的重要组成部分,它主要负责统筹调配 Go 并发编程模型中的三个主要元素:G(goroutine)、P(processor)、M(machine)。
例如以下代码:
package mainimport ( "fmt" "time")func main() { for i := 0; i < 10; i++ { time.Sleep(time.Millisecond) go func() { fmt.Println(i) }() } time.Sleep(time.Second)}------12345678910Process finished with exit code 0
先创建一个通道,长度与我们要手动启用的goroutine
数量一致。在每个协程即将运行完毕时,都要向通道发送一个值。
需要注意的是,在通道声明sign := make(chan struct{}, num)
中,通道的类型为struct{}
,其中的类型字面量struct
有些类似于空接口类型interface{}
,它代表了既不包含任何字段也不拥有任何方法的空结构体类型。而它类型值的表示法只有一个,那就是struct{}{}
。并且,它占用的内存空间是0
字节。
确切的说,
struct{}{}
这个值在整个 Go 程序中永远都只会存在一份。虽然我们可以无数次的使用这个值字面量,但用到的却都是同一个值
package mainimport ( "fmt" //"time")func main() { num := 10 sign := make(chan struct{}, num) for i := 0; i < num; i++ { go func() { fmt.Println(i) sign <- struct{}{} }() } // 办法 1 //time.Sleep(time.Millisecond * 500) // 办法 2 for j := 0; j < num; j++ { <-sign }}------// 结果不唯一10631081010101010Process finished with exit code 0
使用
sync.WaitGroup
会比使用通道更加优雅,之后再来看
package mainimport ( "fmt" "sync/atomic" "time")func main() { var count uint32 trigger := func(i uint32, fn func()) { for { if n := atomic.LoadUint32(&count); n == i { fn() atomic.AddUint32(&count, 1) break } time.Sleep(time.Nanosecond) } } for i := uint32(0); i < 10; i++ { go func(i uint32) { fn := func() { fmt.Println(i) } trigger(i, fn) }(i) } trigger(10, func() { fmt.Println("End in main goroutine") })}------0123456789End in main goroutineProcess finished with exit code 0
略
package mainimport ( "errors" "fmt")func echo(request string) (response string, err error) { if request == "" { err = errors.New("empty request") return } response = fmt.Sprintf("echo: %s", request) return}func main() { for _, req := range []string{"", "hello!"} { fmt.Printf("request: %s\n", req) resp, err := echo(req) if err != nil { fmt.Printf("error: %s\n", err) } fmt.Printf("response: %s\n", resp) }}------request: error: empty requestresponse: request: hello!response: echo: hello!Process finished with exit code 0
package mainimport ( "fmt" "os" "os/exec" "runtime")// underlyingError 会返回已知的操作系统相关错误的潜在错误值。func underlyingError(err error) error { switch err := err.(type) { case *os.PathError: return err.Err case *os.LinkError: return err.Err case *os.SyscallError: return err.Err case *exec.Error: return err.Err } return err}func main() { // 示例1。 r, w, err := os.Pipe() if err != nil { fmt.Printf("unexpected error: %s\n", err) return } // 人为制造 *os.PathError 类型的错误。 r.Close() _, err = w.Write([]byte("hi")) uError := underlyingError(err) fmt.Printf("underlying error: %s (type: %T)\n", uError, uError) fmt.Println() // 示例2。 paths := []string{ os.Args[0], // 当前的源码文件或可执行文件。 "/it/must/not/exist", // 肯定不存在的目录。 os.DevNull, // 肯定存在的目录。 } printError := func(i int, err error) { if err == nil { fmt.Println("nil error") return } err = underlyingError(err) switch err { case os.ErrClosed: fmt.Printf("error(closed)[%d]: %s\n", i, err) case os.ErrInvalid: fmt.Printf("error(invalid)[%d]: %s\n", i, err) case os.ErrPermission: fmt.Printf("error(permission)[%d]: %s\n", i, err) } } var f *os.File var index int { index = 0 f, err = os.Open(paths[index]) if err != nil { fmt.Printf("unexpected error: %s\n", err) return } // 人为制造潜在错误为 os.ErrClosed 的错误。 f.Close() _, err = f.Read([]byte{}) printError(index, err) } { index = 1 // 人为制造 os.ErrInvalid 错误。 f, _ = os.Open(paths[index]) _, err = f.Stat() printError(index, err) } { index = 2 // 人为制造潜在错误为 os.ErrPermission 的错误。 _, err = exec.LookPath(paths[index]) printError(index, err) } if f != nil { f.Close() } fmt.Println() // 示例3。 paths2 := []string{ runtime.GOROOT(), // 当前环境下的Go语言根目录。 "/it/must/not/exist", // 肯定不存在的目录。 os.DevNull, // 肯定存在的目录。 } printError2 := func(i int, err error) { if err == nil { fmt.Println("nil error") return } err = underlyingError(err) if os.IsExist(err) { fmt.Printf("error(exist)[%d]: %s\n", i, err) } else if os.IsNotExist(err) { fmt.Printf("error(not exist)[%d]: %s\n", i, err) } else if os.IsPermission(err) { fmt.Printf("error(permission)[%d]: %s\n", i, err) } else { fmt.Printf("error(other)[%d]: %s\n", i, err) } } { index = 0 err = os.Mkdir(paths2[index], 0700) printError2(index, err) } { index = 1 f, err = os.Open(paths[index]) printError2(index, err) } { index = 2 _, err = exec.LookPath(paths[index]) printError2(index, err) } if f != nil { f.Close() }}------underlying error: The pipe is being closed. (type: syscall.Errno)error(closed)[0]: file already closederror(invalid)[1]: invalid argumentnil errorerror(exist)[0]: Cannot create a file when that file already exists.error(not exist)[1]: The system cannot find the path specified.nil errorProcess finished with exit code 0
一个panic
的示例如下(在运行时抛出):
panic: runtime error: index out of rangegoroutine 1 [running]:main.main() /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q0/demo47.go:5 +0x3dexit status 2
问题:从
panic
被引发到程序终止运行的大致过程是什么?
package mainimport ( "fmt")func main() { fmt.Println("Enter function main.") caller1() fmt.Println("Exit function main.")}func caller1() { fmt.Println("Enter function caller1.") caller2() fmt.Println("Exit function caller1.")}func caller2() { fmt.Println("Enter function caller2.") s1 := []int{0, 1, 2, 3, 4} e5 := s1[5] _ = e5 fmt.Println("Exit function caller2.")}------Enter function main.Enter function caller1.Enter function caller2.panic: runtime error: index out of rangegoroutine 1 [running]:main.caller2() C:/Users/abel1/go/src/github.com/abelsu7/hello/main.go:22 +0x69main.caller1() C:/Users/abel1/go/src/github.com/abelsu7/hello/main.go:15 +0x6dmain.main() C:/Users/abel1/go/src/github.com/abelsu7/hello/main.go:9 +0x6dProcess finished with exit code 2
panic
,这时,初始的panic
详情会被建立起来,并且该程序的控制权会立即从此行代码转移至调用栈的上一级函数,而此行代码所属函数的执行随即终止。panic
详情会被逐渐积累和完善,并会在程序终止之前打印出来。]]>
Go 语言内置了测试包 testing
,可以很方便的为函数编写测试方法
import "testing"
假设源代码文件为_1_sorts/Sort.go
:
package _1_sorts/*冒泡排序、插入排序、选择排序 *///冒泡排序,a是数组,n表示数组大小func BubbleSort(a []int, n int) { if n <= 1 { return } for i := 0; i < n; i++ { // 提前退出标志 flag := false for j := 0; j < n-i-1; j++ { if a[j] > a[j+1] { a[j], a[j+1] = a[j+1], a[j] //此次冒泡有数据交换 flag = true } } // 如果没有交换数据,提前退出 if !flag { break } }}// 插入排序,a表示数组,n表示数组大小func InsertionSort(a []int, n int) { if n <= 1 { return } for i := 1; i < n; i++ { value := a[i] j := i - 1 //查找要插入的位置并移动数据 for ; j >= 0; j-- { if a[j] > value { a[j+1] = a[j] } else { break } } a[j+1] = value }}// 选择排序,a表示数组,n表示数组大小func SelectionSort(a []int, n int) { if n <= 1 { return } for i := 0; i < n; i++ { // 查找最小值 minIndex := i for j := i + 1; j < n; j++ { if a[j] < a[minIndex] { minIndex = j } } // 交换 a[i], a[minIndex] = a[minIndex], a[i] }}// 归并排序func MergeSort(a []int, n int) { if n <= 1 { return } mergeSort(a, 0, n-1)}func mergeSort(a []int, start, end int) { if start >= end { return } mid := (start + end) / 2 mergeSort(a, start, mid) mergeSort(a, mid+1, end) merge(a, start, mid, end)}func merge(a []int, start, mid, end int) { tmpArr := make([]int, end-start+1) i := start j := mid + 1 k := 0 for ; i <= mid && j <= end; k++ { if a[i] < a[j] { tmpArr[k] = a[i] i++ } else { tmpArr[k] = a[j] j++ } } for ; i <= mid; i++ { tmpArr[k] = a[i] k++ } for ; j <= end; j++ { tmpArr[k] = a[j] j++ } copy(a[start:end+1], tmpArr)}func QuickSort(a []int, n int) { separateSort(a, 0, n-1)}func separateSort(a []int, start, end int) { if start >= end { return } i := partition(a, start, end) separateSort(a, start, i-1) separateSort(a, i+1, end)}func partition(a []int, start, end int) int { // 选取最后一位当对比数字 pivot := a[end] i := start for j := start; j < end; j++ { if a[j] < pivot { if !(i == j) { // 交换位置 a[i], a[j] = a[j], a[i] } i++ } } a[i], a[end] = a[end], a[i] return i}
_1_sorts/Sort_test.go
的文件名后缀必须为_test
,文件名前半部分无要求,一般与被测源代码文件相同TestBubbleSort
,函数名必须以Test
为前缀,函数名后半部分无要求test *testing.T
package _1_sortsimport ( "fmt" "math/rand" "testing")func createRandomArr(len int) []int { arr := make([]int, len, len) for i := 0; i < len; i++ { arr[i] = rand.Intn(100) } return arr}func TestBubbleSort(t *testing.T) { arr := []int{1, 5, 9, 6, 3, 7, 5, 10} fmt.Println("排序前:", arr) BubbleSort(arr, len(arr)) fmt.Println("排序后:", arr)}func TestInsertionSort(t *testing.T) { arr := []int{1, 5, 9, 6, 3, 7, 5, 10} fmt.Println("排序前:", arr) InsertionSort(arr, len(arr)) fmt.Println("排序后:", arr)}func TestSelectionSort(t *testing.T) { arr := []int{1, 5, 9, 6, 3, 7, 5, 10} fmt.Println("排序前:", arr) SelectionSort(arr, len(arr)) fmt.Println("排序后:", arr)}func TestMergeSort(t *testing.T) { a := []int{5, 4} MergeSort(a, len(a)) t.Log(a) a = []int{5, 4, 3, 2, 1} MergeSort(a, len(a)) t.Log(a)}func TestQuickSort(t *testing.T) { a := []int{5, 4} QuickSort(a, len(a)) t.Log(a) a = createRandomArr(100) QuickSort(a, len(a)) t.Log(a)}
=== RUN TestBubbleSort排序前: [1 5 9 6 3 7 5 10]排序后: [1 3 5 5 6 7 9 10]--- PASS: TestBubbleSort (0.00s)=== RUN TestInsertionSort排序前: [1 5 9 6 3 7 5 10]排序后: [1 3 5 5 6 7 9 10]--- PASS: TestInsertionSort (0.00s)=== RUN TestSelectionSort排序前: [1 5 9 6 3 7 5 10]排序后: [1 3 5 5 6 7 9 10]--- PASS: TestSelectionSort (0.00s)=== RUN TestMergeSort--- PASS: TestMergeSort (0.00s) Sort_test.go:41: [4 5] Sort_test.go:45: [1 2 3 4 5]=== RUN TestQuickSort--- PASS: TestQuickSort (0.00s) Sort_test.go:51: [4 5] Sort_test.go:55: [0 0 2 2 2 3 3 5 5 5 6 7 8 10 11 11 13 15 18 18 20 21 23 24 25 26 28 28 28 29 31 31 33 33 33 36 37 37 37 38 40 40 41 41 43 43 45 46 46 47 47 47 47 47 51 52 53 53 55 56 56 56 57 58 59 59 59 61 62 63 63 63 66 66 74 76 77 78 78 81 81 83 85 87 87 87 88 88 89 89 90 90 91 94 94 94 95 96 98 99]PASSProcess finished with exit code 0
]]>
Java 集合类库也将接口(interface)与实现(implementation)分离。
例如队列接口的最简形式类似下面的代码:
public interface Queue<E> {// a simplified form of the interface in the standard library void add(E element); E remove(); int size();}
这个接口并没有说明队列是如何实现的,通常有两种实现方式:一种是使用循环数组;另一种是使用链表。
这样做的好处是:当在程序中使用队列时,一旦构建了集合,就不需要知道究竟使用了哪种实现。因此,只有在构建集合对象的时候,使用具体的类才有意义。可以使用接口类型存放集合的引用:
Queue<Customer> expressLane = new CircularArrayQueue<>(100);expressLane.add(new Customer("Harry"));
这样一来,一旦改变了想法,就可以轻松的使用另外一种不同的实现。只需要对程序的一个地方做出修改,即调用构造器的地方。如果觉得LinkedListQueue
是个更好的选择,就将代码修改为:
Queue<Customer> expressLane = new LinkedListQueue<>(100);expressLane.add(new Customer("Harry"));
注:循环数组是一个有界集合,即容量有限。如果程序中要收集的对象数量没有上限,就最好使用链表来实现
在 Java 类库中,集合类的基本接口是Collection
接口,它有两个基本方法:
public interface Collection<E> { boolean add(E element); Iterator<E> iterator(); ...}
方法add
用于向集合中添加元素。如果添加元素确实改变了集合就返回true
,如果集合没有发生变化就返回false
。
方法iterator
用于返回一个实现了Iterator
接口的对象,可以使用这个迭代器依次访问集合中的元素。
接口Iterator
包含了 4 个方法:
public interface Iterator<E> { E next(); boolean hasNext(); void remove(); default void forEachRemaining(Consumet<? super E> action);}
用for each
循环可以简练的表示循环操作:
for (String element : c) { do something with element}
编译器简单的将for each
循环翻译为带有迭代器的循环。for each
循环可以与任何实现了Iterable
接口的对象一起工作,这个接口只包含一个抽象方法:
public interface Iterable<E> { Iterator<E> iterator(); ...}
Collection
接口扩展了Iterable
接口。因此,对于标准类库中的任何集合,对可以使用for each
循环。
在 Java SE 8 中,甚至不用写循环,可以调用
forEachRemaining
方法并提供一个 lambda 表达式:
iterator.forEachRemaining(element -> do something with element);
注:Java 迭代器应该被视作位于两个元素之间。当调用
next
时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用,而Iterator
接口的remove
方法将会删除上次调用next
方法时返回的元素。
例如,删除字符串集合中的第一个元素:
Iterator<String> it = c.iterator();it.next(); // skip over the first elementit.remove(); // now remove it
类似的,要想删除两个相邻的元素,必须先调用next
越过将要删除的元素:
it.remove();it.next();it.remove();
由于Collection
和Iterator
都是泛型接口,可以编写操作任何集合类型的实用方法:
public static <E> boolean contains(Collection<E> c, Object obj) { for (E element : c) if (element.equals(obj)) return true; return false;}
int size()boolean isEmpty()boolean contains(Object obj)boolean containsAll(Collection<?> c)boolean equals(Object other)boolean addAll (Collection<? extends E> from) boolean remove(Object obj)boolean removeAll(Collection<?> c)void clear()boolean retainAll(Col1ection<?> c)Object[] toArray()<T> T[] toArray(T[] arrayToFill)
Java 集合框架为不同类型的集合定义了大量接口:
集合有两个基本接口:Collection
和Map
:
boolean add(E element) // 用于集合iterator.next()V put(K key, V value) // 用于映射V get(K key)
下面展示了 Java 类库中的集合,并简要描述了每个集合类的用途。除了以Map
结尾的类之外,其他类都实现了Collection
接口,而以Map
结尾的类则实现了Map
接口:
==
而不是用equals
比较键值的映射表在 Java 中,所有链表(Linked List)都是双向链接(Doubly Linked)的,即每个节点还存放着指向前驱节点的引用。
例如下面的代码示例,先添加 3 个元素,然后再将第 2 个元素删除:
import java.util.Iterator;import java.util.LinkedList;import java.util.List;public class Main { public static void main(String[] args) { List<String> staff = new LinkedList<>(); staff.add("Amy"); staff.add("Bob"); staff.add("Carl"); System.out.println(staff); Iterator iterator = staff.iterator(); String first = iterator.next().toString(); String second = iterator.next().toString(); iterator.remove(); for (String name : staff) { System.out.println(name); } }}------[Amy, Bob, Carl]AmyCarlProcess finished with exit code 0
但是,链表与泛型集合有一个重要的区别:链表是一个有序集合(Ordered Collection),每个对象的位置十分重要。
关于 Java 中
Iterator
和ListIterator
的区别,可以参考 Java 中 ListIterator 和 Iterator 详解与辨析 | CSDN
另外,链表不支持快速的随机访问。如果要查看链表中第n
个元素,就必须从头开始,越过n-1
个元素,没有捷径可走。鉴于这个原因,在程序需要采用整数索引访问元素时,通常不会采用链表而会选择数组,因为LinkedList
对象根本不做任何缓存位置信息的操作。
package linkedList;import java.util.*;/** * This program demonstrates operations on linked lists. * * @author Cay Horstmann * @version 1.11 2012-01-26 */public class LinkedListTest { public static void main(String[] args) { List<String> a = new LinkedList<>(); a.add("Amy"); a.add("Carl"); a.add("Erica"); List<String> b = new LinkedList<>(); b.add("Bob"); b.add("Doug"); b.add("Frances"); b.add("Gloria"); // merge the words from b into a ListIterator<String> aIter = a.listIterator(); Iterator<String> bIter = b.iterator(); while (bIter.hasNext()) { if (aIter.hasNext()) aIter.next(); aIter.add(bIter.next()); } System.out.println(a); // remove every second word from b bIter = b.iterator(); while (bIter.hasNext()) { bIter.next(); // skip one element if (bIter.hasNext()) { bIter.next(); // skip next element bIter.remove(); // remove that element } } System.out.println(b); // bulk operation: remove all words in b from a a.removeAll(b); System.out.println(a); }}------[Amy, Bob, Carl, Doug, Erica, Frances, Gloria][Bob, Frances][Amy, Carl, Doug, Erica, Gloria]Process finished with exit code 0
集合类库提供了ArrayList
类,这个类也实现了List
接口。ArrayList
封装了一个动态再分配的对象数组。
Vector 类的所有方法都是同步的,可以由两个线程安全的访问一个 Vector 对象。但是,如果由一个线程访问 Vector,代码要在同步操作上耗费大量的时间。而 ArrayList 方法不是同步的,因此,建议在不需要同步的时候使用
ArrayList
,而不要使用Vector
。
散列表(Hash Table)可以快速的查找所需要的对象,而忽略元素出现的次序。散列表为每个对象计算一个整数,称为散列码(Hash Code)。散列码是由对象的实例域产生的一个整数。
在 Java 中,散列表用链表数组实现。每个链表被称为桶(bucket)。要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。
当然,有时候会遇到桶被占满的情况,这种现象被称为散列冲突(hash collision)。
在 Java SE 8 中,桶满时会从链表变为平衡二叉树。
Java 集合类库提供了一个HashSet
类,它实现了基于散列表的集,可以用add
方法添加元素。contains
方法已经被重新定义,用来快速的查看是否某个元素已经出现在集中。它只在某个桶中查找元素,而不必查看集合中的所有元素。
package set;import java.util.*;/** * This program uses a set to print all unique words in System.in. * * @author Cay Horstmann * @version 1.12 2015-06-21 */public class SetTest { public static void main(String[] args) { Set<String> words = new HashSet<>(); // HashSet implements Set long totalTime = 0; try (Scanner in = new Scanner(System.in)) { while (in.hasNext()) { String word = in.next(); long callTime = System.currentTimeMillis(); words.add(word); callTime = System.currentTimeMillis() - callTime; totalTime += callTime; } } Iterator<String> iter = words.iterator(); for (int i = 1; i <= 20 && iter.hasNext(); i++) System.out.println(iter.next()); System.out.println(". . ."); System.out.println(words.size() + " distinct words. " + totalTime + " milliseconds."); }}
树集(TreeSet)与散列集十分类似,不过比散列集有所改进。树集是一个有序集合,可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动的按照字典顺序呈现:
import java.util.SortedSet;import java.util.TreeSet;public class Main { public static void main(String[] args) { SortedSet<String> sorter = new TreeSet<>(); // TreeSet implements SortedSet sorter.add("Bob"); sorter.add("Amy"); sorter.add("Carl"); for (String s : sorter) { System.out.println(s); } }}------AmyBobCarlProcess finished with exit code 0
正如TreeSet
类名所示,排序是用树结构(红黑树)完成的。每次将一个元素添加到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问每个元素。
添加元素到树集中的速度比散列表中要慢,不过还是比数组或链表快。另外,要使用树集,必须能够比较元素,这些元素必须实现
Comparable
接口,或者构造集的时候必须提供一个Comparator
队列(Queue)可以让我们有效的在尾部添加一个元素,在头部删除一个元素。有两个端头的队列,即双端队列(Deque),可以有效的在头部和尾部同时添加或删除元素,但不支持在队列中间添加元素。
在 Java SE 6 中引入了Deque
接口,并由ArrayDeque
和LinkedList
类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。
Queue<String> student = new LinkedList<>();Deque<String> teacher = new ArrayDeque<>();Deque<String> employee = new LinkedList<>();
优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove
方法,总会获得当前优先级队列中最小的元素。
优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一个可以自我调整的二叉树,对树执行添加add
和删除remove
操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。
使用优先级队列的典型示例是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新任务的时候,就将优先级最高的任务从队列中删除
import java.time.LocalDate;import java.util.PriorityQueue;public class Main { public static void main(String[] args) { PriorityQueue<LocalDate> pq = new PriorityQueue<>(); pq.add(LocalDate.of(1906, 12, 9)); // G. Hopper pq.add(LocalDate.of(1815, 12, 10)); // A. Lovelace pq.add(LocalDate.of(1903, 12, 3)); // J. von Neumann pq.add(LocalDate.of(1910, 6, 22)); // K. Zuse System.out.println("Iterating over elements..."); for (LocalDate date : pq) { System.out.println(date); } System.out.println("Removing elements..."); while (!pq.isEmpty()) { System.out.println(pq.remove()); } }}
映射(map)用来存放键/值对,如果提供了键,就能查找到值。
Java 类库为映射提供了两个通用的实现:HashMap
和TreeMap
,这两个类都实现了Map
接口。
HashMap
对键进行散列,TreeMap
用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于键,与键关联的值不能进行散列或比较。
与集一样,
HashMap
比TreeMap
稍微快一些。如果不需要按照排列顺序访问键,就最好选择散列
Map<String, Employee> staff = new HashMap<>(); // HashMap implements MapEmployee harry = new Employee("Harry Hacker", 8000, 1990, 11, 07);staff.put("987-98-9996", harry);// 使用键来检索对象,如果不存在则返回 nullString id = "987-98-9996";Employee e = staff.get(id);
还可以设定一个默认值,用作映射中不存在的键:
Map<String, Integer> scores = ...;int score = scores.get(id, 0); // Gets 0 if the id is not present
put
方法,则第二个值就会覆盖第一个值remove
方法用于从映射中删除给定键对应的元素size
方法用于返回映射中的元素个数forEach
方法,迭代处理映射的键和值scores.forEach((k, v) -> System.out.println("key=" + k + "value=" + v));
如下的示例代码显示了映射的操作过程:
package map;import java.util.*;/** * This program demonstrates the use of a map with key type String and value type Employee. * @version 1.12 2015-06-21 * @author Cay Horstmann */public class MapTest{ public static void main(String[] args) { Map<String, Employee> staff = new HashMap<>(); staff.put("144-25-5464", new Employee("Amy Lee")); staff.put("567-24-2546", new Employee("Harry Hacker")); staff.put("157-62-7935", new Employee("Gary Cooper")); staff.put("456-62-5527", new Employee("Francesca Cruz")); // print all entries System.out.println(staff); // remove an entry staff.remove("567-24-2546"); // replace an entry staff.put("456-62-5527", new Employee("Francesca Miller")); // look up a value System.out.println(staff.get("157-62-7935")); // iterate through all entries staff.forEach((k, v) -> System.out.println("key=" + k + ", value=" + v)); }}
当更新一个之前不存在的键值时会抛出NullPointerException
异常,作为补救,可以使用getOrDefault
方法设置一个默认值:
counts.put(word, counts.getOrDefault(word, 0) + 1);
或者首先调用putIfAbsent
方法,当原先的键不存在时才会放入一个值:
counts.putIfAbsent(word, 0);counts.put(word, counts.get(word) + 1);
还可以使用merge
方法来简化操作。如果键原先不存在,则下面的调用:
counts.merge(word, 1, Integer::sum);
将把word
与1
关联,否则使用Integer::sum
函数组合将原值和1
相加求和。
集合框架不认为映射本身是一个集合。不过,可以得到映射的视图(view)——这是实现了Collection
接口或某个子接口的对象。
有三种视图:键集、值集合(不是一个集)以及键/值对集。键和键/值对可以构成一个集,因为映射中一个键只能有一个副本。下面的方法:
Set<K> keySet()Collection<V> values()Set<Map.Entry<K, V>> entrySet()
会分别返回这三个视图。需要注意的是,keySet
不是HashSet
或者TreeSet
,而是实现了Set
接口的另外某个类的对象。Set
接口扩展了Collection
接口,因此可以像使用集合一样使用ketSet
。例如可以枚举一个映射的所有键:
Set<String> keys = map.keySet();for (String key: keys) { do something with key}
如果想同时查看键和值,可以通过枚举条目来避免查找值:
for (Map.Entry<String, Employee> entry: staff.entrySet()) { String k = entry.getKey(); Employee v = entry.getValue(); do something with k, v}
现在,还可以使用forEach
方法:
counts.forEach((k, v) -> { do something with k, v})
对于类WeakHashMap
,如果有一个值,假定对该值对应的键的最后一次引用已经消亡,不再有任何途径引用这个值的对象了,这时这个键/值对就会被 GC 回收。
LinkedHashSet
与LinkedHashMap
两个类用来记住插入元素项的顺序。当条目插入到散列表中时,就会被并入到双向链表中:
EnumSet
是一个枚举类型元素集的高效实现。可以使用静态工厂方法构造这个集:
enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY };EnumSet<Weekday> always = EnumSet.allOf(Weekday.class);EnumSet<Weekday> never = EnumSet.noneOf(Weekday.class);EnumSet<Weekday> workday = EnumSet.range(Weekday.MONDAY, Weekday.FRIDAY);EnumSet<Weekday> mwf = EnumSet.of(Weekday.MONDAY, Weekday.WEDNESDAY, Weekday.FRIDAY);
EnumMap
是一个键类型为枚举类型的映射,可以直接且高效的用一个值数组实现。在使用时,需要在构造器中指定键类型:
EnumMap<Weekday, Employee> personInCharge = new EnumMap<>(Weekday.class);
在类IdentityHashMap
中,键的散列值不是用hashCode
函数计算的,而是用System.identityHashCode
方法计算的。这是Object.hashCode
方法根据对象的内存地址来计算散列码时所使用的方式。而且,在对两个对象进行比较时,IdentityHashMap
类使用==
,而不是equals
,所以不同的键对象,即使内容相同,也被视为是不同的对象。
Arrays 类的静态方法asList
将返回一个包装了普通 Java 数组的 List 包装器,这个方法可以将数组传递给一个期望得到列表或者集合参数的方法:
Card[] cardDeck = new Card[52];...List<Card> cardList = Arrays.asList(cardDeck);List<String> names = Arrays.asList("Amy", "Bob", "Carl");
可以为很多集合建立子范围(subrange)视图:
import java.util.ArrayList;import java.util.List;public class Main { public static void main(String[] args) { List<String> names = new ArrayList<>(); names.add("Abel"); names.add("Bob"); names.add("Carl"); List group2 = names.subList(0, 2); System.out.println(group2); group2.clear(); System.out.println(names); System.out.println(group2); }}------[Abel, Bob][Carl][]Process finished with exit code 0
对于有序集和映射,可以使用排序顺序而不是元素位置建立子范围:
// 有序集SortedSet<E> subSet(E from, E to)SortedSet<E> headSet(E to)SortedSet<E> tailSet(E from)// 有序映射SortedMap<K, V> subMap(K from, K to)SortedMap<K, V> headMap(K to)SortedMap<K, V> tailMap(K from)
Collections 还有几个方法,用于产生集合的不可修改视图(unmodifiable views)。这些视图对现有集合增加了一个运行时检查,如果发现试图对集合进行修改,就抛出一个异常,同时这个集合将保持未修改的状态:
Collections.unmodifiableCollection Collections.unmodifiableList Collections.unmodifiableSet Collections.unmodifiableSortedSet Collections.unmodifiableNavigableSet Collections.unmodifiableMap Collections.unmodifiableSortedMap Collections.unmodifiableNavigableMap ...List<String> staff = new LinkedList<>();...lookAt(Collections.unmodifiableList(staff));
例如,Collections 类的静态synchronizedMap
方法可以将任何一个映射表转换成具有同步访问方法的 Map:
Map<String, Employee> map = Collections.synchronizedMap(new HashMap<String, Employee>);
这样一来,就可以由多线程访问map
对象了。
List<String> safeStrings = Collections.checkedList(strings, String.class);
视图的add
方法将检测插入的对象是否属于给定的类。如果不属于给定的类,就立即抛出一个ClassCastException
。
可以将max
方法实现为能够接收任何实现了Collections
接口的对象:
public static <T extends Comparable> T max(Collection<T> c) { if (c.isEmpty()) throw new NoSuchElementException(); Iterator<T> iter = c.iterator(); T largest = iter.next(); while (iter.hasNext()) { T next = iter.next(); if (largest.compareTo(next) < 0) largest = next; } return largest;}
Collections 类中的sort
方法可以对实现了List
接口的集合进行排序:
List<String> staff = new LinkedList<>();// fill collectionCollections.sort(staff);
这个方法假定列表元素实现了*8Comparable
接口。如果想采用其他方式对列表进行排序,可以使用List
接口的sort
方法并传入一个Comparator
对象**:
staff.sort(Comparator.comparingDouble(Employee::getSalary));// 逆序排staff.sort(Comparator.reverseOrder());staff.sort(Comparator.comparingDouble(Employee::getSalary).reversed());
Collections 类的binarySearch
方法实现了二分查找算法。需要注意的是,集合必须是排好序的,否则算法将返回错误的答案。要想查找某个元素,必须提供集合(实现List
接口)以及要查找的元素。如果集合没有采用Comparable
接口的compareTo
方法进行排序,就还要提供一个比较器对象:
i = Collections.binarySearch(c, element);i = Collections.binarySearch(c, element, comparator);
只有采用随机访问,二分查找才有意义。如果必须利用迭代方式来一次次的遍历链表,二分查找就完全失去了优势,退化为线性查找
略
coll1.removeAll(coll2);coll1.retainAll(coll2); // 删除所有未在`coll2`中出现的元素
例如求a
和b
的交集:
Set<String> result = new HashSet<>(a);result.retainAll(b);
将数组转换为集合,可利用Arrays.asList
包装器:
String[] values = ...;HashSet<String> staff = new HashSet<>(Arrays.asList(values));
将集合转换为数组:
String[] values = staff.toArray(new String[0]);
HashTable 与 HashMap 类的作用一样,且拥有相同的接口,它本身也是同步的。如果对同步性和遗留代码的兼容性没有任何要求,就应该使用HashMap
。如果需要并发访问,则要使用ConcurrentHashMap
。
E stack.push(E item) // 将 item 压入栈并返回 itemE pop() // 弹出并返回栈顶的 item。如果栈为空,请勿调用E peek() // 返回
BitSet 类提供了一个便于读取、设置或清除各个位的接口。使用这个接口可以避免屏蔽和其他麻烦的位操作。例如,对于一个名为bucketsOfBits
的 BitSet:
bucketOfBits.get(i); // 如果第 i 位有设置过,则返回 truebucketOfBits.set(i); // 将第 i 位设置为 “开” 状态bucketOfBits.clear(i); // 清除第 i 位
筛法求质数:
package sieve;import java.util.*;/** * This program runs the Sieve of Erathostenes benchmark. It computes all primes up to 2,000,000. * @version 1.21 2004-08-03 * @author Cay Horstmann */public class Sieve{ public static void main(String[] s) { int n = 2000000; long start = System.currentTimeMillis(); BitSet b = new BitSet(n + 1); int count = 0; int i; for (i = 2; i <= n; i++) b.set(i); i = 2; while (i * i <= n) { if (b.get(i)) { count++; int k = 2 * i; while (k <= n) { b.clear(k); k += i; } } i++; } while (i <= n) { if (b.get(i)) count++; i++; } long end = System.currentTimeMillis(); System.out.println(count + " primes"); System.out.println((end - start) + " milliseconds"); }}------148933 primes61 millisecondsProcess finished with exit code 0
]]>在 Java 中,异常对象都是派生于Throwable
类的一个实例。
需要注意的是,所有的异常都是由Throwable
继承而来,但在下一层立即分解为两个分支:Error
和Exception
。
Error
类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。如果出现了这样的内部情况,只能通告给用户,并尽力使程序安全的终止,再无能为力了。
Exception
类层次结构又可以分解为两个分支:一个分支派生于RuntimeException
,另一个分支包含其他异常,划分规则是:由程序错误导致的异常属于RuntimeException
,而程序本身没有问题,但由于像I/O
错误这类问题导致的异常属于其他异常。
对于那些可能被他人使用的 Java 方法,应该根据异常规范(exception specification),在方法的首部声明这个方法可能抛出的异常:
class MyAnimation { ... public Image loadImage(String s) throws IOException { ... }}
如果有可能抛出多个受查异常类型,那么就必须在方法的首部列出所有的异常类,每个异常类之间用逗号隔开:
class MyAnimation { ... public Image loadImage(String s) throws FileNotFoundException, EOFException { ... }}
但是,不需要声明 Java 的内部错误,即从
Error
继承的错误。任何程序代码都具有抛出那些异常的可能,而我们对其也没有任何控制能力
throw new EOFException();// 或者EOFException e = new EOFException();throw e;
在异常类中抛出异常:
String readData(Scanner in) throws EOFException { ... while (...) { if (!in.hasNext()) { // EOF encountered if (n < len) throw new EOFException(); } ... } return s;}
class FileFormatException extends IOException { public FileFormatException() {} public FileFormatException(String gripe) { super(gripe); }}
如果某个异常发生的时候没有在任何地方进行捕获,那么程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。
try { code more code} catch (Exception e) { handler for this type}
如果在try
语句块中的任何代码抛出了一个在catch
子句中说明的异常类,那么:
try
语句块的其余代码catch
子句中的handler
代码如果在try
语句块中的代码没有抛出任何异常,那么程序将跳过catch
子句。
如果方法中的任何代码抛出了一个在catch
子句中没有声明的异常类型,那么这个方法就会立刻退出。
public void read(String filename) { try { InputStream in = new FileInputStream(filename); int b; while ((b = in.read()) != -1) { // process input } } catch (IOException e) { e.printStackTrace(); }}
还可以什么都不做,将异常传递给调用者,这样就必须声明这个方法可能会抛出一个IOException
:
public void read(String filename) throws IOException { InputStream in = new FileInputStream(filename); int b; while ((b = in.read()) != -1) { // process input }}
Java 编译器严格的执行
throws
说明符。如果调用了一个抛出受查异常的方法,就必须对它进行处理,或者继续传递
在一个try
语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理:
try { // some code} catch (FileNotFoundException e) { // handle exception} catch (UnknownHostException e) { // handle exception} catch (IOException e) { // handle exception}
还可以通过以下语句获得对象的更多信息:
e.getMessage();e.getClass().getName();
在 Java SE 7 中,同一个catch
子句中可以合并多个异常类型:
try { // some code} catch (FileNotFoundException | UnknownHostException e) { // handle exception} catch (IOException e) { // handle exception}
摘自 《基于 Go 语言构建企业级的 RESTful API 服务》| 掘金小册,更新中…
小册构建了一个账号系统apiserver
,功能如下:
CentOS 7.5.1804
要实现一个 API 服务器,首先要考虑两个方面:API 风格和媒体类型。Go 语言中常用的 API 风格是RPC
和REST
,常用的媒体类型是JSON
、XML
和Protobuf
。在 Go API 开发中常用的组合是gRPC + Protobuf
和REST + JSON
。
REST 代表表现层状态转移(REpresentational State Transfer),是一种软件架构风格,不是技术框架。
REST 有一系列规范,满足这些规范的 API 均可称为 RESTful API,核心规范如下:
URI
,所有行为都应该是在资源上的CRUD
操作RESTful API
请求都包含了所有足够完成本次操作的信息,服务端无需保持 Session无状态对于服务端的弹性扩容来说至关重要
在实际开发中,REST 由于天生和 HTTP 协议相辅相成,因此 HTTP 协议已经成为实现 RESTful API 的事实标准。在 HTTP 协议中通过POST
、DELETE
、PUT
、GET
方法来对应 REST 资源的CRUD
操作,具体对应关系如下:
HTTP 方法 | 行为 | URI | 示例说明 |
---|---|---|---|
GET | 获取资源列表 | /users | 获取用户列表 |
GET | 获取一个具体的资源 | /users/admin | 获取 admin 用户的详细信息 |
POST | 创建一个新的资源 | /users | 创建一个新用户 |
PUT | 以整体的方式更新一个资源 | /users/1 | 更新 id 为 1 的用户 |
DELETE | 删除服务器上的一个资源 | /users/1 | 删除 id 为 1 的用户 |
远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议,它允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外为这个交互操作编程。
RPC 的调用过程如下:
Client
通过本地调用,调用Client Stub
Client Stub
将参数打包(也叫序列化Marshalling
)成一个消息,然后发送这个消息Client
所在的 OS 将消息发送给Server
Server
端接收到消息后,将消息传递给Server Stub
Server Stub
将消息解包(也叫反序列化Unmarshalling
)得到参数Server Stub
调用服务端的子程序(函数),处理完后,将最终结果按照相反的步骤返回给Client
RPC 相比于 REST 的优点主要有以下三点:
RPC+Protobuf
采用 TCP 做传输协议,而REST
直接使用 HTTP 做应用层协议,导致REST
在调用性能上会比RPC+Protobuf
低REST
规范编写 API,而RPC
就不存在这个问题RPC
屏蔽网络细节、易用,和本地调用类似而 REST 相比于 RPC 也有很多优势:
REST
轻量级,简单易用,维护性和扩展性都比较好REST
只要语言支持 HTTP 协议就可以对接,更适合对外。而RPC
会有语言限制,不同语言的RPC
调用起来很麻烦JSON
格式可读性更强,开发调试都很方便REST
规范来编写 API,那么最终 API 看起来会更加清晰,更容易被其他人理解选择JSON
。
request line
,用来说明请求类型、要访问的资源以及所使用的 HTTP 版本header
小节,用来说明服务器要使用的附加信息body
,可以添加任意的其他数据HTTP 响应格式与请求格式类似,也是由四部分组成:状态行、消息报头、空行和响应数据
├── admin.sh # 进程的start|stop|status|restart控制文件├── conf # 配置文件统一存放目录│ ├── config.yaml # 配置文件│ ├── server.crt # TLS配置文件│ └── server.key├── config # 专门用来处理配置和配置文件的Go package│ └── config.go ├── db.sql # 在部署新环境时,可以登录MySQL客户端,执行source db.sql创建数据库和表├── docs # swagger文档,执行 swag init 生成的│ ├── docs.go│ └── swagger│ ├── swagger.json│ └── swagger.yaml├── handler # 类似MVC架构中的C,用来读取输入,并将处理流程转发给实际的处理函数,最后返回结果│ ├── handler.go│ ├── sd # 健康检查handler│ │ └── check.go │ └── user # 核心:用户业务逻辑handler│ ├── create.go # 新增用户│ ├── delete.go # 删除用户│ ├── get.go # 获取指定的用户信息│ ├── list.go # 查询用户列表│ ├── login.go # 用户登录│ ├── update.go # 更新用户│ └── user.go # 存放用户handler公用的函数、结构体等├── main.go # Go程序唯一入口├── Makefile # Makefile文件,一般大型软件系统都是采用make来作为编译工具├── model # 数据库相关的操作统一放在这里,包括数据库初始化和对表的增删改查│ ├── init.go # 初始化和连接数据库│ ├── model.go # 存放一些公用的go struct│ └── user.go # 用户相关的数据库CURD操作├── pkg # 引用的包│ ├── auth # 认证包│ │ └── auth.go│ ├── constvar # 常量统一存放位置│ │ └── constvar.go│ ├── errno # 错误码存放位置│ │ ├── code.go│ │ └── errno.go│ ├── token│ │ └── token.go│ └── version # 版本包│ ├── base.go│ ├── doc.go│ └── version.go├── README.md # API目录README├── router # 路由相关处理│ ├── middleware # API服务器用的是Gin Web框架,Gin中间件存放位置│ │ ├── auth.go │ │ ├── header.go│ │ ├── logging.go│ │ └── requestid.go│ └── router.go├── service # 实际业务处理函数存放位置│ └── service.go├── util # 工具类函数存放目录│ ├── util.go │ └── util_test.go└── vendor # vendor目录用来管理依赖包 ├── github.com ├── golang.org ├── gopkg.in └── vendor.json
]]>
- 《基于 Go 语言构建企业级的 RESTful API 服务》| 掘金小册
- 教程:使用 go 的 gin 和 gorm 框架来构建 RESTful API 微服务 | LearnKu
- Build RESTful API service in golang using gin-gonic framework | Medium
- 对比 RESTful 与 SOAP,深入理解 RESTful | 紫川秀的博客
- RESTful API 设计规范 | 紫川秀的博客
- 如何使用 swagger 设计出漂亮的 RESTful API | 紫川秀的博客
- Go 学习笔记 (六) - 使用 swaggo 自动生成 Restful API 文档 | Razeen’s Blog
- Gin - 高性能 Golang Web 框架的介绍和使用 | 代码成诗
待更新…
package mainimport "fmt"func main() { a := 0 b := 0 for { n, _ := fmt.Scan(&a, &b) if n == 0 { fmt.Println(n) break } else { fmt.Printf("%d\n", a+b) } }}
package mainimport ( "bufio" "fmt" "os" "strconv" "strings")func solution(line string) string { lineArr := strings.Split(line, " ") l, _ := strconv.Atoi(lineArr[0]) r, _ := strconv.Atoi(lineArr[1]) tmp1 := l + r tmp2 := r - l + 1 if (tmp1 % 2) == 0 { tmp1 /= 2 } else { tmp2 /= 2 } ans := ((tmp1 % 15) * (tmp2 % 15)) % 15 return strconv.Itoa(ans)}func main() { r := bufio.NewReaderSize(os.Stdin, 20480) for line, _, err := r.ReadLine(); err == nil; line, _, err = r.ReadLine() { fmt.Println(solution(string(line))) }}
]]>
排序算法有很多种,最常用的包括:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。按照时间复杂度可分为三类:O(n^2)
、O(nlogn)
、O(n)
,如下图所示:
算法的内存消耗可以通过空间复杂度来衡量。不过,针对排序算法的空间复杂度,我们还引入了一个新的概念,原地排序(Sorted in Place)。原地排序算法,就是特指空间复杂度是O(1)
的算法。
本文提到的冒泡、插入、选择排序,都是原地排序算法
仅仅用执行效率和内存消耗来衡量排序算法的好坏是不够的。针对排序算法,还有一个重要的度量指标,稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变,那么这个算法就是稳定的。
1. 有序度
有序度是数组中具有有序关系的元素对的个数:
有序元素对:a[i] <= a[j], 如果 i < j
同理,对于一个倒序排列的数组,例如6, 5, 4, 3, 2, 1
,有序度是0
;对于一个完全有序的数组,例如1, 2, 3, 4, 5, 6
,有序度是n*(n-1)/2
,也就是15
,又称满有序度。
2. 逆序度
逆序度的定义正好跟有序度相反:
逆序元素对:a[i] > a[j], 如果 i < j
3. 有序度和逆序度之间的关系
关于有序度、逆序度、满有序度这三个概念,还可以得到一个计算公式:
逆序度 = 满有序度 - 有序度
本质上,排序的过程就是一种增加有序度、减少逆序度、最后达到满有序度的过程。
冒泡排序(Bubble Sort)只会比较相邻的两个元素,看是否满足大小关系要求,如果不满足就让他俩交换。一次冒泡会让至少一个元素移动到它应该在的位置,重复n
次,就完成了n
个数据的排序工作。
可以看到,经过一次冒泡操作之后,6
这个元素已经存储在正确的位置上。要想完成所有数据的排序,只需要进行n
次这样的冒泡操作就可以:
实际上,上述冒泡的过程还可以进行优化。当某次冒泡操作已经没有数据交换的时候,就说明已经达到了完全有序,不用再继续执行后续的冒泡操作了。如下图的例子,6
个元素只需要进行4
次冒泡:
O(1)
,是一个原地排序算法O(n)
。而最坏情况是要排序的数据刚好都是倒序的,需要进行n
次冒泡操作,所以最坏情况时间复杂度为O(n^2)
再来分析一下平均情况:要排序的数组初始状态下的有序度为3
,元素个数n=6
。所以排序完成之后终态的满有序度为n*(n-1)/2=15
:
冒泡排序包含了两个操作原子:比较和交换。每交换一次,有序度就加 1。所以不管算法怎么改进,交换次数总是确定的,即为逆序度。上图的例子中就是12
,所以要进行 12 次交换操作。
0
,要进行n*(n-1)/2
次交换n*(n-1)/2
,不需要进行交换n*(n-1)/4
,来表示初始有序度既不是很高也不是很低的平均情况所以平均情况下,需要n*(n-1)/4
次交换操作。比较操作肯定要比交换操作多,而复杂度的上限是O(n^2)
,所以经过不严格的推导可得出,冒泡排序平均情况下的时间复杂度为O(n^2)
。
// 冒泡排序,a 表示数组,n 表示数组大小public void bubbleSort(int[] a, int n) { if (n <= 1) return; for (int i = 0; i < n; ++i) { // 提前退出冒泡循环的标志位 boolean flag = false; for (int j = 0; j < n - i - 1; ++j) { if (a[j] > a[j+1]) { // 交换 int tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; flag = true; // 表示有数据交换 } } if (!flag) break; // 没有数据交换,提前退出 }}
注:Go 语言提供了交换操作的语法糖:
a[j], a[j+1] = a[j+1], a[j]
// 冒泡排序,a 表示数组,n 表示数组大小func BubbleSort(a []int, n int) { if n <= 1 { return } for i := 0; i < n; i++ { // 提前退出标志 flag := false for j := 0; j < n-i-1; j++ { if a[j] > a[j+1] { a[j], a[j+1] = a[j+1], a[j] //此次冒泡有数据交换 flag = true } } // 如果没有交换数据,提前退出 if !flag { break } }}
对于一个有序的数组,为了继续保持数据有序,我们只需要遍历数组,找到数据应该插入的位置将其插入即可:
插入排序(Insertion Sort)正是借助上面的思想来实现排序。首先,我们将数组中的数据分为两个区间:已排序区间和未排序区间。初始已排序区间只有数组的第一个元素。插入算法的核心思想就是取未排序区间中的元素,在已排序区间中找到合适的插入位置并将其插入,重复这个过程直到未排序区间中元素为空,算法结束:
插入排序也包含两种操作原子:元素的比较和移动。
O(1)
,也是一个原地排序算法O(n)
。最坏情况下,数据全部逆序,时间复杂度为O(n^2)
。总的来看,平均时间复杂度为O(n^2)
// 插入排序,a 表示数组,n 表示数组大小public void insertionSort(int[] a, int n) { if (n <= 1) return; for (int i = 1; i < n; ++i) { int value = a[i]; int j = i - 1; // 查找插入的位置 for (; j >= 0; --j) { if (a[j] > value) { a[j+1] = a[j]; // 数据移动 } else { break; } } a[j+1] = value; // 插入数据 }}
// 插入排序,a 表示数组,n 表示数组大小func InsertionSort(a []int, n int) { if n <= 1 { return } for i := 1; i < n; i++ { value := a[i] j := i - 1 // 查找要插入的位置并移动数据 for ; j >= 0; j-- { if a[j] > value { a[j+1] = a[j] } else { break } } a[j+1] = value }}
选择排序(Selection Sort)的实现思路类似于插入排序:也是分为已排序区间和未排序区间,但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾:
O(1)
,也是一种原地排序算法O(n^2)
例如
5, 8, 5, 2, 9
这组数据,如果使用选择排序算法来排序的话,第一次找到最小元素为2
,与第一个5
交换位置,导致第一个5
和中间的5
前后顺序发生改变,所以就不稳定了
// 选择排序,a 表示数组,n 表示数组大小public static void selectionSort(int[] a, int n) { if (n <= 1) return; for (int i = 0; i < n - 1; ++i) { // 查找最小值 int minIndex = i; for (int j = i + 1; j < n; ++j) { if (a[j] < a[minIndex]) { minIndex = j; } } // 交换 if (minIndex != i) { int tmp = a[i]; a[i] = a[minIndex]; a[minIndex] = tmp; } }}
// 选择排序,a 表示数组,n 表示数组大小func SelectionSort(a []int, n int) { if n <= 1 { return } for i := 0; i < n; i++ { // 查找最小值 minIndex := i for j := i + 1; j < n; j++ { if a[j] < a[minIndex] { minIndex = j } } // 交换 if minIndex != i { a[i], a[minIndex] = a[minIndex], a[i] } }}
3
个赋值操作,而移动只需要1
个:// 冒泡排序中数据的交换操作:if (a[j] > a[j+1]) { // 交换 int tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; flag = true;}// 插入排序中数据的移动操作:if (a[j] > value) { a[j+1] = a[j]; // 数据移动} else { break;}
因此,虽然冒泡排序和插入排序的时间复杂度都为O(n^2)
,但是如果我们希望把性能优化做到极致,就首选插入排序。
插入排序的算法思路也有很大的优化空间,可以参考 希尔排序 | Wikipedia
]]>
SYN
发起连接、ACK
回复、RST
重新连接、FIN
结束连接TCP 的连接建立,我们称之为“三次握手”,即“请求 -> 应答 -> 应答之应答”。
三次握手除了确保双方建立连接以外,主要还为了沟通 TCP 包的序号问题。
A 要告诉 B,自己发起的包的序号起始是从哪个号开始的,B 同样也要将自己的起始序号告诉 A。为了确保互相包的序号不发生冲突,TCP 的每个连接都要有不同的序号,这个序号的起始序号是随着时间变化的
当双方终于建立了信任,建立了连接之后,为了维护这个连接,双方都要维护一个状态机。在连接建立的过程中,双方的状态变化时序图如下所示:
CLOSED
状态LISTEN
状态SYN
,之后处于SYN-SENT
状态SYN
,并且ACK
客户端的SYN
,之后处于SYN-RCVD
状态SYN
和ACK
之后,发送ACK
的ACK
,之后处于ESTABLISHED
状态,因为客户端一发一收已经成功了ACK
的ACK
之后,处于ESTABLISHED
状态,因为它也一发一收成功了类似的,TCP 在断开连接时,也需要进行四次挥手,如下图所示:
FIN_WAIT_1
状态ACK
,就进入CLOSE_WAIT
状态ACK
后,就进入FIN_WAIT_2
状态。这时如果 B 直接跑路,则 A 将永远停留在这个状态。可以在 Linux 中调整tcp_fin_timeout
这个参数,设置一个超时时间ACK
后,从FIN_WAIT_2
状态结束,进入TIME_WAIT
状态,等待时间为2MSL
(Maximum Segment Lifetime,报文最大生存时间)2MSL
的时间,仍然没有收到它发的FIN
的ACK
,按照 TCP 的原理,B 就会重发这个FIN
,只不过当 A 再收到这个包之后,A 就直接发送RST
回复给 B,这时候 B 就会知道 A 早就跑了TCP 协议为了保证包的顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但这个应答并不是一个一个来的,而是会应答某个之前的 ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)。
为了记录所有发送的包和接收的包,TCP 也需要发送端和接收端分别都有缓存来保存这些记录。发送端里的缓存是按照包的 ID 一个个排列的,根据处理的情况分成四个部分:
在 TCP 里,接收端会给发送端报一个窗口的大小,叫Advertised Window
。它的大小应该等于上面的第二部分加上第三部分,即已经交代了没做完的加上马上要交代的。超过这个窗口的,接收端做不过来,就不能发送了。
为此,发送端需要保持下面的数据结构:
LastByteAcked
:「已发送已确认」的最后一个字节LastByteSent
:「已发送未确认」的最后一个字节AdvertisedWindow
:黑框部分,「已发送未确认」+「未发送可发送」对于接收端来讲,其缓存里的记录和内容要更简单一些:
对应缓存的数据结构如下图所示:
LastByteRead
:之后是已经接收了,但是还没被应用层读取的NextByteExpected
:第一部分和第二部分的分界线MaxRcvBuffer
:最大缓存的量,图中黑框部分第二部分窗口大小
AdvertisedWindow
即为MaxRcvBuffer
减去第一部分「接收已确认」的大小
在 TCP 传输的过程中,顺序问题与丢包问题都可能发生:有些包可能丢了,有些包可能还在路上。还有些可能已经到了,还是因为出现了乱序,所以只能先缓存着但是没办法ACK
。
为了解决这些问题,TCP 有一套确认与重发的机制。
假设
4
的确认收到了,不幸的是,5
的ACK
丢了,6
、7
的数据包丢了,这种时候该怎么办?
一种解决方法就是超时重试,即对每一个发送了但是没有收到ACK
的包,都设置一个定时器,超过一定时间必须重新尝试。这个时间必须大于往返时间 RTT,否则会引起不必要的重传,也不宜过长,导致访问变慢。
估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值。由于重传时间是不断变化的,我们称之为自适应重传算法(Adaptive Retransmission Algorithm)。
当一个包再次超时,又需要重传的时候,TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
TCP 还有一个可以快速重传的机制:当接收方收到一个序号大于下一个所期望的报文段时,就检测到了数据流中的一个间格,于是发送三个冗余的ACK
。客户端收到后,就在定时器过期之前,重传丢失的报文段。
还有一种方式称为 Selective Acknowledgement(SACK)。这种方式需要在 TCP 头里加上SACK
,可以将缓存的地图发送给发送方。例如可以发送ACK6
、SACK8
、SACK9
,有了地图,发送方就可以看出是序号为7
的包丢失了。
TCP 还有流量控制机制,在接收方对于包的确认中,同时会携带一个窗口的大小。
先假设窗口不变的情况,窗口始终为9
。4
的确认来的时候,会右移一格,这时候第13
个包也可以发送了:
这个时候,假设发送端发送过猛,会将第三部分的10
、11
、12
、13
全部发送完毕。由于已发送未确认的包已经占满整个窗口,因此之后就停止发送了,未发送可发送的部分变为0
:
当对于包5
的确认到达时,在客户端相当于窗口再滑动了一格,这个时候,第14
个包就可以发送了:
如果接收方实在处理的太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为
0
,则发送方将暂时停止发送
再假设一个极端情况:接收端的应用一直不读取缓存中的数据,当数据包6
确认后,窗口大小就要缩小一个变为8
:
当新的窗口大小8
通过6
的确认消息到达发送端之后,发送端的窗口就不会再右移,而是仅仅左边的边右移,窗口大小也从9
变成了8
:
如果接收端还是一直不处理数据,则随着确认的包越来越多,窗口也会越来越小,直到为0
:
当这个窗口通过包14
的确认到达发送端的时候,发送端的窗口也调整为0
,停止发送:
除了被动接收窗口信息之外,发送方也会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止底能窗口综合征:可以当窗口太小的时候就停止更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口
最后来谈一下 TCP 的拥塞控制问题,也是通过窗口的大小来控制的。前面流量控制的滑动窗口rwnd
(Receive Window,接收窗口)是怕发送方把接收方缓存塞满,而拥塞窗口cwnd
(Congestion Window,拥塞窗口)是怕把网络塞满。
TCP 的发送速度由拥塞窗口和滑动窗口共同控制:
swnd = LastByteSent - LastByteAcked <= min {cwnd, rwnd}
对于 TCP 协议来讲,整个网络路径就是一个黑盒。TCP 发送包常被比喻为往一个水管里面灌水,而 TCP 的拥塞控制就是在不堵塞、不丢包的情况下,尽量发挥带宽
如果设置发送窗口swnd
,使得发送但未确认的包为通道的容量,就能撑满整个管道:
TCP 的拥塞控制主要用来避免两种现象:包丢失和超时重传。一旦出现了这些现象就说明,发送速度太快了,要慢一点。
cwnd
设置为 1 个报文段,一次只能发送一个cwnd
加 1,于是一次能够发送两个cwnd
加 2,于是一次能够发送四个cwnd
加 4,于是一次能够发送八个这个过程称为 TCP 的慢启动阶段
可以看出在慢启动阶段,cwnd
呈指数性的增长。但当swnd
超过阈值ssthresh
(默认为 65535 字节,即当swnd
为 16 时刚好超过),就要改为线性增长,即每收到一个确认后,cwnd
增加1/cwnd
:
这个过程称为 TCP 的拥塞避免阶段
还是看上图,当cwnd
为 20 时,出现了拥塞(例如丢包,需要超时重传),这个时候,需要将ssthresh
设为cwnd/2
即 10,将cwnd
设为 1,重新开始慢启动。下图是另外一个例子:
虽然这种方式避免了拥塞,但是大大降低了原本处于高速状态的传输速度,还可能造成网络卡顿。
之前提到,TCP 还有一个快速重传算法:当接收端发现丢了一个中间包的时候,发送三次前一个包的ACK
,于是发送端就会快速的重传,不必等待超时再重传,这时cwnd
减半变为cwnd/2
,然后再令ssthresh=cwnd
,当三个包返回时,cwnd=sshthresh+3
,继续维持线性增长(即图中的橙线部分)。
正是 TCP 这种知进退的特点,使得在时延很重要的情况下,反而降低了速度。但其实,TCP 的拥塞控制主要来避免的两个现象都是有问题的:
为了优化这两个问题,后来有了 TCP BBR 拥塞算法(Bottleneck Bandwidth and Round-trip propagation time,由 Google 设计,于 2016 年发布)。它企图找到一个平衡点,就是通过不断的加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样会导致时延增加,在这个平衡点可以很好的达到高带宽和低时延的平衡
cwnd
来解决的]]>
简单来说,TCP 是面向连接的,而 UDP 是面向无连接的。
在互通之前,面向连接的协议会先建立连接。例如,TCP 会三次握手,而 UDP 不会。
所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性
综上,TCP 其实是一个有状态的服务,精确记录着发送和接收的状态;而 UDP 则是无状态服务,不会在意发送和接收是否出现差错。
可以这样比喻:如果 MAC 层定义了本地局域网的传输行为,IP 层定义了整个网络端到端的传输行为,这两层基本定义了这样的基因:网络传输是以包为单位的,二层叫帧,网络层叫包,传输层叫段,暂时笼统的称之为包。包单独传输,自行选路,在不同的设备封装解封装,不保证到达。基于这个基因,生下来的孩子 UDP 完全继承了这些特性,几乎没有自己的思想。
无论应用程序写的使用 TCP 传数据,还是 UDP 传数据,都要监听一个端口。正是这个端口,用来区分应用程序。
其他一些使用 UDP 的场景:
]]>
首先明确三点基础知识:
一个局域网里有多个交换机时,ARP 广播的模式容易产生广播风暴:ARP 广播时,交换机会将一个端口收到的包转发到其他所有的端口上。比如数据包经过交换机 A 到达交换机 B,交换机 B 又将包复制为多分广播出去。如果整个局域网存在一个环路,使得数据包又重新回到了最开始的交换机 A,这个包又会被 A 再次复制多份广播出去。如此循环,数据包会不停的转发,而且越来越多,最终占满带宽,或者使解析协议的硬件过载,形成广播风暴
当两个交换机将两个局域网同时连接起来的时候,就会出现环路问题:
此时就会出现之前提到的广播风暴。
在数据结构中,有一个方法叫做最小生成树。在计算机网络中,生成树的算法叫做 STP(Spanning Tree Protocol)。
STP 协议中的基本概念如下:
Root Bridge ID, Root Path Cost, Bridge ID, and Port ID
略
1. 物理隔离
每个部门有单独的交换机,配置单独的子网,这样部门之间的沟通就需要路由器了。
然而问题在于,有的部门人多,有的部门人少。如果每个部门有单独的交换机,口多了浪费,口少了不够用。
2. 虚拟隔离 VLAN
另一种方式是虚拟隔离,就是我们常说的 VLAN,又称虚拟局域网。使用 VLAN,一个交换机上会连属于多个局域网的机器。
为了让交换机区分哪个机器属于哪个局域网,我们只需要在原来的二层的头上加一个 TAG,里面有一个 VLAN ID,一共 12 位,这样就可以划分 4096 个 VLAN。
如果交换机是支持 VLAN 的,当这个交换机把二层的头取下来的时候,就能够识别这个 VLAN ID。这样只有相同 VLAN 的包,才会互相转发,不同 VLAN 的包是看不到的,这样就解决了广播问题和安全问题。
可以设置交换机每个口所属的 VLAN。而且对于交换机来说,每个 VLAN 的口都是可以重新设置的。例如,一个财务走了,把他所在的座位的口从 VLAN 30 移除掉。来了一个程序员,坐在财务的位置,就把这个口设置为 VLAN 10,十分灵活。
对于支持 VLAN 的交换机,有一种 Trunk 口,可以转发属于任何 VLAN 的口。交换机之间可以通过 Trunk 口相互连接。
ICMP 一般被认为属于网络层,和 IP 协议同一层,是管理和控制 IP 的一种协议
ping 是基于 ICMP 协议工作的。ICMP 全称 Internet Control Message Protocol,即互联网控制报文协议。
ICMP 报文是封装在 IP 包里面的。因为传输指令的时候,肯定需要源地址和目标地址。它本身非常简单,因为作为侦察兵,要轻装上阵,不能携带大量的包袱。
ICMP 报文有很多的类型,不同的类型有不同的代码。最常用的类型是主动请求为 8,主动请求的应答为 0。
1. 查询报文类型
ICMP 的查询报文类型,好比主帅传令侦察兵,会主动查看敌情。例如,常用的ping
就是查询报文,是一种主动请求、并且获得主动应答的 ICMP 协议。
对ping
的主动请求,进行网络抓包,称为 ICMP ECHO REQUEST。同理主动请求的回复,称为 ICMP ECHO REPLY。比起原生的 ICMP,这里面多了两个字段:一个是标识符,用来区分不同的报文;另一个是序号,用来记录报文的顺序。
在选项数据中,
ping
还会存放发送请求的时间值,用来计算往返时间,说明路程的长短
2. 差错报文类型
由异常情况发起的、来报告差错的报文,对应 ICMP 的差错报文类型。
常见的 ICMP 差错报文的例子如下:
下图展示了ping
命令的发送和接收过程:
ping
命令执行的时候,源主机首先会构建一个 ICMP 请求数据包,这个包内包含多个字段,最重要的有两个:第一个是类型字段,对于请求数据包而言该字段为 8;另一个是顺序号,主要用于区分连续ping
的时候发出的多个数据包。每发出一个请求数据包,顺序号会自动加 1。为了能够计算往返时间 RTT,它会在报文的数据部分插入发送时间。
然后,由 ICMP 协议将这个数据包连同地址192.168.1.2
一起交给 IP 层。IP 层将以192.168.1.2
作为目的地址,本机 IP 地址作为源地址,加上一些其他控制信息,构建一个 IP 数据包。
接下来,需要加入 MAC 头。可在 ARP 映射表中查找 IP 地址对应的 MAC 地址,如果没有,则需要发送 ARP 协议查询 MAC 地址。获得 MAC 地址后,由数据链路层构建一个数据帧,目的地址是 IP 层传过来的 MAC 地址,源地址则是本机的 MAC 地址,附加上一些控制信息,最后将它们传送出去。
主机 B 收到这个数据帧后,先检查它的目的 MAC 地址,符合本机则接收,否则就丢弃。接收后检查该数据帧,将 IP 数据包从帧中提取出来,交给本机的 IP 层。同样,IP 层检查后,将有用的信息提取后交给 ICMP 协议。
主机 B 会构建一个 ICMP 应答包,应答数据包的类型字段为 0,顺序号为接收到的请求数据包中的顺序号,然后再发送出去给主机 A。
在规定的时间内,源主机如果没有接到 ICMP 的应答包,则说明目标主机不可达;如果接收到了,则说明可达。此时,源主机会用当前时刻减去该数据包最初从源主机上发出的时刻,就是 ICMP 数据包的时间延迟。
ping
这个程序使用了 ICMP 里面 ECHO REQUEST 和 ECHO REPLY 类型
有一个程序traceroute
,是个“大骗子”,它会使用 ICMP 的规则,故意制造一些能够产生错误的场景。
在进行网卡配置的时候,除了 IP 地址,还需要配置网关(Gateway)。
一旦配置了 IP 地址和网关,往往就能够指定目标地址进行访问了。但在跨网关访问的时候,还牵扯到 MAC 地址和 IP 地址的变化。
在任何一台机器上,当要访问另一个 IP 地址的时候,都会先判断:这个目标 IP 地址和当前机器的 IP 地址是否在同一个网段,需要借助 CIDR(无类型域间选路)和子网掩码来实现。
192.168.1.0/24
这个网段,Gateway 往往会是192.168.1.1/24
或者192.168.1.2/24
网关往往是一个路由器,是一个三层转发设备(即会把 MAC 头和 IP 头都取下来,然后根据里面的内容,看看接下来把包往哪里转发的设备)
静态路由,其实就是在路由器上,配置一条一条规则。
MAC 地址是一个局域网内才有效的地址。因而,MAC 地址只要过了网关,就必定会改变,因为已经换了另一个局域网
对于 IP 头和 MAC 头哪些变、哪些不变的问题,可以分为两种类型:不改变 IP 地址的网关,称为转发网关;改变 IP 地址的网关,称为 NAT 网关。
1. 转发网关
服务器 A 要访问服务器 B,首先,因为不是一个网段的,会向网关192.168.1.1
发送包:
192.168.1.101
这个网口的 MAC192.168.1.101
192.168.4.101
左边的网关收到包后,修改源 MAC 和目标 MAC,向右边的网关转发包:
192.168.56.1
的 MAC 地址192.168.56.2
的 MAC 地址192.168.1.101
192.168.4.101
路由器 B 收到包后,发送 ARP 获取192.168.4.101
的 MAC 地址,然后发送包:
192.168.4.1
的 MAC 地址192.168.4.101
的 MAC 地址192.168.1.101
192.168.4.101
包到达服务器 B,MAC 地址匹配,将包收进来。
从上面的过程可以看出,每到一个局域网,MAC 都是要变的,但是 IP 地址都不变。在 IP 头里面,不会保存任何网关的 IP 地址。所谓的下一跳是,某个 IP 要将这个 IP 地址转换为 MAC 放入 MAC 头。
2. NAT 网关
首先,目标服务器 B 在国际上要有一个国际的身份,例如192.168.56.2
。在网关 上,我们记下来,国际身份192.168.56.2
对应国内身份192.168.1.101
。凡是要访问192.168.56.2
的,都转成192.168.1.101
。
于是,源服务器 A 要访问目标服务器 B,先向路由器 A 发送包,内容为:
192.168.1.1
这个网口的 MAC 地址192.168.1.101
192.168.56.2
之后路由器 A 发送包到路由器 B:
192.168.56.1
的 MAC 地址192.168.56.2
的 MAC 地址192.168.56.1
192.168.56.2
最后,路由器 B 发送包到服务器 B 的内容:
192.168.1.1
的 MAC 地址192.168.1.101
的 MAC 地址192.168.56.1
192.168.1.101
包到达服务器 B,MAC 地址匹配,将包收进来。
从这个过程可以看出,IP 地址也会改变,用英文说就是 Network Address Translation,简称 NAT。
通过之前的内容可以知道,路由器就是一台网络设备,它有多张网卡。当一个入口的网络包送到路由器时,他会根据一个本地的转发信息库,来决定如何正确的转发流量,这个转发信息库通常被称为路由表。
一张路由表中会有多条路由规则,每一条规则至少包含这三项信息:
通过
route
或ip route
命令都可以对路由表进行查询或配置
例如,我们要设置ip route add 10.176.48.0/20 via 10.173.32.1 dev eth0
,就说明要去10.176.48.0/20
这个目标网络,要从eth0
端口出去,经过10.173.32.1
。这种配置方式的核心思想是:根据目的 IP 地址来配置路由。
在真实的复杂网络环境中,除了根据目的 IP 地址来配置路由外,还可以根据多个参数来配置路由,这就称为策略路由。
例如,我们设置:
> ip rule add from 192.168.1.0/24 table 10 > ip rule add from 192.168.2.0/24 table 20
表示从192.168.1.0/24
这个网段来的,使用table 10
中的路由表,而从192.168.2.0/24
网段来的,则使用table 20
的路由表。
在一条路由规则中,也可以走多条路径:
> ip route add default scope global nexthop via 100.100.100.1 weight 1 nexthop via 200.200.200.1 weight 2
这条规则表示下一跳有两个地方,分别是100.100.100.1
和200.200.200.1
,权重分别为1
和2
。
使用动态路由协议的路由器,可以根据路由协议算法生成动态路由表,随网络运行状况的变化而变化。
可以将复杂的网络拓扑路径,抽象为图的结构。因而这就转化成如何在途中找到最短路径的问题。
第一大类的算法称为距离矢量路由(Distance Vector Routing),它是基于 Bellman-Ford 算法的。
它的基本思想是:每个路由器都保存一个路由表,包含多行,每行对应网络中的一个路由器。每一行包含两部分信息:一个是要到目标路由器,从哪条线出去;另一个是到目标路由器的距离。
可以看出,每个路由器都知道全局信息,都知道自己和邻居之间的距离。为了更新路由表,每过几秒,每个路由器都将自己所知的到达所有路由器的距离告知邻居。
这样一来,每个路由器根据新收集的信息,计算和其他路由器的距离。例如一个邻居距离目标路由器的距离是M
,而自己距离邻居是X
,则自己距离目标路由器的距离是X+M
该算法虽然简单,但也存在问题:
第二大类算法是链路状态路由(Link State Routing),基于 Dijkstra 算法。
它的基本思想是:当一个路由器启动时,首先发现邻居,然后将自己和邻居之间的链路状态包广播出去,发送到整个网络的每个路由器。因而,每个路由器都能在本地构建一个完整的图,然后针对这个图使用 Dijkstra 算法,找到两点之间的最短路径。
链路状态路由算法只广播更新的或改变的网络拓扑,这使得更新信息更小,节省了带宽和 CPU 利用率。而且一旦一个路由器挂了,它的邻居都会广播这个消息,可以使得坏消息迅速收敛
OSPF(Open Shortest Path First,开放式最短路径优先)就是这样一个基于链路状态路由算法的动态路由协议,主要用于数据中心内部的路由决策,因而称为内部网关协议(Interior Gateway Protocol,简称 IGP)。
内部网关协议的重点就是找到最短路径。当然,有时候 OSPF 可以发现多个最短的路径,可以在这多个路径中进行负载均衡,这常常被称为等价路由。
一般应用的接入层会有负载均衡 LVS,它可以和 OSPF 一起,实现高吞吐量的接入层设计
外网的路由协议又有所不同,我们称为外网路由协议(Border Gateway Protocol,简称 BGP)。
在网络世界中,一个个局域网成为自治系统 AS(Autonomous System),并根据对外的连接情况分为多种不同的类型。
每个自治系统都有边界路由器,通过它和外面的世界建立联系。
BGP 又分为两类:eBGP 和 iBGP。自治系统的边界路由器之间使用 eBGP 广播路由。而边界路由器要想将 BGP 学习到的路由导入到内部网络,则需要运行 iBGP,使得内部的路由器能够找到到达外网目的地的最优边界路由器。
BGP 协议使用的算法是路径矢量路由协议(Path Vector Protocol),它是距离矢量路由协议的升级版
]]>
net-tools
中的ifconfig
iproute2
中的ip address
net-tools
起源于 BSD,自 2001 年起,Linux 社区已经对其停止维护。而iproute2
旨在取代net-tools
,并提供了一些新功能。一些 Linux 发行版已经停止支持net-tools
,只支持iproute2
。
net-tools
通过 procfs(/proc
) 和 ioctl
系统调用去访问和改变内核网络配置,而iproute2
则通过netlink
套接字接口与内核通讯。
net-tools
中工具的名字比较杂乱,而iproute2
则相对整齐和直观,基本是ip
命令加后面的子命令。
虽然取代意图很明显,但是这么多年过去了,net-tool
依然还在被广泛使用,最好还是两套命令都掌握。
> ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 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 forever2: enp5s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 4c:cc:6a:70:fc:d9 brd ff:ff:ff:ff:ff:ff inet 116.56.129.153/24 brd 116.56.129.255 scope global noprefixroute dynamic enp5s0 valid_lft 1313sec preferred_lft 1313sec inet6 2001:250:3000:2b80:7171:519:ba83:4ff9/64 scope global noprefixroute dynamic valid_lft 2591975sec preferred_lft 604775sec inet6 fe80::f646:5a2b:929:108c/64 scope link noprefixroute valid_lft forever preferred_lft forever
注意:
lo
全称是loopback
,又称环回接口,往往会被分配到127.0.0.1
这个地址,用于本机通信,经过内核处理后直接返回,不会在任何网络中出现
32
位128
位IP 地址总共分为以下 5 类:
无类型域间选路,简称 CIDR,打破了原来设计的几类地址的做法,将 32 位的 IP 地址一分为二,前面是网络号,后面是主机号。例如10.100.122.2/24
,表示在 32 位地址中,前 24 位是网络号,后 8 位是主机号。
伴随 CIDR 而来的还有两个概念:广播地址10.100.122.255
和子网掩码255.255.255.0
。
如果发送广播地址,所有10.100.122
网络里面的机器都可以收到。
将子网掩码和 IP 地址进行AND
计算,结果得到10.100.122.0
,即为网络号。
举个例子
例如16.158.165.91
这个 CIDR,网络号为16.158.<101001>
,而机器号为<01>.91
。
第一个地址是16.158.<101001><00>.1
,即16.158.164.1
。子网掩码是255.255.<111111><00>.0
,即255.255.252.0
。广播地址是16.158.<101001><11>.255
,即16.158.167.255
。
打个比方,IP 是地址,有定位功能;MAC 是身份证,无定位功能。
MAC(Media Access Control)地址是一个网卡的物理地址,用十六进制,6 个 byte 表示,例如fa:16:3e:c7:79:75
。
一个网络包要从一个地方传到另一个地方,除了要有确定的地址,还需要有定位功能。而 MAC 地址更像是身份证,是一个唯一的标识。
MAC 地址是有一定定位功能的,不过通信范围比较小,局限在一个子网里面。例如,从192.168.0.2/24
访问192.168.0.3/24
是可以用 MAC 地址的。一旦跨子网,即从192.168.0.2/24
到192.168.1.2/24
,只用 MAC 地址就不行了,还需要 IP 地址才能起作用。
使用 net-tools:
> sudo ifconfig eth1 10.0.0.1/24> sudo ifconfig eth1 up
使用 iproute2:
> sudo ip addr add 10.0.0.1/24 dev eth1> sudo ip link set up eth1
动态主机配置协议(Dynamic Host Configuration Protocol),简称 DHCP。
数据中心里面的服务器,IP 一旦配置好,基本不会变,相当于买房自己装修。DHCP 的方式相当于租房,自己不用装修,都是帮你配置好的,暂时用一下,用完退租就可以。
最终租约达成的时候,还需要广播通知网络内的所有机器。
客户机会在租期过去 50% 的时候,直接向为其提供 IP 地址的 DHCP Server 发送 DHCP Request 消息包。客户机接收到服务器回应的 DHCP ACK 消息包,会根据包中所提供的新的租期以及其他已经更新的 TCP/IP 参数,更新自己的配置。这样,IP 租用更新就完成了。
]]>
摘自 Linux 查看 CPU 信息,机器型号,内存等信息 | 开源中国
> uname -a # 查看内核/操作系统/CPU信息> head -n 1 /etc/issue # 查看操作系统版本> cat /proc/cpuinfo # 查看CPU信息> hostname # 查看计算机名> lspci -tv # 列出所有PCI设备> lsusb -tv # 列出所有USB设备> lsmod # 列出加载的内核模块> env # 查看环境变量> cat /etc/issue.net # 查看当前操作系统发行版信息> dmidecode | grep 'Product Name' # 查看机器型号 Product Name: HP Z240 Tower Workstation Product Name: 802F
> free -m # 查看内存使用量和交换区使用量> df -h # 查看各分区使用情况> du -sh <目录名> # 查看指定目录的大小> grep MemTotal /proc/meminfo # 查看内存总量> grep MemFree /proc/meminfo # 查看空闲内存量> uptime # 查看系统运行时间、用户数、负载> cat /proc/loadavg # 查看系统负载> tree # 显示目录树状图
> mount | column -t # 查看挂接的分区状态> fdisk -l # 查看所有分区> swapon -s # 查看所有交换分区> hdparm -i /dev/hda # 查看磁盘参数(仅适用于IDE设备)> dmesg | grep IDE # 查看启动时IDE设备检测状况
> ifconfig # 查看所有网络接口的属性> iptables -L # 查看防火墙设置> route -n # 查看路由表> netstat -lntp # 查看所有监听端口> netstat -antp # 查看所有已经建立的连接> netstat -s # 查看网络统计信息
> ps -ef # 查看所有进程> top # 实时显示进程状态
> w # 查看活动用户> id <用户名> # 查看指定用户信息> last # 查看用户登录日志> cut -d: -f1 /etc/passwd # 查看系统所有用户> cut -d: -f1 /etc/group # 查看系统所有组> crontab -l # 查看当前用户的计划任务
> chkconfig --list # 列出所有系统服务> chkconfig --list | grep on # 列出所有启动的系统服务
> rpm -qa # 查看所有安装的软件包
> cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c # 查看 CPU 信息 8 Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz> cat /proc/cpuinfo | grep physical | uniq -c 1 physical id : 0 1 address sizes : 39 bits physical, 48 bits virtual 1 physical id : 0 1 address sizes : 39 bits physical, 48 bits virtual 1 physical id : 0 1 address sizes : 39 bits physical, 48 bits virtual 1 physical id : 0 1 address sizes : 39 bits physical, 48 bits virtual 1 physical id : 0 1 address sizes : 39 bits physical, 48 bits virtual 1 physical id : 0 1 address sizes : 39 bits physical, 48 bits virtual 1 physical id : 0 1 address sizes : 39 bits physical, 48 bits virtual 1 physical id : 0 1 address sizes : 39 bits physical, 48 bits virtual> getconf LONG_BIT64 # 说明当前 CPU 运行在 64 位模式下> cat /proc/cpuinfo | grep flags | grep ' lm ' | wc -l8 # 结果大于 0,说明支持 64 位计算,lm 代表 long mode> dmidecode -sdmidecode: option requires an argument -- 's'String keyword expectedValid string keywords are: bios-vendor bios-version bios-release-date system-manufacturer system-product-name system-version system-serial-number system-uuid baseboard-manufacturer baseboard-product-name baseboard-version baseboard-serial-number baseboard-asset-tag chassis-manufacturer chassis-type chassis-version chassis-serial-number chassis-asset-tag processor-family processor-manufacturer processor-version processor-frequency> dmidecode -s 'processor-version'Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz
> cat /proc/meminfoMemTotal: 3792120 kBMemFree: 313820 kBMemAvailable: 2639360 kBBuffers: 2288 kBCached: 2490216 kBSwapCached: 0 kBActive: 1135928 kBInactive: 1849112 kBActive(anon): 509352 kBInactive(anon): 65012 kBActive(file): 626576 kBInactive(file): 1784100 kBUnevictable: 0 kBMlocked: 0 kBSwapTotal: 0 kBSwapFree: 0 kBDirty: 0 kBWriteback: 0 kBAnonPages: 492500 kBMapped: 190708 kBShmem: 81828 kBSlab: 269808 kBSReclaimable: 201596 kBSUnreclaim: 68212 kBKernelStack: 8000 kBPageTables: 24656 kBNFS_Unstable: 0 kBBounce: 0 kBWritebackTmp: 0 kBCommitLimit: 1896060 kBCommitted_AS: 3072464 kBVmallocTotal: 34359738367 kBVmallocUsed: 348664 kBVmallocChunk: 34358947836 kBHardwareCorrupted: 0 kBAnonHugePages: 159744 kBCmaTotal: 0 kBCmaFree: 0 kBHugePages_Total: 0HugePages_Free: 0HugePages_Rsvd: 0HugePages_Surp: 0Hugepagesize: 2048 kBDirectMap4k: 142584 kBDirectMap2M: 3962880 kBDirectMap1G: 0 kB
]]>
摘自 4 Ways to Disable/Lock Certain Package Updates Using Yum Command | TecMint
待更新…
# 1. 安装 yum install <package> # 安装指定的安装包# 2. 更新和升级 yum update # 全部更新 yum update <package> # 更新指定程序包yum check-update # 检查可更新的程序 yum upgrade <package> # 升级指定程序包# 3. 查找和显示 yum info # 列出所有可以安装或更新的包的信息yum info <package> # 显示安装包信息yum list # 显示所有已经安装和可以安装的程序包 yum list <package> # 显示指定程序包安装情况yum search <package> # 搜索匹配特定字符的包的详细信息# 4. 删除程序 yum remove | erase <package> # 删除程序包yum deplist <package> # 查看程序包依赖情况# 5. 清除缓存 yum clean packages # 清除缓存目录下的软件包 yum clean headers # 清除缓存目录下的 headers yum clean oldheaders # 清除缓存目录下旧的 headers yum clean, yum clean all # (= yum clean packages; yum clean oldheaders) 清除缓存目录下的软件包及旧的 headers
]]>
单组输入l
和r
的值
输出最终结果
Up to
2019-3-1 10:36 GMT+8
如:
10、11、12、13、14
的十六进制分别是a、b、c、d、e
。依次连在一起是abcde
,转换成十进制是703710
,对15
取模为0
10 14685003 898583100 100000000000------0310
package mainimport ( "bufio" "fmt" "os" "strconv" "strings")func solution(line string) string { lineArr := strings.Split(line, " ") l, _ := strconv.Atoi(lineArr[0]) r, _ := strconv.Atoi(lineArr[1]) tmp1 := l + r tmp2 := r - l + 1 if (tmp1 % 2) == 0 { tmp1 /= 2 } else { tmp2 /= 2 } ans := ((tmp1 % 15) * (tmp2 % 15)) % 15 return strconv.Itoa(ans)}func main() { r := bufio.NewReaderSize(os.Stdin, 20480) for line, _, err := r.ReadLine(); err == nil; line, _, err = r.ReadLine() { fmt.Println(solution(string(line))) }}
]]>
待更新…
]]>
最好情况、最坏情况、平均情况、均摊时间复杂度
继续来看四个复杂度分析方面的知识点:最好情况时间复杂度 (Best Case Time Complexity)、最坏情况时间复杂度 (Worst Case Time Complexity)、平均情况时间复杂度 (Average Case Time Complexity)、均摊时间复杂度 (Amortized Time Complexity)。
例如在一个无序数组array
中查找变量x
出现的位置:
// n 表示数组 array 的长度int find(int[] array, int n, int x) { int i = 0; int pos = -1; for (; i < n; ++i) { if (array[i] == x) { pos = i; break; } } return pos;}
O(1)
O(n)
还是上面的例子:要查找变量x
在数组中的位置,有n+1
种情况:在数组的0 ~ n-1
位置中和不在数组中。我们把每种情况下,查找需要遍历的元素个数累加起来,然后再除以n+1
,就可以得到需要遍历的元素个数的平均值,即:
简化后得到的平均时间复杂度为O(n)
。
然而上面的考虑还不够全面:若假设变量x
在数组中与不在数组中的概率都为1/2
,则可得加权平均时间复杂度或者期望时间复杂度:
去掉系数和常量,这段代码的加权平均时间复杂度仍为O(n)
。
大部分情况下,我们并不需要区分最好、最坏、平均三种复杂度。平均复杂度只在某些特殊情况下才会用到,而均摊时间复杂度应用的场景比它更加特殊、更加有限。
// array 表示一个长度为 n 的数组// 代码中的 array.length 就等于 nint[] array = new int[n];int count = 0;void insert(int val) { if (count == array.length) { int sum = 0; for (int i = 0; i < array.length; ++i) { sum = sum + array[i]; } array[0] = sum; count = 1; } array[count] = val; ++count;}
这段代码实现了一个往数组中插入数据的功能。当数组满了之后,用for
循环遍历数组求和,并清空数组,将求和之后的sum
值放到数组的第一个位置,然后再将新的数据插入。但如果数组一开始就有空间空间,则直接将数据插入数组。
O(1)
O(n)
O(1)
还是insert()
函数这个例子:随着n
的不断增长,可以发现每一次O(n)
的插入操作,都会跟着n-1
次O(1)
的插入操作。所以把耗时多的那次操作均摊到接下来的n-1
次耗时少的操作上,这一组连续的操作的均摊时间复杂度就是O(1)
。这就是均摊分析(又称摊还分析)的大致思路。
简单来看,均摊时间复杂度就是一种特殊的平均时间复杂度
]]>
分析、统计算法的执行效率和资源消耗
算法的执行效率简单来说,就是算法代码执行的时间。
例如下面这段代码:
int cal(int n) { int sum = 0; // 1 int i = 1; // 1 for (; i <= n; ++i) { // n sum = sum + i; // n } return sum;}
每一行都执行着类似的操作:读数据-运算-写数据。
假设每行代码执行的时间都相同,为unit_time
。则第 2、3 行代码分别需要 1 个unit_time
的执行时间。第 4、5 行都运行了 n 遍,所以需要2n * unit_time
的执行时间。所以总的执行时间就是(2n+2) * unit_time
。
对于下面这段代码:
int cal(int n) { int sum = 0; // 1 int i = 1; // 1 int j = 1; // 1 for (; i <= n; ++i) { // n j = 1; // n for (; j <= n; ++j) { // n^2 sum = sum + i * j; // n^2 } }}
整段代码的执行时间为T(n) = (2n^2 + 2n + 3) * unit_time
。
可以看到,所有代码的执行时间
T(n)
与每行代码的执行次数成正比。
其中,
T(n)
:表示代码的执行时间n
:表示数据规模的大小f(n)
:表示每行代码执行的次数总和O
:表示代码的执行时间T(n)
与f(n)
表达式成正比大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以也叫做渐进时间复杂度 (Asymptotic Time Complexity),简称时间复杂度。
用大 O 表示法表示刚才两段代码的时间复杂度,分别为
T(n)=O(n)
、T(n)=O(n^2)
可以将上图中的复杂度量级粗略分为两类:多项式量级和非多项式量级。其中,非多项式量级只有两个:O(2^n)
和O(n!)
。
当数据规模n
越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实是非常低效的算法。
只要代码的执行时间不随n
的增大而增长,这样代码的时间复杂度都记作O(1)
。
int i = 8;int j = 6;int sum = i + j;
一般情况下,只要算法中不存在循环、递归语句,即使有成千上万行的代码,其时间复杂度也还是
O(1)
对数阶时间复杂度非常常见,同时也最难分析。例如下面的代码:
i=1;while (i <= n) { i = i * 2;}
实际上,变量i
的取值就是一个等比数列:
可得x=log_2^n
。忽略对数的底,统一表示为O(logn)
。
根据前面提到的乘法法则,如果一段代码的时间复杂度是O(logn)
,循环执行n
遍,时间复杂度就是O(nlogn)
了。
O(nlogn)
也是一种非常常见的算法时间复杂度,例如归并排序、快速排序的时间复杂度都是O(nlogn)
有时代码的复杂度由两个数据的规模来决定:
int cal(int m, int n) { int sum_1 = 0; int i = 1; for (; i < m; ++i) { sum_1 = sum_1 + i; } int sum_2 = 0; int j = 1; for (; j < n; ++j) { sum_2 = sum_2 + j; } return sum_1 + sum_2;}
从代码中可以看出,m
和n
分别表示两个数据规模,我们无法事先评估m
和n
谁的量级更大。因此不能简单的利用加法法则,而要将时间复杂度表示为O(m+n)
。
这种情况下加法法则需要改为:T1(m) + T2(n) = O(f(m) + g(n))
。
而乘法法则继续有效:T1(m) * T2(n) = O(f(m) * f(n))
。
类比时间复杂度,空间复杂度的全称就是渐进空间复杂度 (Asymptotic Space Complexity),表示算法的存储空间与数据规模之间的增长关系。
常见的空间复杂度就是
O(1)
、O(n)
、O(n^2)
,像O(logn)
、O(nlogn)
这样的对数阶复杂度平时基本用不到
O(1)
、O(logn)
、O(n)
、O(nlogn)
、O(n^2)
]]>
程序 = 数据结构 + 算法
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
Updating…
]]>
摘自 Creating a single master cluster with kubeadm | kubernetes.io,更新中…
kubeadm 是 Kubernetes 官方提供的一个 CLI (Command Line Interface) 工具,可以很方便的搭建一套符合官方最佳实践的最小化可用集群。当我们使用kubeadm
搭建集群时,集群可以通过 K8S 的一致性测试,并且kubeadm
还支持其他的集群生命周期功能,比如升级/降级等。
安装kubeadm
前需要在所有节点上检查以下条件是否满足。
部署集群的所有节点主机需运行以下操作系统:
CPU 2 核以上,内存 2 GB 以上。
节点之间需要具备Full network connectivity
,公网、局域网均可。
通过hostname
查看主机名,通过ip link
或ifconfig -a
查看网卡对应的 MAC 地址,确保每台机器各不相同。
通过sudo cat /sys/class/dmi/id/product_uuid
可查看机器的product_uuid
,确保要搭建集群的所有节点的product_uuid
均不相同。
这样做的原因是每个 Node 都有一些信息会被记录进集群内,而此处我们需要保证的这些唯一的信息,便会记录在集群的
nodeInfo
中,比如product_uuid
在集群内以systemUUID
来表示,具体信息则可以通过集群的API Server
获取到。
Kubernetes 集群的每个节点上都有个必需的组件kubelet
。从Kubernetes 1.8
开始,启动kubelet
时需要禁用swap
,或者需要更改kubelet
的启动参数为--fail-swap-on=false
。
摘自《Kubernetes 从上手到实践》:
虽说可以更改参数让其可用,但是我建议还是禁用 swap 除非你的集群有特殊的需求,比如:有大内存使用的需求,但又想节约成本;或者你知道你将要做什么,否则可能会出现一些非预期的情况,尤其是做了内存限制的时候,当某个 Pod 达到内存限制的时候,它可能会溢出到 swap 中,这会导致 k8s 无法正常进行调度。
禁用方法如下:
1.使用cat /proc/swaps
验证swap
配置的设备和文件:
~> cat /proc/swaps Filename Type Size Used Priority/dev/dm-1 partition 8126460 0 -1~> free -h total used free shared buff/cache availableMem: 7.6G 996M 4.5G 12M 2.2G 6.3GSwap: 7.7G 0B 7.7G~> cat /etc/fstab## /etc/fstab# Created by anaconda on Tue Nov 13 11:26:56 2018## Accessible filesystems, by reference, are maintained under '/dev/disk'# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info#/dev/mapper/centos-root / xfs defaults 0 0UUID=aadb6c2e-8a99-46e5-b208-1eaee9944490 /boot xfs defaults 0 0UUID=4797-AB7E /boot/efi vfat umask=0077,shortname=winnt 0 0/dev/mapper/centos-home /home xfs defaults 0 0/dev/mapper/centos-swap swap swap defaults 0 0~> lsblkNAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTsda 8:0 0 931.5G 0 disk ├─sda1 8:1 0 200M 0 part /boot/efi├─sda2 8:2 0 1G 0 part /boot└─sda3 8:3 0 930.3G 0 part ├─centos-root 253:0 0 500G 0 lvm / ├─centos-swap 253:1 0 7.8G 0 lvm [SWAP] └─centos-home 253:2 0 422.6G 0 lvm /homesr0 11:0 1 1024M 0 rom
2.使用swapoff -a
禁用/etc/fstab
中的所有交换区:
使用
swapon -a
即可重新启用/etc/fstab
中的所有交换区。
~> swapoff -a~> cat /proc/swapsFilename Type Size Used Priority~> free -h total used free shared buff/cache availableMem: 7.6G 989M 4.5G 12M 2.2G 6.3GSwap: 0B 0B 0B~> lsblkNAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTsda 8:0 0 931.5G 0 disk ├─sda1 8:1 0 200M 0 part /boot/efi├─sda2 8:2 0 1G 0 part /boot└─sda3 8:3 0 930.3G 0 part ├─centos-root 253:0 0 500G 0 lvm / ├─centos-swap 253:1 0 7.8G 0 lvm └─centos-home 253:2 0 422.6G 0 lvm /homesr0 11:0 1 1024M 0 rom
可以看到swap
分区的挂载点已被卸载。
3.为了确保机器重启或重挂载时,不会再次挂载swap
分区,还需将/etc/fstab
中的swap
分区记录注释掉:
~> vim /etc/fstab## /etc/fstab# Created by anaconda on Tue Nov 13 11:26:56 2018## Accessible filesystems, by reference, are maintained under '/dev/disk'# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info#/dev/mapper/centos-root / xfs defaults 0 0UUID=aadb6c2e-8a99-46e5-b208-1eaee9944490 /boot xfs defaults 0 0UUID=4797-AB7E /boot/efi vfat umask=0077,shortname=winnt 0 0/dev/mapper/centos-home /home xfs defaults 0 0# /dev/mapper/centos-swap swap swap defaults 0 0
Kubernetes 是 C/S 架构,在启动后会固定监听以下端口用于提供服务。
Master node(s):
Worker node(s):
可以通过sudo netstat -ntlp |grep -E '6443|23[79,80]|1025[0,1,2]'
查看Master
端口是否被占用。如果被占用,请手动释放。
若提示
command not found
,则需要先安装netstat
CentOS:sudo yum install net-tools
Debian/Ubuntu:sudo apt install net-tools
需要在所有节点上安装容器运行时(Container Runtime),默认为 Docker。可参考 CentOS 7 安装 Docker CE | 苏易北。
Role | Hostname | OS | CPU | RAM |
---|---|---|---|---|
Master | abelsu7-ubuntu | Ubuntu 18.04 | i7-6700 @ 3.40 GHz,4 核 8 线程 | 32 GB |
Worker | centos-1 | CentOS 7.5 | i5-4590 @ 3.30 GHz,4 核 4 线程 | 4 GB |
Worker | centos-2 | CentOS 7.5 | i5-4590 @ 3.30 GHz,4 核 4 线程 | 8 GB |
注:国内用户安装以上组件时可能会遇到
众所周知的网络问题。我是用代理解决的,可参考 Linux 下使用 SSR + ProxyChains 代理终端流量 | 苏易北
Ubuntu, Debian or HypriotOS:
apt-get update && apt-get install -y apt-transport-https curlcurl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -cat <<EOF >/etc/apt/sources.list.d/kubernetes.listdeb https://apt.kubernetes.io/ kubernetes-xenial mainEOFapt-get updateapt-get install -y kubelet kubeadm kubectlapt-mark hold kubelet kubeadm kubectl
CentOS, RHEL or Fedora:
cat <<EOF > /etc/yum.repos.d/kubernetes.repo[kubernetes]name=Kubernetesbaseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64enabled=1gpgcheck=1repo_gpgcheck=1gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpgexclude=kube*EOF# Set SELinux in permissive mode (effectively disabling it)setenforce 0sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/configyum install -y kubelet kubeadm kubectl --disableexcludes=kubernetessystemctl enable --now kubelet
安装完成后验证版本信息,可以看到此处安装的版本均为v1.13.3
:
~> kubeadm versionkubeadm version: &version.Info{Major:"1", Minor:"13", GitVersion:"v1.13.3", GitCommit:"721bfa751924da8d1680787490c54b9179b1fed0", GitTreeState:"clean", BuildDate:"2019-02-01T20:05:53Z", GoVersion:"go1.11.5", Compiler:"gc", Platform:"linux/amd64"}~> kubectl version --clientClient Version: version.Info{Major:"1", Minor:"13", GitVersion:"v1.13.3", GitCommit:"721bfa751924da8d1680787490c54b9179b1fed0", GitTreeState:"clean", BuildDate:"2019-02-01T20:08:12Z", GoVersion:"go1.11.5", Compiler:"gc", Platform:"linux/amd64"}~> kubelet --versionKubernetes v1.13.3
为了在生产环境中保障各组件的稳定运行,同时也为了便于管理,我们增加对kubelet
的systemd
的配置,由systemd
对服务进行管理:
首先创建/etc/systemd/system/kubelet.service
(若文件已存在则继续下一步),并输入以下内容:
[Unit]Description=kubelet: The Kubernetes Node AgentDocumentation=https://kubernetes.io/docs/[Service]ExecStart=/usr/bin/kubeletRestart=alwaysStartLimitInterval=0RestartSec=10[Install]WantedBy=multi-user.target
之后创建/etc/systemd/system/kubelet.service.d/kubeadm.conf
(若文件已存在则继续下一步),并输入以下内容:
# Note: This dropin only works with kubeadm and kubelet v1.11+[Service]Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml"# This is a file that "kubeadm init" and "kubeadm join" generates at runtime, populating the KUBELET_KUBEADM_ARGS variable dynamicallyEnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env# This is a file that the user can use for overrides of the kubelet args as a last resort. Preferably, the user should use# the .NodeRegistration.KubeletExtraArgs object in the configuration files instead. KUBELET_EXTRA_ARGS should be sourced from this file.EnvironmentFile=-/etc/sysconfig/kubeletExecStart=ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS
最后使用systemctl enable kubelet
启用服务:
~> systemctl enable kubeletCreated symlink from /etc/systemd/system/multi-user.target.wants/kubelet.service to /etc/systemd/system/kubelet.service.
使用kubeadm init
首次创建集群时会从k8s.gcr.io
这个 Registry 下载 Kubernetes 所需的 Docker 镜像。
由于众所周知的网络问题,即使我挂了代理也无法成功下载。好在阿里云上有同步镜像的组件,所以可以提前从阿里云上下载所需镜像,再重新docker tag
上k8s.gcr.io
这个 Registry。
注:可以参考以下三篇文章:
首先需要使用kubeadm config image list
查看所需镜像的版本:
~> kubeadm config images listk8s.gcr.io/kube-apiserver:v1.13.3k8s.gcr.io/kube-controller-manager:v1.13.3k8s.gcr.io/kube-scheduler:v1.13.3k8s.gcr.io/kube-proxy:v1.13.3k8s.gcr.io/pause:3.1k8s.gcr.io/etcd:3.2.24k8s.gcr.io/coredns:1.2.6
之后新建脚本文件docker-k8s-images.sh
,输入以下内容:
#!/bin/bashimages=( kube-apiserver:v1.13.3 kube-controller-manager:v1.13.3 kube-scheduler:v1.13.3 kube-proxy:v1.13.3 pause:3.1 etcd:3.2.24 coredns:1.2.6)for imageName in ${images[@]} ; do docker pull registry.cn-hangzhou.aliyuncs.com/google_containers/${imageName} docker tag registry.cn-hangzhou.aliyuncs.com/google_containers/${imageName} k8s.gcr.io/${imageName} docker rmi registry.cn-hangzhou.aliyuncs.com/google_containers/${imageName}donedocker images
阿里云镜像仓库地址:
registry.cn-hangzhou.aliyuncs.com
registry.aliyuncs.com
最后添加执行权限,运行脚本:
~> chmod +x ./docker-k8s-images.sh~> ./docker-k8s-images.sh...REPOSITORY TAG IMAGE ID CREATED SIZEk8s.gcr.io/kube-apiserver v1.13.3 fe242e556a99 3 weeks ago 181MBk8s.gcr.io/kube-proxy v1.13.3 98db19758ad4 3 weeks ago 80.3MBk8s.gcr.io/kube-controller-manager v1.13.3 0482f6400933 3 weeks ago 146MBk8s.gcr.io/kube-scheduler v1.13.3 3a6f709e97a0 3 weeks ago 79.6MBk8s.gcr.io/coredns 1.2.6 f59dcacceff4 3 months ago 40MBk8s.gcr.io/etcd 3.2.24 3cab8e1b9802 5 months ago 220MBk8s.gcr.io/pause 3.1 da86e6ba6ca1 14 months ago 742kB...
在使用kubeadm init
启动集群时,需要传递--pod-network-cidr
参数以便 Pod 之间可以相互通信。
关于网络的选择,此处不做过多介绍,暂时选择一个被广泛使用的方案flannel
,这时需要指定--pod-network-cidr=10.244.0.0/16
。
另外,在使用flannel
之前,还需查看/proc/sys/net/bridge/bridge-nf-call-iptables
是否已设置为1
:
~> sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-iptables = 1
否则可以通过sysctl net.bridge.bridge-nf-call-iptables=1
更改设置。
Notes: Set
/proc/sys/net/bridge/bridge-nf-call-iptables
to1
by runningsysctl net.bridge.bridge-nf-call-iptables=1
to pass bridged IPv4 traffic to iptables’ chains. This is a requirement for some CNI plugins to work, for more information please see here.
最后,对于Kubernetes v1.7+
之后的版本,记得在下一节的kubeadm init --pod-network-cidr=10.244.0.0/16
命令执行之后,应用flannel
的配置文件:
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
有关
flannel
的更多信息,请查看 the CoreOS flannel repository on GitHub
所有的准备工作已经完成,现在开始创建一个 k8s 集群。
首先使用kubeadm init
初始化集群,并传递--pod-network-cidr=10.244.0.0/16
参数以指定 Pod 网络方案为flannel
:
~> kubeadm init --pod-network-cidr=10.244.0.0/16[init] Using Kubernetes version: v1.13.3[preflight] Running pre-flight checks[preflight] Pulling images required for setting up a Kubernetes cluster[preflight] This might take a minute or two, depending on the speed of your internet connection[preflight] You can also perform this action in beforehand using 'kubeadm config images pull'[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"[kubelet-start] Activating the kubelet service[certs] Using certificateDir folder "/etc/kubernetes/pki"[certs] Generating "front-proxy-ca" certificate and key[certs] Generating "front-proxy-client" certificate and key[certs] Generating "etcd/ca" certificate and key[certs] Generating "etcd/healthcheck-client" certificate and key[certs] Generating "apiserver-etcd-client" certificate and key[certs] Generating "etcd/server" certificate and key[certs] etcd/server serving cert is signed for DNS names [abelsu7-ubuntu localhost] and IPs [222.201.139.151 127.0.0.1 ::1][certs] Generating "etcd/peer" certificate and key[certs] etcd/peer serving cert is signed for DNS names [abelsu7-ubuntu localhost] and IPs [222.201.139.151 127.0.0.1 ::1][certs] Generating "ca" certificate and key[certs] Generating "apiserver" certificate and key[certs] apiserver serving cert is signed for DNS names [abelsu7-ubuntu kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 222.201.139.151][certs] Generating "apiserver-kubelet-client" certificate and key[certs] Generating "sa" key and public key[kubeconfig] Using kubeconfig folder "/etc/kubernetes"[kubeconfig] Writing "admin.conf" kubeconfig file[kubeconfig] Writing "kubelet.conf" kubeconfig file[kubeconfig] Writing "controller-manager.conf" kubeconfig file[kubeconfig] Writing "scheduler.conf" kubeconfig file[control-plane] Using manifest folder "/etc/kubernetes/manifests"[control-plane] Creating static Pod manifest for "kube-apiserver"[control-plane] Creating static Pod manifest for "kube-controller-manager"[control-plane] Creating static Pod manifest for "kube-scheduler"[etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory "/etc/kubernetes/manifests". This can take up to 4m0s[apiclient] All control plane components are healthy after 23.002540 seconds[uploadconfig] storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace[kubelet] Creating a ConfigMap "kubelet-config-1.13" in namespace kube-system with the configuration for the kubelets in the cluster[patchnode] Uploading the CRI Socket information "/var/run/dockershim.sock" to the Node API object "abelsu7-ubuntu" as an annotation[mark-control-plane] Marking the node abelsu7-ubuntu as control-plane by adding the label "node-role.kubernetes.io/master=''"[mark-control-plane] Marking the node abelsu7-ubuntu as control-plane by adding the taints [node-role.kubernetes.io/master:NoSchedule][bootstrap-token] Using token: ar8quq.bx68gpg2ktjzagk8[bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles[bootstraptoken] configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials[bootstraptoken] configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token[bootstraptoken] configured RBAC rules to allow certificate rotation for all node client certificates in the cluster[bootstraptoken] creating the "cluster-info" ConfigMap in the "kube-public" namespace[addons] Applied essential addon: CoreDNS[addons] Applied essential addon: kube-proxyYour Kubernetes master has initialized successfully!To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/configYou should now deploy a pod network to the cluster.Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: https://kubernetes.io/docs/concepts/cluster-administration/addons/You can now join any number of machines by running the following on each nodeas root: kubeadm join 222.201.139.151:6443 --token ar8quq.bx68gpg2ktjzagk8 --discovery-token-ca-cert-hash sha256:125083b871f062c8d4c0c7ab5cefee1ba0b74a6b3fb17c0c4b5ba4d591c1051d
根据提示,使用以下命令配置kubectl
:
~> mkdir -p $HOME/.kube~> sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config~> sudo chown $(id -u):$(id -g) $HOME/.kube/config
最后,应用flannel
配置文件:
~> kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.ymlpodsecuritypolicy.extensions/psp.flannel.unprivileged createdclusterrole.rbac.authorization.k8s.io/flannel createdclusterrolebinding.rbac.authorization.k8s.io/flannel createdserviceaccount/flannel createdconfigmap/kube-flannel-cfg createddaemonset.extensions/kube-flannel-ds-amd64 createddaemonset.extensions/kube-flannel-ds-arm64 createddaemonset.extensions/kube-flannel-ds-arm createddaemonset.extensions/kube-flannel-ds-ppc64le createddaemonset.extensions/kube-flannel-ds-s390x created
稍等片刻,master
即处于Ready
状态。可在其他机器上输入以下命令加入集群:
kubeadm join 222.201.139.151:6443 --token ar8quq.bx68gpg2ktjzagk8 --discovery-token-ca-cert-hash sha256:125083b871f062c8d4c0c7ab5cefee1ba0b74a6b3fb17c0c4b5ba4d591c1051d
可通过kubectl
查看集群节点状态:
~> kubectl cluster-infoKubernetes master is running at https://222.201.139.151:6443KubeDNS is running at https://222.201.139.151:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxyTo further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.~> kubectl get nodesNAME STATUS ROLES AGE VERSIONabelsu7-ubuntu Ready master 15m v1.13.3
可以看到master
已经处于Ready
状态。
我们知道 Kubernetes 中的最小调度单元是Pod
。使用以下命令查看集群中现有的Pod
状态:
~> kubectl get pods --all-namespacesNAMESPACE NAME READY STATUS RESTARTS AGEkube-system coredns-86c58d9df4-9vwct 1/1 Running 0 19mkube-system coredns-86c58d9df4-mvdnh 1/1 Running 0 19mkube-system etcd-abelsu7-ubuntu 1/1 Running 0 18mkube-system kube-apiserver-abelsu7-ubuntu 1/1 Running 0 18mkube-system kube-controller-manager-abelsu7-ubuntu 1/1 Running 0 18mkube-system kube-flannel-ds-amd64-wktkk 1/1 Running 0 17mkube-system kube-proxy-qv2t8 1/1 Running 0 19mkube-system kube-scheduler-abelsu7-ubuntu 1/1 Running 0 18m
根据刚才执行完kubeadm init
后给出的提示信息,分别在新机器centos-1
和centos-2
上执行kubeadm join
命令:
~> kubeadm join 222.201.139.151:6443 --token ar8quq.bx68gpg2ktjzagk8 --discovery-token-ca-cert-hash sha256:125083b871f062c8d4c0c7ab5cefee1ba0b74a6b3fb17c0c4b5ba4d591c1051d[preflight] Running pre-flight checks [WARNING Service-Docker]: docker service is not enabled, please run 'systemctl enable docker.service' [WARNING SystemVerification]: this Docker version is not on the list of validated versions: 18.09.2. Latest validated version: 18.06 [WARNING Hostname]: hostname "centos-1" could not be reached [WARNING Hostname]: hostname "centos-1": lookup centos-1 on 222.201.130.30:53: no such host[discovery] Trying to connect to API Server "222.201.139.151:6443"[discovery] Created cluster-info discovery client, requesting info from "https://222.201.139.151:6443"[discovery] Requesting info from "https://222.201.139.151:6443" again to validate TLS against the pinned public key[discovery] Cluster info signature and contents are valid and TLS certificate validates against pinned roots, will use API Server "222.201.139.151:6443"[discovery] Successfully established connection with API Server "222.201.139.151:6443"[join] Reading configuration from the cluster...[join] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'[kubelet] Downloading configuration for the kubelet from the "kubelet-config-1.13" ConfigMap in the kube-system namespace[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"[kubelet-start] Activating the kubelet service[tlsbootstrap] Waiting for the kubelet to perform the TLS Bootstrap...[patchnode] Uploading the CRI Socket information "/var/run/dockershim.sock" to the Node API object "centos-1" as an annotationThis node has joined the cluster:* Certificate signing request was sent to apiserver and a response was received.* The Kubelet was informed of the new secure connection details.Run 'kubectl get nodes' on the master to see this node join the cluster.
上述命令执行完成后,提示已经成功加入集群。
此时,在master
上查看当前集群状态:
~> kubectl get nodesNAME STATUS ROLES AGE VERSIONabelsu7-ubuntu Ready master 26m v1.13.3centos-1 Ready <none> 24m v1.13.3centos-2 Ready <none> 16m v1.13.3
journalctl -f -u kubeletkubeadm resetcat /var/lib/kubelet/kubeadm-flags.envkubectl get pods --all-namespaceskubectl describe pod kube-flannel-ds-amd64-c2vnq --namespace=kube-system
]]>
- Installing kubeadm | kubernetes.io
- Creating a single master cluster with kubeadm | kubernetes.io
- 《Kubernetes 从上手到实践》| 掘金小册
- Running kubeadm without an internet connection | kubernetes.io
- kubeadm config image 阿里云镜像 | 简书
- 如何成功启动 Docker 自带的 Kubernetes?| 简书
- kubernetes 1.11 集群痛苦搭建过程 | Mr.Cai
- Kubeadm 安装 Kubernetes 环境 | ericnie 的技术博客(RedHat 工程师)
- Kubernetes的 node NotReady 如何查问题,针对问题解决 | CSDN
- Kubernetes 初体验 | 时间轨迹
- Minikube - Kubernetes本地实验环境 | 阿里云栖社区
- 使用 minikube 安装 k8s 集群 | 胡伟煌
- kubeadm 续坑篇 | 漠然
摘自 How to Disable SELinux on CentOS 7 | Linuxize
> getenforceEnabled> sestatusSELinux status: enabledSELinuxfs mount: /sys/fs/selinuxSELinux root directory: /etc/selinuxLoaded policy name: targetedCurrent mode: enforcingMode from config file: enforcingPolicy MLS status: enabledPolicy deny_unknown status: allowedMax kernel policy version: 31
setenforce 0 ## 设置为 Permissive 模式setenforce 1 ## 设置为 Enforcing 模式
> vim /etc/selinux/config# This file controls the state of SELinux on the system.# SELINUX= can take one of these three values:# enforcing - SELinux security policy is enforced.# permissive - SELinux prints warnings instead of enforcing.# disabled - No SELinux policy is loaded.SELINUX=disabled# SELINUXTYPE= can take one of three two values:# targeted - Targeted processes are protected,# minimum - Modification of targeted policy. Only selected processes are protected. # mls - Multi Level Security protection.SELINUXTYPE=targeted
修改为SELINUX=disabled
,重启后生效。
]]>
Across the Great Wall we can reach every corner in the world.
~> wget https://github.com/cndaqiang/shadowsocksr/archive/manyuser.zip~> unzip manyuser.zip~> cd shadowsocksr-manyuser~/shadowsocksr-manyuser> lsapiconfig.py configloader.py Dockerfile initmudbjson.sh mudb.json README.rst setup_cymysql.sh switchrule.pyasyncmgr.py CONTRIBUTING.md importloader.py LICENSE mujson_mgr.py run.sh setup.py tail.shCHANGES db_transfer.py initcfg.bat logrun.sh mysql.json server_pool.py shadowsocks testsconfig.json debian initcfg.sh MANIFEST.in README.md server.py stop.sh utils
SSR 配置文件路径为shadowsocks-manyuser/config.json
:
{ "server": "xxx.xxx.xxx.xxx", // 服务器 IP "server_ipv6": "::", "server_port": 8388, // 服务器端口 "local_address": "127.0.0.1", "local_port": 1080, // 本地端口 "password": "password", // 密码 "method": "aes-256-cfb", // 加密方式 "protocol": "auth_aes128_md5", // 协议 "protocol_param": "", "obfs": "origin", // 混淆 "obfs_param": "", "speed_limit_per_con": 0, "speed_limit_per_user": 0, "additional_ports" : {}, // only works under multi-user mode "additional_ports_only" : false, // only works under multi-user mode "timeout": 120, "udp_timeout": 60, "dns_ipv6": false, "connect_verbose_info": 0, "redirect": "", "fast_open": false}
sudo python ./shadowsocks/local.py -c config.json -d start|stop
~> git clone https://github.com/rofl0r/proxychains-ng.git~> cd proxychains-ng/~/proxychains-ng> ./configure --prefix=/usr --sysconfdir=/etc~/proxychains-ng> make && make install~/proxychains-ng> make install-config
proxychains-ng 配置文件路径为/etc/proxychains.conf
,根据实际情况添加代理socks5 127.0.0.1 1080
:
## you'll need to enable it if you want to use an application that ## connects to localhost.# localnet 127.0.0.0/255.0.0.0## RFC1918 Private Address Ranges# localnet 10.0.0.0/255.0.0.0# localnet 172.16.0.0/255.240.0.0# localnet 192.168.0.0/255.255.0.0# ProxyList format# type ip port [user pass]# (values separated by 'tab' or 'blank')## only numeric ipv4 addresses are valid### Examples:## socks5 192.168.67.78 1080 lamer secret# http 192.168.89.3 8080 justu hidden# socks4 192.168.1.49 1080# http 192.168.39.93 8080 # ## proxy types: http, socks4, socks5# ( auth types supported: "basic"-http "user/pass"-socks )#[ProxyList]# add proxy here ...# meanwile# defaults set to "tor"socks5 127.0.0.1 1080
修改~/.bashrc
,并设置 alias 别名:
# .bashrc# User specific aliases and functionsalias rm='rm -i'alias cp='cp -i'alias mv='mv -i'alias pc='proxychains4'# Source global definitionsif [ -f /etc/bashrc ]; then . /etc/bashrcfi
输入source ~/.bashrc
或重启终端后生效。
要想在终端命令中使用代理,只需在命令前加上pc
。例如:pc yum install kubectl
。
~> pcUsage: proxychains4 -q -f config_file program_name [arguments] -q makes proxychains quiet - this overrides the config setting -f allows one to manually specify a configfile to use for example : proxychains telnet somehost.comMore help in README file~> pc telnet www.google.com 443[proxychains] config file found: /etc/proxychains.conf[proxychains] preloading /usr/lib/libproxychains4.so[proxychains] DLL init: proxychains-ng 4.13-git-10-g1198857Trying 224.0.0.1...[proxychains] Strict chain ... 127.0.0.1:1080 ... www.google.com:443 ... OKConnected to www.google.com.Escape character is '^]'.~> pc curl myip.ipip.net[proxychains] config file found: /etc/proxychains.conf[proxychains] preloading /usr/lib/libproxychains4.so[proxychains] DLL init: proxychains-ng 4.13-git-10-g1198857[proxychains] Strict chain ... 127.0.0.1:1080 ... myip.ipip.net:80 ... OK当前 IP:103.xxx.xxx.xxx 来自于:美国 加利福尼亚州 费利蒙 sakura-host.net
]]>
- Ubuntu 16.04 安装 Python 版 SSR | cndaqiang
- Centos 7 作为 Client 连接 SSR | 简书
- cndaqiang/shadowsocksr | Github
- shadowsocksr-backup/shadowsocksr | Github
- ShadowsocksR 一键安装脚本 | Shadowsocks 非官方网站
- Linux CentOS 7 安装 SSR 过程 以及一些问题的处理 | 逆流水Team
- 通过 ProxyChains-NG 实现终端下任意应用代理 | CSDN
- 2019 优质 VPS 服务商推荐 | 知乎
- 写给非专业人士看的 Shadowsocks 简介 | vc2tea
- 打造基于 ShadowSocks + ProxyChains 的全栈式科学上网工具 | Echo’s Blog
Across the Great Wall we can reach every corner in the world.
注:为了支持
chacha20-ietf-poly1305
加密方式,请勿直接使用pip install shadowsocks
,而是要通过zip
包安装,如下所示
# 安装依赖> yum install epel-release python-pip libsodium# 也可将 zip 包先下载到本地再安装> pip install https://github.com/shadowsocks/shadowsocks/archive/master.zip -U > sslocal --versionShadowsocks 3.0.0
> mkdir -p /etc/shadowsocks> vim /etc/shadowsocks/config.json
添加如下内容:
{ "server": "x.x.x.x", # Server IP "server_port": 14131, # Server Port "local_address": "127.0.0.1", # Local IP "local_port": 1080, # Local Port "password": "password", # Your Password "timeout": 600, # Connection timeout "method": "aes-256-cfb", # Encryption method "fast_open": false, # Use TCP_FASTOPEN, requires Linux 3.7+ "workers": 1 # Number of worker threads}
> vim /etc/systemd/system/shadowsocks.service
添加如下内容:
[Unit]Description=Shadowsocks[Service]TimeoutStartSec=0ExecStart=/usr/bin/sslocal -c /etc/shadowsocks/config.json[Install]WantedBy=multi-user.target
启动shadowsocks
:
> systemctl start shadowsocks.service> systemctl status shadowsocks.service● shadowsocks.service - Shadowsocks Loaded: loaded (/etc/systemd/system/shadowsocks.service; disabled; vendor preset: disabled) Active: active (running) since Mon 2020-02-03 12:08:31 CST; 18min ago Main PID: 28122 (sslocal) Tasks: 1 Memory: 7.1M CGroup: /system.slice/shadowsocks.service └─28122 /usr/bin/python2 /usr/bin/sslocal -c /etc/shadowsocks/config.json
根据实际需要配置服务自启动:
> systemctl is-enabled shadowsocks.service disabled> systemctl enable shadowsocks.service
> curl --socks5 127.0.0.1:1080 http://httpbin.org/ip{ "origin": "xxx.xxx.xxx.xxx"}> pc curl myip.ipip.net[proxychains] config file found: /etc/proxychains.conf[proxychains] preloading /usr/lib/libproxychains4.so[proxychains] DLL init: proxychains-ng 4.14-git-8-gb8fa2a7[proxychains] Strict chain ... 127.0.0.1:1080 ... myip.ipip.net:80 ... OK当前 IP:xxx.xxx.xxx.xxx 来自于:日本 东京都 东京 xx.net
]]>
摘自 Setting Up grub2 on CentOS 7 | CentOS Wiki
最近安装了 CentOS 7 + Windows 10 的双系统,开机进入 grub2 的启动菜单。由于平时用 Windows 居多,可以接受在开机时手动选择 CentOS,所以需要修改默认启动项为Windows Boot Manager
,并缩短等待时间为 3 秒(默认为 5 秒),下面简单介绍修改方法。
grub2 等待时间由/etc/default/grub
中的GRUB_TIMEOUT
控制,首先查看该文件内容:
> cat /etc/default/grubGRUB_TIMEOUT=5GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"GRUB_DEFAULT=savedGRUB_DISABLE_SUBMENU=trueGRUB_TERMINAL_OUTPUT="console"GRUB_CMDLINE_LINUX="crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet"GRUB_DISABLE_RECOVERY="true"
修改为GRUB_TIMOUT=3
后,为使其生效,需要重新生成/boot/efi/EFI/centos/grub2.cfg
:
> grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg
非 UEFI 设备(Legacy)需将上述文件路径替换为
/boot/grub2/grub.cfg
> awk -F\' '$1=="menuentry " {print i++ " : " $2}' /etc/grub2-efi.cfg0 : CentOS Linux (3.10.0-957.el7.x86_64) 7 (Core)1 : CentOS Linux (0-rescue-3af74b34419f4869a2952d4467e303f8) 7 (Core)2 : Windows Boot Manager (on /dev/sda2)
非 UEFI 设备(Legacy)需将上述文件路径替换为
/etc/grub.cfg
或使用下列命令:
> grep "^menuentry" /boot/efi/EFI/centos/grub.cfg | cut -d "'" -f2CentOS Linux (3.10.0-957.el7.x86_64) 7 (Core)CentOS Linux (0-rescue-3af74b34419f4869a2952d4467e303f8) 7 (Core)Windows Boot Manager (on /dev/sda2)
非 UEFI 设备(Legacy)需将上述文件路径替换为
/boot/grub2/grub.cfg
默认启动项由/etc/default/grub
中的GRUB_DEFAULT
控制。
如果GRUB_DEFAULT=saved
,则该参数将存储在/boot/grub2/grubenv
中。可使用grub2-editenv list
查看:
> grub2-editenv listsaved_entry=CentOS Linux (3.10.0-957.el7.x86_64) 7 (Core)
通过grub2-set-default
命令修改默认启动项。由之前的输出可知,Windows Boot Manager
的启动序号为2
:
> grub2-set-default 2> grub2-editenv listsaved_entry=2
重启即可生效,修改完成。
]]>
RPM(Redhat Package Manager)的五种操作模式:安装、卸载、升级、查询、验证。
摘自 TecAdmin.net
> rpm -ivh vsftpd-2.3.5-2.el6.i686.rpmwarning: vsftpd-2.3.5-2.el6.i686.rpm: Header V3 DSA/SHA1 Signature, key ID e9bc4ae1: NOKEYPreparing... ########################################### [100%] 1:vsftpd ########################################### [100%]
参数含义如下:
-i
:执行安装操作-v
:显示正在安装的文件信息-h
:显示安装进度-l
:显示安装包中的所有文件被安装到哪些目录下其他附加参数:
--force
:强制执行操作--requires
:显示该包的依赖关系--nodeps
:忽略依赖关系并继续操作> rpm -Uvh vsftpd-2.3.5-2.el6.i686.rpm
> rpm -q vsftpdvsftpd-2.3.5-2.el6.i686
> rpm -qa
Below command will erase (uninstall) rpm package from your system.
> rpm -e vsftpdvsftpd-2.3.5-2.el6.i686
> rpm -qip vsftpd-2.3.5-2.el6.i686.rpmwarning: vsftpd-2.3.5-2.el6.i686.rpm: Header V3 DSA/SHA1 Signature, key ID e9bc4ae1: NOKEYName : vsftpd Relocations: (not relocatable)Version : 2.3.5 Vendor: (none)Release : 2.el6 Build Date: Thu 23 Feb 2012 07:38:59 AM ISTInstall Date: (not installed) Build Host: localhostGroup : System Environment/Daemons Source RPM: vsftpd-2.3.5-2.el6.src.rpmSize : 453460 License: GPLv2 with exceptionsSignature : DSA/SHA1, Fri 11 Jan 2013 06:48:45 PM IST, Key ID 8fbd1684e9bc4ae1URL : http://vsftpd.devnet.ruSummary : Very Secure Ftp DaemonDescription :vsftpd is a Very Secure FTP daemon. It was written completely fromscratch.
> rpm -qlp vsftpd-2.3.5-2.el6.i686.rpmwarning: vsftpd-2.3.5-2.el6.i686.rpm: Header V3 DSA/SHA1 Signature, key ID e9bc4ae1: NOKEY/etc/logrotate.d/vsftpd/etc/pam.d/vsftpd/etc/rc.d/init.d/vsftpd/etc/vsftpd/etc/vsftpd/ftpusers/etc/vsftpd/user_list/etc/vsftpd/vsftpd-403-serv.html/etc/vsftpd/vsftpd-403.html/etc/vsftpd/vsftpd-404.html
> rpm -qf /etc/vsftpd/ftpusersvsftpd-2.3.5-2.el6.i686
> rpm -qpR vsftpd-2.3.5-2.el6.i686.rpm
> rpm -Uvh --oldpackage vsftpd-<old-version>.el6.i686.rpm
]]>
2019-3-1 更新
校招官网 | 微信 | 内推 |
---|---|---|
腾讯 | 腾讯招聘 | 可内推 |
网易游戏 | 网易游戏综合招聘 | 可内推 |
网易游戏-互娱 | 网易游戏互娱校园招聘 | 已内推 |
网易游戏-雷火 | 网易游戏雷火伏羲招聘 | 可内推 |
网易互联网 | 网易招聘 | 暂未开始 |
网易有道 | 有道招聘 | 暂未开始 |
字节跳动 | 字节跳动招聘 | 可内推 |
招银网络科技 | 招银网络科技 | 可内推 |
顺丰科技 | 顺丰科技招聘 | 暂未开始 |
平安科技 | 平安科技招聘 | 仅应届 |
华为 | 华为招聘 | 暂未开始 |
阿里 | 阿里技术栈 | 暂未开始 |
哔哩哔哩 | 哔哩哔哩招聘 | 暂未开始 |
小米 | 小米校园招聘 | 暂未开始 |
1000
个苹果,放到10
个框里,怎么样能够保证任意数量的苹果都可以被表示出来?
参考这里,按位表示,模拟二进制
解决思路:最小堆
]]>摘自 CentOS 7 下 Yum 安装 MySQL 5.7 | Zhanming’s blog
下载并安装 MySQL 源安装包:
> sudo yum localinstall https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm
检查 yum 源是否安装成功:
> sudo yum repolist enabled | grep "mysql.*-community.*"mysql-connectors-community/x86_64 MySQL Connectors Community 95mysql-tools-community/x86_64 MySQL Tools Community 84mysql57-community/x86_64 MySQL 5.7 Community Server 327
> sudo yum install mysql-community-server
> sudo systemctl enable mysqld> sudo systemctl start mysqld> sudo systemctl status mysqld● mysqld.service - MySQL Server Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled) Active: active (running) since Sat 2019-02-16 00:41:07 CST; 2 days ago Docs: man:mysqld(8) http://dev.mysql.com/doc/refman/en/using-systemd.html Main PID: 29143 (mysqld) Tasks: 31 Memory: 193.2M CGroup: /system.slice/mysqld.service └─29143 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pidFeb 16 00:41:06 localhost.localdomain systemd[1]: Starting MySQL Server...Feb 16 00:41:07 localhost.localdomain systemd[1]: Started MySQL Server.
MySQL 5.7 启动后,会在/var/log/mysqld.log
文件中为用户root
生成一个随机的初始密码。使用以下命令查看初始密码:
> grep 'temporary password' /var/log/mysqld.log2019-02-15T16:29:45.738097Z 1 [Note] A temporary password is generated for root@localhost: jHsg<YlYu5id
登录 MySQL 并修改密码:
> mysql -u root -pEnter password:mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'MyNewPass4!';
MySQL 5.7 默认安装了密码安全检查插件(validate_password),默认密码检查策略要求密码必须包含:大小写字母、数字和特殊符号,并且长度不少于 8 位。
通过 MySQL 环境变量可以查看密码策略的相关信息:
mysql> SHOW VARIABLES LIKE 'validate_password%';+--------------------------------------+--------+| Variable_name | Value |+--------------------------------------+--------+| validate_password_check_user_name | OFF || validate_password_dictionary_file | || validate_password_length | 8 || validate_password_mixed_case_count | 1 || validate_password_number_count | 1 || validate_password_policy | MEDIUM || validate_password_special_char_count | 1 |+--------------------------------------+--------+7 rows in set (0.01 sec)
指定密码校验策略:
> sudo vi /etc/my.cnf[mysqld]# 添加如下键值对, 0=LOW, 1=MEDIUM, 2=STRONGvalidate_password_policy=0
禁用密码策略:
> sudo vi /etc/my.cnf[mysqld]# 禁用密码校验策略validate_password = off
重启 MySQL 服务,使配置生效:
sudo systemctl restart mysqld
MySQL 默认只允许root
用户在本地登录。如果要从其他机器上连接 MySQL,必须允许root
用户从远程登录,或者添加一个允许远程连接的账户。以下命令将添加一个新用户admin
,并允许其从远程登录:
mysql> GRANT ALL PRIVILEGES ON *.* TO 'admin'@'%' IDENTIFIED BY 'secret' WITH GRANT OPTION;
如果需要修改权限允许root
远程登录,则先以root
用户登录 MySQL,再进行如下操作:
mysql> USE mysql;Database changedmysql> SELECT User, Host -> FROM user;+-----------+---------------+| Host | User |+-----------+---------------+| % | admin || localhost | mysql.session || localhost | mysql.sys || localhost | root |+-----------+---------------+4 rows in set (0.02 sec)mysql> UPDATE user -> SET Host = '%' -> WHERE User = 'root';Query OK, 1 row affected (0.04 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> SELECT User, Host -> FROM user;+-----------+---------------+| Host | User |+-----------+---------------+| % | admin || % | root || localhost | mysql.session || localhost | mysql.sys |+-----------+---------------+4 rows in set (0.00 sec)
MySQL 默认编码为latin1
,查看字符集:
mysql> SHOW VARIABLES LIKE 'character%';+--------------------------+----------------------------+| Variable_name | Value |+--------------------------+----------------------------+| character_set_client | utf8 || character_set_connection | utf8 || character_set_database | latin1 || character_set_filesystem | binary || character_set_results | utf8 || character_set_server | latin1 || character_set_system | utf8 || character_sets_dir | /usr/share/mysql/charsets/ |+--------------------------+----------------------------+8 rows in set (0.00 sec)
在配置文件/etc/my.cnf
中将其修改为utf8
:
> vim /etc/my.cnf[mysqld]# 在myslqd下添加如下键值对character_set_server=utf8init_connect='SET NAMES utf8'
重启 MySQL,使配置生效:
> sudo systemctl restart mysqld
再次查看字符集:
mysql> SHOW VARIABLES LIKE 'character%';+--------------------------+----------------------------+| Variable_name | Value |+--------------------------+----------------------------+| character_set_client | utf8 || character_set_connection | utf8 || character_set_database | utf8 || character_set_filesystem | binary || character_set_results | utf8 || character_set_server | utf8 || character_set_system | utf8 || character_sets_dir | /usr/share/mysql/charsets/ |+--------------------------+----------------------------+8 rows in set (0.00 sec)
> sudo firewall-cmd --zone=public --add-port=3306/tcp --permanent> sudo firewall-cmd --reload
]]>
Bind Mount 与 Data Volume、共享数据、Volume 生命周期管理
Docker 为容器提供了两种存放数据的资源:
先来回顾一下 Docker 镜像的分层结构:
容器由最上面一个可写的容器层,以及若干只读的镜像层组成,容器的数据就存放在这些层中。这样的分层结构最大的特性是 Copy-on-Write:
分层结构使镜像和容器的创建、共享以及分发变得非常高效,而这些都要归功于 Docker Storage Driver。正是 storage driver 实现了多层数据的堆叠并为用户提供一个单一的合并之后的统一视图。
Docker 会默认优先使用 Linux 发行版默认的 Storage Driver。
Docker 安装时会根据当前系统的配置选择默认的 driver。默认 driver 具有最好的稳定性,因为默认 driver 在发行版上经过了严格的测试。
运行docker info
可查看 Storage Driver 的相关信息:
[root@localhost ~]# docker infoContainers: 3 Running: 0 Paused: 0 Stopped: 3Images: 4Server Version: 18.09.2Storage Driver: overlay2 Backing Filesystem: xfs Supports d_type: true Native Overlay Diff: trueLogging Driver: json-fileCgroup Driver: cgroupfsPlugins: Volume: local Network: bridge host macvlan null overlay Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslogSwarm: inactiveRuntimes: runcDefault Runtime: runcInit Binary: docker-initcontainerd version: 9754871865f7fe2f4e74d43e2fc7ccd237edcbcerunc version: 09c8266bf2fcf9519a651b04ae54c967b9ab86ecinit version: fec3683Security Options: seccomp Profile: defaultKernel Version: 3.10.0-957.el7.x86_64Operating System: CentOS Linux 7 (Core)OSType: linuxArchitecture: x86_64CPUs: 6Total Memory: 7.487GiBName: localhost.localdomainID: ZUUO:D7MX:3YFL:D67V:Y3DR:B57B:JAVB:IN7B:ZMWO:Q2SN:YNP4:TXP5Docker Root Dir: /var/lib/dockerDebug Mode (client): falseDebug Mode (server): falseRegistry: https://index.docker.io/v1/Labels:Experimental: falseInsecure Registries: 127.0.0.0/8Live Restore Enabled: falseProduct License: Community Engine
Data Volume 本质上是 Docker Host 文件系统中的目录或文件,能够直接被 mount 到容器的文件系统中。
Data Volume 有以下特点:
在具体使用中应遵循这样的原则:
在具体使用上,Docker 提供了两种类型的 volume:bind mount 和 Docker managed volume。
bing mount 是将 host 上已存在的目录或文件 mount 到容器。
例如 Docker host 上有目录$HOME/htdocs
:
通过-v
将其 mount 到 httpd 容器:
参数-v
的格式为<host_path>:<container_path>
,原有的同名目录会被隐藏起来,这与 Linux 系统中的mount
命令的行为是一致的。
bind mount 可以让 host 与容器共享数据,这在管理上是非常方便的。
另外,使用 bind mount 时还可以指定数据的读写权限,默认是可读可写,可指定为只读:
ro
设置了只读权限。在容器中是无法对 bind mount 数据进行修改的,只有 host 有权修改数据,提高了安全性。
除了 bind mount 目录,还可以单独指定一个文件:
使用 bind mount 单个文件的场景是:只需要向容器添加文件,不希望覆盖整个目录。
使用单一文件有一点要注意:host 中的源文件必须要存在,不然会当作一个新目录 bind mount 给容器。
bind mount 的使用直观高效,易于理解,但它也有不足的地方:bind mount 需要指定 host 文件系统的特定路径,这就限制了容器的可移植性,当需要将容器迁移到其他 host,而该 host 没有要 mount 的数据或者数据不在相同的路径时,操作会失败。
移植性更好的方式是 docker managed volume。
docker managed volume 与 bind mount 在使用上的最大区别是不需要指定 mount 源,指明 mount point 就行了。
以 httpd 容器为例:
[root@localhost ~]# docker run -d -p 80:80 -v /usr/local/apache2/htdocs httpd2e973f7246b72d60c42e0a124d8bcb4e8ba053c4eb946de6c6d2ec5d2fb29cdd
上述命令通过-v
告诉 Docker 需要一个 data volume,并将其 mount 到/usr/local/apache2/htdocs
。
执行docker inspect
命令:
[root@localhost ~]# docker inspect 2e97... "Mounts": [ { "Type": "volume", "Name": "bb76558c26ec96b8f2fd9aced58105495b610119520dbe84c3f83bc347bc2209", "Source": "/var/lib/docker/volumes/bb76558c26ec96b8f2fd9aced58105495b610119520dbe84c3f83bc347bc2209/_data", "Destination": "/usr/local/apache2/htdocs", "Driver": "local", "Mode": "", "RW": true, "Propagation": "" } ],...
可以看到Source
就是该 Volume 在 host 上的目录。每当容器申请一个 docker managed volume 时,Docker 都会在/var/lib/docker/volumes
下生成一个目录,这个目录就是 mount 源。查看该 volume 下的文件内容:
[root@localhost ~]# ls /var/lib/docker/volumes/bb76558c26ec96b8f2fd9aced58105495b610119520dbe84c3f83bc347bc2209/_dataindex.html
总结一下 docker managed volume 的创建过程:
/abc
“。/var/lib/docker/volumes
中生成一个随机目录作为 mount 源/abc
已经存在,则将数据复制到 mount 源/abc
还可以通过docker volume
命令查看 docker managed volume,不过看不到 bing mount,而且也无法知道 volume 对应的容器:
[root@localhost ~]# docker volume lsDRIVER VOLUME NAMElocal bb76558c26ec96b8f2fd9aced58105495b610119520dbe84c3f83bc347bc2209
相同点:都是 host 文件系统中的某个路径
不同点:
bind mount | docker managed volume | |
---|---|---|
volume 位置 | 可任意指定 | /var/lib/docker/volumes/… |
对已有 mount point 影响 | 隐藏并替换为 volume | 原有数据复制到 volume |
是否支持单个文件 | 支持 | 不支持,只能是目录 |
权限控制 | 可设置为只读,默认为读写权限 | 无控制,均为读写权限 |
移植性 | 移植性弱,与 host path 绑定 | 移植性强,无需指定 host 目录 |
bing mount 与 docker managed volume 均可实现在容器与 host 之间共享数据。
用 bind mount 来共享数据非常简单:直接将要共享的目录 mount 到容器。
而对与 docker managed volume,由于 volume 位于 host 中的目录,是在容器启动时才生成,所以需要使用docker cp
将共享数据拷贝到 volume 中:
第一种方法是将共享数据放在 bind mount 中,然后将其 mount 到多个容器。
另一种在容器之间共享数据的方式是使用 volume container。
volume container 是专门为其他容器提供 volume 的容器。它提供的卷可以是 bind mount,也可以是 docker managed volume。下面我们创建一个 volume container:
将容器命名为vc_data
。这里执行的是docker create
命令,这是因为 volume container 的作用只是提供数据,它本身不需要处于运行状态。容器 mount 了两个 volume:
其他容器可以通过--volumes-from
使用vc_data
这个 volume container:
volume container 的特点如下:
之前的例子中 volume container 的数据还是在 host 上,其实还可以将数据打包到镜像中,然后通过 docker managed volume 共享。通常我们称这种容器为 data-packed volume container。
首先使用下面的 Dockerfile 构建镜像:
FROM busybox:latestADD htdocs /usr/local/apache2/htdocsVOLUME /usr/local/apache2/htdocs
之后构建新镜像 datapacked:
用新镜像创建 data-packed volume container:
因为在 Dockerfile 中已经使用了VOLUME
指令,这里就不需要指定 volume 的 mount point 了。启动 httpd 容器并使用 data-packed volume container:
容器能够正确读取 volume 中的数据。data-packed volume container 是自包含的,不依赖 host 提供数据,具有很强的移植性,非常适合只使用静态数据的场景,比如应用的配置信息、web server 的静态文件等。
因为 volume 实际上是 host 文件系统中的目录和文件,所以 volume 的备份实际上是对文件系统的备份。
所有的本地镜像都存在 host 的/myregistry
目录中,我们要做的就是定期备份这个目录。
volume 的恢复也很简单,如果数据损坏了,直接用之前备份的数据拷贝到/myregistry
就可以了。
如果想使用更新版本的 Registry,就涉及到数据迁移:
docker stop
当前 Registry 容器docker run -d -p 5000:5000 -v /myregistry:/var/lib/registry registry:latest
在启用新容器前要确认新版本的默认数据路径是否发生变化。
Docker 不会销毁 bind mount。删除 bind mount 数据的工作只能由 host 负责。
对于 docker managed volume,在执行docker rm
删除容器时可以加上-v
参数,Docker 会将容器使用到的 volume 一并删除,但前提是没有容器 mount 该 volume。
如果在删除容器时没有带-v
,就会产生孤儿 volume:
可以使用docker volome rm
删除这些孤儿 volume:
如果想批量删除孤儿 volume,可以使用以下命令:
docker volume rm $(docker volume ls -q)
本章包括以下内容:
三种默认的网络模式、容器间的通信方式、容器与外部网络互连
Docker 网络从覆盖范围可分为单个 host 上的容器网络和跨多个 host 的网络,本章将主要讨论前一种,即单个 host 上的容器网络。
Docker 安装时会自动在 host 上创建三个网络,可以用docker network ls
命令查看:
顾名思义,none 网络就是什么都没有的网络。挂在 none 网络下的容器除了 lo,没有其他任何网卡。容器创建时,可以通过--network=none
指定使用 none 网络:
这种封闭的网络同时也意味着隔离,一些对安全性要求高并且不需要联网的应用可以使用 none 网络。
连接到 host 网络的容器共享 Docker host 的网络栈,容器的网络配置与 host 完全一样。可以通过--network=host
指定使用 host 网络:
在容器中可以看到 host 的所有网卡,并且连 hostname 也是 host 的。
直接使用 Docker host 的网络最大的好处就是性能,如果容器对网络传输效率有较高要求,则可以选择 host 网络。当然不便之处就是牺牲一些灵活性,比如要考虑端口冲突问题,Docker host 上已经使用的端口就不能再用了。
Docker host 的另一个用途是让容器可以直接配置 host 网络。比如某些跨 host 的网络解决方案,其本身也是以容器方式运行的,这些方案需要对网络进行配置,比如管理 iptables。
Docker 安装时会创建一个名为 docker0
的 Linux bridge。如果不指定--network
,那么创建的容器默认都会挂到docker0
上:
当前docker0
上没有任何其他网络设备,下面创建一个容器看看有什么变化:
一个新的网络接口veth28c57df
被挂到了docker0
上,veth28c57df
就是新创建容器的虚拟网卡。之后来看容器的网络配置:
容器有一个网卡eth0@if34
。实际上eth0@if34
和veth28c57df
是一对 veth pair。veth pair 是一种成对出现的特殊网络设备,可以把它们想象成由一根虚拟网线连接起来的一对网卡,网卡的一头eth0@if34
在容器中,另一头veth28c57df
挂在网桥docker0
上,其效果就是将eth0@if34
也挂在了docker0
上。
我们还可以看到,eth0@if34
已经配置了 IP 172.17.0.2
。通过docker network inspect bridge
查看 bridge 网络的配置信息:
易知,bridge 网络配置的 subnet 就是172.17.0.0/16
,并且网关是172.17.0.1
。这个网关正是docker0
的 IP 地址:
最后,当前容器网络拓扑结构如下图所示:
容器创建时,docker 会自动从172.17.0.0/16
中分配一个 IP,这里使用 16 位的掩码可以保证有足够多的 IP 地址可供容器使用。
除了 none, host, bridge 这三个自动创建的网络,用户也可以根据业务需要创建 user-defined 网络。
Docker 提供三种 user-defined 网络驱动:bridge, overlay 和 macvlan。overlay 和 macvlan 用于创建跨主机的网络,后面的章节将单独讨论。
可通过 bridge 驱动创建类似前面默认的 bridge 网络:
查看一下当前 host 的网络结构变化:
可以看到新增了一个网桥br-eaed97dc9a77
,这里eaed97dc9a77
就是新建 bridge 网络my_net
的短ID。
执行docker network inspect
查看一下my_net
的配置信息:
这里172.18.0.0/16
是 Docker 自动分配的 IP 网段。
也可以自己指定 IP 网段,只需在创建网段时指定--subnet
和--gateway
参数:
这里创建了新的 bridge 网络my_net2
,网段为172.22.16.0/24
,网关为172.22.16.1
。与之前一样,网关在my_net2
对应的网桥br-5d863e9f78b6
上:
容器要使用新的网络,需要在启动时通过--network
指定:
到目前为止,容器的 IP 都是 Docker 自动从 subnet 中分配的。除此之外,我们还可以通过--ip
参数来指定容器使用静态 IP 地址:
只有使用
--subnet
创建的网络才能指定静态 IP,否则 Docker 将会报错。
当前 docker host 的网络拓扑如下:
推荐阅读理解容器之间的连通性 - 每天5分钟玩转 Docker 容器技术(34)
容器之间可通过 IP,Docker DNS Server 或 joined 容器三种方式通信。
两个容器要能通信,必须要有属于同一个网络的网卡。满足这个条件后,容器就可以通过 IP 交互了。具体做法是在容器创建时通过--network
指定相应的网络,或者通过docker network connect
将现有容器加入到指定网络。
从 Docker 1.10 版本开始,docker daemon 实现了一个内嵌的 DNS server,使容器可以直接通过容器名通信:
docker run -it --network=my_net2 --name=bbox1 busyboxdocker run -it --network=my_net2 --name=bbox2 busybox
然后,bbox2
就可以直接 ping 到bbox1
了:
使用 docker DNS 有个限制:只能在 user-defined 网络中使用,默认的 bridge 网络是无法使用 DNS 的。
joined 容器是另一种实现容器间通信的方式。
它可以使两个或多个容器共享一个网络栈,共享网卡和配置信息,joined 容器之间可以通过127.0.0.1
直接通信。
joined 容器非常适合以下场景:
容器默认就能访问外网(容器网络以外的网络),这是通过网桥的 NAT(网络地址转换)实现的。
具体参考容器如何访问外部世界?- 每天5分钟玩转 Docker 容器技术(36)
外部网络通过端口映射访问到容器内部。
Docker 可将容器对外提供服务的端口映射到 host 的某个端口,外网通过该端口访问容器。容器启动时通过-p
参数映射端口:
容器启动后,可通过docker ps
或docker port
查看动态映射到 host 的端口。
除了映射动态端口,也可以在-p
中指定映射到 host 某个特定端口,例如可将容器的 80 端口映射到 host 的 8080 端口:
每一个映射的端口,host 都会启动一个docker-proxy
进程来处理访问容器的流量:
以0.0.0.0:32773->80/tcp
为例:
docker-proxy
监听 host 的 32773 端口10.0.2.15:32773
时,docker-proxy
转发给容器172.17.0.2:80
运行容器、容器常用操作、容器资源限制、实现容器的底层技术
docker run
是启动容器的方法。例如:
> docker run ubuntu pwd/
容器启动时执行pwd
,返回的/
是容器中的当前目录。
执行docker ps
或docker container ls
可以查看 Docker Host 中当前运行的容器,添加-a
参数会显示所有状态的容器。
若想让容器保持运行状态而不占用终端窗口,可以加上-d
参数,以后台方式启动容器:
docker run --name "my_http_server" -d httpd
我们经常需要进到容器里去做一些工作,比如查看日志、调试、启动其他进程等。有两种方法进入容器:attach 和 exec。
通过docker attach
可以连接到容器启动命令的终端,例如:
这里通过 “长ID” attach 到了容器的启动命令终端,之后看到的是echo
每隔一秒打印的信息。
可通过
Ctrl+P
然后Ctrl+Q
组合键退出 attach 终端
通过docker exec
进入相同的容器:
说明如下:
-it
以交互模式打开 pseudo-TTY,执行 bash,其结果就是打开了一个 bash 终端ps -elf
显示了容器启动进程while
以及当前的bash
进程exit
退出容器,回到 docker host
docker exec -it <container> bash|sh
是执行 exec 最常用的方式
主要区别如下:
当然,如果只是为了查看启动命令的输出,可以使用docker logs
命令:
-f
作用与tail -f
类似,能够持续打印输出。
按用途容器大致可分为两类:服务类容器和工具类容器。
-d
以后台方式启动这类容器是非常合适的。如果要排查问题,可以通过exec -it
进入容器。run -it
方式运行。执行exit
退出终端,同时容器停止。工具类容器多使用基础镜像,例如 busybox、debian、ubuntu 等。
容器运行的相关知识点:
docker run
命令行指定的命令运行结束时,容器停止。-d
参数在后台启动容器。exec -it
可进入容器并执行命令。指定容器的三种方法:
--name
为容器命名容器按用途可分为两类:
通过docker stop
可以停止运行的容器。
容器在 docker host 中实际是一个进程。docker stop
命令本质上是向该进程发送一个 SIGTERM 信号。如果想快速停止容器,可使用docker kill
命令,其作用是向容器进程发送 SIGKILL 信号。
对于处于停止状态的容器,可以通过docker start
重新启动。
docker start
会保留容器的第一次启动时的所有参数。
docker restart
可以重启容器,其作用就是依次执行docker stop
和docker start
。
容器可能会因某种错误而停止运行。对于服务类容器,我们通常希望在这种情况下容器能够自动重启。启动容器时设置--restart
就可以达到这个效果:
docker run -d --restart=always httpd
--restart=always
意味着无论容器因何种原因退出(包括正常退出),就立即重启。该参数的形式还可以是--restart=on-failure:3
,意思是如果启动进程退出代码非 0,则重启容器,最多重启3次。
有时我们只是希望暂时让容器暂停工作一段时间,比如要对容器的文件系统打个快照,或者 docker host 需要使用 CPU,这时可以执行docker pause
。
处于暂停状态的容器不会占用 CPU 资源,直到通过
docker unpause
恢复运行。
使用 docker 一段时间后,host 上可能会有大量已经退出了的容器。这些容器依然会占用 host 的文件系统资源,如果确认不会再重启此类容器,可以通过docker rm
删除。
docker rm
一次可指定多个容器。如果希望批量删除所有已经退出的容器,可以执行如下命令:
docker rm -v $(docker ps -aq -f status=exited)
docker rm
是删除容器,而docker rmi
则是删除镜像。
有两点需要补充:
1) 可以先创建容器,稍后再启动:
> docker create httpd> docker start 989e12e4d8ea
docker create
创建的容器处于 Created 状态docker start
将以后台方式启动容器。docker run
命令实际上是docker create
和docker start
命令的组合2) 只有当容器的启动进程退出时,--restart
才生效。
退出包括正常退出或者非正常退出。这里举了两个例子:启动进程正常退出或发生 OOM,此时 docker 会根据--restart
的策略判断是否需要重启容器。但如果容器是因为执行docker stop
或docker kill
退出,则不会自动重启。
与操作系统类似,容器可使用的内存包括两部分:物理内存和 swap。Docker 通过下面两组参数来控制容器内存的使用量:
-m
或--memory
:设置内存的使用限额,例如100M
、2G
--memory-swap
:设置 内存+swap 的使用限额例如执行如下命令:
docker run -m 200M --memory-swap=300M ubuntu
其含义是允许该容器最多使用 200M 的内存和 100M 的 swap。默认情况下,上面两组参数为 -1,即对容器内存和 swap 的使用没有限制。
可使用progrium/stress
镜像来实验一下,该镜像可用于对容器执行压力测试。执行如下命令:
docker run -it -m 200M --memory-swap=300M progrium/stress --vm 1 --vm-bytes 280M
其中:
--vm 1
:启动 1 个内存工作线程--vm-bytes 280M
:每个线程分配 280M 内存注意:如果在启动容器时只指定-m
而不指定--memory-swap
,那么--memory-swap
默认为-m
的两倍。
默认设置下,所有容器可以平等地使用 host CPU 资源并且没有限制。
Docker 可以通过-c
或--cpu-shares
设置容器使用 CPU 的权重。如果不指定,默认值为 1024。
与内存限额不同,通过-c
设置的 cpu share 并不是 CPU 资源的绝对数量,而是一个相对的权重值。某个容器最终能分配到的 CPU 资源取决于它的 cpu share 占所有容器 cpu share 总和的比例。
即:通过 cpu share 可以设置容器使用 CPU 的优先级。
例如在 host 中启动了两个容器:
docker run --name "container_A" -c 1024 ubuntudocker run --name "container_B" -c 512 ubuntu
container_A 的 cpu share 1024,是 container_B 的两倍。当两个容器都需要 CPU 资源时,container_A 可以得到的 CPU 是 container_B 的两倍。
这种按权重分配 CPU 只会发生在 CPU 资源紧张的情况下。如果 container_A 处于空闲状态,这时,为了充分利用 CPU 资源,container_B 也可以分配到全部可用的 CPU。
Block IO 是另一种可以限制容器使用的资源。Block IO 指的是磁盘的读写,docker 可通过设置权重、限制 bps 和 iops 的方式控制容器读写磁盘的带宽。
目前 Block IO 限额只对 direct IO(不使用文件缓存)有效
默认情况下,所有容器能平等地读写磁盘,可以通过设置--blkio-weight
参数来改变容器 block IO 的优先级。
--blkio-weight
与--cpu-shares
类似,设置的是相对权重值,默认为 500。在下面的例子中,container_A 读写磁盘的带宽是 container_B 的两倍:
docker run -it --name container_A --blkio-weight 600 ubuntudocker run -it --name container_B --blkio-weight 300 ubuntu
可以通过以下参数控制容器的 bps 和 iops:
--device-read-bps
:限制读某个设备的 bps--device-write-bps
:限制写某个设备的 bps--device-read-iops
:限制读某个设备的 iops--device-write-iops
:限制写某个设备的 iops例如以下命令将限制容器写/dev/sda
的速率为 30MB/s:
docker run -it --device-write-bps /dev/sda:30MB ubuntu
在容器的底层实现技术中,cgroup 和 namespace 是最重要的两种技术。cgroup 实现资源限额, namespace 实现资源隔离。
cgroup 全称 Control Group。Linux 操作系统通过 cgroup 可以设置进程使用 CPU、内存 和 IO 资源的限额。之前提到的--cpu-shares
、-m
、--device-write-bps
实际上就是在配置 cgroup,可以在 host 的/sys/fs/cgroup
中找到它。
例如,启动一个容器,设置--cpu-shares=512
:
查看容器 ID:
在/sys/fs/cgroup/cpu/docker
目录中,Linux 会为每个容器创建一个 cgroup 目录,以容器的长ID命名:
目录中包含所有与 cpu 相关的 cgroup 配置,文件cpu.shares
保存的就是--cpu-shares
的配置,值为 512。
同样的,/sys/fs/cgroup/memory/docker
和/sys/fs/cgroup/blkio/docker
中保存的是内存以及 Block IO 的 cgroup 配置。
Linux 实现容器间资源隔离的技术是 namespace。namespace 管理着 host 中全局唯一的资源,并可以让每个容器都觉得只有自己在使用它。
Linux 使用了六种 namespace,分别对应六种资源:Mount、UTS、IPC、PID、Network 和 User。
Mount namespace 让容器看上去拥有整个文件系统。
容器有自己的/
目录,可以执行 mount 和 umount 命令。当然这些操作只在当前容器中生效,不会影响到 host 和其他容器。
简单的说,UTS namespace 让容器有自己的 hostname。 默认情况下,容器的 hostname 是它的短ID,可以通过-h
或--hostname
参数设置:
IPC namespace 让容器拥有自己的共享内存和信号量(semaphore)来实现进程间通信,而不会与 host 和其他容器的 IPC 混在一起。
容器在 host 中以进程的形式运行。例如当前 host 中运行了两个容器:
通过ps axf
可以查看容器进程:
所有容器的进程都挂在 dockerd 进程下,同时也可以看到容器自己的子进程。 如果我们进入到某个容器,ps
就只能看到自己的进程了:
而且进程的 PID 不同于 host 中对应进程的 PID,容器中 PID=1 的进程当然也不是 host 的 init 进程。也就是说:容器拥有自己独立的一套 PID,这就是 PID namespace 提供的功能。
Network namespace 让容器拥有自己独立的网卡、IP、路由等资源。
User namespace 让容器能够管理自己的用户,host 不能看到容器中创建的用户。
下面是容器的常用操作命令:
-it
参数-f
参数持续打印tmux 相关文章收集,待更新…
Ctrl-b
+z
:最大化当前面板Ctrl-b
+[
:进入滚屏模式,按q
退出Ctrl-b
+o
:下一个面板Ctrl-b
+Ctrl-o
:旋转所有面板:resize-pane -R 20
]]>
Glances 相关文章收集
sudo yum install epel-releasesudo yum install python-pipsudo yum clean all
pip install --upgrade pip
pip install glances
glances
]]>未完待续~
接口(interface)技术主要用来描述类具有什么功能,而并不给出每个功能的具体实现。
一个类可以实现(implement)一个或多个接口,并在需要接口的地方,随时使用实现了相应接口的对象。
在 Java 程序设计语言中,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
public interface Comparable { int compareTo(Object other);}
接口中的所有方法都会自动的属于 public。因此,在接口中声明方法时,不必提供关键字 public。
除此之外,有些接口可能包含多个方法。在接口中还可以定义常量。然而,接口绝不能包含有实例域。
要将类声明为实现某个接口,需要使用关键字 implements:
class Employee implements Comparable
接口不是类,尤其不能使用 new 运算符实例化一个接口:
x = new Comparable(...); // ERROR
然而,尽管不能构造接口的对象,却能声明接口的变量:
Comparable x; // OK
接口变量必须引用实现了接口的类对象:
x = new Employee(...); // OK provided Employee implements Comparable
类似的,可以使用instanceof
检查一个对象是否实现了某个特定的接口:
if (anObject instanceof Comparable) {...}
与建立类的继承关系一样,接口也可以被扩展。
与接口中的方法都自动的被设置为 public 一样,接口中的域将被自动设为 public static final。
使用抽象类表示通用属性存在这样一个问题:每个类只能扩展于一个类;但每个类可以实现多个接口。
Java 的设计者选择了不支持多继承(multiple inheritance),其主要原因是多继承会让语言本身变得非常复杂,效率也会降低。
事实上,接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。
在 Java SE 8中,允许在接口中增加静态方法。目前为止,通常的做法都是将静态方法放在伴随类中。在标准库中,你会看到成对出现的接口和实用工具类,如Collection/Collections
或Path/Paths
。
可以为接口方法提供一个默认实现,必须用 default 修饰符标记:
public interface Comparable<T> { default int compareTo(T other) { return 0; } // By default, all elements are the same}
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,就会发生冲突。Java 解决默认方法冲突有以下两个规则:
回调(callback)是一种常见的程序设计模式。在回调中,可以指出某个特定时间发生时应该采取的动作。
例如,java.swing
包中有一个 Timer 定时器类,它需要知道调用哪一个方法,并要求传递的对象所属的类实现了java.awt.event
包的 ActionListener 接口:
package timer;/** @version 1.01 2015-05-12 @author Cay Horstmann*/import java.awt.*;import java.awt.event.*;import java.util.*;import javax.swing.*;import javax.swing.Timer; // to resolve conflict with java.util.Timerpublic class TimerTest{ public static void main(String[] args) { ActionListener listener = new TimePrinter(); // construct a timer that calls the listener // once every 10 seconds Timer t = new Timer(10000, listener); t.start(); JOptionPane.showMessageDialog(null, "Quit program?"); System.exit(0); }}class TimePrinter implements ActionListener{ public void actionPerformed(ActionEvent event) { System.out.println("At the tone, the time is " + new Date()); Toolkit.getDefaultToolkit().beep(); }}
假设我们希望按长度递增的顺序对字符串进行排序,Arrays.sort
方法还有第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了 Comparator 接口的类的实例:
public interface Comparator<T> { int compare(T first, T second);}
要按长度比较字符串,可以如下定义一个实现 Comparator
class LengthComparator implements Comparator<String> { public int compare(String first, String second) { return first.length() - second.length(); }}
具体完成比较时,需要建立一个实例:
Comparator<String> comp = new LengthComparator();if (comp.compare(words[i], words[j]) > 0) ...
略
略
关键字 extends 表示继承:
public class Manager extends Employee { ....}
在 Java 中,所有的继承都是公有继承,而没有 C++ 中的私有继承和保护继承。
关键字 extends 表明正在构造的新类派生于一个已存在的类。已存在的类称为超类(superclass)、基类(base class)或父类(parent class),新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。
子类比超类封装了更多的数据,拥有更多的功能。
可以在子类中提供一个新的方法来覆盖(override)超类中的同名方法:
public double getSalary() { double baseSalary = super.getSalary(); return baseSalary + bonus;}
super 不是一个对象的引用,不能将 super 赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
public Manager(String name, double salary, int year, int month, int day) { super(name, salary, year, month, day); bonus = 0;}
如果子类的构造器没有显式地调用超类的构造器, 则将自动地调用超类默认(没有参数) 的构造器。
如果超类没有不带参数的构造器, 并且在子类的构造器中又没有显式地调用超类的其他构造器,则 Java 编译器将报错。
继承并不仅限于一个层次,由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。在继承层次中,从某个特定类到其祖先的路径被称为该类的继承链(inheritance chain)。
一个祖先类可以拥有多个子孙继承链。另外,Java 不支持多继承。
在 Java 中,is-a 规则表明子类的每个对象也是超类的对象。它的另一种表述法是置换法则,即程序中出现超类对象的任何地方都可以用子类对象置换。
略
有时候,可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为 final 类。
public final class Executive extends Manager { ...}
类中的特定方法也可以被声明为 final,这样一来子类就不能覆盖这个方法。
final 类中的所有方法自动成为 final 方法。
public class Employee { public final String getName() { return name; }}
将方法或类声明为 final 的主要目的是:确保它们不会在子类中改变语义。
对象引用的转换语法与数值表达式的类型转换类似:
Manager boss = (Manager) staff[0];
需要注意的是:
instanceof
进行检查使用 abstract 关键字修饰类中的抽象方法:
// no implementation requiredpublic abstract String getDescription();
为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的:
public abstract class Person { ... public abstract String getDescription();}
除了抽象方法之外,抽象类还可以包含具体数据和具体方法。
在有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为 protected。
下面归纳一下 Java 用于控制可见性的 4 个访问修饰符:
Object 类是 Java 中所有类的始祖,在 Java 中每个类都是由它扩展而来的:
在 Java 中,只有基本类型(primitive types)不是对象,例如数值、字符和布尔类型的值。而所有的数组类型都扩展了 Object 类
可以使用 Object 类型的变量引用任何类型的对象:
Object obj = new Employee("Abel Su", 35000);
当然,Object 类型的变量只能用于作为各种值的通用持有者。要想对其中的内容进行具体的操作,还需要清楚对象的原始数据类型,并进行相应的类型转换:
Employee e = (Employee) obj;
Object 类中的equals
方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用。
在子类中定义equals
方法时,首先调用超类的equals
。如果检测失败,对象就不可能相等。如果超类中的域都相等,就需要比较子类中的实例域。
// java.util.Arrays// 如果两个数组长度相同,并且在对应的位置上数据元素也均相同,则返回 truestatic boolean equals(type[] a, type[] b)// java.util.Objects// 如果 a 和 b 都为 null,返回 true;如果只有其中之一为 null,则返回 false;否则返回 a.equals(b)static boolean equals(Object a, Object b)
散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的,如果 x 和 y 是两个不同的对象,那么x.hashCode()
和y.hashCode()
基本上不会相同。
字符串的散列码是由内容导出的
由于 hashCode 方法定义在 Object 类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。
在 Object 中还有一个重要的方法,就是toString
方法,它用于返回表示对象值的字符串。例如 Point 类的toString
方法将返回下面的字符串:
java.awt.Point[x=10,y=20]
绝大多数(但不是全部)的 toString 方法都遵循这样的格式:类的名字,随后是一对方括号括起来的赋值。最好通过
getClass().getName()
获得类名的字符串。
随处可见 toString 方法的主要原因是:只要对象与一个字符串通过操作符+
连接起来,Java 编译器就会自动的调用 toString 方法,以便获得这个对象的字符串描述。
ArrayList 是一个采用类型参数(type parameter)的泛型类(generic class):
ArrayList<Employee> staff = new ArrayList<Employee>();
在 Java SE 7 中,可以省去右边的类型参数:
ArrayList<Employee> staff = new ArrayList<>();
使用 add 方法可以将元素添加到数组列表中:
staff.add(new Employee("Abel Su", ...));staff.add(new Employee("Harry Potter", ...));
数组列表管理着对象引用的一个内部数组。最终,数组的全部空间有可能被用尽。如果调用 add 且内部数组已经满了,数组列表就将自动的创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
如果已经清楚或能够估计出数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity
方法:
staff.ensureCapacity(100);
这个方法调用将分配一个包含 100 个对象的内部数组。然后调用 100 次 add, 而不用重新分配空间。
另外,还可以把初始容量传递给 ArrayList 构造器:
ArrayList<Employee> staff = new ArrayList<>(100);
size
方法将返回数组列表中包含的实际元素数目。
一旦能够确认数组列表的大小不再发生变化,就可以调用trimToSize
方法,该方法会将存储区域的大小调整为当前元素数量所需要的存储空间数目,垃圾回收器(GC)将回收多余的存储空间。
使用get
和set
方法实现访问或改变数组元素的操作,而不能使用类似数组中的[]
语法格式。
void set(int index, E obj)E get(int index)void add(int index, E obj)E remove(int index) // 删除一个元素,并将后面的元素向前移动。被删除的元素由返回值返回。
假设有下面这个遗留下来的类:
public class EmployeeDB { public void update(ArrayList list) { ... } public ArrayList find(String query) { ... }}
可以将一个类型化的数组列表传递给 update 方法,而并不需要进行任何类型转换。
有时,需要将 int 这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类,这些类称为包装器(wrapper)。这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character、Void 和 Boolean(前 6 个类派生于公共的超类 Number)。
ArrayList<Integer> list = new ArrayList<>();
由于每个值分别包装在对象中,所以 ArrayList
的效率远远低于 int[] 数组 。因此,应该用它构造小型集合,此时程序员操作的方便性要比执行效率更加重要。
有一个很有用的特性,从而便于添加 int 类型的元素到 ArrayListlist.add(3)
将自动变换成:
list.add(Integer.valueOf(3));
这种变换被称为自动装箱(autoboxing)。
相反的,当将一个 Integer 对象赋给一个 int 值时,将会自动拆箱,编译器会将int n = list.get(i)
翻译成:
int n = list.get(i).intValue();
装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时, 插人必要的方法调用。虚拟机只是执行这些字节码。
用户也可以自己定义可变参数的方法,并将参数指定为任意类型, 甚至是基本类型:
public static double max(double... values) { double largest = Double.NEGATIVE_INFINITY; for (double v : values) { if (v > largest) { largest = v; } } return largest;}
然后就可以调用该方法:
double m = max(3.1, 40.4, -5);
编译器会将new double[]{3.1, 40.4, -5}
传递给max
方法。
public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE};...String s_small = Size.SMALL.toString();Size s = Enum.valueOf(Size.class, "SMALL");Size[] values = Size.values();int pos = Size.MEDIUM.ordinal(); // 返回枚举常量的位置
能够分析类能力的程序称为反射(reflective)。反射机制的功能十分强大,可以用来:
在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。 这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。
Object 类中的getClass()
方法将会返回一个 Class 类型的实例:
Employee e;...Class cl = e.getClass();
最常用的 Class 方法是getName()
,这个方法将返回类的名字:
System.out.println(e.getClass().getName() + " " + e.getName());-------Employee Harry Hacker
如果类在一个包中,包的名字也会作为类名的一部分,如
java.util.Random
还可以调用静态方法forName(className)
获得类名对应的 Class 对象:
String className = "java.util.Random";Class cl = Class.forName(className);
获得 Class 类对象的第三种方法非常简单:如果 T 是任意的 Java 类型(或 void 关键字),T.class
将代表匹配的类的对象:
Class cl1 = Random.class; // if you import java.util.*;Class cl2 = int.class;Class cl3 = Double[].class;
虚拟机为每个类型管理一个 Class 对象。因此,可以利用==
运算符实现两个类对象比较的操作:
if (e.getClass() == Employee.class) ...
另一个很有用的方法newInstance()
可以用来动态的创建一个类的实例:
String s = "java.util.Random";Object m = Class.forName(s).newInstance();
try { String name = ...; // get class name Class cl = Class.forName(name); // might throw exception do something with cl} catch (Exception e) { e.printStackTrace();}
反射机制最重要的内容——检查类的结构。
package reflection;import java.util.*;import java.lang.reflect.*;/** * This program uses reflection to print all features of a class. * @version 1.1 2004-02-21 * @author Cay Horstmann */public class ReflectionTest{ public static void main(String[] args) { // read class name from command line args or user input String name; if (args.length > 0) name = args[0]; else { Scanner in = new Scanner(System.in); System.out.println("Enter class name (e.g. java.util.Date): "); name = in.next(); } try { // print class name and superclass name (if != Object) Class cl = Class.forName(name); Class supercl = cl.getSuperclass(); String modifiers = Modifier.toString(cl.getModifiers()); if (modifiers.length() > 0) System.out.print(modifiers + " "); System.out.print("class " + name); if (supercl != null && supercl != Object.class) System.out.print(" extends " + supercl.getName()); System.out.print("\n{\n"); printConstructors(cl); System.out.println(); printMethods(cl); System.out.println(); printFields(cl); System.out.println("}"); } catch (ClassNotFoundException e) { e.printStackTrace(); } System.exit(0); } /** * Prints all constructors of a class * @param cl a class */ public static void printConstructors(Class cl) { Constructor[] constructors = cl.getDeclaredConstructors(); for (Constructor c : constructors) { String name = c.getName(); System.out.print(" "); String modifiers = Modifier.toString(c.getModifiers()); if (modifiers.length() > 0) System.out.print(modifiers + " "); System.out.print(name + "("); // print parameter types Class[] paramTypes = c.getParameterTypes(); for (int j = 0; j < paramTypes.length; j++) { if (j > 0) System.out.print(", "); System.out.print(paramTypes[j].getName()); } System.out.println(");"); } } /** * Prints all methods of a class * @param cl a class */ public static void printMethods(Class cl) { Method[] methods = cl.getDeclaredMethods(); for (Method m : methods) { Class retType = m.getReturnType(); String name = m.getName(); System.out.print(" "); // print modifiers, return type and method name String modifiers = Modifier.toString(m.getModifiers()); if (modifiers.length() > 0) System.out.print(modifiers + " "); System.out.print(retType.getName() + " " + name + "("); // print parameter types Class[] paramTypes = m.getParameterTypes(); for (int j = 0; j < paramTypes.length; j++) { if (j > 0) System.out.print(", "); System.out.print(paramTypes[j].getName()); } System.out.println(");"); } } /** * Prints all fields of a class * @param cl a class */ public static void printFields(Class cl) { Field[] fields = cl.getDeclaredFields(); for (Field f : fields) { Class type = f.getType(); String name = f.getName(); System.out.print(" "); String modifiers = Modifier.toString(f.getModifiers()); if (modifiers.length() > 0) System.out.print(modifiers + " "); System.out.println(type.getName() + " " + name + ";"); } }}
ObjectAnalyzer.java
:
package objectAnalyzer;import java.lang.reflect.AccessibleObject;import java.lang.reflect.Array;import java.lang.reflect.Field;import java.lang.reflect.Modifier;import java.util.ArrayList;public class ObjectAnalyzer{ private ArrayList<Object> visited = new ArrayList<>(); /** * Converts an object to a string representation that lists all fields. * @param obj an object * @return a string with the object's class name and all field names and * values */ public String toString(Object obj) { if (obj == null) return "null"; if (visited.contains(obj)) return "..."; visited.add(obj); Class cl = obj.getClass(); if (cl == String.class) return (String) obj; if (cl.isArray()) { String r = cl.getComponentType() + "[]{"; for (int i = 0; i < Array.getLength(obj); i++) { if (i > 0) r += ","; Object val = Array.get(obj, i); if (cl.getComponentType().isPrimitive()) r += val; else r += toString(val); } return r + "}"; } String r = cl.getName(); // inspect the fields of this class and all superclasses do { r += "["; Field[] fields = cl.getDeclaredFields(); AccessibleObject.setAccessible(fields, true); // get the names and values of all fields for (Field f : fields) { if (!Modifier.isStatic(f.getModifiers())) { if (!r.endsWith("[")) r += ","; r += f.getName() + "="; try { Class t = f.getType(); Object val = f.get(obj); if (t.isPrimitive()) r += val; else r += toString(val); } catch (Exception e) { e.printStackTrace(); } } } r += "]"; cl = cl.getSuperclass(); } while (cl != null); return r; }}
ObjectAnalyzerTest.java
:
package objectAnalyzer;import java.util.ArrayList;/** * This program uses reflection to spy on objects. * @version 1.12 2012-01-26 * @author Cay Horstmann */public class ObjectAnalyzerTest{ public static void main(String[] args) { ArrayList<Integer> squares = new ArrayList<>(); for (int i = 1; i <= 5; i++) squares.add(i * i); System.out.println(new ObjectAnalyzer().toString(squares)); }}
package arrays;import java.lang.reflect.*;import java.util.*;/** * This program demonstrates the use of reflection for manipulating arrays. * @version 1.2 2012-05-04 * @author Cay Horstmann */public class CopyOfTest{ public static void main(String[] args) { int[] a = { 1, 2, 3 }; a = (int[]) goodCopyOf(a, 10); System.out.println(Arrays.toString(a)); String[] b = { "Tom", "Dick", "Harry" }; b = (String[]) goodCopyOf(b, 10); System.out.println(Arrays.toString(b)); System.out.println("The following call will generate an exception."); b = (String[]) badCopyOf(b, 10); } /** * This method attempts to grow an array by allocating a new array and copying all elements. * @param a the array to grow * @param newLength the new length * @return a larger array that contains all elements of a. However, the returned array has * type Object[], not the same type as a */ public static Object[] badCopyOf(Object[] a, int newLength) // not useful { Object[] newArray = new Object[newLength]; System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength)); return newArray; } /** * This method grows an array by allocating a new array of the same type and * copying all elements. * @param a the array to grow. This can be an object array or a primitive * type array * @return a larger array that contains all elements of a. */ public static Object goodCopyOf(Object a, int newLength) { Class cl = a.getClass(); if (!cl.isArray()) return null; Class componentType = cl.getComponentType(); int length = Array.getLength(a); Object newArray = Array.newInstance(componentType, newLength); System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength)); return newArray; }}
java.lang.reflect.Method
:
public Object invoke(Object implicitParameter, Object[] explicitParameters)// 调用这个对象所描述的方法,传递给定参数,并返回方法的返回值// 对于静态方法,把 null 作为隐式参数传递
MethodTableTest.java
:
package methods;import java.lang.reflect.*;/** * This program shows how to invoke methods through reflection. * @version 1.2 2012-05-04 * @author Cay Horstmann */public class MethodTableTest{ public static void main(String[] args) throws Exception { // get method pointers to the square and sqrt methods Method square = MethodTableTest.class.getMethod("square", double.class); Method sqrt = Math.class.getMethod("sqrt", double.class); // print tables of x- and y-values printTable(1, 10, 10, square); printTable(1, 10, 10, sqrt); } /** * Returns the square of a number * @param x a number * @return x squared */ public static double square(double x) { return x * x; } /** * Prints a table with x- and y-values for a method * @param from the lower bound for the x-values * @param to the upper bound for the x-values * @param n the number of rows in the table * @param f a method with a double parameter and double return value */ public static void printTable(double from, double to, int n, Method f) { // print out the method as table header System.out.println(f); double dx = (to - from) / (n - 1); for (double x = from; x <= to; x += dx) { try { double y = (Double) f.invoke(null, x); System.out.printf("%10.4f | %10.4f%n", x, y); } catch (Exception e) { e.printStackTrace(); } } }}
面向对象程序设计(OOP)是当今主流的程序设计范型,它已经取代了 20 世纪 70 年代的「结构化」过程化程序设计开发技术。Java 是完全面向对象的。
类
类(class)是构造对象的模板或蓝图,由类构造(construct)对象的过程称为创建类的实例(instance)。
封装
封装(encapsulation,有时称为数据隐藏)是与对象有关的一个重要概念,它将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。
对象中的数据称为实例域(instance field),操纵数据的过程称为方法(method)。对于每个特定的类实例(对象)都有一组特定的实例域值,这些值的集合就是这个对象的当前状态(state)。
实现封装的关键在于绝对不能让类中的方法直接访问其他类的实例域。程序仅通过对象的方法与对象数据进行交互。
继承
可以通过扩展一个类来建立另外一个新的类,在扩展一个已有的类时,扩展后的新类具有所扩展的类的全部属性和方法,这个过程称为继承(inheritance)。
在 Java 中,所有的类都源自于超类 Object。
对象的三个主要特性:
识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。
在类之间,最常见的关系有:
要想使用对象,首先要构造对象,并指定其初始状态,然后对对象应用方法。
使用对象变量之前必须首先初始化。可以用新构造的对象初始化这个变量:
Date deadline = new Date();
也可以让这个变量引用一个已经存在的对象:
Date birthday = new Date();Date deadline = birthday;
现在,这两个变量将引用同一个对象:
注意:一个对象变量并没有实际包含一个对象,而仅仅引用一个对象!
在 Java 中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new 操作符的返回值也是一个引用。
可以显示的将对象变量设置为 null,表明这个对象变量目前没有引用任何对象:
deadline = null;...if (deadline != null) { System.out.println(deadline);}
局部变量不会自动的初始化为 null,而必须通过调用 new 或将它们设置为 null 进行初始化。
为了易于理解,可以将 Java 的对象变量看作 C++ 的对象指针。例如:
int id; // Java
实际上,等同于:
int* id; // C++
在 Java 中的 null 引用对应 C++ 中的 NULL 指针。如果把一个变量的值赋给另一个变量,两个变量就指向同一个日期,即它们是同一个对象的指针。
所有的 Java 对象都存储在堆中。当一个对象包含另一个对象变量时,这个变量依然包含着指向另一个堆对象的指针。另外,在 Java 中,必须使用 clone 方法获得对象的完整拷贝。
Java 标准类库中的 Date 类的实例有一个状态,即特定的时间点。时间使用距离一个固定时间点的毫秒数(可正可负)来表示,这个点就是所谓的纪元(epoch)。它是 UTC 时间1970 年 1 月 1 日 00:00:00
。
UTC 是 Coordinated Universal Time 的缩写,与 GMT(Greenwich Mean Time,格林威治时间)一样,是一种具有实践意义的科学标准时间。
Java 的类库设计者决定将保存时间与给时间点命名分开,所以标准 Java 类库分别包含了两个类:一个是用来表示时间点的 Date 类,另一个是用来表示日历表示法的 LocalDate 类。
LocalDate now = LocalDate.now();LocalDate newYearsEve = LocalDate.of(1999, 12, 31);int year = newYearsEve.getYear(); // 1999int month = newYearsEve.getMonth(); // 12int day = newYearsEve.getDay(); // 31
新日期对象也可以通过计算获得:
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);year = aThousandDaysLater.getYear(); // 2002month = aThousandDaysLater.getMonth(); // 09day = aThousandDaysLater.getDay(); // 26
更改对象状态的方法称为更改器方法(mutator method),只访问对象而不修改对象的方法称为访问器方法(accessor method)。
import java.time.*;/** * @author Cay Horstmann * @version 1.5 2015-05-08 */public class CalendarTest { public static void main(String[] args) { LocalDate date = LocalDate.now(); int month = date.getMonthValue(); int today = date.getDayOfMonth(); date = date.minusDays(today - 1); // Set to start of month DayOfWeek weekday = date.getDayOfWeek(); int value = weekday.getValue(); // 1 = Monday, ... 7 = Sunday System.out.println("Mon Tue Wed Thu Fri Sat Sun"); for (int i = 1; i < value; i++) System.out.print(" "); while (date.getMonthValue() == month) { System.out.printf("%3d", date.getDayOfMonth()); if (date.getDayOfMonth() == today) System.out.print("*"); else System.out.print(" "); date = date.plusDays(1); if (date.getDayOfWeek().getValue() == 1) System.out.println(); } if (date.getDayOfWeek().getValue() != 1) System.out.println(); }}------Mon Tue Wed Thu Fri Sat Sun 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
要想创建一个完整的 Java 程序,应该将若干类组合在一起,其中只有一个类有main
方法。
import java.time.*;/** * This program tests the Employee class. * * @author Cay Horstmann * @version 1.12 2015-05-08 */public class EmployeeTest { public static void main(String[] args) { // fill the staff array with three Employee objects Employee[] staff = new Employee[3]; staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15); staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1); staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15); // raise everyone's salary by 5% for (Employee e : staff) e.raiseSalary(5); // print out information about all Employee objects for (Employee e : staff) System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay=" + e.getHireDay()); }}class Employee { private String name; private double salary; private LocalDate hireDay; public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; hireDay = LocalDate.of(year, month, day); } public String getName() { return name; } public double getSalary() { return salary; } public LocalDate getHireDay() { return hireDay; } public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; }}------name=Carl Cracker,salary=78750.0,hireDay=1987-12-15name=Harry Hacker,salary=52500.0,hireDay=1989-10-01name=Tony Tester,salary=42000.0,hireDay=1990-03-15
在上面的示例中,一个源文件包含了两个类。许多程序员习惯于将每一个类存在一个单独的源文件中。例如,将 Employee 类存放在文件Employee.java
中, 将 EmployeeTest 类存放在文件EmployeeTest.java
中。
这种情况就有两种编译源程序的方法:
> javac Employee*.java# or> javac EmployeeTest.java
虽然第二种方法并没有显式的编译
Employee.java
,但当 Java 编译器发现EmployeeTest.java
使用了 Employee 类时会查找名为Employee.class
的文件。如果没有找到,就会自动搜索Employee.java
,然后对它进行编译。更重要的是,如果Employee.java
版本较已有的Employee.class
文件版本新,Java 编译器就会自动的重新编译这个文件。
Employee 类包含 1 个构造器、4 个方法以及 3 个实例域:
// 构造器public Employee(String n, double s, int year, int month, int day)// 方法public String getName()public double getSalary()public LocalDate getHireDay()public void raiseSalary(double byPercent)// 实例域private String name;private double salary;private LocalDate hireDay;
先来看看 Employee 类的构造器:
public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; hireDay = LocalDate.of(year, month, day);}
可以看到,构造器与类同名。在构造 Employee 类的对象时,构造器会运行,以便将实例域初始化为所希望的状态。
构造器总是伴随着 new 操作符的执行被调用,而不能对一个已经存在的对象调用构造器,来达到重新设置实例域的目的。
方法用于操作对象以及存取它们的实例域。例如:
public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise;}
将调用这个方法的对象的 salary 实例域设置为新值。看下面这个调用:
number007.raiseSalary(5);
具体将执行下列指令:
double raise = number007.salary * 5 / 100;number007.salary += raise;
raiseSalary 方法有两个参数。第一个参数称为隐式(implicit)参数,是出现在方法名前的 Emploee 类对象number007
。第二个参数是位于方法名后面括号中的数值,是一个显式(explicit)参数。
在每一个方法中,关键字 this 表示隐式参数。如果需要的话,可以使用下列方式编写 raiseSalary 方法:
public void raiseSalary(double byPercent) { double raise = this.salary * byPercent / 100; this.salary += raise;}
这样可以将实例域与局部变量明显的区分开来。
public String getName() { return name;}public double getSalary() { return salary;}public LocalDate getHireDay() { return hireDay;}
这些都是典型的访问器方法。由于它们只返回实例域值,因此又称为域访问器。
当需要获得或设置实例域值的时候,应该提供以下三项内容:
这样做有下列明显的好处:
方法可以访问所调用对象的私有数据,还可以访问其所属类的所有对象的私有数据。
在实现一个类时,由于公有数据非常危险,所以应该将所有的数据域都设置为私有的。
在 Java 中,要实现一个私有的方法,只需将关键字 public 改为 private 即可。
可以将实例域定义为 final,构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能再对它进行修改。
class Employee { private final String name; ...}
final 修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域。例如,String 类就是一个不可变的类。
如果将域定义为 static,每个类中只有一个这样的域,而每一个对象对于所有的实例域却都有自己的一份拷贝。例如,假定需要给每一个雇员赋予唯一的标识码。这里给 Employee 类添加一个实例域id
和一个静态域nextId
:
class Employee { private static int nextId = 1; private int id;}
现在,每一个 Employee 对象都有一个自己的id
域,但这个类的所有实例将共享一个nextId
域。即使没有一个 Employee 对象,静态域nextId
也存在。它属于类,而不属于任何独立的对象。
在绝大多数的面向对象程序设计语言中,静态域也被称为类域。
例如,在 Math 类中定义了一个静态常量PI
:
public class Math { ... public static final double PI = 3.14159265358979323846; ...}
另一个多次使用的静态常量是System.out
:
public class System { ... public static final PrintStream out = ...;}
静态方法是一种不能向对象实施操作的方法。例如,Math 类的pow
方法就是一个静态方法:
Math.pow(x, a);
可以认为静态方法是没有 this 参数的方法,另外静态方法可以访问自身类中的静态域。
静态方法还有另外一种常见的用途,类似 LocalDate 和 NumberFormat 的类使用静态工厂方法(factory method)来构造对象:
NumberFormat currencyFormatter = NumberFormat.getCurrencylnstance();NumberFormat percentFormatter = NumberFormat.getPercentlnstance();double x = 0.1;System.out.println(currencyFormatter.format(x)); // prints $0.10System.out.println(percentFomatter.format(x)); // prints 10%
之所以 NumberFormat 类不利用构造器完成这些操作,是因为:
main 方法也是一个静态方法,它不对任何对象进行操作。
事实上,在启动程序时还没有任何一个对象。静态的 main 方法将执行并创建程序所需要的对象。
按值调用(call by name)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。
Java 程序设计语言总是采用按值调用。
Java 提供了多种编写构造器的机制。
如果多个方法有相同的名字、不同的参数,编译器必须挑选出具体执行那个方法,这种特征叫做重载(overloading)。
StringBuilder messages = new StringBuilder();StringBuilder todoList = new StringBuilder("To do:\n");
如果编译器找不到匹配的参数,就会产生编译时错误,这个过程被称为重载解析(overloading resolution)。
如果在构造器中没有显式的给域赋予初值,那么就会被自动的赋给默认值:数值为0
,布尔值为 false,对象引用为 null。
public Employee() { name = ""; salary = 0; hireDay = LocalDate.now();}
如果在编写一个类时没有编写构造器, 那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。
如果类中提供了至少一个构造器, 但是没有提供无参数的构造器, 则在构造对象时如果没有提供参数就会被视为不合法。
初始值不一定是常量值,可以调用方法对域进行初始化:
class Employee { private static int nextId; private int id = assignId(); ... private static int assignId() { int r = nextId; nextId++; return r; } ...}
public Employee(String aName, double aSalary) { name = aName; salary = aSalary;}
当参数变量和实例域同名时,可以通过 this 访问实例域:
public Employee(String name, double salary) { this.name = name; this.salary = salary;}
如果构造器的第一个语句形如this(...)
,这个构造器将调用同一个类的另一个构造器:
public Employee(double s) { // calls Employee(String, double) this("Employee #" + nextId, s); nextId++;}
采用这种方式使用 this 关键字非常有用,这样对公共的构造器代码部分只编写一次即可。
之前已经提到两种初始化数据域的方法:
事实上,Java 还有第三种机制,称为初始化块(initialization block)。在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。
class Employee { private static int nextId; private int id; private String name; private double salary; // object initialization block { id = nextId; nextId++; } public Employee(String n, double s) { name = n; salary = s; } public Employee() { name = ""; salary = 0; } ...}
由于 Java 有自动的 GC,不需要人工回收内存,所以 Java 不支持析构器。
可以为任何一个类添加 finalize 方法,它将在垃圾回收器清除对象之前调用。
Java 允许使用包(package)将类组织起来。标准的 Java 类库分布在多个包中,包括 java.lang、java.util、java.net 等。标准的 Java 包具有一个层次结构,如同硬盘的目录嵌套一样,所有标准的 Java 包都处于 java 和 javax 包层次中。
使用包的主要原因是确保类名的唯一性,建议将公司的因特网域名以逆序的形式作为包名,并且对于不同的项目使用不同的子包,例如com.horstmann.corejava
。
java.time.LocalDate today = java.time.LocalDate.now();// orimport java.util.LocalDate;// orimport java.util.*;
import 语句不仅可以导入类,还增加了导入静态方法和静态域的功能:
import static java.lang.System.*;...out.println("Hello, world!"); // i.e., System.outexit(0); // i.e., System.exit
要想将一个类放入包中,就必须将包的名字放在源文件的开头,定义类的代码之前。
如果没有在源文件中放置 package 语句, 这个源文件中的类就被放置在一个默认包 (defaulf package) 中。默认包是一个没有名字的包。
需要将包中的文件放到与完整的包名匹配的子目录中,编译器将类文件也放在相同的目录结构中。
编译器在编译源文件的时候不检查目录结构。如果包与目录不匹配,虚拟机就找不到类。
当没有将类定义为 public 时,默认只有同一个包中的其他类才可以访问该类。
采用-classpath
或-cp
选项指定类路径:
java -classpath /home/usr/classdir:.:/home/user/archieves/archive.jar MyProg
JDK 中包含了一个很有用的工具 javadoc,它可以由源文件生成一个 HTML 文档,以专用的定界符/**...*/
标记。
javadoc -d docDirectory nameOfPackagejavadoc -d docDirectory nameOfPackage1 nameOfPackage2 ...// 文件在默认包中javadoc -d docDirectory *.java
翻译自 Get Docker CE for CentOS | Docker Docs,以 Docker 官方文档 为准
centos-extras
repository。在 CentOS 7 中这个仓库是默认启用的,如果之前有将其禁用,则需要重新启用overlay2
作为 Docker 的存储驱动旧版本的 Docker 在 CentOS 中的包名为docker
或docker-engine
。如果之前安装了 Docker 的旧版本,需要先卸载旧版 Docker 及相关依赖:
> sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-selinux \ docker-engine-selinux \ docker-engine
若yum
提示卸载成功或没有找到相关包,即可进行下一步操作。
注意:/var/lib/docker/
目录下的内容,包括镜像、容器、卷组、网络等文件将被保留。Docker CE 的新包名为docker-ce
。
有以下三种方法安装 Docker CE,可根据实际需要选择:
在首次安装 Docker CE 前需要建立 Docker repository,之后可通过仓库安装并更新 Docker。
1.安装所需软件包。yum-utils
提供了yum-config-manager
工具,存储驱动devicemapper
则依赖于device-mapper-persistent-data
和lvm2
:
> sudo yum install -y yum-utils \ device-mapper-persistent-data \ lvm2
2.使用以下命令建立stable
版本的 repository:
> sudo yum-config-manager \ --add-repo \ https://download.docker.com/linux/centos/docker-ce.repo
3.可选:启用edge
和test
仓库。这些仓库包含在docker.repo
文件中,但默认是禁用的。可以将它们与stable
仓库共同启用。
> sudo yum-config-manager --enable docker-ce-edge> sudo yum-config-manager --enable docker-ce-test
使用带--disable
参数的yum-config-manager
命令即可禁用edge
或test
仓库,使用--enable
参数则会重新启用。例如下面的命令将禁用edge
仓库:
> sudo yum-config-manager --disable docker-ce-edge
从 Docker
17.06
版本开始,stable
仓库的 releases 也会推送至edge
及test
仓库中。
点击此处查看 Docker 官方关于stable
和edge
的说明。
1.使用以下命令安装最新版 Docker CE:
> sudo yum install docker-ce
如果提示是否接受 GPG 密钥,则需验证密钥指纹是否符合下面的内容,若符合即可点击 accept 继续安装:
060A 61C5 1B55 8A7F 742B 77AA C52F EB6B 621E 9F35
如果启用了多个 Docker 仓库,并且在
yum install
或yum update
命令中没有指明版本,则会安装所有仓库中版本号最新的 Docker。
2.要安装指定版本的 Docker CE,则需要从仓库中列出所有可用的版本,再根据需要选择安装:
> yum list docker-ce --showduplicates | sort -rdocker-ce.x86_64 18.09.0.ce-1.el7.centos docker-ce-stable
此时安装包名的格式为docker-ce-<VERSION STRING>
。例如安装18.03.0
版本的 Docker CE:
> sudo yum install docker-ce-18.03.0.ce
此时 Docker 应该已经安装完成,但还没有启动。新的用户组docker
也已创建,目前为空。
3.启动 Docker:
> sudo systemctl start docker
4.运行hello-world
镜像以验证 Docker 是否正确安装:
> sudo docker run hello-worldHello from Docker!This message shows that your installation appears to be working correctly.To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal.To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bashShare images, automate workflows, and more with a free Docker ID: https://hub.docker.com/For more examples and ideas, visit: https://docs.docker.com/get-started/
如需升级 Docker CE,则可根据上述安装教程,选择安装最新版docker-ce
,即可完成升级。
如果无法使用 Docker 仓库,可以下载.rpm
安装包手动安装 Docker CE。
1.前往https://download.docker.com/linux/centos/7/x86_64/stable/Packages/,下载对应版本的 RPM 安装包。
2.使用yum
命令安装 RPM 包:
> sudo yum install /path/to/package.rpm
3.启动 Docker:
> sudo systemctl start docker
4.运行hello-world
镜像以验证 Docker 是否正确安装:
> sudo docker run hello-worldHello from Docker!This message shows that your installation appears to be working correctly.To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal.To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bashShare images, automate workflows, and more with a free Docker ID: https://hub.docker.com/For more examples and ideas, visit: https://docs.docker.com/get-started/
如需升级,则可下载新版本的 RPM 安装包,使用yum upgrade
命令升级:
> sudo yum -y upgrade /path/to/package.rpm
通过 Docker 提供的一键安装脚本可以在开发环境中快速安装 Docker CE,且无需交互。get.docker.com 及 test.docker.com 分别对应edge
和test
版本,脚本源码存放在 docker-install 仓库 中。
Docker 官方不推荐在生产环境中使用安装脚本
下面的示例将使用 get.docker.com 提供的脚本安装 Docker CE 的最新发布版本。如果要安装最新测试版本,只需将脚本替换为 test.docker.com,并将下面示例命令中的get
替换为test
:
> curl -fsSL https://get.docker.com -o get-docker.sh> sudo sh get-docker.sh<output truncated>
如果需要让非root
用户使用 Docker,则使用以下命令将用户添加至docker
用户组:
> sudo usermod -aG docker your-user
注销并重新登录,即可生效。之后启动 Docker:
> sudo systemctl start docker
运行hello-world
镜像以验证 Docker 是否正确安装:
> sudo docker run hello-worldHello from Docker!This message shows that your installation appears to be working correctly.To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal.To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bashShare images, automate workflows, and more with a free Docker ID: https://hub.docker.com/For more examples and ideas, visit: https://docs.docker.com/get-started/
> sudo yum remote docker-ce
主机上的镜像、容器、卷组以及自定义的配置文件需要手动删除:
> sudo rm -rf /var/lib/docker
]]>参考资料
public class FirstSample{ public static void main(String[] args) { System.out.println("We will not use 'Hello, World!'"); }}
使用//
或/* */
。第 3 种注释以/**
开始,以*/
结束,可以用来自动生成文档:
/** * This is the first sample program in Core Java Chapter 3 * @version 1.01 1997-03-22 * @author Gary Cornell */public class FirstSample{ public static void main(String[] args) { System.out.println("We will not use 'Hello, World!'"); }}
在 Java 中,
/* */
注释不能嵌套。
Java 是一种强类型语言,必须为每一个变量声明一种类型。在 Java 中一共有 8 种基本类型(primitive type):4 种整型、2 种浮点类型、1 种字符类型char
和 1 种用于表示真值的boolean
类型。
Java 提供了如下 4 种整型:
int 类型的正数部分正好超过 20 亿
L
0x
或0X
0
,例如010
对应八进制中的8
0b
或0B
就可以写二进制数1_000_000
表示一百万。这些下划线只是为了让人更易读,Java 编译器会把它们去除掉。浮点类型用于表示有小数部分的数值。在 Java 中有两种浮点类型:
类型 | 存储需求 | 取值范围 | 有效数字 |
---|---|---|---|
float | 4 字节 | 大约 ±3.402 823 47E+38F | 6~7 位 |
double | 8 字节 | 大约 ±1.797 693 134 862 315 70E+308 | 15 位 |
F
或f
。没有后缀F
的浮点数值默认为 double 类型。也可以在浮点数值后面添加后缀D
或d
有三个用于表示溢出和出错情况的特殊浮点数值:
Double.POSITIVE_INFINITY
Double.NEGATIVE_INFINITY
Double.NaN
不能检测一个特定值是否等于Double.NaN
,因为所有“非数值”的值都认为是不相同的。可以使用Double.isNaN
方法:
if (x == Double.NaN) // is never trueif (Double.isNaN(x)) // check whether x is "Not a Number"
浮点数值不适用于无法接收舍入误差的场景。例如,以下命令将打印出
0.8999999999999999
而不是0.9
,这种舍入误差的主要原因是浮点数值采用二进制系统表示,而在二进制系统中无法精确的表示分数1/10
:
System.out.println(2.0 - 1.1);------0.8999999999999999
如果在数值计算中不允许有任何舍入误差,就应该使用 BigDecimal 类。
char 类型的字面量值要用单引号括起来。例如:'A'
是编码值为 65 所对应的字符常量,它与"A"
不同,"A"
是包含一个字符 A 的字符串。
char 类型可以表示为十六进制,其范围从
\u0000
到\uffff
。
转义序列 | 名称 | Unicode 值 |
---|---|---|
\b | 退格 | \u0008 |
\t | 制表 | \u0009 |
\n | 换行 | \u000a |
\r | 回车 | \u000d |
\” | 双引号 | \u0022 |
\’ | 单引号 | \u0027 |
\\ | 反斜杠 | \u005c |
特别注意:
\u
,例如// Look inside c:\users
码点(code point)是指与一个编码表中的某个字符对应的代码值。在 Unicode 标准中,码点采用十六进制书写,并加上前缀
U+
,例如U+0041
就是拉丁字母 A 的码点。
在 Java 中,char 类型描述了 UTF-16 编码中的一个代码单元。
boolean(布尔)类型有两个值:false 和 true,用来判定逻辑条件。
整型值和布尔值之间不能进行相互转换。
在 Java 中,每个变量都有一个类型,变量名必须是一个以字母开头并由字母或数字构成的序列。
可以使用 Character 类的
isJavaIdentifierStart
和isJavaIdentifierPart
方法来检查那些 Unicode 字符属于 Java 中的字母。
另外,不要在代码中使用$
字符,它只用在 Java 编译器或其他工具生成的名字中。
在 Java 中,变量的声明尽可能的靠近变量第一次使用的地方,这是一种良好的程序编写风格。
声明一个变量后,必须使用赋值语句对变量进行显式初始化,使用未初始化的变量编译器会报错:
int vacationDays;System.out.println(vacationDays); // ERROR--variable not initialized
关键字 final 用来指示常量:
final double PI = 3.14;
final 表示这个变量只能被赋值一次,一旦被赋值之后,就不能够再更改了。习惯上,常量名使用全大写。
在 Java 中,经常希望某个常量可以在一个类中的多个方法使用,通常将这些常量称为类常量,可以使用关键字 static final 来修饰:
public class Main { public static final double CM_PER_INCH = 2.54; public static void main(String[] args) { double paperWidth = 8.5; double paperHeight = 11; System.out.println("Paper size in centimeters: " + paperWidth * CM_PER_INCH + " by " + paperHeight * CM_PER_INCH); }}------Paper size in centimeters: 21.59 by 27.94
类常量的定义位于
main
方法的外部。因此,在同一个类的其他方法中也可以使用这个常量。而且,如果一个常量被声明为 public,那么其他类的方法也可以使用这个常量。
当参与/
运算的两个操作数都是整数时,表示整数除法;否则,表示浮点除法。
另外,整数被 0 除将会产生一个异常,而浮点数被 0 除将会得到无穷大或 NaN 结果。
如果将一个类标记为 strictfp,那么这个类中的所有方法都要使用严格的浮点计算。
在 Math 类中,包含了各种各样的数学函数:
import static java.lang.Math.*;// 开方、乘幂、取余sqrt(x);pow(x, a);floorMod(x, y);// 三角函数sin(a);cos(a);tan(a);atan(a);atan2(y, x);// 指数及对数exp(a);log(a);log10(a);// 常量近似值Math.PI;Math.E;
高精度数值类型转换为低精度数值类型,可能会发生精度损失。
强制类型转换通过截断小数部分将浮点值转换为整型:
double x = 9.997;int nx = (int) x; // x = 9
如果想对浮点数进行舍入运算,那就需要使用Math.round()
方法:
double x = 9.997;int nx = (int) Math.round(x); // x = 10
如果试图将一个数值从一种类型强制转换为另一种类型,而又超出了目标类型的表示范围,结果就会截断成一个完全不同的值。例如,
(byte) 300
的实际值为 44。
x += 4;x += 3.5; // 将发生强制类型转换,等价于 (int)(x + 3.5)
int n = 12;n++;
由于这些运算符会改变变量的值,所以它们的操作数不能是数值。例如,
4++
就不是一个合法的语句。
后缀和前缀形式都会使变量值加 1 或减 1,但用在表达式中,二者就有区别了。前缀形式会先完成加 1,而后缀形式会使用变量原来的值:
int m = 7;int n = 7;int a = 2 * ++m; // now a is 16, m is 8int b = 2 * n++; // now b is 14, n is 8
逻辑运算符&&
和||
是按照「短路」方式来求值的:如果第一个操作数已经能够确定表达式的值,第二个操作数就不必计算了。如果用&&
运算符合并两个表达式,就可以利用这一点来避免错误:
x != 0 && 1 / x > x + y // no division by 0
另外,Java 支持三元操作符? :
:
x < y ? x : y
会返回 x 和 y 中较小的一个。
处理整型类型时,可以直接对组成整型数值的各个位完成操作,这意味着可以使用掩码技术得到整数中的各个位:
& ("and") | ("or") ^ ("XOr") ~ ("not")
利用
&
并结合使用适当的 2 的幂,可以把其他位掩掉,而只保留其中的某一位。另外,&
和|
运算符不采用「短路」方式来求值。
另外,还有>>
和<<
运算符将位模式左移或右移:
int fourthBitFromRight = (n & (1 << 3)) >> 3;
最后,>>>
运算符会用 0 填充高位,这与>>
不同,它会用符号位填充高位,不存在<<<
运算符。
可以用Integer.toBinaryString()
方法将整型类型转换类二进制字符串。
移位运算符的右操作数要完成模 32 的运算(除非左操作数是 long 类型,在这种情况下需要对右操作数模 64)。例如,
1 << 35
的值等同于1 << 3
的值即为 8。
同一个级别的运算符按照从左到右的次序进行计算(除了右结合运算符)。
运算符 | 结合性 |
---|---|
[] . () 函数调用 | 从左向右 |
! ~ ++ -- + 一元 - 一元 () 强制类型转换 new | 从右向左 |
* / % | 从左向右 |
+ - | 从左向右 |
<< >> >>> | 从左向右 |
< <= > >= instanceof | 从左向右 |
== != | 从左向右 |
& | 从左向右 |
^ | 从左向右 |
l | 从左向右 |
&& | 从左向右 |
ll | 从左向右 |
?: | 从右向左 |
= += -= *= /= %= &= != ^= <<= >>= >>>= | 从右向左 |
有时候,变量的取值只在一个有限的集合内。这时可以自定义枚举类型。枚举类型包括有限个命名的值:
enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE}Size s = Size.MEDIUM;
Size 类型的变量只能存储这个类型声明中给定的某个枚举值,或者 null 值。null 表示这个变量没有设置任何值。
Java 字符串就是 Unicode 字符序列。例如,串Java\u2122
由 5 个 Unicode 字符J
、a
、v
、a
、和™
组成。Java 没有内置的字符串类型,而是在标准 Java 类库中提供了一个预定义类 String。每个用双引号括起来的字符串都是 String 类的一个实例:
String e = ""; // an empty stringString greeting = "Hello";
String greeting = "Hello";String s = greeting.substring(0, 3);System.out.println(s);------Hel
字符串s.substring(a, b)
的长度为b-a
。
Java 语言允许使用+
号拼接两个字符串。另外如果需要把多个字符串放在一起,用一个定界符分隔,可以使用静态 join 方法:
String all = String.join(" / ", "S", "M", "L", "XL");System.out.println(s);------S / M / L / XL
String 类没有提供用于修改字符串的方法,所以在 Java 文档中将 String 类对象称为不可变字符串。虽然通过拼接来创建新字符串的效率确实不高,但是不可变字符串却有一个优点:编译器可以让字符串共享。
可以想象将各种字符串存放在公共的存储池中,字符串变量指向存储池中相应的位置。如果复制一个字符串变量,原始字符串与复制的字符串共享相同的字符。
一定不要使用
==
运算符检测两个字符串是否相等,==
只能确定两个字符串是否放置在同一个位置上。
s.equals(t);s.equalsIgnoreCase(t);
空串""
是长度为 0 的字符串。可以调用以下代码检查一个字符串是否为空:
if (str.length() == 0)if (str.equals(""))
空串是一个 Java 对象,有自己的串长度(0)和内容(空)。不过,String 变量还可以存放一个特殊的值,名为 null,表示目前没有任何对象与该变量关联。要检查一个字符串是否为空,可以使用以下条件:
if (str == null)
有时要检查一个字符串既不是 null 也不为空串,这种情况下就需要使用以下条件:
if (str != null && str.length() != 0)
Java 字符串由 char 值序列组成。
length()
方法将返回采用 UTF-16 编码表示的给定字符串所需要的代码单元数量:
String greeting = "Hello";int n = greeting.length(); // is 5.
想要得到实际的长度,即码点数量,可以调用:
int cpCount = greeting.codePointCount(0, greeting.length());
调用s.charAt(n)
将返回位置 n 的代码单元,n 介于0
和s.length()-1
之间,例如:
char first = greeting.charAt(0); // first is 'H'char last = greeting.charAt(4); // last is 'o'
要想得到第 i 个码点,则使用下列语句:
int index = greeting.offsetByCodePoints(0, i);int cp = greeting.codePointAt(index);
Java 中的 String 类包含了 50 多个方法,最常用的如下:
char charAt(int index) // 返回给定位置的代码单元int codePointAt(int index) // 返回从给定位置开始的码点int offsetByCodePoints(int startIndex, int cpCount) // 返回从 startIndex 代码点开始,位移 cpCount 后的码点索引int compareTo(String other) // 按照字典顺序,如果字符串位于 other 之前,返回一个负数IntStream codePoints() // 将这个字符串的码点作为一个流返回new String(int[] codePoints, int offset, int count) // 用数组中从 offset 开始的 count 个码点构造一个字符串boolean equals(Object other) // 如果字符串与 other 相等,返回 trueboolean equalsIgnoreCase(String other) // 如果字符串与 other 相等(忽略大小写),返回 trueboolean startsWith(String prefix)boolean endsWith(String suffix) // 如果字符串以 suffix 开头或结尾,则返回 trueint indexOf(String str)int indexOf(String str, int fromIndex)int indexOf(int cp)int indexOf(int cp, int fromIndex) // 返回与字符串 str 或代码点 cp 匹配的第一个字串的开始位置int lastIndexOf(String str)int lastIndexOf(String str, int fromIndex)int lastIndexOf(int cp)int lastIndexOf(int cp, int fromIndex) // 返回与字符串 str 或代码点 cp 匹配的最后一个子串的开始位置int length() // 返回字符串的长度int codePointCount(int startIndex, int endIndex) // 返回 startIndex 和 endIndex-1 之间的代码点数量String replace(CharSequence oldString, CharSequence newString)String substring(int beginIndex)String substring(int beginIndex, int endIndex)String toLowerCase()String toUpperCase()String trim()String join(CharSequence delimiter, CharSequence... elements)
有些时候,需要由较短的字符串构建字符串,采用字符串连接的方式达到此目的的效率比较低。每次连接字符串,都会构建一个新的 String 对象,既耗时又浪费空间。使用 StringBuilder 类可以避免这个问题发生。
如果需要用许多小段的字符串构建一个字符串,首先构建一个空的字符串构建器:
StringBuilder builder = new StringBuilder();
当每次需要添加一部分内容时,就调用append
方法:
builder.append(ch); // appends a single characterbuilder.append(str); // appends a string
在需要构建字符串时调用toString
方法,就可以得到一个 String 对象,其中包含了构建器中的字符序列:
String completedString = builder.toString();
重要方法如下:
StringBuilder() // 构造一个空的字符串构建器int length() // 返回构建器或缓冲器中的代码单元数量StringBuilder append(String str) // 追加一个字符串并返回 thisStringBuilder append(char c) // 追加一个代码单元并返回 thisStringBuilder appendCodePoint(int cp) // 追加一个代码点,并将其转换为一个或两个代码单元并返回 thisvoid setCharAt(int i, char c) // 将第 i 个代码单元设置为 cStringBuilder insert(int offset, String str) // 在 offset 位置插入一个字符串并返回 thisStringBuilder insert(int offset, Char c) // 在 offset 位置插入一个代码单元并返回 thisStringBuilder delete(int startIndex, int endIndex) // 删除偏移量从 startIndex 到 endIndex-1 的代码单元并返回 thisString toString() // 返回一个与构建器或缓冲器内容相同的字符串
要想通过控制台进行输入,首先需要构造一个 Scanner 对象,并与标准输入流 System.in 关联,Scanner 类定义在java.util
包中:
import java.util.*;/** * This program demonstrates console input. * @version 1.10 2004-02-10 * @author Cay Horstmann */public class InputTest{ public static void main(String[] args) { Scanner in = new Scanner(System.in); // get first input System.out.print("What is your name? "); String name = in.nextLine(); // get second input System.out.print("How old are you? "); int age = in.nextInt(); // display output on console System.out.println("Hello, " + name + ". Next year, you'll be " + (age + 1)); }}
常用方法如下:
Scanner (InputStream in) // 用给定的输入流创建一个 Scanner 对象String nextLine() // 读取输入的下一行内容String next() // 读取输入的下一个单词(以空格分隔)int nextInt()double nextDouble()boolean hasNext() // 检测输入中是否还有其他单词boolean hasNextInt()boolean hasNextDouble()
Java 沿用了 C 语言库函数中的printf
方法,例如:
System.out.printf("%8.2f", x);
会使用 8 个字符的宽度和小数点后两个字符的精度打印x
。
每一个以%
字符开始的格式说明符都用相应的参数替换,格式说明符尾部的转换符将指示被格式化的数值类型:f
表示浮点数,s
表示字符串,d
表示十进制整数。
用于 printf 的转换符如下所示:
转换符 | 类型 | 举例 |
---|---|---|
d | 十进制整数 | 159 |
x | 十六进制整数 | 9f |
o | 八进制整数 | 237 |
f | 定点浮点数 | 15.9 |
e | 指数浮点数 | 1.59e+01 |
g | 通用浮点数 | — |
a | 十六进制浮点数 | 0x1.fccdp3 |
s | 字符串 | Hello |
c | 字符 | H |
b | 布尔 | True |
h | 散列码 | 42628b2 |
Tx | 日期时间 | 已经过时,应改为java.time 类 |
% | 百分号 | % |
n | 与平台有关的行分隔符 | — |
另外,还可以给出控制格式化输出的各种标志:
可以使用静态的String.format
方法创建一个格式化的字符串,而不打印输出:
String string = String.format("Hello, %s, Next year, you'll be %d", name, age);
printf 方法中还有关于日期与时间的格式化选项。格式包括两个字母,以t
开始,以下表中的任意字母结束:
要想对文件进行读取,就需要用一个 File 对象来构造一个 Scanner 对象。如果文件名中包含反斜杠\
符号,就要使用转义字符\\
:
Scanner in = new Scanner(Paths.get("C:\\Users\\abel1\\IdeaProjects\\CoreJava\\src\\myfile.txt"), "UTF-8");...in.close();
要想写入文件,就需要构造一个 PrintWriter 对象。在构造器中,只需要提供文件名。如果文件不存在,则会自动创建该文件:
PrintWriter out = new PrintWriter("myfile.txt", "UTF-8");...out.close();
注意:可以构造一个带有字符串参数的 Scanner,但这个 Scanner 将字符串解释为数据,而不是文件名。例如:
Scanner in = new Scanner("myfile.txt"); // ERROR?
这个 scanner 会将参数作为包含 10 个字符的数据:
m
、y
、f
等。
当指定一个相对文件名时,文件位于 Java 虚拟机启动路径的相对位置。可以使用下面的调用方式找到路径的位置:
String dir = System.getProperty("user.dir");
如果 Scanner 和 PrintWriter 中指定的文件不存在或无法创建,就会发生异常。在已知有可能出现「输入/输出」异常的情况下,需要在main
方法中用throws
子句标记:
public static void main(String[] args) throws IOException { Scanner in = new Scanner(Path.get("myfile.txt"), "UTF-8"); ... in.close();}
常用方法如下:
Scanner(File f) // 构造一个从给定文件读取数据的 ScannerScanner(String data) // 构造一个从给定字符串读取数据的 ScannerPrintWriter(String fileName) // 构造一个将数据写入文件的 PrintWriter。文件名由参数指定static Path get(String pathname) // 根据指定的路径名构造一个 Path
条件必须用括号括起来
if (condition) { statement}
while (condition) { statement}
while 循环语句首先检测循环条件。如果希望循环体至少执行一次,则应该将检测条件放到最后:
do { statement} while (condition);
例如下面的例子,只要用户回答
N
,循环就重复执行:
import java.util.*;/** * This program demonstrates a <code>do/while</code> loop. * @version 1.20 2004-02-10 * @author Cay Horstmann */public class Retirement2{ public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.print("How much money will you contribute every year? "); double payment = in.nextDouble(); System.out.print("Interest rate in %: "); double interestRate = in.nextDouble(); double balance = 0; int year = 0; String input; // update account balance while user isn't ready to retire do { // add this year's payment and interest balance += payment; double interest = balance * interestRate / 100; balance += interest; year++; // print current balance System.out.printf("After year %d, your balance is %,.2f%n", year, balance); // ask if ready to retire and get input System.out.print("Ready to retire? (Y/N) "); input = in.next(); } while (input.equalsIgnoreCase("N")); }}
for 语句的第 1 部分通常用于对计数器初始化;第 2 部分给出每次新一轮循环执行前要检测的循环条件;第 3 部分指示如何更新计数器。
for 语句也可以看作 while 语句的一种简化形式。
for (int i = 10; i > 0; i--) { System.out.println("Counting down..." + i);}System.out.println("Blastoff!");
Scanner in = new Scanner(System.in);System.out.print("Select an option (1, 2, 3, 4) ");int choice = in.nextInt();switch (choice) { case 1: // do something break; case 2: // do something break; case 3: // do something break; case 4: // do something break; default: // bad input break;}
如果在某个 case 分支语句的末尾没有 break 语句,那么就会接着执行下一个 case 分支语句,这种情况很容易引发错误。可以在编译代码时加上-Xlint:fallthrough
选项:
javac -Xlint:fallthrough Test.java
这样一来,如果某个分支最后缺少一个 break 语句,编译器就会给出一个警告信息。
如果确实是想使用这种“直通式”(fallthrough)行为,可以为其外围方法加一个标注
@SuppressWarnings("fallthrough")
,这样就不会对这个方法生成警告了。
case 标签可以是:
String input = ...;switch (input.toLowerCase()) { case "yes": // OK since Jave SE 7 ... break; ...}
不带标签的 break 语句用于退出当前循环语句。另外,Java 还提供了一种带标签的 break 语句,用于跳出多重嵌套的循环语句:
Scanner in = new Scanner(System.in);int n;read_data:while (...) { // this loop statement is tagged with the label ... for (...) { // this inner loop is not labeled System.out.print("Enter a number >=0: "); n = in.nextInt(); if (n < 0) { // should never happen - can't go on break read_data; // break out of read_data loop } ... }}// this statement is executed immediately after the labeled breakif (n < 0) { // check for bad situation // deal with bad situation} else { // carry out normal processing}
如果输入有误,通过执行带标签的 break 跳转到带标签的语句块末尾。对于任何使用 break 语句的代码都需要检测循环是正常结束,还是由 break 跳出。
事实上,可以将标签应用到任何语句中,甚至是 if 语句或者块语句。另外需要注意,break 只能跳出语句块,而不能跳入语句块:
label:{ ... if (condition) break label; // exits block ...}// jumps here when the break statement executes
最后,还有一个 continue 语句,它将中断正常的控制流程,并将控制转移到最内层循环的首部。例如:
Scanner in = new Scanner(System.in);while (sum < goal) { System.out.print("Enter a number: "); n = in.nextInt(); if (n < 0) continue; sum += n; // not executed if n < 0}
如果n < 0
,则 continue 语句越过了当前循环体的剩余部分,立刻跳到循环首部sum < goal
。
如果将 continue 语句用于 for 循环中,就可以跳到 for 循环的更新部分:
for (count = 1; count <= 100; count++) { System.out.print("Enter a number, -1 to quit: "); n = in.nextInt(); if (n < 0) continue; sum += n; // not executed if n < 0}
如果n < 0
,则会跳到count++
语句。
还有一种带标签的 continue 语句,将跳到与标签匹配的循环首部。
如果基本的整数和浮点数精度不能满足需求,可以使用java.math
包中的两个很有用的类:BigInteger 和 BigDecimal,这两个类可以处理任意长度数字序列的数值。
import java.math.BigInteger;import java.math.BigDecimal;
使用静态的valueOf()
方法可以将普通的数值转化为大数值:
BigInteger a = BigInteger.valueOf(100);
不能直接使用算数运算符(如+
、*
)来处理大数值,而需要使用大数值类中的 add 和 multiply 方法:
import java.math.BigInteger;public class Main { public static void main(String[] args) { BigInteger a = BigInteger.valueOf(Long.MAX_VALUE); BigInteger b = BigInteger.valueOf(Long.MAX_VALUE); BigInteger sum = a.add(b); BigInteger product = a.multiply(b); System.out.printf("%d + %d = %d\n", a, b, sum); System.out.printf("%d * %d = %d", a, b, product); }}------9223372036854775807 + 9223372036854775807 = 184467440737095516149223372036854775807 * 9223372036854775807 = 85070591730234615847396907784232501249Process finished with exit code 0
BigInteger 常用方法如下:
BigInteger add(BigInteger other)BigInteger subtract(BigInteger other)BigInteger multiply(BigInteger other)BigInteger divide(BigInteger other)BigInteger mod(BigInteger other)int compareTo(BigInteger other) // 如果与另一个大整数 other 相等则返回 0,大于返回正数,小于返回负数static BigInteger valueOf(long x) // 返回值等于 x 的大整数
BigDecimal 常用方法如下:
BigDecimal add(BigDecimal other)BigDecimal subtract(BigDecimal other)BigDecimal multiply(BigDecimal other)BigDecimal divide(BigDecimal other RoundingMode mode) // 要想计算商,必须给出舍入方式。RoundingMode.HALF_UP 即为四舍五入int compareTo(BigDecimal other) // 如果与另一个大实数 other 相等则返回 0,大于返回正数,小于返回负数static BigDecimal valueOf(long x)static BigDecimal valueOf(long x, int scale) // 返回值为 x 或 x/10^scale 的一个大实数
在声明数组变量时,需要指出数组类型和数组变量名,可以使用 new 运算符创建数组:
int[] a = new int[100];
创建一个数字数组时,所有元素都初始化为0
。boolean 数组的元素会初始化为false
。对象数组的元素则初始化为一个特殊值null
,表示这些元素还未存放任何对象。
一旦创建了数组,就不能再改变它的大小。如果经常需要在运行过程中扩展数组的大小,就应该使用另一种数据结构——数组列表(ArrayList)。
for each 循环可以用来依次处理数组中的每个元素而不必为指定下标值而分心:
for (variable : collection) { statement}
collection 这一集合表达式必须是一个数组或者是一个实现了 Iterable 接口的类对象(例如 ArrayList)。
有个更简单的方式打印数组中的所有值,即利用 Arrays 类的
toString
方法:
import java.util.Arrays;...System.out.println(Arrays.toString(a));
Java 提供了一种创建数组对象并同时赋予初始值的简化书写形式:
int[] smallPrimes = {2, 3, 5, 7, 11, 13};
还可以初始化一个匿名的数组,这种表示法将创建一个数组并利用括号中提供的值进行初始化,数组的大小就是初始值个数。使用这种语法可以在不创建新变量的情况下重新初始化一个数组:
smallPrimes = new int [] {17, 19, 23, 29, 31, 37};
在 Java 中,允许将一个数组变量拷贝给另一个数组变量。这时,两个变量将引用同一个数组:
import java.io.PrintWriter;import java.util.Arrays;import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); PrintWriter out = new PrintWriter(System.out); int[] smallPrimes = new int[]{2, 3, 5, 7, 11, 13}; out.println("smallPrimes: " + Arrays.toString(smallPrimes)); int[] luckyNumbers = smallPrimes; out.println("luckyNumbers: " + Arrays.toString(luckyNumbers)); luckyNumbers[5] = 12; out.println("After Change:"); out.println("smallPrimes: " + Arrays.toString(smallPrimes)); out.println("luckyNumbers: " + Arrays.toString(luckyNumbers)); in.close(); out.close(); }}------smallPrimes: [2, 3, 5, 7, 11, 12]luckyNumbers: [2, 3, 5, 7, 11, 12]After Change:smallPrimes: [0, 0, 0, 0, 0, 0]luckyNumbers: [0, 0, 0, 0, 0, 0]
如果希望将一个数组的所有值拷贝到一个新的数组中,就要使用 Arrays 类的copyOf
方法:
int[] copiedLuckyNumbers = Arrays.copyOf(luckNumbers.length);
第 2 个参数是新数组的长度,这个方法通常用来增加数组的大小:
luckyNumbers = Arrays.copyOf(luckyNumbers, 2 * luckyNumbers.length);
如果数组元素是数值型,那么多余的元素将被赋值为 0。如果是布尔型,则将赋值为 false。相反,如果长度小于原始数组的长度,则只拷贝最前面的数据元素。
每一个 Java 应用程序都有一个带String[] args
参数的 main 方法。这个参数表明 main 方法将接收一个字符串数组,也就是命令行参数。例如:
public class Message { public static void main(String[] args) { if (args.length == 0 || args[0].equals("-h")) { System.out.print("Hello,"); } else if (args[0].equals("-g")) { System.out.print("Goodbye,"); } // print the other command-line arguments for (int i = 1; i < args.length; i++) { System.out.print(" " + args[i]); } System.out.println("!"); }}
如果使用下面的命令运行程序:
java Message -g cruel world
则 args 数组将包含以下内容:
args[0]: "-g"args[1]: "cruel"args[2]: "world"
程序将输出以下信息:
Goodbye, cruel world!
要想对数值型数组进行排序,可以使用 Arrays 类中的sort
方法。Arrays.sort()
使用了优化的快速排序算法:
int[] a = new int[10000];...Arrays.sort(a);
下面的程序用到了数组,它将产生一个抽彩游戏中的随机数组合:
import java.util.Arrays;import java.util.Scanner;/** * This program demonstrates array manipulation. * * @author Cay Horstmann * @version 1.20 2004-02-10 */public class LotteryDrawing { public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.print("How many numbers do you need to draw? "); int k = in.nextInt(); System.out.print("What is the highest number you can draw? "); int n = in.nextInt(); // fill an array with numbers 1 2 3 ... n int[] numbers = new int[n]; for (int i = 0; i < numbers.length; i++) { numbers[i] = i + 1; } // draw k numbers and put them into a second array int[] result = new int[k]; for (int i = 0; i < result.length; i++) { // make a random index between 0 and n - 1 int r = (int) (Math.random() * n); // pick the element at the random location result[i] = numbers[r]; // move the last element into the random location numbers[r] = numbers[n - 1]; n--; } // print the sorted array Arrays.sort(result); System.out.println("Bet the following combination. It'll make you rich!"); for (int r : result) { System.out.println(r); } }}------ How many numbers do you need to draw? 6What is the highest number you can draw? 49Bet the following combination. It'll make you rich!278173438
数组类 Arrays 的常用方法如下:
static String toString(type[] a)static type copyOf(type[] a, int length)static type copyOfRange(type[] a, int start, int end) // 包含 start, 不包含 endstatic void sort(type[] a) // 采用优化的快速排序static int binarySearch(type[] a, type v)static int binarySearch(type[] a, int start, int end, type v) // 二分查找值 v。成功则返回下标值,否则返回一个负数static void fill(type[] a, type v) // a 与 v 数据元素类型相同static boolean equals(type[] a, type[] b) // 如果两个数组大小相同、下标相同的元素都对应相等,则返回 true
在 Java 中,声明一个二维数组相当简单:
double[][] balance = new double[NYEARS][NRATES];
如果知道数组元素,也可以不调用 new,直接使用简化形式对多维数组进行初始化:
int[][] magicSquare = { {16, 3, 2, 13}, {5, 10, 11, 8}, {9, 6, 7, 12}, {4, 15, 14, 1}};
for each 循环语句不能自动处理二维数组的每一个元素。它是按照行,也就是一维数组处理的。要想访问二维数组magicSquare
的所有元素,需要使用两个嵌套的循环:
for (int[] row : magicSquare) { for (int value : row) { System.out.println(value); }}
另外,要想快速的打印一个二维数组的数据元素列表,可以调用 Arrays 类的deepToString
方法:
System.out.println(Arrays.deepToString(magicSquare));
Java 实际上没有多维数组,只有一维数组。多维数组被解释为「数组的数组」。
下面是一个使用数组来打印杨辉三角的例子:
/** * This program demonstrates a triangular array. * @version 1.20 2004-02-10 * @author Cay Horstmann */public class LotteryArray{ public static void main(String[] args) { final int NMAX = 10; // allocate triangular array int[][] odds = new int[NMAX + 1][]; for (int n = 0; n <= NMAX; n++) odds[n] = new int[n + 1]; // fill triangular array for (int n = 0; n < odds.length; n++) for (int k = 0; k < odds[n].length; k++) { /* * compute binomial coefficient n*(n-1)*(n-2)*...*(n-k+1)/(1*2*3*...*k) */ int lotteryOdds = 1; for (int i = 1; i <= k; i++) lotteryOdds = lotteryOdds * (n - i + 1) / i; odds[n][k] = lotteryOdds; } // print triangular array for (int[] row : odds) { for (int odd : row) System.out.printf("%4d", odd); System.out.println(); } }}------ 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 1 1 6 15 20 15 6 1 1 7 21 35 35 21 7 1 1 8 28 56 70 56 28 8 1 1 9 36 84 126 126 84 36 9 1 1 10 45 120 210 252 210 120 45 10 1
]]>经典云计算架构包括 IaaS(Infrastructure as a Service,基础设施即服务)、PaaS(Platform as a Service,平台即服务)、SaaS(Software as a Service,软件即服务)三层服务,如下图所示。
可以看出,容器技术生态系统自上而下分别覆盖了 IaaS 层和 PaaS 层所涉及的各类问题,包括资源调度、编排、部署、监控、配置管理、存储网络管理、安全、容器化应用支撑平台等。容器技术主要带来了以下几点好处:
容器云以容器为资源分割和调度的基本单位,封装整个软件运行时环境,为开发者和系统管理员提供用于构建、发布和运行分布式应用的平台。
- 当容器云专注于资源共享与隔离、容器编排与部署时,它更接近传统的 IaaS
- 当容器云渗透到应用支撑与运行时环境时,它更接近传统的 PaaS
容器云并不仅限于 Docker,基于 rkt 容器的 CoreOS 项目也是容器云。Docker 最初发布时只是一个单机下的容器管理工具,随后 Docker 公司发布了 Compose、Machine、Swarm 等编排部署工具,并收购了 Socketplane 解决集群化后的网络问题。
除了 Docker 公司之外,业界许多云计算厂商也对基于 Docker 的容器云做了巨大的投入。例如 Fleet、Flynn、Deis 以及目前成为事实主流标准的 Kubernetes,都是基于 Docker 技术构建的广为人知的容器云。
安装 Docker 的基本要求如下:
3.10
及以上安装过程可参考 CentOS 7 安装 Docker CE。
docker
命令的执行一般都需要 root 权限,因为 Docker 的命令行工具docker
与 Docker daemon 是同一个二进制文件,而 Docker daemon 负责接收并执行来自docker
的命令,它的运行需要 root 权限。同时,从 Docker0.5.2
版本开始,Docker daemon 默认绑定一个 UNIX Socket 来代替原有的 TCP 端口,该 UNIX Socket 默认是属于 root 用户的。
用户在使用 Docker 时,需要使用 Docker 命令行工具docker
与 Docker daemon 建立通信。Docker daemon 是 Docker 守护进程,负责接收并分发执行 Docker 命令。可以使用docker
或docker help
命令获取docker
的命令清单:
> dockerUsage: docker [OPTIONS] COMMANDA self-sufficient runtime for containersOptions: --config string Location of client config files (default "/root/.docker") -D, --debug Enable debug mode -H, --host list Daemon socket(s) to connect to -l, --log-level string Set the logging level ("debug"|"info"|"warn"|"error"|"fatal") (default "info") --tls Use TLS; implied by --tlsverify --tlscacert string Trust certs signed only by this CA (default "/root/.docker/ca.pem") --tlscert string Path to TLS certificate file (default "/root/.docker/cert.pem") --tlskey string Path to TLS key file (default "/root/.docker/key.pem") --tlsverify Use TLS and verify the remote -v, --version Print version information and quitManagement Commands: config Manage Docker configs container Manage containers image Manage images network Manage networks node Manage Swarm nodes plugin Manage plugins secret Manage Docker secrets service Manage services stack Manage Docker stacks swarm Manage Swarm system Manage Docker trust Manage trust on Docker images volume Manage volumes
例如可以使用docker start --help
命令来获取子命令start
的详细信息:
> docker start --helpUsage: docker start [OPTIONS] CONTAINER [CONTAINER...]Start one or more stopped containersOptions: -a, --attach Attach STDOUT/STDERR and forward signals --detach-keys string Override the key sequence for detaching a container -i, --interactive Attach container's STDIN
推荐阅读:
根据命令的用途,可将 Docker 子命令进行如下分类:
从docker
命令的使用出发,可以梳理出如下的命令结构图:
下面选择每个功能分类中常用的子命令进行用法和操作参数的解读。
docker info
命令用于检查 Docker 是否正确安装。如果 Docker 正确安装,该命令会输出 Docker 的配置信息:
> docker infoContainers: 33 Running: 20 Paused: 0 Stopped: 13Images: 23Server Version: 18.06.1-ceStorage Driver: overlay2...Kernel Version: 4.15.0-38-genericOperating System: Ubuntu 18.04.1 LTS...
docker info
命令一般结合docker version
命令使用,两者结合能够提取到足够详细的 Docker 环境信息:
> docker versionClient: Version: 18.06.1-ce API version: 1.38 Go version: go1.10.3 Git commit: e68fc7a Built: Tue Aug 21 17:24:56 2018 OS/Arch: linux/amd64 Experimental: falseServer: Engine: Version: 18.06.1-ce API version: 1.38 (minimum version 1.12) Go version: go1.10.3 Git commit: e68fc7a Built: Tue Aug 21 17:23:21 2018 OS/Arch: linux/amd64 Experimental: false
容器生命周期管理涉及容器启动、停止等功能。
docker run
命令使用方法如下:
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
docker run
命令用来基于特定的镜像创建一个容器,并依据选项来控制该容器:
> docker run ubuntu echo "Hello Docker"Hello Docker
该命令从 ubuntu 镜像启动一个容器,并执行echo
命令打印 Hello Docker。执行完echo
命令后,容器将停止运行。docker run
命令启动的容器会随机分配一个容器 IDCONTAINER ID
,用以标识该容器。
root@ubuntu:~> docker run -i -t --name mytest ubuntu:latest /bin/bashroot@eb9dda25b0fe:/>
上例中,docker run
命令启动一个容器,并为它分配一个伪终端执行/bin/bash
命令,用户可以在该伪终端与容器进行交互。其中:
-i
:表示使用交互模式,始终保持输入流开放-t
:表示分配一个伪终端,一般两个参数结合时使用-it
--name
:可以指定启动的容器的名字。若无此选项,Docker 将为容器随机分配一个名字-c
:用于给运行在容器中的所有进程分配 CPU 的 shares 值,这是一个相对权重,实际处理速度还与宿主机的 CPU 有关-m
:用于限制为容器中所有进程分配的内存总量,以 B、K、M、G 为单位-v
:用于挂载一个 volume,可以用多个-v
参数同时挂载多个 volume。volume 的格式为[host-dir]:[container-dir]:[rw|ro]
-p
:用于将容器的端口暴露给宿主机的端口,其常用格式为hostPort:containerPort
。这样外部主机就可以通过宿主机暴露的端口来访问容器内的应用对于已经存在的容器,可以通过docker start/stop/restart
命令来启动、停止和重启,一般利用CONTAINER ID
标识来确定具体容器,某些情况下也使用容器名来确定容器。
docker start
命令使用-i
选项来开启交互模式,并始终保持输入流开放。使用-a
选项来附加标准输入、输出或错误输出。此外,docker stop
和docker restart
命令使用-t
选项来设定容器停止前的等待时间。
Docker registry 是存储容器镜像的仓库,用户可以通过 Docker client 与 Docker registry 进行通信,以此来完成镜像的搜索、下载和上传等相关操作。
Docker Hub 是 Docker 公司官方提供的镜像仓库,提供镜像的公有与私有存储服务,是目前最主要的镜像来源。除此之外,用户还可以自行搭建私有服务器来实现镜像仓库功能。
用于从 Docker registry 中拉取 image 或 repository:
docker pull [OPTIONS] NAME[:TAG @DIGEST]
使用示例如下:
# 从官方 Hub 拉取 ubuntu:latest 镜像> docker pull ubuntu# 从官方 Hub 拉取指明 "ubuntu 16.04" tag 的镜像> docker pull ubuntu:16.04# 从特定的仓库拉取 ubuntu 镜像> docker pull SEL/ubuntu# 从其他服务器拉取镜像> docker pull 10.10.103.215:5000/sshd
用于将本地的 image 或 repository 推送到 Docker Hub 的公共或私有镜像库,以及私有服务器:
docker push [OPTIONS] NAME[:TAG]
使用示例如下:
> docker push SEL/ubuntu
用户可以在本地保存镜像资源,为此 Docker 提供了相应的管理子命令。
通过docker images
命令可以列出主机上的镜像,默认只列出最顶层的镜像。使用-a
选项可以显示所有镜像:
docker images [OPTIONS] [REPOSITORY[:TAG]]
使用示例如下:
> docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEubuntu 16.04 b0ef3016420a 11 days ago 117MBinfluxdb latest 623f651910b3 7 weeks ago 238MBmemcached latest 8230c836a4b3 7 weeks ago 62.2MBmongo 3.2 fb885d89ea5c 7 weeks ago 300MBmist/mailmock latest 95c29bda552f 7 weeks ago 299MBmist/docker-socat latest f00ed0eed13f 7 weeks ago 7.8MBmistce/logstash v3-3-1 0f90a36d12c8 2 months ago 730MBmistce/api v3-3-1 4a21b676352f 2 months ago 705MBmistce/nginx v3-3-1 4f55dd9b39e0 2 months ago 109MBmistce/gocky v3-3-1 ee93caf66f70 2 months ago 440MBmistce/elasticsearch-manage v3-3-1 10a48b9ea0e1 2 months ago 65.8MBmistce/ui v3-3-1 b8fdbe0ccb23 2 months ago 626MBubuntu-with-vi-dockerfile latest 74ba87f80b96 2 months ago 169MBubuntu-with-vi latest 9d2fac08719d 2 months ago 169MBubuntu latest ea4c82dcd15a 2 months ago 85.8MBcentos latest 75835a67d134 3 months ago 200MBhello-world latest 4ab4c602aa5e 4 months ago 1.84kBelasticsearch 5.6.10 73e6fdf8bd4f 4 months ago 486MBmistce/landing v3-3-1 b0e433749aa9 5 months ago 532MBkibana 5.6.10 bc661616b61c 5 months ago 389MBhello-world <none> 2cb0d9787c4d 6 months ago 1.85kBtraefik v1.5 fde722950ccf 9 months ago 49.7MBmist/swagger-ui latest 0b5230f1b6c4 10 months ago 24.8MBrabbitmq 3.6.6-management c74093aa9895 22 months ago 179MB
上例中,从REPOSITORY
属性可以判断出镜像是来自于官方镜像、私人仓库还是私有服务器。
docker rmi
命令用于删除镜像,docker rm
命令用于删除容器。它们可以同时删除多个镜像或容器,也可以按条件来删除:
docker rm [OPTIONS] CONTAINER [CONTAINER...]docker rmi [OPTIONS] IMAGE [IMAGE...]
使用
docker rmi
命令删除镜像时,如果已有基于该镜像启动的容器存在,则无法直接删除,需要首先删除启动的容器。当然,这两个子命令都提供了-f
选项,可以强制删除存在容器的镜像或启动中的容器。
作为 Docker 的核心,容器的操作是重中之重,Docker 也为用户提供了丰富的容器运维操作命令。
docker attach
命令可以连接到正在运行的容器,观察该容器的运行情况,或与容器的主进程进行交互:
docker attach [OPTIONS] CONTAINER
docker inspect
命令可以查看镜像和容器的详细信息,默认会列出全部信息,可以通过--format
参数来指定输出的模板格式,以便输出特定信息:
docker inspect [OPTIONS] CONTAINER|IMAGE [CONTAINER|IMAGE...]
具体示例如下:
> docker inspect --format='{{.NetworkSettings.IPAddress}}' ee36172.17.0.8
docker ps
命令可以查看容器的相关信息,默认只显示正在运行的容器的信息。可以查看到的信息包括CONTAINER ID
、NAMES
、IMAGE
、STATUS
、容器启动后执行的COMMAND
、创建时间CREATED
和绑定开启的端口PORTS
:
docker ps [OPTIONS]
docker ps
命令常用的选项有-a
和-l
。-a
选项可以查看所有容器,包括停止的容器。-l
选项则只查看最新创建的容器,包括不在运行中的容器。
> docker ps -aCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES8befe85aa9b2 ubuntu "/bin/bash" 4 minutes ago Exited (0) 4 minutes ago elegant_hawkingeb9dda25b0fe ubuntu:latest "/bin/bash" About an hour ago Exited (0) About an hour ago mytest33be0880de8a ubuntu "echo 'Hello Docker'" About an hour ago Exited (0) About an hour ago loving_neumann9dbd65001cc2 ubuntu "echo hello" About an hour ago Exited (0) About an hour ago zealous_mendeleevee10555e84be hello-world "/hello" About an hour ago Exited (0) About an hour ago friendly_mestorf4219345c98a0 ubuntu-with-vi-dockerfile "/bin/bash" 2 months ago Exited (0) 2 months ago ecstatic_wilson7257b9828da4 centos "/bin/bash" 2 months ago Exited (0) 2 months ago hopeful_chaplygin26119a6e11bd centos "/bin/bash" 2 months ago Exited (0) 2 months ago brave_khoranaf48bc1339340 ubuntu-with-vi "/bin/bash" 2 months ago Exited (127) 2 months ago agitated_hugle1abe6e7341ca ubuntu "/bin/bash" 2 months ago Exited (0) 2 months ago laughing_leavitt5c5eabb13be4 hello-world "/hello" 2 months ago Exited (0) 2 months ago eloquent_wiles8f2f6854078c 2cb0d9787c4d "/hello" 4 months ago Exited (0) 4 months ago goofy_sinoussi> docker ps -lCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES8befe85aa9b2 ubuntu "/bin/bash" 6 minutes ago Exited (0) 6 minutes ago elegant_hawking
docker commit
命令可以将一个容器固化为一个新的镜像:
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
提交保存时,只能选用正在运行的容器来制作新的镜像。在制作特定镜像时,直接使用
docker commit
命令只是一个临时性的辅助命令,不推荐使用。官方建议通过docker build
命令结合 Dockerfile 来创建和管理镜像。
docker events/history/logs
这 3 个命令用于查看 Docker 的系统日志信息。docker events
命令会打印出实时的系统事件。docker history
命令会打印出指定镜像的历史版本信息,即构建该镜像的每一层镜像的命令记录。docker logs
命令会打印出容器中进程的运行日志:
docker events [OPTIONS]docker history [OPTIONS] IMAGEdocker logs [OPTIONS] CONTAINER
Docker 的设计理念是希望用户能够保证一个容器只运行一个进程,即只提供一种服务。通常情况下,用户需要利用多个容器,分别提供不同的服务,并在不同容器间互连通信,最后形成一个 Docker 集群,以实现特定的功能。
基于 Docker 集群构建的应用称为 Docker App Stack,即 Docker 应用栈。
以下示例将在单台机器上利用 Docker 自带的命令行工具,搭建一个 Docker 应用栈,利用多个容器来组成一个特定的应用。
在开始搭建过程前,需要对所要搭建的应用栈进行简单的设计和描述:我们将搭建一个包含 6 个节点的 Docker 应用栈,其中包括 1 个代理节点、2 个 Web 应用节点、1 个主数据库节点及 2 个从数据库节点。应用栈具体结构如下图所示:
如图所示,HAProxy 是负载均衡代理节点。Redis 是非关系型的数据库,它由一个主数据库节点和两个从数据库节点组成。App 是应用,这里将使用 Python 语言、基于 Django 架构设计一个访问数据库的基础 Web 应用。
在搭建过程中,可以从 Docker Hub 获取现有可用的镜像,在这些镜像的基础上启动容器,按照需求进行修改来实现既定的功能。
> docker pull ubuntu> docker pull django> docker pull haproxy> docker pull redis> docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEhaproxy latest d23194a3929a 40 hours ago 72MBredis latest 5d2989ac9711 12 days ago 95MBubuntu latest 1d9c17228a9e 12 days ago 86.7MBdjango latest eb40dcf64078 2 years ago 436MB
鉴于在同一主机下搭建容器应用栈的环境,只需要完成容器互联来实现容器间的通信即可,可以采用docker run
命令的--link
选项建立容器间的互联关系。使用示例如下:
> docker run --link redis:redis --name console ubuntu bash
上例将在 ubuntu 镜像上启动一个容器,并命名为console
,同时将新启动的console
容器连接到名为redis
的容器上。
通过
--link
选项来建立容器间的连接,不但可以避免容器的 IP 和端口暴露到外网所导致的安全问题,还可以防止容器在重启后 IP 地址变化导致的访问失效,原理类似于 DNS 的域名和地址映射。
回到应用栈的搭建,应用栈各节点的连接信息如下:
redis-master
容器节点redis-slave
容器节点启动时要连接到redis-master
上redis-master
上综上所述,容器的启动顺序为:
redis-master --> redis-slave --> APP --> HAProxy
此外,为了能够从外网访问应用栈,并通过 HAProxy 节点来访问应用栈中的 App,在启动 HAProxy 容器节点时,需要利用-p
参数暴露端口给主机,即可从外网访问搭建的应用栈。以下是整个应用栈的搭建流程示例。
# 启动 Redis 容器> docker run -it --name redis-master redis /bin/bash> docker run -it --name redis-slave1 --link redis-master:master redis /bin/bash> docker run -it --name redis-slave2 --link redis-master:master redis /bin/bash# 启动 Django 容器,即应用> docker run -it --name APP1 --link redis-master:db -v ~/Projects/Django/App1:/usr/src/app django /bin/bash> docker run -it --name APP2 --link redis-master:db -v ~/Projects/Django/App2:/usr/src/app django /bin/bash# 启动 HAProxy 容器> docker run -it --name HAProxy --link APP1:APP1 --link APP2:APP2 -p 6301:6301 -v ~/Projects/HAProxy:/tmp haproxy /bin/bash
启动的容器信息可以通过docker ps
命令查看:
> docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES733e71e16ac5 haproxy "/docker-entrypoint.…" 30 seconds ago Up 29 seconds 0.0.0.0:6301->6301/tcp HAProxy3f91ac2a23a6 django "/bin/bash" 47 seconds ago Up 46 seconds APP2e94c7ff2c319 django "/bin/bash" 3 minutes ago Up 3 minutes APP15e7994e6ad59 redis "docker-entrypoint.s…" 5 minutes ago Up 4 minutes 6379/tcp redis-slave26fac6db730c3 redis "docker-entrypoint.s…" 8 minutes ago Up 8 minutes 6379/tcp redis-slave1936c426faa29 redis "docker-entrypoint.s…" 8 minutes ago Up 8 minutes 6379/tcp redis-master
至此,所有搭建应用栈所需容器的启动工作已经完成。
Redis Master 主数据库容器节点启动后,我们需要在容器中添加 Redis 的启动配置文件,以启动 Redis 数据库。
由于容器的轻量化设计,其中缺乏相应的文本编辑命令工具,这时可以利用 volume 来实现文件的创建。在容器启动时,利用
-v
参数挂载 volume,在主机和容器之间共享数据,就可以直接在主机上创建和编辑相关文件。
在利用 Redis 镜像启动容器时,镜像中已经集成了 volume 的挂载命令,通过docker inspect
命令查看redis-master
所挂载 volume 的情况:
> docker inspect --format "{{.Mounts}}" redis-master[{volume a77509a99df7d7a9d78313c1a1bb19619bac98fedadd78dbab17f072a49a905c /var/lib/docker/volumes/a77509a99df7d7a9d78313c1a1bb19619bac98fedadd78dbab17f072a49a905c/_data /data local true }]
可以发现,该 volume 在主机中的目录为/var/lib/docker/volumes/a77509a99df7d7a9d78313c1a1bb19619bac98fedadd78dbab17f072a49a905c/_data
,在容器中的目录为/data
。进入主机目录创建 Redis 的启动配置文件:
> cd /var/lib/docker/volumes/a77509a99df7d7a9d78313c1a1bb19619bac98fedadd78dbab17f072a49a905c/_data> cp <your-own-redis-dir>/redis.conf redis.conf> vim redis.conf
对于 Redis 主数据库,需要修改模板文件中的如下几个参数:
daemonize yespidfile /var/run/redis.pidprotected-mode no # 关闭保护模式
在主机创建好启动配置文件后,切换到容器中的 volume 目录,并复制redis.conf
到 Redis 的执行工作目录,然后启动 Redis 服务器:
> cd /data> cp redis.conf /usr/local/bin/> cd /usr/local/bin/> redis-server redis.conf
与redis-master
容器节点类似,在启动redis-slave
容器节点后,首先需要查看 volume 信息,然后将redis.conf
复制到对应的目录中。不同的是,对于 Redis 从数据库,需要修改如下几个参数:
daemonize yespidfile /var/run/redis.pidprotected-mode no # 关闭保护模式replicaof master 6379 # 之前是 slaveof
replicaof
参数的使用格式为replicaof <masterip> <masterport>
在主机修改好redis.conf
配置文件后,切换到容器中的/data
目录,并复制配置文件到 Redis 的执行工作目录,然后启动 Redis 服务器:
> cd /data> cp redis.conf /usr/local/bin/> cd /usr/local/bin/> redis-server redis.conf594:C 10 Jan 2019 23:10:43.936 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo594:C 10 Jan 2019 23:10:43.936 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=594, just started594:C 10 Jan 2019 23:10:43.936 # Configuration loaded
同理,可以完成对另一个 Redis Slave 容器节点的配置。至此,便完成了所有 Redis 数据库容器节点的配置。
Redis 数据库容器节点的测试
完成 Redis Master 和 Redis Slave 容器节点的配置以及服务器的启动后,可以通过启动redis-cli
来测试数据库。
首先,在redis-master
容器内,启动redis-cli
,并存储一个数据:
> redis-cli127.0.0.1:6379> info replication# Replicationrole:masterconnected_slaves:2slave0:ip=172.17.0.3,port=6379,state=online,offset=1260,lag=0slave1:ip=172.17.0.4,port=6379,state=online,offset=1260,lag=0master_replid:295c948cc1bbdf21eb49fdd8417ba5b4b76fc32bmaster_replid2:0000000000000000000000000000000000000000master_repl_offset:1260second_repl_offset:-1repl_backlog_active:1repl_backlog_size:1048576repl_backlog_first_byte_offset:1repl_backlog_histlen:1260127.0.0.1:6379> set master 936cOK127.0.0.1:6379> get master"936c"
随后,在redis-slave1
和redis-slave2
两个容器中,分别启动redis-cli
并查询先前在redis-master
数据库中存储的数据:
> redis-cli127.0.0.1:6379> info replication# Replicationrole:slavemaster_host:mastermaster_port:6379master_link_status:upmaster_last_io_seconds_ago:3master_sync_in_progress:0slave_repl_offset:1330slave_priority:100slave_read_only:1connected_slaves:0master_replid:295c948cc1bbdf21eb49fdd8417ba5b4b76fc32bmaster_replid2:0000000000000000000000000000000000000000master_repl_offset:1330second_repl_offset:-1repl_backlog_active:1repl_backlog_size:1048576repl_backlog_first_byte_offset:127repl_backlog_histlen:1204127.0.0.1:6379> get master"936c"
可以看到redis-master
主数据库中的数据已经自动同步到了两个从数据库中。至此,应用栈的数据库部分已搭建完成,并通过测试。
Django 容器启动后,需要利用 Django 框架,开发一个简单的 Web 程序。
为了访问数据库,需要在容器中安装 Python 语言的 Redis 支持包:
> pip install redis
安装完成后,验证 Redis 支持包是否安装成功:
> pythonPython 3.4.5 (default, Dec 14 2016, 18:54:20) [GCC 4.9.2] on linuxType "help", "copyright", "credits" or "license" for more information.>>> import redis>>> print(redis.__file__)/usr/local/lib/python3.4/site-packages/redis/__init__.py
如果没有报错,就说明已经可以使用 Python 语言来调用 Redis 数据库。接下来开始创建 Web 程序。以APP1
为例,首先在容器的 volume 目录/usr/src/app/
下创建 APP:
# 在容器内> cd /usr/src/app/> mkdir dockerweb> cd dockerweb/> django-admin.py startproject redisweb> lsredisweb> cd redisweb> lsmanage.py redisweb> python manage.py startapp helloworld> lshelloworld manage.py redisweb
在容器内创建好 APP 后,切换到主机的 volume 目录~/Projects/Django/App1
,进行相应的编辑来配置 APP:
# 在主机内> cd ~/Projects/Django/App1> lsdockerweb
可以看到,在容器内创建的 APP 文件在主机的 volume 目录下同样可见。之后修改helloworld
应用的视图文件views.py
:
> cd dockerweb/redisweb/helloworld> lsadmin.py __init__.py models.py views.py apps.py migrations tests.py> vim views.py
为了简化设计,只要求完成 Redis 数据库信息输出,以及从 Redis 数据库存储和读取数据的结果输出。viwes.py
文件如下:
from django.shortcuts import renderfrom django.http import HttpResponse# Create your views here.import redisdef hello(request): str = redis.__file__ str += "<br>" r = redis.Redis(host="db", port=6379, db=0) info = r.info() str += ("Set Hi <br>") r.set('Hi', 'HelloWorld-APP1') str += ("Get Hi: %s <br>" % r.get('Hi')) str += ("Redis Info: <br>") str += ("Key: Info Value") for key in info: str += ("%s: %s <br>" % (key, info[key])) return HttpResponse(str)
完成views.py
文件修改后,接下来修改redisweb
项目的配置文件setting.py
,并添加新建的helloworld
应用:
> cd ../redisweb/> ls__init__.py __pycache__ settings.py urls.py wsgi.py> vim settings.py
在settings.py
文件中的INSTALLED_APPS
选项下添加 helloworld,并修改ALLOWED_HOSTS
:
# SECURITY WARNING: don't run with debug turned on in production!DEBUG = TrueALLOWED_HOSTS = ['*']# Application definitionINSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'helloworld']
此处为了演示方便将
ALLOWED_HOSTS
设置为['*']
即允许所有连接,在实际开发环境中请勿按此设置。另外在生产环境中还需将DEBUG
选项设置为False
。
最后,修改redisweb
项目的 URL 模式文件urls.py
,它将设置访问应用的 URL 模式,并为 URL 模式调用视图函数之间的映射表:
> vim urls.py
在urls.py
文件中,引入 helloworld 应用的hello
视图,并为hello
视图添加一个urlpatterns
变量。urls.py
文件内容如下:
from django.conf.urls import urlfrom django.contrib import adminfrom helloworld.views import hellourlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^helloworld$', hello),]
在主机下修改完成这几个文件后,需要再次进入APP1
容器,在目录/usr/src/app/dockerweb/redisweb
下完成项目的生成:
> python manage.py makemigrationsNo changes detected> python manage.py migrateOperations to perform: Apply all migrations: admin, auth, contenttypes, sessionsRunning migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying sessions.0001_initial... OK> python manage.py createsuperuserUsername (leave blank to use 'root'): adminEmail address: admin@gmail.comPassword: Password (again): Superuser created successfully.
旧版本的 Django 使用
syncdb
命令来同步数据库并创建admin
账户。在新版 Django 中syncdb
命令已被移除,使用createsuperuser
命令创建管理员账户。
至此,APP1
容器的所有配置已经完成,另一个APP2
容器配置也是同样的过程,这样就完成了应用栈 APP 部分的全部配置。
在启动 APP 的 Web 服务器时,可以指定服务器的端口和 IP 地址。为了通过 HAProxy 容器节点接受外网所有的公共 IP 地址访问,实现负载均衡,需要指定服务器的 IP 地址和端口。对于APP1
使用 8001 端口,而APP2
则使用 8002 端口。同时,都使用0.0.0.0
地址。以APP1
为例,启动服务器的过程如下:
> python manage.py runserver 0.0.0.0:8001Performing system checks...System check identified no issues (0 silenced).January 11, 2019 - 03:35:58Django version 1.10.4, using settings 'redisweb.settings'Starting development server at http://0.0.0.0:8001/Quit the server with CONTROL-C.[11/Jan/2019 03:37:01] "GET /helloworld HTTP/1.1" 200 3999[11/Jan/2019 03:37:14] "GET /admin/ HTTP/1.1" 200 2779...
在完成了数据库和 APP 部分的应用栈部署后,最后部署一个 HAProxy 负载均衡代理的容器节点,所有对应用栈的访问将通过它来实现负载均衡。
首先,将 HAProxy 的启动配置文件复制进容器中。在主机的 volume 目录~/Projects/HAProxy
下,执行以下命令:
> cd ~/Projects/HAProxy> vim haproxy.cfg
其中,haproxy.cfg
配置文件的内容如下:
global log 127.0.0.1 local0 # 日志输入配置,所有日志都记录在本机,通过 local0 输出 maxconn 4096 # 最大连接数 chroot /usr/local/sbin # 改变当前工作目录 daemon # 以后台形式运行 HAProxy 实例 nbproc 4 # 启动 4 个 HAProxy 实例 pidfile /usr/local/sbin/haproxy.pid # pid 文件位置defaults log 127.0.0.1 local3 # 日志文件的输出定向 mode http # { tcp|http|health } 设定启动实例的协议类型 option dontlognull # 保证 HAProxy 不记录上级负载均衡发送过来的用于检测状态没有数据的心跳包 option redispatch # 当 serverId 对应的服务器挂掉后,强制定向到其他健康>的服务器 retries 2 # 重试 2 次连接失败就认为服务器不可用,主要通过后面的 check 检查 maxconn 2000 # 最大连接数 balance roundrobin # balance 有两个可用选项:roundrobin 和 source,其中,roundrobin 表示 # 轮询,而 source 表示 HAProxy 不采用轮询的策略,而是把来自某个 IP 的请求转发给一个固定 IP 的后端 timeout connect 5000ms # 连接超时时间 timeout client 50000ms # 客户端连接超时时间 timeout server 50000ms # 服务器端连接超时时间listen redis_proxy bind 0.0.0.0:6301 stats enable stats uri /haproxy-stats server APP1 APP1:8001 check inter 2000 rise 2 fall 5 # 你的均衡节点 server APP2 aPP2:8002 check inter 2000 rise 2 fall 5
随后,进入到容器的 volume 目录/tmp
下,将 HAProxy 的启动配置文件复制到 HAProxy 的工作目录中:
# 在容器中> cd /tmp> cp haproxy.cfg /usr/local/sbin/> cd /usr/local/sbin/> lshaproxy haproxy.cfg
接下来利用该配置文件来启动 HAProxy 代理:
> haproxy -f haproxy.cfg
另外,如果修改了配置文件的内容,需要先结束所有的 HAProxy 进程,并重新启动代理。Docker 镜像为了精简体积,本身并没有安装ps
、killall
等进程管理命令,需要手动在容器中安装:
> apt-get update> apt-get install procps # ps、pkill> apt-get install psmisc # killall> killall haproxy
至此,完成了 HAProxy 容器节点的全部部署,同时也完成了整个 Docker 应用栈的部署。
参考结构图可知,整个应用栈群的访问是通过 HAProxy 代理节点来进行的。HAProxy 在启动时通过-p 6301:6301
参数,映射了容器访问的端口到主机上,因此可在其他主机上通过本地主机的 IP 地址和端口来访问搭建好的应用栈。
首先在本地主机上进行测试。在浏览器中访问http://172.17.0.7:6301/helloworld
,可以查看来自 APP1 或 APP2 的页面内容,具体访问到的 APP 容器节点会由 HAProxy 代理进行均衡分配。其中,172.17.0.7
为 HAProxy 容器的 IP 地址。
本地测试通过后,尝试在其他主机上通过应用栈入口主机的 IP 地址和暴露的 6301 端口来访问该应用栈,即访问http://116.56.129.153:6301/helloworld
,可看到来自 APP1 或 APP2 容器节点的页面,访问http://116.56.129.153:6301/haproxy-stats
则可看到 HAProxy 的后台管理页面及统计数据。其中,116.56.129.153
为 宿主机的 IP 地址。
摘自 《深度实践 KVM》
更新中~
]]>更新中~
Linux 内核由 C 语言编写,符合 POSIX 标准。但是 Linux 内核并不能称为操作系统,内核只提供基本的设备驱动、文件管理、资源管理等功能,是 Linux 操作系统的核心组件。
Linux 内核版本有稳定版和开发版两种,内核版本号一般由 3 组数字组成,比如 2.6.18 内核版本:
2
表示目前发布的内核主版本6
表示稳定版本,如为奇数则表示开发中版本18
表示修改的次数前两组数字用于描述内核系列,可以通过uname -r
查看当前使用的内核版本。
点击查看常见的 Linux 发行版
CentOS (Community Enterprise Operating System) 最初是由一个社区主导的操作系统,其来源于另一个最重要的发行版 RHEL (Red Hat Enterprise Linux)。由于 CentOS 是免费的且得到社区的大力支持,因此得到了市场的青睐。
2014 年初,CentOS 和 Red Hat 共同宣布,CentOS 将加入 Red Hat。目前 CentOS 由红帽公司和社区共同维护。
相比与 CentOS 6,CentOS 7 的主要改进之处在于:
3.10.0
:新版本的内核将对 swap 内存空间进行压缩,显著提高了 I/O 性能;优化 KVM 虚拟化支持;开启固态硬盘和机械硬盘框架,同时使用将会提速;更新和改进了图形、音频驱动等Linux 系统的磁盘分区类型包括:
明确了分区类型的概念之后,安装 CentOS 时还需要制订一个分区方案。在 Windows 系统中,不同的分区被 C、D、E 等盘符替代。但在 Linux 系统中没有盘符的概念,不同的分区被挂载在不同的目录下,目录称为挂载点。只要进入挂载点目录就进入了相应的分区,这样做的好处是用户可以根据需求为某个目录单独扩展空间。
一个最简单的分区方案如下:
/boot
:创建一个约 300MB~500MB 的分区挂载到/boot
目录下,这个分区主要用来存放系统引导时使用的文件。swap
:这个分区没有挂载点,大小通常为内存的 2 倍。系统运行时,当物理内存不足时,系统会将内存中不常用的数据存放到 swap 中,即 swap 此时被当作虚拟内存。/
:根分区的挂载点是/
,这个目录是系统的起点,可以将剩余的空间都分到这个分区中。/home
:用户的家目录,可根据实际需求划分适当容量的分区空间。对于普通用户而言,直接对硬盘分区然后挂载这种使用静态分区的方法几乎没有什么问题。但对于某些特定的生产环境而言,这种方法弊大于利。
例如要求不间断运行的数据库中心,这类服务会随着时间增加而逐渐占用大量硬盘空间。如果使用静态分区方案,这类服务会在硬盘空间耗尽后自动停止,即使运维工程师及早发现,也会在更换硬盘时停止服务。因此这类要求不间断运行的服务,最好不要使用静态分区方案。
为了防止需要不间断运行的服务因硬盘空间耗尽而停止,应该采用更加先进的逻辑卷管理(Logical Volume Manager,LVM)方案。LVM 先将硬盘分区转化为物理卷 PV,然后将 PV 组成卷组 VG,然后在卷组的基础上再划分逻辑卷 LV,最后就可以使用逻辑卷来存放数据。
使用 LVM 有以下优点:
安装 CentOS 7 可搜索网上教程,此处略
如果想切换到命令模式,可在进入系统后在终端输入init 3
,即可完成运行级别的转变。Linux 运行级别如下表所示:
级别 | 说明 |
---|---|
0 | 停机 |
1 | 单用户模式 |
2 | 多用户模式 |
3 | 完全多用户模式,服务器一般运行在此级别 |
4 | 一般不用,仅在一些特殊情况下使用 |
5 | X11 模式,一般发行版的默认运行级别,可以启动图形桌面系统 |
6 | 重新启动 |
推荐阅读
Linux 的目录类似树形结构,任何目录、文件和设备都在根目录/
之下。
Linux 常见目录如下:
路径 | 说明 |
---|---|
/ | 根目录,文件系统的最顶端。 |
/bin | 存放系统所需的重要命令。另外 /usr/bin 也存放了一些系统命令,这些命令对应的文件都是可执行的。 |
/boot | 存放 Linux 启动时内核及引导系统程序所需的核心文件。内核文件和 grub 系统引导管理器都位于此目录。 |
/dev | 存放 Linux 系统下的设备文件。访问该目录下的某个文件相当于访问某个硬件设备,常用的是挂载光驱。 |
/etc | 一般存放系统的配置文件,作为一些软件启动默认配置文件的读取目录,例如 /etc/fstab 存放系统分区信息。 |
/home | 系统默认的用户主目录,可以用 HOME 环境变量表示当前用户的主目录。 |
/lib | 主要存放动态链接库.so 文件,在 64 位系统中还有 /lib64 目录。类似的目录有 /usr/lib、usr/local/lib 等。 |
/lost_found | 存放一些当系统意外崩溃或意外关机时产生的文件碎片。 |
/mnt | 用于存放挂载存储设备的目录,如光驱、共享存储等。 |
/proc | 存放操作系统运行时的运行信息,如进程信息、内核信息、网络信息等。此目录的内容存在于内存中,实际不占用磁盘空间。如 /proc/cpuinfo 存放 CPU 的相关信息。 |
/root | Linux 超级权限用户 root 的主目录。 |
/sbin | 存放一些系统管理命令,一般只能由 root 用户执行。大多数命令普通用户无权限执行,类似 /sbin/ifconfig,不过使用绝对路径也可执行。类似的目录有 /usr/sbin、/usr/local/sbin。 |
/tmp | 临时文件目录,任何人都可以访问。系统软件或用户运行程序时产生的临时文件都存放在这里。此目录数据需要定期清除。此目录空间不宜过小。 |
/usr | 应用程序存放目录,如命令、帮助文件等。安装 Linux 软件包会默认安装到 /usr/local 目录下,例如 /usr/share/fonts 存放系统字体,/usr/share/man 存放帮助文档,/usr/include 存放软件的头文件等。/usr/local 目录建议单独分区并设置较大的磁盘空间。 |
/var | 这个目录的内容是经常变动的,/var/log 用于存放系统日志,/var/lib 存放系统库文件等。 |
/sys | 目录与/proc 类似,是一个虚拟的文件系统,主要记录与系统核心相关的信息,如系统当前已经载入的模块信息等。类似的,这个目录实际不占用磁盘空间。 |
ping 用来测试目标主机或域名是否可达。
> ping abelsu7.top> ping 192.168.3.100> ping -c 3 192.168.3.100 # -c 指定次数> ping -c 3 -i 0.01 192.168.3.100 # -i 指定间隔
ifconfig 命令用于查看、配置、启用或禁用指定网络接口。语法如下:
> ifconfig interface [[-net -host] address [parameters]]
例如:
> ifconfig docker0docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255 inet6 fe80::42:77ff:fefe:d330 prefixlen 64 scopeid 0x20<link> ether 02:42:77:fe:d3:30 txqueuelen 0 (Ethernet) RX packets 5607 bytes 2293055 (2.1 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 6232 bytes 10143929 (9.6 MiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
输出信息说明:
Ethernet
(以太网)表示连接类型,ether
为网卡 MAC 地址设置 IP 地址使用以下命令:
> ifconfig eno1 192.168.100.100 netmask 255.255.255.0> ifconfig eno1 hw ether 00:0c:29:0b:07:77 # 更改网卡的 MAC 地址> ifconfig eno1 192.168.100.170/24 UP> ifconfig eno1 down
在 CentOS 和 RHEL 中使用命令
ifup
和ifdown
加网络接口名,可以启用、禁用对应的网络接口。
route 命令用于查看或编辑计算机的 IP 路由表,语法如下:
> routeKernel IP routing tableDestination Gateway Genmask Flags Metric Ref Use Ifacedefault gateway 0.0.0.0 UG 100 0 0 enp5s0116.56.129.0 0.0.0.0 255.255.255.0 U 100 0 0 enp5s0172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0192.168.122.0 0.0.0.0 255.255.255.0 U 0 0 0 virbr0# 添加一条路由:发往 192.168.60.0 网段的全部要经过网关 192.168.19.1> route add -net 192.168.60.0 netmask 255.255.255.0 gw 192.168.19.1# 删除一条路由,删除的时候不需要指明网关> route del -net 192.168.60.0 netmask 255.255.255.0
scp 命令可以将本地文件传送到远程主机或从远程主机拉取文件到本地。常用参数如下:
-P
:指定远程连接端口-q
:把进度参数关掉-r
:递归的复制整个文件夹-V
:Verbose。打印排错信息方便问题定位# 将本地文件传送至远程主机 192.168.3.100 的 /usr 路径下> scp -P 12345 myfile root@192.168.3.100:/usr# 拉取远程主机文件至本地路径> scp -P 12345 root@192.168.3.100:/etc/hosts ./# 使用参数 -r 传送目录> scp -r -P 12345 root@192.168.3.100:/usr/local/apache2 ./# 将本地目录传送至远程主机指定目录> scp -r apache2 root@192.168.3.100:/data
rsync 是 Linux 系统下常用的数据镜像备份工具,用于在不同的主机之间同步文件。
除了单个文件,rsync 还可以镜像保存整个目录树和文件系统,支持增量同步,并保持文件原有的属性(如权限、时间戳等)。
另外,rsync 的数据传输过程是加密的,可以保证数据的安全性。
]]>快学 Go 语言 - 老钱 | 知乎专栏
《快学 Go 语言》最新内容大全
代码在线运行 - 在线工具
更新中…
很多著名的计算机语言都是那么一两个人业余时间捣鼓出来的,但是 Go 语言是 Google 养着一帮团队打造出来的。这个团队非常豪华,它被称之为 Go Team,成员之一就有大名鼎鼎的 Unix 操作系统的创造者 Ken Thompson,C 语言就是他和已经过世的 Dennis Ritchie 一起发明的。
package mainimport "fmt"func main() { fmt.Println("hello world!")}
直接运行源文件main.go
:
> go run main.go
编译二进制文件:
> go build main.go
环境变量 GOPATH 指向一个目录,以后我们下载的第三方包和我们自己开发的程序代码包都要放在这个目录里面,它就是 Go 语言的工作目录。
当你在源码里使用import
语句导入一个包时,编译器都会来 GOPATH 目录下面寻找这个包。
Mac 和 Linux 用户的 GOPATH 通常设置为~/go
。将下面环境变量的设置命令追加到~/.bashrc
或~/.zshrc
的文件末尾,然后重启终端:
> export GOPATH=~/go
在 Go 语言的早期版本中,还需要用户设置 GOROOT 环境变量,指代 Go 语言开发包的目录,类似于 Java 语言里面的
JAVA_HOME
环境变量。不过后来 Go 取消了 GOROOT 的设置,也就是说用户可以不必再操心这个环境变量了,当它不存在就行。
package mainimport "fmt"func main() { var s1 int = 42 // 显式定义,可读性最强 var s2 = 42 // 编译器自动推导变量类型 s3 := 42 // 自动推导类型 + 赋值 fmt.Println(s1, s2, s3)}-------------42 42 42
- 如果一个变量很重要,建议使用第一种显式声明类型的方式来定义,比如全局变量的定义就比较偏好第一种定义方式。
- 如果要使用一个不那么重要的局部变量,就可以使用第三种,比如循环下标变量。
var
关键字无法直接写进循环条件的初始化语句中。
for i:=0; i<10; i++ { doSomething()}
如果在第一种声明变量的时候不赋初值,编译器就会自动赋予相应类型的「零值」,不同类型的零值不尽相同,比如字符串的零值不是nil
,而是空串,整型的零值就是0
,布尔类型的零值是false
。
package mainimport "fmt"func main() { var i int fmt.Println(i)}-----------0
局部变量定义在函数内部,函数调用结束就随之消亡。全局变量则定义在函数外部,在程序运行期间会一直存在。
package mainimport "fmt"var globali int = 24func main() { var locali int = 42 fmt.Println(globali, locali)}---------------24 42
内部的全局变量只有当前包内的代码可以访问,外面包的代码是不能看见的。另外,Go 语言没有静态变量。
常量关键字const
用来定义常量,可以是全局常量也可以是局部常量,大小写规则与变量一致。常量必须初始化,因为它无法二次赋值。不可以对常量进行修改,否则编译器会报错。
package mainimport "fmt"const globali int = 24func main() { const locali int = 42 fmt.Println(globali, locali)}---------------24 42
Go 语言被称为互联网时代的 C 语言,它延续使用了 C 语言的指针类型。指针符号*
和取地址符&
在功能和使用上同 C 语言几乎一模一样。同 C 语言一样,指针还支持二级指针、三级指针,不过在日常应用中很少遇到。
package mainimport "fmt"func main() { var value int = 42 var p1 *int = &value var p2 **int = &p1 var p3 ***int = &p2 fmt.Println(p1, p2, p3) fmt.Println(*p1, **p2, ***p3)}----------0xc4200160a0 0xc42000c028 0xc42000c03042 42 42
指针变量本质上就是一个整型变量,里面存储的值是另一个变量的内存地址。
*
和&
符号都只是它的语法糖,是用来在形式上方便使用和理解指针的。*
操作符存在两次内存读写,第一次获取指针变量的值,也就是内存地址,然后再去拿这个内存地址所在的变量内容。
如果普通变量是一个储物箱,那么指针变量就是另一个储物箱,这个储物箱里存放了普通变量所在储物箱的钥匙。通过多级指针来读取变量值就好比在玩一个解密游戏。
package mainimport "fmt"func main() { // 有符号整数,可以表示正负 var a int8 = 1 // 1 字节 var b int16 = 2 // 2 字节 var c int32 = 3 // 4 字节 var d int64 = 4 // 8 字节 fmt.Println(a, b, c, d) // 无符号整数,只能表示非负数 var ua uint8 = 1 var ub uint16 = 2 var uc uint32 = 3 var ud uint64 = 4 fmt.Println(ua, ub, uc, ud) // int 类型,在32位机器上占4个字节,在64位机器上占8个字节 var e int = 5 var ue uint = 5 fmt.Println(e, ue) // bool 类型 var f bool = true fmt.Println(f) // 字节类型 var j byte = 'a' fmt.Println(j) // 字符串类型 var g string = "abcdefg" fmt.Println(g) // 浮点数 var h float32 = 3.14 var i float64 = 3.141592653 fmt.Println(h, i)}-------------1 2 3 41 2 3 45 5trueabcdefg3.14 3.14159265397
另外还有几个不太常用的数据类型:
complex64
和complex128
rune
uinitptr
上面的等式并不是什么严格的数学公式,它只是对一般程序的简单认知:
Go 语言没有三元操作符a > b ? a : b
,另外分支与循环语句的条件也不需要用括号括起来。
package mainimport "fmt"func main() { fmt.Println(sign(max(min(24, 42), max(24, 42))))}func max(a int, b int) int { if a > b { return a } return b}func min(a int, b int) int { if a < b { return a } return b}func sign(a int) int { if a > 0 { return 1 } else if a < 0 { return -1 } else { return 0 }}------------1
switch 语句有两种匹配模式:一种是变量值匹配,另一种是表达式匹配。
package mainimport "fmt"func main() { fmt.Println(prize1(60)) fmt.Println(prize2(60))}// 值匹配func prize1(score int) string { switch score / 10 { case 0, 1, 2, 3, 4, 5: return "差" case 6, 7: return "及格" case 8: return "良" default: return "优" }}// 表达式匹配func prize2(score int) string { // 注意 switch 后面什么也没有 switch { case score < 60: return "差" case score < 80: return "及格" case score < 90: return "良" default: return "优" }}
Go 语言虽然没有提供 while 和 do while 语句,不过这两个语句都可以使用 for 循环的形式来模拟。平时使用 while 语句来写死循环while (true) {}
,Go 语言可以这么写:
package mainimport "fmt"func main() { for { fmt.Println("hello world!") }}
或者:
package mainimport "fmt"func main() { for true { fmt.Println("hello world!") }}
for 什么条件也不带的,相当于 loop 语句。for 带一个条件的,相当于 while 语句。for 带三个条件的就是普通的 for 语句。
package mainimport "fmt"func main() { for i := 0; i < 10; i++ { fmt.Println("hello world!") }}
Go 语言支持 continue 和 break 语句来控制循环,除此之外还支持 goto 语句。
Go 语言里面的数组其实很不常用,这是因为数组是定长静态的,一旦定义好长度就无法更改,而且不同长度的数组属于不同的类型,之间不能相互转换与赋值,用起来多有不便。
切片 (slice) 是动态的数组,是可以扩充内容增加长度的数组。当切片长度不变时,用起来和普通数组一样。当长度不同时,它们也属于相同的类型,之间可以相互赋值。这就决定了数组的应用领域都广泛的被切片取代了。
在切片的底层实现中,数组是切片的基石,是切片的特殊语法隐藏了内部的细节,让用户不能直接看到内部隐藏的数组。可以说切片是数组的一个包装。
只声明类型,不赋初值,这时编译器会给数组默认赋上「零值」。
package mainimport "fmt"func main() { var a [9]int fmt.Println(a)}------------[0 0 0 0 0 0 0 0 0]
另外三种变量定义形式如下,效果都是一样的的:
package mainimport "fmt"func main() { var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9} var b [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} c := [8]int{1, 2, 3, 4, 5, 6, 7, 8} fmt.Println(a) fmt.Println(b) fmt.Println(c)}---------------------[1 2 3 4 5 6 7 8 9][1 2 3 4 5 6 7 8 9 10][1 2 3 4 5 6 7 8]
使用下标访问数组中的元素:
package mainimport "fmt"func main() { var squares [9]int for i := 0; i < len(squares); i++ { squares[i] = (i + 1) * (i + 1) } fmt.Println(squares)}--------------------[1 4 9 16 25 36 49 64 81]
Go 语言会对数组访问下标越界进行编译器检查:
package mainimport "fmt"func main() { var a = [5]int{1,2,3,4,5} a[101] = 255 fmt.Println(a)}-----./main.go:7:3: invalid array index 101 (out of bounds for 5-element array)
而当数组下标是变量时,Go 会在编译后的代码中插入下标越界检查的逻辑,在运行时也会提示数组下标越界。所以数组的下标访问效率是要打折扣的,比不上 C 语言的数组访问性能。
package mainimport "fmt"func main() { var a = [5]int{1,2,3,4,5} var b = 101 a[b] = 255 fmt.Println(a)}------------panic: runtime error: index out of rangegoroutine 1 [running]:main.main() /Users/qianwp/go/src/github.com/pyloque/practice/main.go:8 +0x3dexit status 2
同样的子元素类型并且是同样长度的数组才可以相互赋值,否则就是不同的数组类型,不能赋值。数组的赋值本质上是一种浅拷贝操作,赋值的两个数组变量的值不会共享。
package mainimport "fmt"func main() { var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9} var b [9]int b = a a[0] = 12345 fmt.Println(a) fmt.Println(b)}--------------------------[12345 2 3 4 5 6 7 8 9][1 2 3 4 5 6 7 8 9]
从上面代码的运行结果中可以看出赋值后的两个数组并没有共享内部元素。如果数组的长度很大,那么拷贝操作是有一定的开销的,使用的时候要多加注意。
数组除了可以使用下标进行遍历之外,还可以使用range
关键字来进行遍历。range
遍历提供了下面两种形式:
package mainimport "fmt"func main() { var a = [5]int{1,2,3,4,5} for index := range a { fmt.Println(index, a[index]) } for index, value := range a { fmt.Println(index, value) }}------------0 11 22 33 44 50 11 22 33 44 5
学过 Java 语言的人会比较容易理解切片,因为它的内部结构非常类似于 ArrayList,ArrayList 的内部实现也是一个数组。当数组容量不够需要扩容时,就会换新的数组,还需要将老数组的内容拷贝到新数组。ArrayList 内部有两个非常重要的属性capacity
和length
。capacity
表示内部数组的总长度,length
表示当前已经使用的数组的长度。length
永远不能超过capacity
。
上图中的一个切片变量包含三个域,分别是底层数组的指针、切片的长度length
和切片的容量capacity
。切片支持 append 操作可以将新内容追加到底层数组,也就是填充上图中的灰色格子。如果格子满了,切片就需要扩容,底层的数组就会更换。
切片的创建有多种方式,先来看最通用的创建方法,那就是内置的 make 函数:
package mainimport "fmt"func main() { var s1 []int = make([]int, 5, 8) var s2 []int = make([]int, 8) // 满容切片 fmt.Println(s1) fmt.Println(s2)}-------------[0 0 0 0 0][0 0 0 0 0 0 0 0]
使用 make 函数创建切片,需要提供三个参数:切片的类型、切片的长度和容量。其中第三个参数是可选的,如果不声明切片的容量,那么长度和容量相等,也就是说切片是满容的。
切片和普通变量一样,也可以使用类型自动推导,省区类型定义以及var
关键字:
package mainimport "fmt"func main() { var s1 = make([]int, 5, 8) s2 := make([]int, 8) fmt.Println(s1) fmt.Println(s2)}-------------[0 0 0 0 0][0 0 0 0 0 0 0 0]
使用 make 函数创建的切片内容是「零值切片」,也就是内部数组的元素都是零值。Go 语言还提供了另一种创建切片的语法,允许我们给它赋初值,使用这种方式创建的切片是满容的:
package mainimport "fmt"func main() { var s []int = []int{1,2,3,4,5} // 满容的 fmt.Println(s, len(s), cap(s))}---------[1 2 3 4 5] 5 5
Go 语言提供了内置函数len()
和cap()
可以直接获得切片的长度和容量属性。
在创建切片时,还有两个非常特殊的情况需要考虑,那就是容量和长度都是零的切片,叫做「空切片」。这个不同与之前提到的「零值切片」。
package mainimport "fmt"func main() { var s1 []int var s2 []int = []int{} var s3 []int = make([]int, 0) fmt.Println(s1, s2, s3) fmt.Println(len(s1), len(s2), len(s3)) fmt.Println(cap(s1), cap(s2), cap(s3))}-----------[] [] []0 0 00 0 0
上面三种形式创建的切片都是「空切片」,不过在内部结构上这三种形式还是有所差异的,准确来说第一种应该称为「nil 切片」,但是二者形式上几乎一模一样,用起来差不多没有区别,所以初级用户暂时可以不必区分。
切片的赋值是一次浅拷贝操作,拷贝的是切片变量的三个域。拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容。
package mainimport "fmt"func main() { var s1 = make([]int, 5, 8) // 切片的访问和数组差不多 for i := 0; i < len(s1); i++ { s1[i] = i + 1 } var s2 = s1 fmt.Println(s1, len(s1), cap(s1)) fmt.Println(s2, len(s2), cap(s2)) // 尝试修改切片内容 s2[0] = 255 fmt.Println(s1) fmt.Println(s2)}--------[1 2 3 4 5] 5 8[1 2 3 4 5] 5 8[255 2 3 4 5][255 2 3 4 5]
从上面的输出可以看到赋值的两切片共享了底层数组。
切片在遍历的语法上和数组是一样的,除了支持下标遍历外,那就是使用 range 关键字。
package mainimport "fmt"func main() { var s = []int{1,2,3,4,5} for index := range s { fmt.Println(index, s[index]) } for index, value := range s { fmt.Println(index, value) }}--------0 11 22 33 44 50 11 22 33 44 5
之前有提到切片是动态的数组,其长度是可以变化的,可以通过追加操作来改变切片的长度。
切片每一次追加后都会形成新的切片变量,如果底层数组没有扩容,那么追加前后的两个切片变量就共享底层数组;如果底层数组扩容了,那么追加前后的底层数组是分离的不共享的。
如果底层数组是共享的,那么一个切片的内容变化就会影响到另一个切片,这点需要特别注意。
package mainimport "fmt"func main() { var s1 = []int{1,2,3,4,5} fmt.Println(s1, len(s1), cap(s1)) // 对满容的切片进行追加会分离底层数组 var s2 = append(s1, 6) fmt.Println(s1, len(s1), cap(s1)) fmt.Println(s2, len(s2), cap(s2)) // 对非满容的切片进行追加会共享底层数组 var s3 = append(s2, 7) fmt.Println(s2, len(s2), cap(s2)) fmt.Println(s3, len(s3), cap(s3))}--------------------------[1 2 3 4 5] 5 5[1 2 3 4 5] 5 5[1 2 3 4 5 6] 6 10[1 2 3 4 5 6] 6 10[1 2 3 4 5 6 7] 7 10
正是因为切片追加后是新的切片变量,所以 Go 编译器禁止追加了切片后不使用这个新的切片变量,以避免用户以为追加操作的返回值和原切片变量是同一个变量。
package mainimport "fmt"func main() { var s1 = []int{1,2,3,4,5} append(s1, 6) fmt.Println(s1)}--------------./main.go:7:8: append(s1, 6) evaluated but not used
如果真的不需要使用这个新的变量,可以将 append 的结果赋值给下划线变量_
。
下划线变量
_
是 Go 语言特殊的内置变量,它就像一个黑洞,可以将任意变量赋值给它,但是却不能读取这个特殊变量。
package mainimport "fmt"func main() { var s1 = []int{1,2,3,4,5} _ = append(s1, 6) fmt.Println(s1)}----------[1 2 3 4 5]
还需要注意的是追加虽然会导致底层数组发生扩容、更换的新的数组,但是旧数组并不会立即被销毁被回收,因为老切片还指向着旧数组。
需要仔细思考
我们刚才说切片的长度是可以变化的,为什么又说切片是只读的呢?这不是矛盾么。这是为了提醒读者注意切片追加后形成了一个新的切片变量,而老的切片变量的三个域其实并不会改变,改变的只是底层的数组。这里说的是切片的「域」是只读的,而不是说切片是只读的。切片的「域」就是组成切片变量的三个部分,分别是底层数组的指针、切片的长度和切片的容量。
切片的切割可以类比字符串的子串,它并不是要把切片割断,而是从母切片中拷贝一个子切片出来,子切片和母切片共享底层数组。
package mainimport "fmt"func main() { var s1 = []int{1,2,3,4,5,6,7} // start_index 和 end_index,不包含 end_index // [start_index, end_index) var s2 = s1[2:5] fmt.Println(s1, len(s1), cap(s1)) fmt.Println(s2, len(s2), cap(s2))}------------[1 2 3 4 5 6 7] 7 7[3 4 5] 3 5
上面的输出需要特别注意的是:既然切割前后共享底层数据,那为什么容量不一样呢?下图可以解释这个问题。
可以注意到子切片的内部数据指针指向了数组的中间位置,而不再是数组的开头了。子切片容量的大小是从中间的位置开始直到切片末尾的长度,母子切片依旧共享底层数组。
子切片语法上要提供起始和结束位置,这两个位置都是可选的。不提供起始位置,默认就是从母切片的初始位置开始(不是底层数组的初始位置)。不提供结束位置,默认就结束到母切片尾部(是长度线,不是容量线)。
package mainimport "fmt"func main() { var s1 = []int{1, 2, 3, 4, 5, 6, 7} var s2 = s1[:5] var s3 = s1[3:] var s4 = s1[:] fmt.Println(s1, len(s1), cap(s1)) fmt.Println(s2, len(s2), cap(s2)) fmt.Println(s3, len(s3), cap(s3)) fmt.Println(s4, len(s4), cap(s4))}-----------[1 2 3 4 5 6 7] 7 7[1 2 3 4 5] 5 7[4 5 6 7] 4 4[1 2 3 4 5 6 7] 7 7
上面的s1[:]
与普通的切片赋值没有区别,同样是共享底层数组,同样是浅拷贝。另外,Go 语言中切片的下标不支持负数。
对数组进行切割可以转换成切片。切片将原数组作为内部底层数组,也就是说修改了原数组会影响到新切片,对切片的修改也会影响到原数组。
package mainimport "fmt"func main() { var a = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} var b = a[2:6] fmt.Println(b) a[4] = 100 fmt.Println(b)}-------[3 4 5 6][3 4 100 6]
Go 语言还内置了一个 copy 函数,用来进行切片的深拷贝。不过其实也没那么深,只是深到底层的数组而已。如果数组里面装的是指针,比如[]*int
类型,那么指针指向的内容还是共享的。
func copy(dst, src []T) int
copy 函数不会因为原切片和目标切片的长度问题而额外分配底层数组的内存,它只负责拷贝数组的内容,从原切片拷贝到目标切片,拷贝的量是原切片和目标切片长度的较小值min(len(src), len(dst))
,函数返回的是拷贝的实际长度。
package mainimport "fmt"func main() { var s = make([]int, 5, 8) for i:=0;i<len(s);i++ { s[i] = i+1 } fmt.Println(s) var d = make([]int, 2, 6) var n = copy(d, s) fmt.Println(n, d)}-----------[1 2 3 4 5]2 [1 2]
当比较短的切片扩容时,系统会多分配 100% 的空间,也就是说分配的数组容量是切片长度的 2 倍。但当切片长度超过 1024 时,扩容策略调整为多分配 25% 的空间,这是为了避免空间的过多浪费。
package mainimport "fmt"func main() { s1 := make([]int, 6) s2 := make([]int, 1024) s1 = append(s1, 1) s2 = append(s2, 2) fmt.Println(len(s1), cap(s1)) fmt.Println(len(s2), cap(s2))}-------------------------------------------7 121025 1344
数组切片让我们具备了可以操作一块连续内存的能力,它是对同质元素的统一管理。而字典则赋予了不连续不同类的内存变量的关联性,它表达的是一种因果关系,字典的 key 是因,字典的 value 是果。
指针、数组切片和字典都是容器型变量。字典比数组切片在使用上要简单很多,但是内部结构却非常复杂。
在创建字典时,必须要给 key 和 value 指定类型。创建字典也可以使用 make 函数:
package mainimport "fmt"func main() { var m map[int]string = make(map[int]string) fmt.Println(m, len(m))}----------map[] 0
使用 make 函数创建的字典是空的,长度为零,内部没有任何元素。如果需要给字典提供初始化的元素,就需要使用另一种创建字典的方式:
package mainimport "fmt"func main() { var m map[int]string = map[int]string{ 90: "优秀", 80: "良好", 60: "及格", // 注意这里逗号不可缺少,否则会报语法错误 } fmt.Println(m, len(m))}---------------map[90:优秀 80:良好 60:及格] 3
字典变量同样支持类型推导,上面的变量定义可以简写成:
var m = map[int]string { 90: "优秀", 80: "良好", 60: "及格",}
如果提前知道字典内部键值对的数量,那么还可以给 make 函数传递一个整数值,通知运行时提前分配好相应的内存,这样可以避免字典在长大的过程中要经历的多次扩容操作:
var m = make(map[int]string, 16)
字典可以使用中括号[]
来读写内部元素,使用 delete 函数来删除元素:
package mainimport "fmt"func main() { var fruits = map[string]int { "apple": 2, "banana": 5, "orange": 8, } // 读取元素 var score = fruits["banana"] fmt.Println(score) // 增加或修改元素 fruits["pear"] = 3 fmt.Println(fruits) // 删除元素 delete(fruits, "pear") fmt.Println(fruits)}-----------------------5map[apple:2 banana:5 orange:8 pear:3]map[orange:8 apple:2 banana:5]
删除操作时,如果对应的 key 不存在,delete 函数会静默处理。读操作时,如果 key 不存在,也不会抛出异常,它会返回 value 类型对应的零值。
可以通过字典的特殊语法来判断对应的 key 是否存在:
package mainimport "fmt"func main() { var fruits = map[string]int { "apple": 2, "banana": 5, "orange": 8, } var score, ok = fruits["durin"] if ok { fmt.Println(score) } else { fmt.Println("durin not exists") } fruits["durin"] = 0 score, ok = fruits["durin"] if ok { fmt.Println(score) } else { fmt.Println("durin still not exists") }}-------------durin not exists0
字典的下标读取可以返回两个值,使用第二个返回值都表示对应的 key 是否存在。它只是 Go 语言提供的语法糖,内部并没有太多的玄妙。
正常的函数调用可以返回多个值,但是并不具备这种“随机应变”的特殊能力 —— 「多态返回值」。
字典的遍历提供了以下两种方式:一种是需要携带 value,另一种是只需要 key,需要使用到 Go 语言的 range 关键字:
package mainimport "fmt"func main() { var fruits = map[string]int { "apple": 2, "banana": 5, "orange": 8, } for name, score := range fruits { fmt.Println(name, score) } for name := range fruits { fmt.Println(name) }}------------orange 8apple 2banana 5applebananaorange
然而,Go 语言的字典并没有提供例如keys()
或values()
这样的方法,意味着如果要获取 key 列表,就得自己循环一下:
package mainimport "fmt"func main() { var fruits = map[string]int { "apple": 2, "banana": 5, "orange": 8, } var names = make([]string, 0, len(fruits)) var scores = make([]int, 0, len(fruits)) for name, score := range fruits { names = append(names, name) scores = append(scores, score) } fmt.Println(names, scores)}----------[apple banana orange] [2 5 8]
注意:遍历的时候,直接得到的 value 是拷贝过后的,会影响性能。在遍历中,使用
map[key]
的方式可以直接用索引获取数据,速度要比使用 value 快将近一倍,但要注意指针安全的问题。
Go 语言的内置字典不是线程安全的,如果需要线程安全,必须使用锁来控制。
字典变量里存的只是一个地址指针,这个指针指向字典的头部对象。所以字典变量占用的空间是一个字,也就是一个指针的大小,64 位机器是 8 字节,32 位机器是 4 字节。
可以使用 unsafe 包提供的Sizeof
函数来计算一个变量的大小:
package mainimport ( "fmt" "unsafe")func main() { var m = map[string]int{ "apple": 2, "pear": 3, "banana": 5, } fmt.Println(unsafe.Sizeof(m))}------8
字符串通常有两种设计,一种是「字符」串,一种是「字节」串。「字符」串中的每个字都是定长的,而「字节」串中每个字是不定长的。Go 语言里的字符串是「字节」串,英文字符占用 1 个字节,非英文字符占多个字节。这意味着无法通过位置来快速定位出一个完整的字符来,而必须通过遍历的方式来逐个获取单个字符。
我们所说的字符通常是指 unicode 字符,一个 unicode 字符通常用 4 个字节来表示,对应的 Go 语言中的字符 rune 占 4 个字节。
在 Go 语言的源码中可以看到,rune 类型是一个衍生类型,它在内存里面使用
int32
类型的 4 个字节存储。
type rune int32
其中 codepoint 是每个「字」的实际偏移量。Go 语言的字符串采用 utf-8 编码,中文汉字通常需要占用 3 个字节,英文只需要 1 个字节。len()
函数得到的是字节的数量,通过下标来访问字符串得到的是「字节」。
字符串可以通过下标来访问内部字节数组具体位置上的字节,字节是 byte 类型:
package mainimport "fmt"func main() { var s = "嘻哈china" for i:=0;i<len(s);i++ { fmt.Printf("%x ", s[i]) }}-----------e5 98 bb e5 93 88 63 68 69 6e 61
package mainimport "fmt"func main() { var s = "嘻哈china" for codepoint, runeValue := range s { fmt.Printf("%d %d ", codepoint, int32(runeValue)) }}-----------0 22075 3 21704 6 99 7 104 8 105 9 110 10 97
对字符串进行 range 遍历,每次迭代出两个变量codepoint
和runeValue
,codepoint 表示字符起始位置,runeValue表示对应的 unicode 编码(类型是 rune)。
字符串的内存结构不仅包含前面提到的字节数组,编译器还为它分配了头部字段来存储 长度信息 和 指向底层字节数组的指针,如上图所示,结构非常类似于切片,区别是头部少了一个容量字段。
可以使用下标来读取字符串指定位置的字节,但是无法修改这个位置上的字节内容。如果尝试使用下标赋值,编译器在语法上直接拒绝:
package mainfunc main() { var s = "hello" s[0] = 'H'}--------./main.go:5:7: cannot assign to s[0]
字符串在内存形式上比较接近于切片,它也可以像切片一样进行切割来获取子串。子串和母串共享底层字节数组。
package mainimport "fmt"func main() { var s1 = "hello world" var s2 = s1[3:8] fmt.Println(s2)}-------lo wo
在使用 Go 语言进行网络编程时,经常需要将来自网络的字节流转换成内存字符串,同时也需要将内存字符串转换成网络字节流。Go 语言直接内置了字节切片和字符串的相互转换语法:
package mainimport "fmt"func main() { var s1 = "hello world" var b = []byte(s1) // 字符串转字节切片 var s2 = string(b) // 字节切片转字符串 fmt.Println(b) fmt.Println(s2)}--------[104 101 108 108 111 32 119 111 114 108 100]hello world
注意:字节切片和字符串的底层字节数组不是共享的,底层字节数组会被拷贝。这是因为字节切片的底层数组内容是可以修改的,而字符串的底层字节数组是只读的,如果共享了,就会导致字符串的只读属性不再成立。
Go 语言结构体里面装的是基础类型、数组、切片、字典以及其他类型结构体等。
结构体和其它高级语言里的「类」比较相似:
type Circle struct { x int y int Radius int}
需要特别注意的是结构体内部变量的大小写,首字母大写是公开变量,首字母小写是内部变量,分别相当于类成员变量的 public 和 private 类别。内部变量只有属于同一个 package 的代码才能直接访问。
最常见的创建形式是「KV 形式」,通过显式指定结构体内部字段的名称和初始值来初始化结构体,没有指定初值的字段会自动初始化为相应类型的「零值」:
package mainimport "fmt"type Circle struct { x int y int Radius int}func main() { var c1 Circle = Circle{ x: 100, y: 100, Radius: 50, } var c2 Circle = Circle{ Radius: 50, } var c3 Circle = Circle{} fmt.Printf("%+v\n", c1) fmt.Printf("%+v\n", c2) fmt.Printf("%+v\n", c3)}----------{x:100 y:100 Radius:50}{x:0 y:0 Radius:50}{x:0 y:0 Radius:0}
结构体的第二种创建形式是不指定字段名称来顺序字段初始化,需要显式提供所有字段的初值,一个都不能少。这种形式称之为「顺序形式」:
package mainimport "fmt"type Circle struct { x int y int Radius int}func main() { var c Circle = Circle{100, 100, 50} fmt.Printf("%+v\n", c)}-------{x:100 y:100 Radius:50}
结构体变量和普通变量都有指针形式,使用取地址符&
就可以得到结构体的指针类型:
package mainimport "fmt"type Circle struct { x int y int Radius int}func main() { var c *Circle = &Circle{100, 100, 50} fmt.Printf("%+v\n", c)}-----------&{x:100 y:100 Radius:50}
结构体变量创建的第三种形式是使用全局的new()
函数来创建一个「零值」结构体,所有字段都被初始化为相应类型的零值:
package mainimport "fmt"type Circle struct { x int y int Radius int}func main() { var c *Circle = new(Circle) fmt.Printf("%+v\n", c)}----------&{x:0 y:0 Radius:0}
第四种创建形式也是零值初始化:
package mainimport "fmt"type Circle struct { x int y int Radius int}func main() { var c Circle fmt.Printf("%+v\n", c)}----------{x:0 y:0 Radius:0}
三种零值初始化形式对比:
var c1 Circle = Circle{}var c2 Circlevar c3 *Circle = new(Circle)
nil 结构体是指结构体指针变量没有指向一个实际存在的内存。这样的指针变量只会占用 1 个指针的存储空间,也就是一个机器字的内存大小。
var c *Circle = nil
而零值结构体则会实际占用内存空间,只不过每个字段都是零值。
Go 语言的 unsafe 包提供了获取结构体内存占用的函数Sizeof()
:
package mainimport "fmt"import "unsafe"type Circle struct { x int y int Radius int}func main() { var c Circle = Circle{Radius: 50} fmt.Println(unsafe.Sizeof(c))}-------24
64 位机器上每个 int 类型都是 8 字节。而 32 位机器上,Circle 结构体就只会占用 12 字节。
结构体之间可以相互赋值,本质上是一次浅拷贝操作,拷贝了结构体内部的所有字段。
结构体指针之间也可以相互赋值,本质上也是一次浅拷贝操作,不过它拷贝的仅仅是指针地址值,结构体的内容是共享的。
package mainimport "fmt"type Circle struct { x int y int Radius int}func main() { var c1 Circle = Circle{Radius: 50} var c2 Circle = c1 fmt.Printf("%+v\n", c1) fmt.Printf("%+v\n", c2) c1.Radius = 100 fmt.Printf("%+v\n", c1) fmt.Printf("%+v\n", c2) var c3 *Circle = &Circle{Radius: 50} var c4 *Circle = c3 fmt.Printf("%+v\n", c3) fmt.Printf("%+v\n", c4) c3.Radius = 100 fmt.Printf("%+v\n", c3) fmt.Printf("%+v\n", c4)}----------------------{x:0 y:0 Radius:50}{x:0 y:0 Radius:50}{x:0 y:0 Radius:100}{x:0 y:0 Radius:50}&{x:0 y:0 Radius:50}&{x:0 y:0 Radius:50}&{x:0 y:0 Radius:100}&{x:0 y:0 Radius:100}
通过观察 Go 语言的底层源码,可以发现所有的 Go 语言内置的高级数据结构都是由结构体来完成的。
之前分析了数组与切片在内存形式上的区别:数组只有「体」,切片除了「体」之外,还有「头」部。切片的头部和内容体是分离的,使用指针关联起来。
package mainimport "fmt"import "unsafe"type ArrayStruct struct { value [10]int}type SliceStruct struct { value []int}func main() { var as = ArrayStruct{[...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}} var ss = SliceStruct{[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}} fmt.Println(unsafe.Sizeof(as), unsafe.Sizeof(ss))}-------------80 24
注意代码中的数组初始化使用了[...]
语法糖,表示让编译器自动推导数组的长度。
函数调用时参数传递结构体变量,值传递涉及到结构体字段的浅拷贝,指针传递则会共享结构体内容,只拷贝指针地址,规则上和赋值是等价的:
package mainimport "fmt"type Circle struct { x int y int Radius int}func expandByValue(c Circle) { c.Radius *= 2}func expandByPointer(c *Circle) { c.Radius *= 2}func main() { var c = Circle{Radius: 50} expandByValue(c) fmt.Println(c) expandByPointer(&c) fmt.Println(c)}---------{0 0 50}{0 0 100}
package mainimport ( "fmt" "math")type Circle struct { x int y int Radius int}// 面积func (c Circle) Area() float64 { return math.Pi * float64(c.Radius) * float64(c.Radius)}// 周长func (c Circle) Circumference() float64 { return 2 * math.Pi * float64(c.Radius)}func main() { var c = Circle{Radius: 50} fmt.Println(c.Area(), c.Circumference()) // 指针变量调用方法形式上是一样的 var pc = &c fmt.Println(pc.Area(), pc.Circumference())}-----------7853.981633974483 314.15926535897937853.981633974483 314.1592653589793
self
和this
这样的关键字来指代当前的对象.
操作符结构体的值方法无法改变结构体内部状态。例如,使用下面的方法无法扩大 Circle 的半径:
func (c Circle) expand() { c.Radius *= 2}
这是因为参数传递是值传递,复制了一份结构体内容。要想修改结构体内部状态,就必须要使用结构体的指针方法:
func (c *Circle) expand() { c.Radius *= 2}
通过指针访问内部的字段需要 2 次内存读取操作,第一步是取得指针地址,第二步是读取地址的内容,它比值访问要慢。但是在方法调用时,指针传递可以避免结构体的拷贝操作,结构体比较大时,这种性能的差距就会比较明显。
还有一些特殊的结构体不允许被复制,比如结构体内部包含有锁时,这时就必须使用它的指针形式来定义方法,否则会发生一些莫名其妙的问题。
结构体作为一种变量它可以放进另外一个结构体作为一个字段来使用,这种内嵌结构体的形式在 Go 语言里称之为「组合」:
package mainimport "fmt"type Point struct { x int y int}func (p Point) show() { fmt.Println(p.x, p.y)}type Circle struct { loc Point Radius int}func main() { var c = Circle{ loc: Point{ x: 100, y: 100, }, Radius: 50, } fmt.Printf("%+v\n", c) fmt.Printf("%+v\n", c.loc) fmt.Printf("%d %d\n", c.loc.x, c.loc.y) c.loc.show()}----------------{loc:{x:100 y:100} Radius:50}{x:100 y:100}100 100100 100
还有一种特殊的内嵌结构体形式,内嵌的结构体不提供名称。这时外面的结构体将直接继承内嵌结构体所有的内部字段和方法,匿名的结构体字段将会自动获得以结构体类型的名字命名的字段名称:
package mainimport "fmt"type Point struct { x int y int}func (p Point) show() { fmt.Println(p.x, p.y)}type Circle struct { Point // 匿名内嵌结构体 Radius int}func main() { var c = Circle{ Point: Point{ x: 100, y: 100, }, Radius: 50, } fmt.Printf("%+v\n", c) fmt.Printf("%+v\n", c.Point) fmt.Printf("%d %d\n", c.x, c.y) // 继承了字段 fmt.Printf("%d %d\n", c.Point.x, c.Point.y) c.show() // 继承了方法 c.Point.show()}-------{Point:{x:100 y:100} Radius:50}{x:100 y:100}100 100100 100100 100100 100
这里的继承仅仅是形式上的语法糖,c.show()
转换成二进制代码后和c.Point.show()
是等价的,c.x
和c.Point.x
也是等价的。
Go 语言不是面向对象语言在于它的结构体明确不支持多态,外结构体的方法不能覆盖内部结构体的方法。
多态是指父类定义的方法可以调用子类实现的方法,不同的子类有不同的实现,从而给父类的方法带来了多样的不同行为。
package mainimport "fmt"type Fruit struct{}func (f Fruit) eat() { fmt.Println("eat fruit")}func (f Fruit) enjoy() { fmt.Println("smell first") f.eat() fmt.Println("clean finally")}type Apple struct { Fruit}func (a Apple) eat() { fmt.Println("eat apple")}type Banana struct { Fruit}func (b Banana) eat() { fmt.Println("eat banana")}func main() { var apple = Apple{} var banana = Banana{} apple.enjoy() banana.enjoy()}----------smell firsteat fruitclean finallysmell firsteat fruitclean finally
可以看到,enjoy
方法调用的eat
方法还是 Fruit 自己的eat
方法,它没能被外面的结构体方法覆盖掉,这意味着面向对象的代码习惯不能直接用到 Go 语言中。
接口是一个对象的对外能力的展现,我们使用一个对象时,往往不需要知道一个对象的内部复杂实现,通过它暴露出来的接口,就知道了这个对象具备哪些能力以及如何使用这个能力。
Go 语言的接口类型非常特别,它的作用和 Java 语言的接口一样,但是在形式上有很大的差别。Java 语言需要在类的定义上显式实现了某些接口,才可以说这个类具备了接口定义的能力。但是 Go 语言的接口是隐式的,只要结构体上定义的方法在形式上(名称、参数和返回值)和接口定义的一样,那么这个结构体就自动实现了这个接口,我们就可以使用这个接口变量来指向结构体对象。
package mainimport "fmt"// 可以闻type Smellable interface { smell(s string)}// 可以吃type Eatable interface { eat(s string)}// 苹果既可以闻又可以吃type Apple struct { name string}func (a Apple) smell(s string) { fmt.Println(s + " can smell")}func (a Apple) eat(s string) { fmt.Println(s + " can eat")}// 花只可以闻type Flower struct { name string}func (f Flower) smell(s string) { fmt.Println(s + " can smell")}func main() { var s1 Smellable var s2 Eatable var apple = Apple{ name: "Apple", } var flower = Flower{ name: "Flower", } s1 = apple s1.smell(apple.name) s1 = flower s1.smell(flower.name) s2 = apple s2.eat(apple.name)}--------------------apple can smellflower can smellapple can eat
Apple 结构体同时实现了Smellable
和Eatable
这两个接口,而 Flower 结构体只实现了Smellable
接口。可以看到在 Go 语言中,无需使用类似于 Java 语言的 implements 关键字,结构体和接口就自动产生了关联。
如果一个接口里面没有定义任何方法,那么它就是空接口,任意结构体都隐式的实现了空接口。
Go 语言为了避免用户重复定义,自己内置了一个名为interface{}
的空接口。空接口里没有方法,所以它也不具备任何能力,其作用相当于 Java 的 Object 类型,可以容纳任意对象,是一个万能容器。比如一个字典的 key 是字符串,但是希望 value 可以容纳任意类型的对象,类似于 Java 语言的 Map 类型,这时候就可以使用空接口类型interface{}
。
package mainimport "fmt"func main() { var user = map[string]interface{}{ "age": 30, "address": "Guangdong Guangzhou", "married": true, } fmt.Println(user) // 类型转换语法 var age = user["age"].(int) var address = user["address"].(string) var married = user["married"].(bool) fmt.Println(age, address, married)}-------------map[age:30 address:Guangdong Guangzhou married:true]30 Guangdong Guangzhou true
因为 user 字典变量的类型是map[string]interface{}
,从这个字典中直接读取得到的 value 类型是interface{}
,所以需要通过类型转换才能得到期望的变量。
可以将 Go 语言中的接口看成一个特殊的容器:这个容器只能容纳一个对象,只有实现了这个接口类型的对象才可以放进去。
查看 Go 语言的源码发现,接口变量也是由结构体来定义的。这个结构体包含两个指针字段,所以接口变量的内存占用是 2 个机器字:
package mainimport "fmt"import "unsafe"func main() { var s interface{} fmt.Println(unsafe.Sizeof(s)) var arr = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} fmt.Println(unsafe.Sizeof(arr)) s = arr fmt.Println(unsafe.Sizeof(s))}----------168016
接口是一种特殊的容器,可以容纳多种不同的对象。那么只要这些对象都同样实现了接口定义的方法,再将容纳的对象替换成另一个对象,就可以模拟实现多态:
package mainimport ( "fmt")type Fruitable interface { eat()}type Fruit struct { Name string // 属性变量 Fruitable // 匿名内嵌接口变量}func (f Fruit) want() { fmt.Printf("I like ") f.eat() // 外结构体会自动继承匿名内嵌变量的方法}type Apple struct{}func (a Apple) eat() { fmt.Println("eating apple")}type Banana struct{}func (b Banana) eat() { fmt.Println("eating banana")}func main() { var f1 = Fruit{"Apple", Apple{}} var f2 = Fruit{"Banana", Banana{}} f1.want() f2.want()}---------I like eating appleI like eating banana
使用这种方式模拟多态本质上是通过组合属性变量 Name 和接口变量 Fruitable 来做到的。属性变量是对象的数据,而接口变量是对象的功能,将它们组合到一块就形成了一个完整的多态性结构体。
接口的定义也支持组合继承:
type Smellable interface { smell()}type Eatable interface { eat()}type Fruitable interface { Smellable Eatable}
这时 Fruitable 接口就自动包含了smell()
和eat()
两个方法,和下面的定义是等价的:
type Fruitable interface { smell() eat()}
变量的赋值本质上是一次内存浅拷贝:切片的赋值是拷贝了切片头,字符串的赋值是拷贝了字符串的头部,而数组的赋值则是直接拷贝了整个数组。
package mainimport "fmt"type Rect struct { Width int Height int}func main() { var a interface{} var r = Rect{50, 50} a = r var rx = a.(Rect) r.Width = 100 r.Height = 100 fmt.Println(rx)}------{50 50}
可以根据上面的输出结果推断出结构体的内存发生了复制,这是因为赋值a = r
和类型转换rx = a.(Rect)
两者都发生了数据内存的赋值——浅拷贝。
将上面的例子改成指针,将接口变量指向结构体指针,就会得到不一样的结果:
package mainimport "fmt"type Rect struct { Width int Height int}func main() { var a interface{} var r = Rect{50, 50} a = &r // 指向了结构体指针 var rx = a.(*Rect) r.Width = 100 r.Height = 100 fmt.Println(rx)}-------&{100 100}
可以看到指针变量 rx 指向的内存和变量 r 的内存是同一份,因为在类型转换的过程中只发生了指针变量的内存复制,而指针变量指向的内存是共享的。
Go 语言规定凡是实现了错误接口的对象都是错误对象,这个错误接口只定义了一个方法:
type error interface { Error() string}
编写一个错误对象很简单:写一个结构体,然后挂在Error
方法里:
package mainimport "fmt"type SomeError struct { Reason string}func (s SomeError) Error() string { return s.Reason}func main() { var err error = SomeError{"something happened"} fmt.Println(err)}---------------something happened
Go 语言内置了一个通用错误类型,在 errors 包里面。这个包还提供了一个New()
函数来方便的创建一个通用错误:
var err = errors.New("something happened")
还可以使用 fmt 包提供的Errorf
函数来给错误字符串定制一些参数:
var thing = "something"var err = fmt.Errorf("%s happened", thing)
在 Java 语言中,如果遇到 I/O 问题通常会抛出IOException
类型的异常。然而在 Go 语言中,它不会抛出异常,而是以返回值的形式来通知上层逻辑来处理错误。
package mainimport ( "fmt" "os")func main() { // 打开文件 var f, err = os.Open("quick.go") if err != nil { // 文件不存在、权限等原因 fmt.Println("open file failed reason: " + err.Error()) return } // 推迟到函数尾调用,确保文件会关闭 defer f.Close() // 存储文件内容 var content = []byte{} // 临时的缓冲,按块读取,一次最多读取 100 字节 var buf = make([]byte, 100) for { // 读文件,将读到的内容填充到缓冲 n, err := f.Read(buf) if n > 0 { // 将读到的内容聚合起来 content = append(content, buf[:n]...) } if err != nil { // 遇到流结束或者其它错误 break } } // 输出文件内容 fmt.Println(string(content))}-------package mainimport "os"import "fmt".....
首先需要使用go get
指令下载 redis 包:
go get github.com/go-redis/redis
下面实现一个小功能:获取 Redis 中两个整数值,然后相乘,再存入 Redis 中:
package mainimport "fmt"import "strconv"import "github.com/go-redis/redis"func main() { // 定义客户端对象,内部包含一个连接池 var client = redis.NewClient(&redis.Options{ Addr: "localhost:6379", }) // 定义三个重要的整数变量值,默认都是零 var val1, val2, val3 int // 获取第一个值 valstr1, err := client.Get("value1").Result() if err == nil { val1, err = strconv.Atoi(valstr1) if err != nil { fmt.Println("value1 not a valid integer") return } } else if err != redis.Nil { fmt.Println("redis access error reason:" + err.Error()) return } // 获取第二个值 valstr2, err := client.Get("value2").Result() if err == nil { val2, err = strconv.Atoi(valstr2) if err != nil { fmt.Println("value1 not a valid integer") return } } else if err != redis.Nil { fmt.Println("redis access error reason:" + err.Error()) return } // 保存第三个值 val3 = val1 * val2 ok, err := client.Set("value3", val3, 0).Result() if err != nil { fmt.Println("set value error reason:" + err.Error()) return } fmt.Println(ok)}------OK
redis.Nil
就是客户端专门为 key 不存在这种情况而定义的错误对象Go 语言提供了 panic 和 recover 全局函数让我们可以抛出异常、捕获异常,类似于 try、throw、catch语句,但是又很不一样。比如 panic 函数可以抛出任意对象:
package mainimport "fmt"var negErr = fmt.Errorf("negative number")func main() { fmt.Println(fact(5)) fmt.Println(fact(10)) fmt.Println(fact(15)) fmt.Println(fact(-20))}func fact(a int) int { if a <= 0 { panic(negErr) } var result = 1 for i := 1; i <= a; i++ { result *= i } return result}-------12036288001307674368000panic: negative numbergoroutine 1 [running]:main.fact(0xffffffffffffffec, 0x1) C:/Users/abel1/go/src/hello/quickgo.go:16 +0x7emain.main() C:/Users/abel1/go/src/hello/quickgo.go:11 +0x15eProcess finished with exit code 2
上面的代码抛出了negErr
,直接导致了程序崩溃,程序最后打印了异常堆栈信息。下面我们可以使用 recover 函数来保护它,需要结合 defer 语句一起使用,这样可以确保recover()
逻辑在程序异常时也可以得到调用:
package mainimport "fmt"var negErr = fmt.Errorf("negative number")func main() { defer func() { if err := recover(); err != nil { fmt.Println("error catched", err) } }() fmt.Println(fact(5)) fmt.Println(fact(10)) fmt.Println(fact(15)) fmt.Println(fact(-20))}func fact(a int) int { if a <= 0 { panic(negErr) } var result = 1 for i := 1; i <= a; i++ { result *= i } return result}-------12036288001307674368000error catched negative numberProcess finished with exit code 0
可以看到程序成功捕获了异常,并且不再崩溃,但异常点后面的逻辑也不会再继续执行了,
我们经常还需要对recover()
返回的结果进行判断,以挑选出我们愿意处理的异常对象类型。对于那些不愿意处理的,可以选择再次抛出,让上层来处理:
defer func() { if err := recover(); err != nil { if err == negErr { fmt.Println("error catched", err) } else { panic(err) // rethrow } }}()
Go 语言官方表态不要轻易使用 panic recover,除非你真的无法预料中间可能会发生的错误,或者它能非常显著地简化你的代码。除非逼不得已,否则不要使用它。
有时我们需要在一个函数里使用多次 defer 语句。例如拷贝文件,需要同时打开源文件和目标文件,那就需要调用两次defer f.Close
:
package mainimport ( "fmt" "os")func main() { fsrc, err := os.Open("source.txt") if err != nil { fmt.Println("open source file failed") return } defer fsrc.Close() fdes, err := os.Open("target.txt") if err != nil { fmt.Println("open target file failed") return } defer fdes.Close() fmt.Println("do something here")}------open source file failedProcess finished with exit code 0
需要注意的是 defer 语句的执行顺序和代码编写的顺序是相反的,也就是说最先 defer 的语句最后执行。
package mainimport "fmt"import "os"func main() { fsrc, err := os.Open("source.txt") if err != nil { fmt.Println("open source file failed") return } defer func() { fmt.Println("close source file") fsrc.Close() }() fdes, err := os.Open("target.txt") if err != nil { fmt.Println("open target file failed") return } defer func() { fmt.Println("close target file") fdes.Close() }() fmt.Println("do something here")}--------do something hereclose target fileclose source fileProcess finished with exit code 0
Go 语言里协程被称为goroutine
,通道被称为channel
。
Go 语言里创建一个协程非常简单:使用go
关键词加上一个函数调用就可以了。
Go 语言会启动一个新的协程,函数调用将成为这个协程的入口。
package mainimport ( "fmt" "time")func main() { fmt.Println("run in main goroutine") go func() { fmt.Println("run in child goroutine") go func() { fmt.Println("run in grand child goroutine") go func() { fmt.Println("run in grand grand child goroutine") }() }() }() time.Sleep(time.Second) fmt.Println("main goroutine will quit")}------run in main goroutinerun in child goroutinerun in grand child goroutinerun in grand grand child goroutinemain goroutine will quitProcess finished with exit code 0
在 Go 语言里只有一个主协程,其它都是它的子协程,子协程之间是平行关系。
子协程的异常退出会将异常传播到主协程,直接会导致主协程也跟着挂掉,进而导致程序崩溃。
为了保护子协程的安全,通常我们会在协程的入口函数开头增加recover()
语句来恢复协程内部发生的异常,阻断它传播到主协程导致程序崩溃:
package mainimport ( "fmt" "time")func main() { fmt.Println("run in main goroutine") go func() { fmt.Println("run in child goroutine") go func() { fmt.Println("run in grand child goroutine") go func() { fmt.Println("run in grand grand child goroutine") defer func() { if err := recover(); err != nil { // log error fmt.Println("wtf error happen!") } }() panic("wtf") }() }() }() time.Sleep(time.Second) fmt.Println("main goroutine will quit")}------run in main goroutinerun in child goroutinerun in grand child goroutinerun in grand grand child goroutinewtf error happen!main goroutine will quitProcess finished with exit code 0
Go 语言中的协程有如下特点:
线程的调度是由操作系统负责的,调度算法运行在内核态。而协程的调用是由 Go 语言的运行时负责的,调度算法运行在用户态。
协程可以简化为三种状态:
操作系统对线程的调度是抢占式的,也就是说单个线程的死循环不会影响其它线程的执行,每个线程的连续运行受到时间片的限制。
Go 语言运行时对协程的调度并不是抢占式的。如果单个协程通过死循环霸占了线程的执行权,那这个线程就没有机会去运行其它协程了,可以说这个线程假死了。
每个线程都会包含多个就绪态的协程形成了一个就绪队列。Go 语言运行时调度器采用了work-stealing
算法,当某个线程空闲时,也就是该线程上所有的协程都在休眠(或者一个协程都没有),它就会去其它线程的就绪队列上去偷一些协程来运行。正常情况下,运行时会尽量平均分配工作任务。
默认情况下,Go 运行时会将线程数会被设置为机器 CPU 逻辑核心数。同时它内置的runtime
包提供了GOMAXPROCS(n int)
函数允许我们动态调整线程数。
如果参数
n <=0
,就不会产生修改效果,等价于读取当前的线程数
package mainimport ( "fmt" "runtime")func main() { // 读取默认的线程数 fmt.Println(runtime.GOMAXPROCS(0)) // 设置线程数为 10 runtime.GOMAXPROCS(10) // 读取当前的线程数 fmt.Println(runtime.GOMAXPROCS(0))}------410Process finished with exit code 0
获取当前的协程数量可以使用 runtime 包提供的NumGoroutine()
方法:
package mainimport ( "fmt" "runtime" "time")func main() { fmt.Println(runtime.NumGoroutine()) for i := 0; i < 10; i++ { go func() { for { time.Sleep(time.Second) } }() } fmt.Println(runtime.NumGoroutine())}------111Process finished with exit code 0
在日常互联网应用中,Go 语言的协程主要应用在 HTTP API 应用、消息推送系统、聊天系统等。
不同的并行协程之间交流的方式有两种,一种是通过共享变量,另一种是通过队列。
Go 语言鼓励使用队列的形式来交流,它单独为协程之间的队列数据交流定制了特殊的语法——通道。
通道是协程的输入和输出。作为协程的输出,通道是一个容器,它可以容纳数据。作为协程的输入,通道是一个生产者,它可以向协程提供数据。
通道作为容器是有限定大小的,满了就写不进去,空了就读不出来。
通道还有它自己的类型,它可以限定进入通道的数据的类型。
创建通道只有一种语法,那就是make
全局函数,提供第一个类型参数限定通道可以容纳的数据类型,再提供第二个整数参数作为通道的容器大小。
// 缓冲型通道,里面只能放整数var bufferedChannel = make(chan int, 1024)// 非缓冲型通道var unbufferedChannel = make(chan int)
大小参数是可选的,如果不填,那这个通道的容量为零,叫做「非缓冲型通道」。
非缓冲型通道必须确保有协程正在尝试读取当前通道,否则写操作就会阻塞直到有其它协程来从通道中读东西。
非缓冲型通道总是处于既满又空的状态。与之对应的有限定大小的通道就是缓冲型通道。
在 Go 语言里不存在无界通道,每个通道都是有限定最大容量的
Go 语言为通道的读写设计了特殊的箭头语法糖<-
,把箭头写在通道变量的右边就是写通道,把箭头写在通道的左边就是读通道。需要注意的是,一次只能读写一个元素。
package mainimport "fmt"func main() { var ch chan int = make(chan int, 4) for i := 0; i < cap(ch); i++ { ch <- i // 写通道 } for len(ch) > 0 { fmt.Printf("current len: %d, cap: %d\n", len(ch), cap(ch)) var value int = <-ch // 读通道 fmt.Printf("value: %d\n", value) }}------current len: 4, cap: 4value: 0current len: 3, cap: 4value: 1current len: 2, cap: 4value: 2scurrent len: 1, cap: 4value: 3Process finished with exit code 0
通道作为容器,可以像切片一样,使用cap()
和len()
全局函数获得通道的容量和当前内部的元素个数。
通道一般作为不同的协程交流的媒介,不过在同一个协程里也是可以使用的
通道满了,写操作就会阻塞,协程就会进入睡眠,直到有其它协程读通道挪出了空间,协程才会被唤醒。如果有多个协程的写操作都阻塞了,一个读操作只会唤醒一个协程。
通道空了,读操作就会阻塞,协程也会进入睡眠,直到有其它协程写通道装进了数据才会被唤醒。如果有多个协程的读操作阻塞了,一个写操作也只会唤醒一个协程。
package mainimport ( "fmt" "math/rand" "time")func send(ch chan int) { for { var value = rand.Intn(100) ch <- value fmt.Printf("send %d\n", value) }}func recv(ch chan int) { for { value := <-ch fmt.Printf("recv %d\n", value) time.Sleep(time.Second) }}func main() { var ch = make(chan int, 1) // 子协程循环读 go recv(ch) // 主协程循环写 send(ch)}------send 81send 87recv 81recv 87send 47recv 47send 59...
Go 语言的通道不但支持读写操作,还支持关闭。读取一个已经关闭的通道会立即返回通道类型的「零值」,而写入一个已经关闭的通道会抛出异常。
如果通道里的元素是整型的,读操作不能通过返回值来确定通道是否关闭
package mainimport "fmt"func main() { var ch = make(chan int, 4) ch <- 1 ch <- 2 close(ch) value := <-ch fmt.Println(value) value = <-ch fmt.Println(value) value = <-ch fmt.Println(value)}------120Process finished with exit code 0
还可以使用for range
语法取代箭头操作符<-
来遍历通道。当通道空了,循环会暂停阻塞。当通道关闭时,阻塞停止,循环也跟着结束了。当循环结束时,我们就知道通道已经关闭了。
package mainimport "fmt"func main() { var ch = make(chan int, 4) ch <- 1 ch <- 2 close(ch) // for range 遍历通道 for value := range ch { fmt.Println(value) }}------12Process finished with exit code 0
如果将上面关闭通道的语句注释掉,使用for range
语法遍历通道就会报错:
package mainimport "fmt"func main() { var ch = make(chan int, 4) ch <- 1 ch <- 2 // close(ch) // for range 遍历通道 for value := range ch { fmt.Println(value) }}------12fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan receive]:main.main.func1(0xc00007e000) C:/Users/abel1/go/src/hello/quickgo.go:13 +0xa1main.main() C:/Users/abel1/go/src/hello/quickgo.go:17 +0xa1Process finished with exit code 2
通道如果没有显式关闭,当它不再被程序使用的时候,会自动关闭被垃圾回收掉。不过优雅的程序应该将通道看成资源,显式关闭每个不再使用的资源是一种良好的习惯
写通道时一定要确保通道没有被关闭,否则会抛出异常:
package mainimport "fmt"// 写通道func send(ch chan int) { i := 0 for { i++ ch <- i }}// 读通道func recv(ch chan int) { value := <-ch fmt.Println(value) value = <-ch fmt.Println(value) close(ch)}func main() { var ch = make(chan int, 4) go recv(ch) send(ch)}------12panic: send on closed channelgoroutine 1 [running]:main.send(0xc00007e000) C:/Users/abel1/go/src/hello/quickgo.go:10 +0x4bmain.main() C:/Users/abel1/go/src/hello/quickgo.go:26 +0x6dProcess finished with exit code 2
确保通道写安全的最好方式是由负责写通道的协程自己来关闭通道,读通道的协程不要去关闭通道:
package mainimport "fmt"func send(ch chan int) { ch <- 1 ch <- 2 ch <- 3 ch <- 4 close(ch)}func recv(ch chan int) { for v := range ch { fmt.Println(v) }}func main() { var ch = make(chan int, 1) go send(ch) recv(ch)}------1234Process finished with exit code 0
这样可以应对单写多读的场景。不过在多写单读的场景下,任意一个读写通道的协程都不可以随意关闭通道,否则会导致其它写通道协程抛出异常。
这个时候就需要使用内置sync
包提供的WaitGroup
对象,使用计数来等待指定事件完成,即等待所有的写通道协程都结束运行后才关闭通道:
package mainimport ( "fmt" "sync" "time")func send(ch chan int, wg *sync.WaitGroup) { defer wg.Done() // 计数值减 1 i := 0 for i < 4 { i++ ch <- i }}func recv(ch chan int) { for value := range ch { fmt.Println(value) }}func main() { var ch = make(chan int, 4) var wg = new(sync.WaitGroup) wg.Add(2) // 增加计数值 go send(ch, wg) // 写 go send(ch, wg) // 写 go recv(ch) // Wait() 阻塞等待所有的写通道协程结束 // 待计数值变成零,Wait() 才会返回 wg.Wait() // 关闭通道 close(ch) time.Sleep(time.Second)}------12341234Process finished with exit code 0
当消费者有多个消费来源时,只要有一个来源生产了数据,消费者就可以读这个数据进行消费。这时候可以将多个来源通道的数据汇聚到目标通道,然后统一在目标通道进行消费。
package mainimport ( "fmt" "time")// 每隔一段时间产生一个数func send(ch chan int, gap time.Duration) { i := 0 for { i++ ch <- i time.Sleep(gap) }}// 将多个原通道内容拷贝到单一的目标通道func collect(source chan int, target chan int) { for v := range source { target <- v }}// 从目标通道消费数据func recv(ch chan int) { for v := range ch { fmt.Printf("receive %d\n", v) }}func main() { var ch1 = make(chan int) // 原通道 1 var ch2 = make(chan int) // 原通道 2 var ch3 = make(chan int) // 目标通道 go send(ch1, time.Second) go send(ch2, 2*time.Second) go collect(ch1, ch3) go collect(ch2, ch3) recv(ch3)}------receive 1receive 1receive 2receive 2receive 3receive 4receive 3receive 5receive 6receive 4receive 7receive 8receive 5receive 9receive 10receive 6...
但是这种形式比较繁琐:一来需要单独编写collect()
汇聚函数,二来一旦多路通道的规模很大,就需要为每一种消费来源都单独启动一个汇聚协程。好在 Go 语言为这种使用场景提供了「多路复用」语法糖——select
语句,它可以同时管理多个通道的读写:如果所有通道都不能读写,它就整体阻塞,只要有一个通道可以读写,它就会继续:
package mainimport ( "fmt" "time")// 每隔一段时间产生一个数func send(ch chan int, gap time.Duration) { i := 0 for { i++ ch <- i time.Sleep(gap) }}// 从目标通道消费数据func recv(ch1 chan int, ch2 chan int) { for { select { case v := <-ch1: fmt.Printf("recv %d from ch1\n", v) case v := <-ch2: fmt.Printf("recv %d from ch2\n", v) } }}func main() { var ch1 = make(chan int) var ch2 = make(chan int) go send(ch1, time.Second) go send(ch2, time.Second * 2) recv(ch1, ch2)}------recv 1 from ch2recv 1 from ch1recv 2 from ch1recv 2 from ch2recv 3 from ch1recv 4 from ch1recv 3 from ch2recv 5 from ch1recv 6 from ch1recv 4 from ch2recv 7 from ch1...
可以观察到向ch2
写数据的生产者go send(ch2, time.Second * 2)
要更慢一些。
上面是多路复用select
语句的读通道形式,下面是它的写通道形式,只要有一个通道能写进去,它就会打破阻塞:
select { case ch1 <- v: fmt.Printf("Send %d to ch1\n", v) case ch2 <- v: fmt.Printf("Send %d to ch2\n", v)}
关于如何在多路复用时关闭通道,可以参考 多路复用 channel 的时候,如何优雅的关闭通道 | Go 语言中文网
前面讲的读写都是阻塞读写,Go 语言还提供了通道的非阻塞读写:当通道空时,读操作不会阻塞,当通道满时,写操作也不会阻塞。
非阻塞读写需要依靠select
语句的default
分支。当select
语句所有通道都不可读写时,如果定义了default
分支,那就会执行default
分支逻辑,这样就起到了不阻塞的效果。
下面演示一个单生产者多消费者的场景。生产者同时向两个通道写数据,写不进去就丢弃:
package mainimport ( "fmt" "time")func send(ch1 chan int, ch2 chan int) { i := 0 for { i++ select { case ch1 <- i: fmt.Printf("send ch1 %d\n", i) case ch2 <- i: fmt.Printf("send ch2 %d\n", i) default: } }}func recv(ch chan int, gap time.Duration, name string) { for v := range ch { fmt.Printf("receive %s %d\n", name, v) time.Sleep(gap) }}func main() { // 无缓冲通道 var ch1 = make(chan int) var ch2 = make(chan int) // 两个消费者的休眠时间不一样,名称不一样 go recv(ch1, time.Second, "ch1") go recv(ch2, time.Second * 2, "ch2") send(ch1, ch2)}------send ch1 429send ch2 430receive ch1 429receive ch2 430send ch1 10062541receive ch1 10062541send ch2 20457524receive ch2 20457524send ch1 20467243receive ch1 20467243send ch1 30294965receive ch1 30294965send ch2 40021595receive ch2 40021595send ch1 40041927receive ch1 40041927send ch1 49448528receive ch1 49448528send ch2 58807676receive ch2 58807676...
可以看到很多数据被丢弃了,消费者读到的数据是不连续的。
将select
语句里面的default
分支去掉,再运行一次:
send ch2 1send ch1 2receive ch1 2receive ch2 1receive ch1 3send ch1 3receive ch2 4send ch2 4receive ch1 5send ch1 5receive ch1 6send ch1 6receive ch2 7send ch2 7receive ch1 8send ch1 8receive ch1 9send ch1 9receive ch2 10...
可以看到消费者读到的数据都连续了,写通道又恢复为阻塞的。
select
语句的default
分支非常关键,它决定了通道读写操作是否阻塞
Go 语言的通道内部结构是一个循环数组,通过读写偏移量来控制元素发送和接收。它为了保证线程安全,内部会有一个全局锁来控制并发。对于发送和接收操作都会有一个队列来容纳处于阻塞状态的协程。Go 语言中通道的源码位于$GOROOT/src/runtime/chan.go
:
type hchan struct { qcount uint // 通道有效元素个数 dataqsiz uint // 通道容量,循环数组总长度 buf unsafe.Pointer // 数组地址 elemsize uint16 // 内部元素的大小 closed uint32 // 是否已关闭,0 或者 1 elemtype *_type // 内部元素类型信息 sendx uint // 循环数组的写偏移量 recvx uint // 循环数组的读偏移量 recvq waitq // 阻塞在读操作上的协程队列 sendq waitq // 阻塞在写操作上的协程队列 // lock protects all fields in hchan, as well as several // fields in sudogs blocked on this channel. // // Do not change another G's status while holding this lock // (in particular, do not ready a G), as this can deadlock // with stack shrinking. lock mutex // 全局锁}
这个循环队列和 Java 语言内置的ArrayBlockingQueue
结构如出一辙,所以可以从这个数据结构中得出结论:队列在本质上是使用共享变量加锁的方式来实现的,共享变量才是并行交流的本质。
并发编程不同的协程共享数据的方式除了通道之外还有就是共享变量。虽然 Go 语言官方推荐使用通道的方式来共享数据,但是通过变量来共享才是基础,因为通道在底层也是通过共享变量的方式来实现的。通道的内部数据结构包含一个数组,对通道的读写就是对内部数组的读写。
并发环境下共享读写变量必须使用锁来控制数据结构的安全。Go 语言内置了sync
包,里面包含了我们平时需要经常使用的互斥锁对象sync.Nutex
。
Go 语言内置的字典不是线程安全的,可以使用互斥锁对象来保护字典,让它变成线程安全的
Go 语言从
1.9
版本之后自带线程安全的字典sync.map
,主要操作有:Store
、LoadOrStore
、Load
、Delete
、Range
Go 语言内置了数据结构「竞态检查」工具来帮我们检查程序中是否存在线程不安全的代码。关于 Go 语言的竞态检测器,可以参考 Go 的竞态检测器 | Go 语言中文网。例如下面这段代码:
package mainimport "fmt"func write(d map[string]int) { d["fruit"] = 2}func read(d map[string]int) { fmt.Println(d["fruit"])}func main() { d := map[string]int{} go read(d) write(d)}
读、写分别是两个协程,存在明显的安全隐患,运行竞态检查指令go run -race quickgo.go
观察输出结果:
C:\Users\abel1\go\src\hello>go run -race quickgo.go==================WARNING: DATA RACERead at 0x00c000052240 by goroutine 6: runtime.mapaccess1_faststr() C:/Go/src/runtime/map_faststr.go:12 +0x0 main.read() C:/Users/abel1/go/src/hello/quickgo.go:10 +0x64Previous write at 0x00c000052240 by main goroutine: runtime.mapassign_faststr() C:/Go/src/runtime/map_faststr.go:190 +0x0 main.main() C:/Users/abel1/go/src/hello/quickgo.go:6 +0x8fGoroutine 6 (running) created at: main.main() C:/Users/abel1/go/src/hello/quickgo.go:15 +0x60====================================WARNING: DATA RACERead at 0x00c0000422f8 by goroutine 6: main.read() C:/Users/abel1/go/src/hello/quickgo.go:10 +0x77Previous write at 0x00c0000422f8 by main goroutine: main.main() C:/Users/abel1/go/src/hello/quickgo.go:6 +0xa4Goroutine 6 (running) created at: main.main() C:/Users/abel1/go/src/hello/quickgo.go:15 +0x60==================2Found 2 data race(s)exit status 66
竞态检查工具是基于运行时代码检查,而不是通过代码静态分析来完成的。这意味着那些没有机会运行到的代码逻辑中如果存在安全隐患,它是检查不出来的。
让字典变的线程安全,就需要使用互斥锁对字典的所有读写操作进行保护:
package mainimport ( "fmt" "sync" "time")type SafeDict struct { data map[string]int mutex *sync.Mutex}func NewSafeDict(data map[string]int) *SafeDict { return &SafeDict{ data: data, mutex: &sync.Mutex{}, }}func (d *SafeDict) Len() int { d.mutex.Lock() defer d.mutex.Unlock() return len(d.data)}func (d *SafeDict) Put(key string, value int) (int, bool) { d.mutex.Lock() defer d.mutex.Unlock() old_value, ok := d.data[key] d.data[key] = value return old_value, ok}func (d *SafeDict) Get(key string) (int, bool) { d.mutex.Lock() defer d.mutex.Unlock() old_value, ok := d.data[key] return old_value, ok}func (d *SafeDict) Delete(key string) (int, bool) { d.mutex.Lock() defer d.mutex.Unlock() old_value, ok := d.data[key] if ok { delete(d.data, key) } return old_value, ok}func write(d *SafeDict, key string, value int) { d.Put(key, value)}func read(d *SafeDict, key string) { fmt.Println(d.Get(key))}func main() { d := NewSafeDict(map[string]int{ "apple": 2, "peach": 3, }) go read(d, "peach") write(d, "peach", 10) time.Sleep(time.Second)}------10 trueProcess finished with exit code 0
再次使用竞态检查工具运行上面的代码,发现没有之前的警告输出,说明Get()
和Put()
方法已经做到了协程安全,但是还不能说明Delete()
方法是否安全,因为它没有机会得到运行。
需要注意的是,sync.Mutex
是一个结构体对象,这个对象在使用的过程中要避免被复制(浅拷贝)。复制将会导致锁被「分裂」了,起不到保护的作用。所以在平时的使用中要尽量使用它的指针类型。
我们知道外部结构体可以自动继承匿名内部结构体的所有方法。如果将锁字段匿名,就可以简化代码:
package mainimport "fmt"import "sync"type SafeDict struct { data map[string]int *sync.Mutex}func NewSafeDict(data map[string]int) *SafeDict { return &SafeDict{data, &sync.Mutex{}}}func (d *SafeDict) Len() int { d.Lock() defer d.Unlock() return len(d.data)}func (d *SafeDict) Put(key string, value int) (int, bool) { d.Lock() defer d.Unlock() old_value, ok := d.data[key] d.data[key] = value return old_value, ok}func (d *SafeDict) Get(key string) (int, bool) { d.Lock() defer d.Unlock() old_value, ok := d.data[key] return old_value, ok}func (d *SafeDict) Delete(key string) (int, bool) { d.Lock() defer d.Unlock() old_value, ok := d.data[key] if ok { delete(d.data, key) } return old_value, ok}func write(d *SafeDict) { d.Put("banana", 5)}func read(d *SafeDict) { fmt.Println(d.Get("banana"))}func main() { d := NewSafeDict(map[string]int{ "apple": 2, "pear": 3, }) go read(d) write(d)}
日常应用中,大多数并发数据结构都是读多写少的,对于读多写少的场合,可以将互斥锁换成读写锁,可以有效提升性能。读写锁sync.RWMutex
提供了四个常用方法,分别是:写加锁Lock()
、写释放锁Unlock()
、读加锁RLock()
和读释放锁RUnlock()
。
写锁是排他锁,加写锁时会阻塞其它协程再加读锁和写锁。读锁是共享锁,加读锁还可以允许其它协程再加读锁,但是会阻塞加写锁。另外,读写锁在写并发高的情况下性能退化为普通的互斥锁
将上面代码中SafeDict
的互斥锁改造成读写锁:
package mainimport "fmt"import "sync"type SafeDict struct { data map[string]int *sync.RWMutex}func NewSafeDict(data map[string]int) *SafeDict { return &SafeDict{data, &sync.RWMutex{}}}func (d *SafeDict) Len() int { d.RLock() defer d.RUnlock() return len(d.data)}func (d *SafeDict) Put(key string, value int) (int, bool) { d.Lock() defer d.Unlock() old_value, ok := d.data[key] d.data[key] = value return old_value, ok}func (d *SafeDict) Get(key string) (int, bool) { d.RLock() defer d.RUnlock() old_value, ok := d.data[key] return old_value, ok}func (d *SafeDict) Delete(key string) (int, bool) { d.Lock() defer d.Unlock() old_value, ok := d.data[key] if ok { delete(d.data, key) } return old_value, ok}func write(d *SafeDict) { d.Put("banana", 5)}func read(d *SafeDict) { fmt.Println(d.Get("banana"))}func main() { d := NewSafeDict(map[string]int{ "apple": 2, "pear": 3, }) go read(d) write(d)}
使用 Go 语言内置的unsafe
包可以直接操纵指定内存地址的内存。
Pointer
代表着变量的内存地址,可以将任意变量的地址转换成Pointer
类型,也可以将Pointer
类型转换成任意的指针类型,它是不同指针类型之间互转的中间类型。另外,Pointer
本身也是一个整型的值。
type Pointer int
在 Go 语言中,编译器禁止Pointer
类型直接进行加减运算。如果要进行运算,需要将Pointer
类型转换为uintptr
类型进行加减,然后再将uintptr
转换成Pointer
类型。uintptr
其实也是一个整型:
type uintptr int
package mainimport ( "fmt" "unsafe")type Rect struct { Width int Height int}func main() { var r = Rect{50, 50} // *Rect => Pointer => *int => int var width = *(*int)(unsafe.Pointer(&r)) // *Rect => Pointer => uintptr => Pointer => *int => int var height = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&r)) + uintptr(8))) fmt.Println(width, height)}------50 50Process finished with exit code 0
上面的代码使用unsafe
包来读取结构体的内容,下面尝试修改结构体的值:
package mainimport ( "fmt" "unsafe")type Rect struct { Width int Height int}func main() { var r = Rect{50, 50} // *Rect => Pointer => *int => int var pwidth = (*int)(unsafe.Pointer(&r)) // *Rect => Pointer => uintptr => Pointer => *int => int var pheight = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&r)) + unsafe.Offsetof(r.Height))) *pwidth = 100 *pheight = 200 fmt.Println(r.Width, r.Height)}------100 200Process finished with exit code 0
注意可以使用unsafe.Offsetof(r.Height)
替换uintptr(8)
,直接得到字段在结构体内的偏移量。
Go 语言的切片分为切片头和内部数组两部分,使用unsafe
包来验证一下切片的内部数据结构:
package mainimport "fmt"import "unsafe"func main() { // head = {address, 10, 10} // body = [1,2,3,4,5,6,7,8,9,10] var s = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} var address = (**[10]int)(unsafe.Pointer(&s)) var len = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8))) var cap = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16))) fmt.Println(address, *len, *cap) var body = **address for i := 0; i < *len; i++ { fmt.Printf("%d ", body[i]) }}------0xc000044400 10 101 2 3 4 5 6 7 8 9 10 Process finished with exit code 0
需要注意的是address
是一个二级指针变量:
字节切片和字符串之间的转换需要复制内存,而unsafe
包则提供了另一种高效的转换方法,让转换前后的字符串和字节切片共享内部存储:
字符串和字节切片的不同点在于头部,字符串的头部 2 个
int
字节,切片的头部 3 个int
字节
package mainimport "fmt"import "unsafe"func main() { fmt.Println(bytes2str(str2bytes("hello")))}func str2bytes(s string) []byte { var strhead = *(*[2]int)(unsafe.Pointer(&s)) var slicehead [3]int slicehead[0] = strhead[0] slicehead[1] = strhead[1] slicehead[2] = strhead[1] return *(*[]byte)(unsafe.Pointer(&slicehead))}func bytes2str(bs []byte) string { return *(*string)(unsafe.Pointer(&bs))}------helloProcess finished with exit code 0
注意:通过这种方式转换得到的字节切片切记不能修改,因为其底层字节数组是共享的,修改会破坏字符串的只读规则。另外只可以用作临时的局部变量,因为被共享的字节数组随时可能会被回收
可参考原文,此处略
接口类型和结构体类型似乎是两个不同的世界。只有接口类型之间的赋值和转换会共享数据,其它情况都会复制数据。其它情况包括结构体之间的赋值,结构体转接口,接口转结构体。
不同接口变量之间的转换本质上只是调整了接口变量内部的类型指针,数据指针并不会发生改变。
Go 语言的reflect
包定义了十几种内置的「元类型」,每一种元类型都有一个整数编号,这个编号使用reflect.Kind
类型表示。不同的结构体是不同的类型,但是它们都是同一个元类型Struct
。包含不同子元素的切片也是不同的类型,但是它们都是同一个元类型Slice
。
$GOROOT/src/reflect/type.go
中部分源码如下:
// A Kind represents the specific kind of type that a Type represents.// The zero Kind is not a valid kind.type Kind uintconst ( Invalid Kind = iota // 不存在的无效类型 Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr // 指针的整数类型,对指针进行整数运算时使用 Float32 Float64 Complex64 Complex128 Array // 数组类型 Chan // 通道类型 Func // 函数类型 Interface // 接口类型 Map // 字典类型 Ptr // 指针类型 Slice // 切片类型 String // 字符串类型 Struct // 结构体类型 UnsafePointer // unsafe.Pointer 类型)
reflect
包提供了两个基础反射方法,分别是TypeOf()
和ValueOf()
方法,分别用于获取变量的类型和值:
func TypeOf(v interface{}) Typefunc ValueOf(v interface{}) Value
对结构体变量进行反射:
package mainimport ( "fmt" "reflect")type Rect struct { Width int Height int}func main() { var s int = 42 fmt.Println(reflect.TypeOf(s)) fmt.Println(reflect.ValueOf(s)) var r = Rect{200, 100} fmt.Println(reflect.TypeOf(r)) fmt.Println(reflect.ValueOf(r))}------int42main.Rect{200 100}Process finished with exit code 0
这两个方法的参数是interface{}
类型,所以调用时编译器首先会将目标变量转换成interface{}
类型。接口类型包含两个指针,一个指向类型,一个指向值。上面两个方法的作用就是将接口变量进行解剖从而分离出类型和值。
TypeOf()
方法返回变量的类型信息得到的是一个类型为reflect.Type
的变量ValueOf()
方法返回变量的值信息得到的是一个类型为reflect.Value
的变量它是一个接口类型,里面定义了非常多的方法用于获取和这个类型相关的一切信息:
type Type interface { ... Method(i int) Method // 获取挂在类型上的第 i'th 个方法 ... NumMethod() int // 该类型上总共挂了几个方法 Name() string // 类型的名称 PkgPath() string // 所在包的名称 Size() uintptr // 占用字节数 String() string // 该类型的字符串形式 Kind() Kind // 元类型 ... Bits() // 占用多少位 ChanDir() // 通道的方向 ... Elem() Type // 数组,切片,通道,指针,字典(key)的内部子元素类型 Field(i int) StructField // 获取结构体的第 i'th 个字段 ... In(i int) Type // 获取函数第 i'th 个参数类型 Key() Type // 字典的 key 类型 Len() int // 数组的长度 NumIn() int // 函数的参数个数 NumOut() int // 函数的返回值个数 Out(i int) Type // 获取函数 第 i'th 个返回值类型 common() *rtype // 获取类型结构体的共同部分 uncommon() *uncommonType // 获取类型结构体的不同部分}
所有的类型结构体都包含一个共同的部分信息,这部分信息使用rtype
结构体描述,rtype
实现了Type
接口的所有方法:
// 基础类型 rtype 实现了 Type 接口type rtype struct { size uintptr // 占用字节数 ptrdata uintptr hash uint32 // 类型的hash值 ... kind uint8 // 元类型 ...}// 切片类型type sliceType struct { rtype elem *rtype // 元素类型}// 结构体类型type structType struct { rtype pkgPath name // 所在包名 fields []structField // 字段列表}...
不同于reflect.Type
的复杂,reflect.Value
是一个非常简单的结构体:
type Value struct { typ *rtype // 变量的类型结构体 ptr unsafe.Pointer // 数据指针 flag uintptr // 标志位}
来看一个简单的例子:
package mainimport ( "fmt" "reflect")func main() { type SomeInt int var s SomeInt = 42 var t = reflect.TypeOf(s) var v = reflect.ValueOf(s) // reflect.ValueOf(s).Type() 等价于 reflect.TypeOf(s) fmt.Println(t == v.Type()) fmt.Println(v.Kind() == reflect.Int) // 元类型 // 将 Value 还原成原来的变量 var is = v.Interface() fmt.Println(is.(SomeInt))}------truetrue42Process finished with exit code 0
Value
结构体虽然简单,但是其附带的方法非常多,主要是用来方便用户读写ptr
字段指向的数据内存。使用Value
结构体提供的方法要比unsafe
包更加简单直接:
func (v Value) SetLen(n int) // 修改切片的 len 属性func (v Value) SetCap(n int) // 修改切片的 cap 属性func (v Value) SetMapIndex(key, val Value) // 修改字典 kvfunc (v Value) Send(x Value) // 向通道发送一个值func (v Value) Recv() (x Value, ok bool) // 从通道接受一个值// Send 和 Recv 的非阻塞版本func (v Value) TryRecv() (x Value, ok bool)func (v Value) TrySend(x Value) bool// 获取切片、字符串、数组的具体位置的值进行读写func (v Value) Index(i int) Value// 根据名称获取结构体的内部字段值进行读写func (v Value) FieldByName(name string) Value// 将接口变量装成数组,一个是类型指针,一个是数据指针func (v Value) InterfaceData() [2]uintptr// 根据名称获取结构体的方法进行调用// Value 结构体的数据指针 ptr 可以指向方法体func (v Value) MethodByName(name string) Value...
第一条定律的意思是反射将接口变量转换成反射对象Type
和Value
:
func TypeOf(v interface{}) Typefunc ValueOf(v interface{}) Value
第二条定律的意思是可以通过反射对象Value
还原成原先的接口变量,指的就是Value
结构体提供的Interface
方法:
func (v Value) Interface() interface{}
注意:
v.Interface
得到的是一个接口变量,还需要经过一次造型才能还原成原先的变量
第三条定律的意思是值类型的变量不可以通过反射来修改,因为在反射之前,传参的时候需要将值变量转换成接口变量,值内容会被浅拷贝,所以reflect
包直接禁止了通过反射来修改值类型的变量:
package mainimport "reflect"func main() { var s int = 42 var v = reflect.ValueOf(s) v.SetInt(43)}------panic: reflect: reflect.Value.SetInt using unaddressable valuegoroutine 1 [running]:reflect.flag.mustBeAssignable(0x82) C:/Go/src/reflect/value.go:234 +0x15ereflect.Value.SetInt(0x47b4a0, 0xc00004c000, 0x82, 0x2b) C:/Go/src/reflect/value.go:1472 +0x36main.main() C:/Users/abel1/go/src/hello/routine.go:8 +0xc7Process finished with exit code 2
可以看到当尝试通过反射来修改整型变量时,程序直接抛出了异常。而通过反射来修改指针变量指向的值还是可行的:
panic: reflect: reflect.Value.SetInt using unaddressable valuegoroutine 1 [running]:reflect.flag.mustBeAssignable(0x82) C:/Go/src/reflect/value.go:234 +0x15ereflect.Value.SetInt(0x47b4a0, 0xc00004c000, 0x82, 0x2b) C:/Go/src/reflect/value.go:1472 +0x36main.main() C:/Users/abel1/go/src/hello/routine.go:8 +0xc7Process finished with exit code 2------43Process finished with exit code 0
结构体也是值类型,也必须通过指针类型来修改。下面尝试使用反射来动态修改结构体内部字段的值:
package mainimport "fmt"import "reflect"type Rect struct { Width int Height int}func SetRectAttr(r *Rect, name string, value int) { var v = reflect.ValueOf(r) var field = v.Elem().FieldByName(name) field.SetInt(int64(value))}func main() { var r = Rect{50, 100} SetRectAttr(&r, "Width", 100) SetRectAttr(&r, "Height", 200) fmt.Println(r)}-----{100 200}Process finished with exit code 0
Go 语言有很多内置包,内置包的使用需要用户手工import
进来。Go 语言的内置包都是已经编译好的「包对象」,使用时编译器不需要进行二次编译:
// go sdk 安装路径> go env GOROOT/usr/local/go> go env GOOSdarwin> go env GOARCHamd64> ls /usr/local/go/darwin_amd64total 22264drwxr-xr-x 4 root wheel 136 11 3 05:11 archive-rw-r--r-- 1 root wheel 169564 11 3 05:06 bufio.a-rw-r--r-- 1 root wheel 177058 11 3 05:06 bytes.adrwxr-xr-x 7 root wheel 238 11 3 05:11 compressdrwxr-xr-x 5 root wheel 170 11 3 05:11 container-rw-r--r-- 1 root wheel 93000 11 3 05:06 context.adrwxr-xr-x 21 root wheel 714 11 3 05:11 crypto-rw-r--r-- 1 root wheel 24002 11 3 05:02 crypto.a...
Go 语言的GOPATH
路径下存放了全局的第三方依赖包,当我们在代码里面import
某个第三方包时,编译器都会到GOPATH
路径下面来寻找。
> go env GOPATH/Users/qianwp/go
GOPATH
下有三个重要的子目录,分别是:
src
:存放第三方包的源代码pkg
:存放编译好的第三方包对象bin
:存放第三方包提供的二进制可执行文件当我们导入第三方包时,编译器优先寻找已经编译好的包对象,如果没有包对象,就会去源码目录寻找相应的源码来编译。使用包对象的编译速度会明显快于使用源码
可以使用go get
指令直接去相应的网站上拉取包代码,默认使用 HTTPS 协议下载代码仓库,可以使用-insecure
参数切换到 HTTP 协议。
import "github.com/go-redis/redis"import "golang.org/x/net"import "gopkg.in/mgo.v2"import "myhost.com/user/repo" // 个人提供的仓库
现在尝试编写第一个 Go 语言算法模块mathy
,提供两个方法:Fib
用来计算斐波那契数,Fact
用来计算阶乘:
> mkdir -p $GOPATH/src/github.com/abelsu7/mathy> cd $GOPATH/src/github.com/abelsu7/mathy
然后创建mathy.go
文件:
package mathy// 函数名大写,其它的包才可以看的见func Fib(n int) int64 { if n <= 1 { return 1 } var s = make([]int64, n+1) s[0] = 1 s[1] = 1 for i := 2; i <= n; i++ { s[i] = s[i-1] + s[i-2] } return s[n]}func Fact(n int) int64 { if n <= 1 { return 1 } var s int64 = 1 for i := 2; i <= n; i++ { s *= int64(i) } return s}
之后去其他的任意空目录下编写main.go
文件来使用mathy
,但是不能在当前目录,因为同一个目录只能有同一个包名:
package mainimport ( "fmt" "github.com/abelsu7/mathy")func main() { fmt.Println(mathy.Fib(10)) fmt.Println(mathy.Fact(10))}------893628800Process finished with exit code 0
将代码推送到 Github 上,之后在任意 GO 语言环境下使用go get github.com/abelsu7/mathy
即可将代码拉取到$GOPATH/src/
目录下。
import pmathy "github.com/pyloque/mathy"import omathy "github.com/other/mathy"
Go 语言还支持一种罕见的导入语法可以将其它包的所有类型变量都导入到当前的文件中,在使用相关类型变量时可以省去包名前缀:
package mainimport "fmt"import . "github.com/pyloque/mathy"func main() { fmt.Println(Fib(10)) fmt.Println(Fact(10))}
Go 提供了三个比较的常用的指令go get
、go build
、go install
用来进行全局的包管理。
go build
:仅编译。如果当前包里有main
包,就会生成二进制文件。如果没有main
包,则仅仅用来检查编译是否可以通过,编译完成后会丢弃所有临时包对象。如果指定-i
参数,则会将编译成功的第三方依赖包对象安装到$GOPATH/pkg
目录go install
:先编译,再安装。将编译成的包对象安装到$GOPATH
的pkg
目录中,将编译成的可执行文件安装到$GOPATH
的bin
目录中。如果指定-i
参数,还会安装编译成功的第三方依赖包对象go get
:下载代码、编译和安装。安装内容包括包对象和可执行文件,但是不包括依赖包使用
go run
指令时如果发现程序启动了很久,就可以考虑先执行go build -i
指令,将编译成功的依赖包都安装到$GOPATH/pkg
,这样再次运行go run
指令就会快很多
多版本依赖有一个专业的名称叫「钻石型」依赖。
为了解决这个问题,Go 1.6 引入了 Vendor 机制,就是在当前项目的目录下增加vendor
子目录,将自己项目依赖的所有第三方包放到vendor
目录里。这样当你导入第三方包的时候,优先去vendor
目录里找你需要的第三方包。如果没有,再去$GOPATH
全局路径下找。
]]>使用 Vendor 有一个限制,那就是你不能将 Vendor 里面依赖的类型暴露到外面去,Vendor 里面的依赖包提供的功能仅限于当前项目使用,这就是 Vendor 的「隔离沙箱」。正是因为这个沙箱才使得项目里可以存在因为依赖传递导致的同一个依赖包的多个版本
重启或关闭 Linux 系统是诸多风险操作之一,务必慎之又慎。
Linux 系统在重启或关闭之前,会通知所有已登录的用户和进程。如果在命令中加入了时间参数,系统还将拒绝新的用户登入请求。
推荐阅读:
下面将依次介绍以下命令
shutdown
、halt
、poweroff
、reboot
:用于休眠、重启或关机init
:initialization 的简称,是系统启动的第一个进程systemctl
:systemd 是 Linux 系统和服务器的管理程序shutdown
命令用于重启或关闭本地/远程的 Linux 设备,并提供了多个选项。如果定义了时间参数,则系统会在关机的 5 分钟前创建/run/nologin
文件,以确保后续的登录请求会被拒绝。
通用语法如下:
> shutdown [OPTION] [TIME] [MESSAGE]
运行以下命令则会立即关闭 Linux 设备。-h now
表示立刻杀死所有进程,并关闭系统:
-h
:如果不特指-halt
选项,则等价于-poweroff
选项
> shutdown -h now
另外我们可以使用带有-halt
选项的shutdown
命令立即关闭设备:
-H
、--halt
:停止设备运行
> shutdown --halt now # 或者> shutdown -H now
还可以使用带有poweroff
选项的shutdown
命令:
-P
、--poweroff
:切断电源(默认)
> shutdown --poweroff now# 或者> shutdown -P now
如果没有使用时间选项运行以下命令,则命令会在一分钟之后执行:
[root@centos-1~] > shutdown -hShutdown scheduled for Mon 2018-10-08 06:42:31 EDT, use 'shutdown -c' to cancel.[root@centos-2~] >Broadcast message from root@centos-1 (Mon 2018-10-08 06:41:31 EDT):The system is going down for power-off at Mon 2018-10-08 06:42:31 EDT!
若要取消关机计划,则可使用shutdown -c
:
[root@centos-1~] > shutdown -cBroadcast message from root@centos-1 (Mon 2018-10-08 06:39:09 EDT):The system shutdown has been cancelled at Mon 2018-10-08 06:40:09 EDT!
同样的,其他登录用户都能在中断中看到如下的广播消息:
[root@centos-2~] >Broadcast message from root@centos-1 (Mon 2018-10-08 06:41:35 EDT):The system shutdown has been cancelled at Mon 2018-10-08 06:42:35 EDT!
如果想在指定时间(例如N
秒)后执行重启或关机操作,则可添加时间参数,并可以为所有登录用户添加自定义广播消息。例如,我们将在五分钟后重启设备:
[root@centos-1~] > shutdown -r +5 "To activate the latest Kernel"Shutdown scheduled for Mon 2018-10-08 07:13:16 EDT, use 'shutdown -c' to cancel.[root@centos-2~] >Broadcast message from root@vps.2daygeek.com (Mon 2018-10-08 07:08:16 EDT):To activate the latest KernelThe system is going down for reboot at Mon 2018-10-08 07:13:16 EDT!
运行以下命令则会立即杀死所有进程并重启系统:
> shutdown -r now
reboot
命令同样可以重启或关闭本地/远程的 Linux 设备。
执行不带任何参数的reboot
命令以重启 Linux 设备:
> reboot
执行带-p
参数的reboot
命令以关闭 Linux 设备电源:
-p
、--poweroff
:调用halt
或poweroff
命令,切断设备电源
> reboot -p
执行带-f
参数的reboot
命令以强制重启 Linux 设备(类似按压机器上的电源键):
-f
、--force
:立刻强制终端,切断电源或重启
> reboot -f
init
进程是 Linux 系统启动的第一个进程。
它会检查/etc/inittab
文件并决定 Linux 的运行级别。同时,允许用户在 Linux 设备上执行关机或重启操作。级别范围为0~6
,共七个运行等级。
执行以下命令关闭系统:
0
:停机 - 关闭系统
> init 0
执行以下命令重启设备:
6
:重启 - 重启设备
> init 6
halt
命令用来切断电源或关闭本地/远程 Linux 设备。它会中断所有进程并关闭 CPU:
> halt
poweroff
命令同样用来切断电源或关闭本地/远程 Linux 设备。poweroff
很像halt
,但不同的是它可以关闭设备硬件:poweroff
会给主板发送 ACPI 指令,主板再将信号发送给电源并切断电源:
> poweroff
systemd 是一款适用于所有主流 Linux 发行版的全新 init 系统和系统管理器,它是内核启动的第一个进程,并持有序号为1
的进程 PID。
systemd
是一切进程的父进程,Fedora 15 是第一个适配安装 systemd(替代 upstart)的 Linux 发行版。
systemctl
是命令行下管理 systemd 守护进程和服务的主要工具。常用命令包括:start
、restart
、stop
、enable
、disable
、reload
和status
。
systemd 使用.service
文件而不是 SysV init 使用的 bash 脚本。systemd 将所有守护进程归于自身的 Linux cgroups 用户组下,可以浏览/cgroup/systemd
文件查看该系统的层次等级。
> systemctl halt> systemctl poweroff> systemctl reboot> systemctl suspend> systemctl hibernate
]]>参考文章
Google 在今年一月发布了golang.org 的镜像站golang.google.cn,中国大陆可直接访问。详情参见 Hello, 中国! | The Go Blog
微软官方开发的 Go for Visual Studio Code 插件为Go 语言 提供了丰富的支持。在VS Code 中首次打开 Go 工作区后,VS Code 会自动检测当前开发环境为 Go 并推荐安装上述插件。
然而 Go 插件的安装并不顺利:输出窗口的安装信息提示其中一些依赖工具安装失败:
Installing github.com/mdempsky/gocode FAILEDInstalling github.com/ramya-rao-a/go-outline FAILEDInstalling github.com/acroca/go-symbols FAILEDInstalling golang.org/x/tools/cmd/guru FAILEDInstalling golang.org/x/tools/cmd/gorename FAILEDInstalling github.com/stamblerre/gocode FAILEDInstalling github.com/ianthehat/godef FAILEDInstalling github.com/sqs/goreturns FAILEDInstalling golang.org/x/lint/golint FAILED9 tools failed to install.
手动使用go get -v github.com/mdempsky/gocode
等命令同样提示网络连接失败。
原因其实很简单:golang.org 在国内由于一些众所周知的原因无法直接访问,而go get
在获取gocode
、go-def
、golint
等插件依赖工具的源码时,需要从golang.org 上拉取部分代码至GOPATH
,自然就导致了最后这些依赖于golang.org 代码的依赖工具安装失败。
解决也并不复杂:先通过git clone
命令手动将依赖工具的源码拉取至GOPATH
的对应路径,再通过go install
命令安装依赖工具。
以 Windows 为例,首先进入%GOPATH%\src\
目录,并创建golang.org\x
。
之后进入%GOPATH%\src\golang.org\x
,使用下列命令下载插件依赖工具的源码:
git clone https://github.com/golang/tools.git tools
git clone
命令执行完毕后,所需的工具源码就都保存在tools
目录中。
最后进入%GOPATH%
目录,根据之前的安装失败提示信息安装对应的依赖工具:
go install github.com/mdempsky/gocodego install github.com/ramya-rao-a/go-outlinego install github.com/acroca/go-symbolsgo install golang.org/x/tools/cmd/gurugo install golang.org/x/tools/cmd/gorenamego install github.com/stamblerre/gocodego install github.com/ianthehat/godefgo install github.com/sqs/goreturnsgo install golang.org/x/lint/golint
在执行go install
命令安装 golint 时,提示信息如下:
go install golang.org/x/lint/golintcan't load package: package golang.org/x/lint/golint: cannot find package "golang.org/x/lint/golint" in any of: C:\Go\src\golang.org\x\lint\golint (from $GOROOT) C:\Users\abel1\go\src\golang.org\x\lint\golint (from $GOPATH)
这是因为 golint 的源码在lint
下,而不是tools
,需要单独拉取 golint 源码。
进入%GOPATH%\src\golang.org\x
,执行下列命令拉取 golint 源码:
git clone https://github.com/golang/lint
最后回到%GOPATH%
,通过go install
安装 golint:
go install golang.org/x/lint/golint
重启 VS Code 后,插件就可以正常使用了。Now let’s go for Go!
]]>参考文章
参考文献格式应符合GB7714-1987《文后参考文献著录规则》
根据GB3469-83《文献类型与文献载体代码》规定,各类常用文献以单字母标识:
M: 专著C: 论文集N: 报纸文章J: 期刊文章D: 学位论文R: 研究报告S: 标准P: 专利A: 专著、论文集中的析出文献Z: 其他未说明的文献类型
电子文献类型以双字母作为标识:
DB: 数据库CP: 计算机程序EB: 电子公告
非纸张型载体电子文献,在参考文献标识中同时标明其载体类型:
DB/OL: 联机网上的数据库 DB/MT: 磁带数据库M/CD: 光盘图书CP/DK: 磁盘软件J/OL: 网上期刊EB/OL: 网上电子公告
# 应标明年、卷、期,尤其注意区分卷和期[序号] 主要作者.文献题名[J].刊名,出版年份,卷号(期号):起止页码.# 例如:[1] 袁庆龙,候文义.Ni-P 合金镀层组织形貌及显微硬度研究[J].太原理工大学学报,2001,32(1):51-53.
# 应标明出版地及所参阅内容在原文献中的位置[序号] 著者.书名[M].出版地:出版者,出版年:起止页码.# 例如:[2] 刘国钧,王连成.图书馆史研究[M].北京:高等教育出版社,1979:15-18,31.
# 应标明出版信息及起止页码[序号] 著者.文献题名[C].编者.论文集名.出版地:出版者,出版年:起止页码.# 例如:[3] 孙品一.高校学报编辑工作现代化特征[C].中国高等学校自然科学学报研究会.科技编辑学论文集(2).北京:北京师范大学出版社,1998:10-22.
# 应标明保存单位及发表年份[序号] 作者.题名[D].保存地:保存单位,年份.# 例如:[4] 张和生.地质力学系统理论[D].太原:太原理工大学,1998.
# 应标明报告会主办单位及年份[序号] 作者.文献题名[R].报告地:报告会主办单位,年份.# 例如:[5] 冯西桥.核反应堆压力容器的LBB 分析[R].北京:清华大学核能技术设计研究院,1997.
# 应标明专利所有者及发布日期[序号] 专利所有者.专利题名[P].专利国别:专利号,发布日期.# 例如:[6] 姜锡洲.一种温热外敷药制备方案[P].中国专利:881056078,1983-08-12.
# 应标明出版地、出版者及出版年份[序号] 标准代号,标准名称[S].出版地:出版者,出版年.# 例如:[7] GB/T 16159—1996,汉语拼音正词法基本规则[S].北京:中国标准出版社,1996.
# 应标明出版日期及印刷批次[序号] 作者.文献题名[N].报纸名,出版日期(版次).# 例如:[8] 谢希德.创造学习的思路[N].人民日报,1998-12-25(10).
# 应标明载体类型及可获得地址[序号] 作者.电子文献题名[文献类型/载体类型].电子文献的出版或可获得地址,发表或更新的期/引用日期(任选).# 例如:[9] 王明亮.中国学术期刊标准化数据库系统工程的[EB/OL].
在百度学术中搜索想要引用的参考文献,点击引用或批量引用即可自动生成引用格式。
支持GB/T 7714(国标)、MLA、APA三种格式,在批量引用中可一键复制全部的参考文献引用格式。
同样支持GB/T 7714(国标)、MLA、APA。
]]>参考文章
判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
Try it on Leetcode
示例 1
输入: 121输出: true
示例 2
输入: -121输出: false解释: 从右向左读为 121-, 因此不是回文数
示例 3
输入: 10输出: false解释: 从右向左读为 01, 因此不是回文数
映入脑海的第一个想法是将数字转换为字符串,并检查字符串是否为回文。但是,这需要额外的非常量空间来创建问题描述中所不允许的字符串。
第二个想法是将数字本身反转,然后将反转后的数字与原始数字进行比较,如果它们是相同的,那么这个数字就是回文数。但是,如果反转后的数字大于Integer.MAX_VALUE
,就会遇到整数溢出的问题。
按照第二个想法,为了避免数字反转可能导致的溢出问题,可以考虑只反转int
数字的一半。如果输入的是回文数,那么其后半部分反转后应该与原始数字的前半部分相同。
class Solution { public boolean isPalindrome(int x) { /** * if x < 0 or end up with 0 ( except 0 itself ) * then return false */ if (x < 0 || (x % 10 == 0 && x != 0)) { return false; } int reverseNumber = 0; while (x > reverseNumber) { reverseNumber = reverseNumber * 10 + x % 10; x /= 10; } return x == reverseNumber || x == reverseNumber / 10; }}
func isPalindrome(x int) bool { if x < 0 || (x % 10 == 0 && x != 0) { return false } reverseNumber := 0 for x > reverseNumber { reverseNumber = reverseNumber * 10 + x % 10 x /= 10 } return x == reverseNumber || x == reverseNumber / 10}
10
]]>
你和你的朋友,两人一起玩 Nim 游戏:桌子上有一堆石头,每次你们轮流拿掉
1-3
块石头,你作为先手,拿掉最后一块石头的人获胜。你们是聪明人,每一步都是最优解。编写一个函数,来判断你是否可以在给定石头数量的情况下赢得游戏。
Try it on Leetcode
Input : 4Output : false
如果堆中有4
块石头,那么你永远不会赢得游戏,因为最后一块石头总是会被你的朋友拿走。
根据 巴什博奕,若有:
n % (m + 1) != 0
则可知先手必胜。
class Solution { public boolean canWinNim(int n) { // Bash Game - n % (m + 1) != 0. First will win. return n % 4 != 0; }}
]]>
给定一个二叉树,检查它是否是镜像对称的。
Try it on Leetcode
例如,二叉树[1,2,2,3,4,4,3]
是对称的
1 / \ 2 2 / \ / \3 4 4 3
但是下面这个1,2,2,null,3,null,3
则不是镜像对称的
1 / \ 2 2 \ \ 3 3
首先,给出我们将要使用的树的节点TreeNode
的定义:
/*** Definition for a binary tree node. */public class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; }}
如果一个树的左子树和右子树镜像对称,那么这个树就是对称的。
因此,该问题可以转化为:两个树在什么情况下互为镜像?
如果同时满足下列条件,则两个树互为镜像:
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public boolean isSymmetric(TreeNode root) { return isMirror(root, root); } public boolean isMirror(TreeNode t1, TreeNode t2) { if (t1 == null && t2 == null) return true; if (t1 == null || t2 == null) return false; return (t1.val == t2.val) && isMirror(t1.right, t2.left) && isMirror(t1.left, t2.right); }}
/** * Definition for a binary tree node. * type TreeNode struct { * Val int * Left *TreeNode * Right *TreeNode * } */func isSymmetric(root *TreeNode) bool { return isMirror(root, root)}func isMirror(t1 *TreeNode, t2 *TreeNode) bool { switch { case t1 == nil && t2 == nil: return true case t1 == nil || t2 == nil: return false default: return (t1.Val == t2.Val) && isMirror(t1.Right, t2.Left) && isMirror(t1.Left, t2.Right) }}
除了递归的方法外,还可以利用队列进行迭代。队列中每两个连续的结点应该是相等的,而且它们的子树互为镜像。
最开始,队列中包含的是root
和root
。该算法的工作原理类似于 BFS,但存在一些关键差异。
每次提取两个节点并比较它们的值。然后将两个节点的左右子节点按相反的顺序插入队列中。当队列为空时,或我们检测到树不对称(即从队列中取出两个不相等的连续节点)时,算法结束。
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public boolean isSymmetric(TreeNode root) { Queue<TreeNode> q = new LinkedList<>(); q.add(root); q.add(root); while (!q.isEmpty()) { TreeNode t1 = q.poll(); TreeNode t2 = q.poll(); if (t1 == null && t2 == null) continue; if (t1 == null || t2 == null) return false; if (t1.val != t2.val) return false; q.add(t1.left); q.add(t2.right); q.add(t1.right); q.add(t2.left); } return true; }}
]]>
给定一个二叉树,找出其最大深度。
Try it on Leetcode
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
给定二叉树[3,9,20,null,null,15,7]
3 / \ 9 20 / \ 15 7
返回它的最大深度3
。
首先,给出我们将要使用的树的节点TreeNode
的定义:
/*** Definition for a binary tree node. */public class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; }}
最直观的方法是通过递归来解决该问题,上图演示了 DFS(深度优先搜索)策略的求解过程。
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public int maxDepth(TreeNode root) { return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; }}
N
是节点的数量。N
次(树的高度),因此保持调用栈的存储为 $O(N)$。但在最好的情况下(树是完全平衡的),树的高度为 $\log(N)$,此时空间复杂度为 $O(\log(N))$。迭代的思路是使用 DFS 策略访问每个节点,同时在每次访问时更新最大深度。
所以从包含根节点且相应深度为1
的栈开始,然后继续迭代:将当前节点弹出栈并推入子节点。并且每一步都会更新深度。
import java.util.LinkedList;import java.util.Queue;import javafx.util.Pair;/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public int maxDepth(TreeNode root) { Queue<Pair<TreeNode, Integer>> stack = new LinkedList<>(); if (root != null) { stack.add(new Pair(root, 1)); } int depth = 0; while (!stack.isEmpty()) { Pair<TreeNode, Integer> current = stack.poll(); root = current.getKey(); int current_depth = current.getValue(); if (root != null) { depth = Math.max(depth, current_depth); stack.add(new Pair(root.left, current_depth + 1)); stack.add(new Pair(root.right, current_depth + 1)); } } return depth; }}
]]>
给定一个非负整数
numRows
,生成杨辉三角的前numRows
行。
Try it on Leetcode
输入: 5输出:[ [1], [1,1], [1,2,1], [1,3,3,1], [1,4,6,4,1]]
如果能够知道一行杨辉三角,我们就可以根据每对相邻的值轻松地计算出它的下一行。
首先,初始化整个 triangle
列表,三角形的每一行都以子列表的形式存储。
之后,检查行数为 0
的特殊情况,若为 0
则直接返回 []
。
如果 numRows > 0
,则用 [1]
作为第一行来初始化 triangle
,并按如下方式继续填充:
class Solution { public List<List<Integer>> generate(int numRows) { List<List<Integer>> triangle = new ArrayList<>(); // Case 1: if numRows equals zero, then return zero rows. if (0 == numRows) { return triangle; } // Case 2: the first row is always [1]. triangle.add(new ArrayList<>()); triangle.get(0).add(1); // Case 3: if numRows > 1, calculate according to the previous row. for (int curRow = 1; curRow < numRows; curRow++) { List<Integer> row = new ArrayList<>(); List<Integer> preRow = triangle.get(curRow - 1); // The first element is 1. row.add(1); // DP: row[i][j] = row[i-1][j-1] + row[i-1][j] for (int j = 1; j < curRow; j++) { row.add(preRow.get(j-1) + preRow.get(j)); } // The last element is 1. row.add(1); triangle.add(row); } return triangle; }}
class Solution: def generate(self, num_rows): triangle = [] for row_num in range(num_rows): # The first and last row elements are always 1. row = [None for _ in range(row_num+1)] row[0], row[-1] = 1, 1 # Each triangle element is equal to the sum of the elements # above-and-to-the-left and above-and-to-the-right. for j in range(1, len(row)-1): row[j] = triangle[row_num-1][j-1] + triangle[row_num-1][j] triangle.append(row) return triangle
]]>
📷图片存起来干啥,留着过年吗?
🚴没错(理直气壮)!
某种程度上,现代生活已经离不开微信了。微信公众号也正在变成一个越来越庞大的内容集散地。
维护过个人公众号的朋友应该知道,在制作新素材时要上传图片作为文章封面,而在用户的手机端只能看到封面图片,并不能直接保存。
但有时我们会看到非常喜欢的封面图片,想存起来又该怎么办呢?例如最近看到为什么程序员需要了解数学?| 纯洁的微笑这篇文章,虽然是篇广告文。。不过封面图片很吸引我,也许以后写文会用得上:
想要存图也很简单:直接 PC 端开调试看源码就好。
首先在浏览器中打开文章链接,Ctrl+U 查看源码,Ctrl+F 搜索 var msg
,找到如下的匹配字段,
其中:
msg_title
对应图文标题msg_desc
对应图文摘要msg_cdn_url
即为我们需要的封面图片 urlcdn_url_1_1
则对应分享至朋友圈时显示的正方形缩略图其他字段的对应关系此处不再阐述~
参考文章
数值分析例题整理
证明:设 $x$ 的非零近似值 记为规格化形式 ,其中 $k$ 为整数,$a_1,a_2,\cdots a_n \in \{ 0,1,\cdots,9; \ a \ne 0 \}$,则
1.如果 有 $n$ 位有效数字,则
2.如果
则 $x^*$ 至少有 $n$ 位有效数字。
要使 $\sqrt{20}$ 的近似值的相对误差不超过 $0.1\%$,问用计算器计算时,应取几位有效数字?
计算球体积 $V=\displaystyle{\frac{4}{3}} \pi r^3$ 时,为使 $V$ 的相对误差不超过 $0.3\%$,求半径 $r$ 的相对误差允许范围。
设 $t=1.21$,$\mu = 3.65$,$\upsilon = 9.81$ 均准确到小数点后两位,试估计下列计算的相对误差:$x=t \mu + \upsilon$。
分析递推公式
的数值稳定性,设实际计算时,取 $\sqrt{783} \approx 27.982$ 进行近似计算。
试导出计算 $I_n = \displaystyle{\int_{0}^{1}}x^ne^x\mathrm{d}x \ (n=0,1,2,\cdots)$ 的递推公式,并讨论其数值稳定性。
证明函数序列 $f_n(x) = \displaystyle{\frac{x}{1+n^2x^2}}$ 在 $[0,1]$ 上一致收敛于 $0$,即 $\lim \limits_{n \to \infty} f_n(x) = 0$。
设 $\pmb{x} = (2,-4,3)^T$,求 、、。
设 ,求 、、、。
证明:设已知 $[a,b]$ 上的函数 $f$ 在 $n+1$ 个互异节点 $x_i \in [a,b]$ 上的值 $f_i=f(x_i) \ (i=0,1,\cdots,n)$,则存在唯一的次数 $\leqslant n$ 的多项式 $p_n(x) \in \pmb{P}_n$ 满足:
求过三个样点 $A(0,1)$,$B(1,2)$,$C(2,3)$ 的插值多项式。
设已知 $f(x) = e^{-x}$ 的四个函数值如下表所示:
试用 $Lagrange$ 公式的线性插值求 $e^{-1.4}$ 的近似值,用二次插值求 $e^{-2.1}$ 的近似值。
证明:由 $n+1$ 个互异节点 $x_i \in [a,b]$ 构成的 $n+1$ 个插值基函数 $l_i(x) \ (i=0,1,\cdots,n)$ 具有性质:
对函数 $f(x) = \ln (x)$,给定:
试用三次 $Hermite$ 插值多项式 $H_3(x)$ 计算 $f(1.5)$ 的近似值并估计误差。
试用多种解法求一个次数 $\leqslant 3$ 的插值多项式 $H_3(x)$,满足插值条件:
已知实验数据如下:
试求其拟合曲线。
设已知实验数据如下表:
试求其二次多项式拟合模型:
已知离散数据如下(权 $\rho \equiv 1$):
求形如 $s(x) = ae^{bx}$ 的拟合曲线。
已知离散数据如下(权 $\rho \equiv 1$):
求形如 $s(x) = \displaystyle{\frac{x}{ax+b}}$ 的拟合曲线。
求下列超定方程组的近似解:
已知实验数据如下:
以法方程的矩阵形式求解。
已知实验数据如下:
按最小二乘拟合求形如 $s(x)=ax+bx^2$ 的经验公式。
用顺序 $Gauss$ 消去法(消去过程加回代过程)解方程组:
用列主元 $Gauss$ 消去法解方程组,并求系数矩阵行列式值 $\det \pmb{A}$:
用直接三角分解法解方程组:
已知方程组:
分别用
以 $\pmb{x}^{(0)}=(0,0,0)^T$ 为初始向量,计算其前三个迭代值,并与精确解 $\pmb{x}^*=(3,2,1)^T$ 比较。
用 $Jacobi$ 迭代法和 $Gauss$-$Seidel$ 迭代法求解方程组 $\pmb{A}\pmb{x}=\pmb{b}$,其中:
取 $\pmb{x}^{(0)} = (1,-1,1)^T$,$\varepsilon = \displaystyle{\frac{1}{2}} \times 10^{-6}$。
已知方程组:
使用 $J$ 法和 $GS$ 法求解此方程的收敛性。
试证明解下列方程组:
的 $J$ 法收敛;而 $GS$ 法发散。
设方程组:
试写出解此方程组的收敛的 $J$ 迭代公式和 $GS$ 迭代公式。
设方程组 $\pmb{A}\pmb{x}=\pmb{b}$,其中 ,,用下列迭代公式求解:
设方程 $f(x) = e^x + 10x -2 = 0$,试:
用不动点迭代法求方程 $x^3 + 4x^2 - 10=0$ 在 $[1,2]$ 内的一个实根。
证明:设迭代函数 $\varphi \in C[a,b]$ 满足条件:
则可得:
用不动点迭代法求解方程:
要求相对误差 $\Delta < 10^{-8}$。
对迭代函数 $\varphi(x) = x + \lambda(x^2 - 5)$,试找出使迭代公式 $x_{k+1} = \varphi(x_k) \ (k=0,1,\cdots)$ 局部收敛于 $x^*=\sqrt{5}$ 的 $\lambda$ 的取值范围。
为使下列形式的迭代公式:
所产生的序列 $\{x_k\}$ 收敛于 $\sqrt[3]{a}$,并且有尽可能高的收敛阶,试确定其中常数 $p$,$q$,$r$。
用 $Newton$ 迭代法求下列方程的近似根:
用 $Newton$ 迭代法解非线性方程组:
用乘幂法求矩阵 $\pmb{A}$ 的特征值和特征向量,其中:
用反幂法求矩阵 $\pmb{A}$ 的特征值和特征向量,其中:
]]>矩阵特征值和特征向量、乘幂法、反幂法
设 $\pmb{A}$ 是 $n$ 阶方阵,$\pmb{x}=(x_1,x_2,\cdots,x_n)^T$ 是 $n$ 维列向量。若有数值 $\lambda$,使得下面的矩阵方程:
有非零解 $\pmb{x}$,则称数值 $\lambda$ 为方阵 $\pmb{A}$ 的特征值,而对应的非零解 $\pmb{x}$ 称为对应特征值 $\lambda$ 的特征向量。
将上式变换为:
于是,当且仅当方程的系数矩阵行列式为零时,即
则方程有非零解。将上式等号左边按 $\lambda$ 展开,得到 $\lambda$ 的 $n$ 次多项式,称为 $\pmb{A}$ 的特征多项式,上式也变成多项式方程:
特征值 $\lambda$ 即为 $n$ 次多项式方程的解。
输入:矩阵 $\pmb{A}$,精度要求 $\varepsilon$,迭代次数上限 $N$
输出:模数最大的特征值 $\lambda_1$ 及对应的特征向量 $\pmb{x}$
算法过程:
输入:矩阵 $\pmb{A}$,精度要求 $\varepsilon$,迭代次数上限 $N$
输出:模数最小的特征值 $\lambda_n$ 及对应的特征向量 $\pmb{x}$
算法过程:
二分法、不动点迭代法、切线法
对 $f$ 不是 $x$ 的线性函数的方程,统称为非线性方程或一次方程。
方程的解 满足 ,也称为方程的根、函数的零点、不动点。
若 $f$ 在 $x^*$ 的邻域上可表示为
其中,$m$ 为正整数,则称 是方程的 $m$ 重根或函数 $f$ 的 $m$ 重零点。$m=1$ 时,称为单重根或单重零点。
若 $x^*$ 是 $f(x)=0$ 的 $m$ 重根,且 $g(x)$ 充分光滑,则可表示为:
若上式成立,则 $f(x)$ 在点 $x^*$ 处的 $Taylor$ 展开式为:
求根思想:把有根区间或隔离区间逐步缩小
如果方程 $f(x)=0$ 中,$f \in C[a,b]$,且 $f(a) \cdot f(b) < 0$,则由二分法产生的序列 $\{x_n\}$ 收敛于方程的根 $x^*$,且有误差估计:
将方程改写为等价方程 $x=\varphi(x)$,从某个取定的初值 $x_0$ 开始,对应上式构建迭代公式:
这种求根的方法就称为迭代法或函数迭代法,式中的 $\varphi(x)$ 称为迭代函数。如果 对函数 $\varphi(x)$ 满足 ,则称 为 $\varphi(x)$ 的不动点,因此函数迭代法也称为不动点迭代法。
设迭代公式中的迭代函数 $\varphi \in C[a,b]$ 满足条件:
则可得:
设 为 $\varphi$ 的不动点,$\varphi{}’(x)$ 在 的某个邻域 $\Delta$ 上存在、连续且 ,则迭代公式 $x_{k+1}=\varphi(x_k)$ 局部收敛
解一元非线性方程 $f(x) = 0$
$Newton$ 迭代公式在单根附近至少是 2 阶局部收敛的。
设 ,,且在 $x^*$ 的邻域上 $f{}’’$ 存在、连续,则:
方法 1
如果已知重根的重数 $m(m>1)$,则利用 $m$ 构造新迭代公式:
此时迭代函数为 $\varphi(x) = x - m \displaystyle{\frac{f(x)}{f{}’(x)}}$
这种方法的缺点是要事先知道重根的重数,但实际应用中往往并不知道。
方法 2
作 $F(x) = \displaystyle{\frac{f(x)}{f{}’(x)}}$,如果 是 $f(x)=0$ 的 $m$ 重根($m>1$),则 是 $f{}’(x)=0$ 的 $m-1$ 重根,从而 $x^*$ 是 $F(x)=0$ 的单根。新迭代公式:
这种方法的缺点是需要 $f$ 的 2 阶导数
$Newton$ 迭代法常用于求方根。如求平方根 $\sqrt{c}(c>0)$,令 $x=\sqrt{c}$,有 $x^2=c$,可得方程
则其正根 $x^*>0$,即为 $\sqrt{c}$。现用 $Newton$ 法可得相应的迭代公式:
整理成通用公式:
其意义就是把开方运算通过加法和除法来实现,这也是计算机系统(内部)做开方运算的实际做法。
$Newton$ 迭代的每一步都要计算导数值 $f{}’(x_k)$,当 $f(x)$ 的导数不存在时迭代公式还不能用。为此,考虑用函数 $f(x)$ 的差商代替求导:
于是代入 $Newton$ 迭代公式,可得:
对某种迭代过程 $x_{k+1} = \varphi(x_k)$ 的 $Aitken$(埃特金)加速方案:
令 $\Delta x_k = x_{k+1} - x_k$,$\Delta^2x_k = \Delta(\Delta x_k) = x_{k+2} - 2x_{k+1} + x_k$,则有:
称为 $Aitken\Delta^2$ 加速方案。
考虑非线性方程组 $\pmb{F}(x)=0$,其中:
当 $n=1$ 时,$\pmb{F}(\pmb{x})=f(x)$,是微分学中的一元函数,类似于一维情况下的不动点迭代方法。一元方程的 $Newton$ 迭代公式为:
将此迭代公式推广到方程组的情形,可得 $\pmb{F}(\pmb{x})=0$ 的 $Newton$ 迭代公式为:
其中,导数矩阵
为 $\pmb{F}(\pmb{x})$ 的 $Jacobi$ 矩阵,$\left(\pmb{F}{}’(\pmb{x})\right)^{-1}$ 为 $\pmb{F}\left(\pmb{x}\right)$ 的导数矩阵的逆矩阵。
上面的 $Newton$ 迭代公式只是一种形式记号,实际计算可采用下列形式:
最后,$Newton$ 法由 $\pmb{x}^{(k)}$ 计算 $\pmb{x}^{(k+1)}$ 的步骤是:
迭代法基本概念和迭代公式、迭代法收敛性理论
解线性代数方程组
($\pmb{A} \in \pmb{R}^{n \times n}$ 非奇异,$\pmb{b}=(b_1,b_2,\cdots,b_n)^T \neq 0$,$\pmb{x}=(x_1,x_2,\cdots,x_n)^T$ 为解向量)的迭代法具体做法是,将上述方程组变形为等价形式:
特别的,这里仅研究其线性的形式:
其中,$\pmb{B} \in \pmb{R}^{n \times n}$ 非奇异,$\pmb{f} \in \pmb{R}^n$。构造迭代公式:
设方程组 $\pmb{A}\pmb{x}=\pmb{b}$ 中 $\pmb{A}=(a_{ij}) \in \pmb{R}^{n \times n}$,$\pmb{b} = (b_i)\in \pmb{R}^{n \times n}$ 且 $a_{ii} \neq 0 \ (i=1,2,\cdots,n)$
假设系数矩阵 $A$ 的对角元 $a_{ii} \neq 0 \ (i=1,2,\cdots,n)$,则对角矩阵 $\pmb{D}=diag(a_{11},a_{22},\cdots,a_{nn})$ 非奇异。可将矩阵 $\pmb{A}$ 分解为:
其中,
此时原方程组可改写为:
其中,
则 $Jacobi$ 迭代公式为:
原方程组可改写为:
其中,
则 $Gauss-Seidel$ 迭代公式为:
设方程组为 $\pmb{x} = \pmb{B} \pmb{x} + \pmb{f}$,对任意的初始向量 $\pmb{x}^{(0)}$,解此方程组的迭代法
收敛的充分必要条件是迭代矩阵 $\pmb{B}$ 的谱半径 $\rho(\pmb{B}) < 1$
如果迭代法 $\pmb{x}^{(k+1)} = \pmb{B} \pmb{x}^{k} + \pmb{f} \quad (k=0,1,\cdots)$ 的迭代矩阵 $\pmb{B}$ 的某一种算子范数 ,则:
或
设 $\pmb{A} = (a_{ij}) \in \pmb{R}^{n \times n}$,若满足
则称 $\pmb{A}$ 为严格对角占优矩阵;若满足其中至少有一个严格不等式成立,则称 $\pmb{A}$ 为弱对角占优矩阵。
定理:若方程组 $\pmb{A}\pmb{x}=\pmb{b}$ 中,$\pmb{A} = (a_{ij}) \in \pmb{R}^{n \times n}$ 为严格对角占优矩阵,或为不可约弱对角占优矩阵,则解此方程组的 $J$ 法和 $GS$ 法均收敛。
设迭代法收敛,定义
称 $R(\pmb{B})$ 为迭代法的渐近收敛速度。由定义可知,$R(\pmb{B})$ 越大,收敛越快,也即 $\rho(\pmb{B})(0<\rho(\pmb{B})<1)$ 谱半径越小,收敛速度越快。
]]>上/下三角矩阵的回代/前推、顺序/列主元消去、矩阵三角分解
具有 $n$ 个未知数 $n$ 个方程的 $n$ 阶线性代数方程组的一般形式记为:
或写成向量-矩阵形式:
其中,
$\pmb{A}$ 称为系数矩阵,$\pmb{x}$ 称为解向量,$\pmb{b}$ 称为右端常数向量。实际应用中,主要处理实数情形的方程组,即 $\pmb{A} \in \pmb{R}^{n \times n}$,$\pmb{b} \in \pmb{R}^n$。
根据 $Grammer$(克兰姆)法则,若系数矩阵 $\pmb{A}$ 非奇异(或者说 $\pmb{A}$ 的行列式值 $\det \pmb{A} \neq 0$),则方程组存在唯一解:
其中,$D$ 表示 $\pmb{A}$ 对应的行列式值 $\det \pmb{A}$,$D_i$ 表示在 $D$ 中第 $i$ 列用 $\pmb{b}$ 替换。
假如方程组 $\pmb{A}\pmb{x}=\pmb{b}$($\pmb{A} \in \pmb{R}^{n \times n}$ 非奇异)已被约化为如下形状的上三角方程组:
则通过回代过程可求解:
类似的,假如方程组 $\pmb{A}\pmb{x}=\pmb{b}$($\pmb{A} \in \pmb{R}^{n \times n}$ 非奇异)已被约化为如下形状的下三角方程组:
则通过前推过程可求解:
待更新
待更新
待更新
把矩阵 $\pmb{A}$ 分解成两个三角矩阵 $\pmb{L}$ 与 $\pmb{U}$ 的乘积,$\pmb{A}=\pmb{L}\pmb{U}$,消元乘数为:
其中,$\pmb{L}$ 为单位下三角矩阵,$\pmb{U}$ 为上三角矩阵:
这样一来,解方程组 $\pmb{A}\pmb{x}=\pmb{b}$ 就转化为解方程组 $\pmb{L}\pmb{U}\pmb{x}=\pmb{b}$。令其中 $\pmb{U}\pmb{x}=\pmb{y}$,则解方程组 $\pmb{L}\pmb{U}\pmb{x}=\pmb{b}$ 又相当于依次解两个三角形方程组:
]]>最小二乘拟合、法方程的矩阵形式
设 $f$ 是在 $m+1$ 个节点 $x_j\in [a,b]$ 上给定的离散函数,即给定离散数据
要在某个指定空间 $\Phi$ 中,找出一个函数 作为 $f$ 的近似的连续模型,要求 在 $x_j$ 处的值 与 $f(x_j)$ 的误差
的平方和最小,即记 $\pmb{\delta}=(\delta_0,\delta_1,\cdots,\delta_m)^T$,有
或为了体现数据的重要性不同,引入对应 $[a,b]$ 上不同点 $x_j$ 的权函数值 $\rho(x_j)>0$,从而将上式改写成更一般的带权形式
这就是最小二乘拟合问题,$s^*(x)$ 称为 $f$ 在 $m+1$ 个节点 $x_j \ (j=0,1,\cdots,m)$ 上的最小二乘解,或称为拟合曲线或经验公式或回归线。
通常,在简单情形下,选择 $\Phi$ 为多项式空间(或其子空间),$\Phi = P_n = Span\{1,x,\cdots,x^n\}$,这时,若 $s(x) \in P_n$,则 $s(x)$ 的形式为
在一般情形下,选择 $\Phi$ 为线性空间 $\Phi = Span\{ \varphi_0(x),\varphi_1(x),\cdots,\varphi_n(x) \}$,其中 $\varphi_i(x)$ 是 $[a,b]$ 上已知的线性无关组,这时,若 $s(x)\in \Phi$,则有
两式中关于待定参数(也称回归系数)$a_0,a_1,\cdots,a_n$ 都是一次的,所以 $s(x)$ 是一种线性模型,而上述问题称为线性最小二乘拟合。
法方程及平方误差
法方程(组)或正则方程(组)可表示如下:
对 的误差估计,可使用平方误差:
或均方误差:
其中,平方误差还可导出另一种表示形式:
这种表示的优点是,计算平方误差时可以直接利用求解法方程过程中的信息,而无需调用计算 $s^*(x)$ 的子程序。
求最小二乘拟合曲线的主要步骤
根据已知数据求最小二乘拟合曲线有两个主要步骤:
二次多项式模型及权函数 $\rho_j \equiv 1$ 时对应的法方程
1. 指数模型:$s(x)=ae^{bx}$
对模型 $s(x)=ae^{bx}$,两边取对数
令 $Y=\ln{s(x)}$,$A=\ln{a}$,则上式为
并将原数据变化为 $(x_j,\ln{f(x_j)})$
2. 指数模型:$s(x)=ae^{\frac{b}{x}}$
对模型 $s(x)=ae^{\frac{b}{x}}$,两边取对数
令 $Y=\ln{s(x)}$,$A=\ln{a}$,$X=\displaystyle{\frac{1}{x}}$,则上式为
并将原数据变化为 $(\displaystyle{\frac{1}{x_j}},\ln{f(x_j)})$
3. 对数模型:$s(x)=\displaystyle{\frac{1}{a+bx}}$
对模型 $s(x)=\displaystyle{\frac{1}{a+bx}}$,取倒数为 $\displaystyle{\frac{1}{s(x)}}=a+bx$。令 $Y=\displaystyle{\frac{1}{s(x)}}$ ,即有一次拟合模型
并将原数据变化为 $\left( x_j, \displaystyle{\frac{1}{f(x_j)}} \right)$
4. 对数模型:$s(x)=\displaystyle{\frac{x}{ax+b}}$
对模型 $s(x)=\displaystyle{\frac{x}{ax+b}}$,取倒数为 $\displaystyle{\frac{1}{s(x)}}=a+b\displaystyle{\frac{1}{x}}$。令 $Y=\displaystyle{\frac{1}{s(x)}}$,$X=\displaystyle{\frac{1}{x}}$,即有一次拟合模型
并将原数据变化为 $\left( \displaystyle{\frac{1}{x_j}}, \displaystyle{\frac{1}{f(x_j)}} \right)$
超定方程组(或称矛盾方程组)即独立方程个数多余未知数个数的方程组,解超定方程组的一种方法是采用最小二乘原理求其近似解。
例如求下列超定方程组的近似解:
显然,如果方程组中每个方程的左、右两端不相等而是近似,则相差越小,方程组近似解越精确。为此,记各方程左、右两端之差(即误差)为:
按最小二乘原理,作误差平方和:
求最小值,即令
化简得法方程
解之得超定方程组的近似解 ,,它们也称为超定方程的最小二乘解。
一般的,若对数据 $(x_j,f_j),j=0,1,\cdots,m$,取最小二乘拟合模型为
并引入矩阵 $\pmb{A}$
向量 $\pmb{\alpha}=(a_0,a_1,\cdots,a_n)^T$,$\pmb{d}=(f_0,f_1,\cdots,f_m)^T$,则求最小二乘解的法方程的矩阵形式为
]]>多项式插值方法、带导插值
设 $f$ 是定义在 $[a,b]$ 上的实值函数,已知在 $[a,b]$ 上的 $n+1$ 个互异节点 $x_i$ 及其相应函数值 $f_i=f(x_i)$,要求构建近似函数 $p$,使得:
设已知 $[a,b]$ 上的函数 $f$ 在 $n+1$ 个互异节点 $x_i \in [a,b]$ 上的值 $f_i=f(x_i)(i=0,1,\cdots,n)$,则存在唯一的次数 $\leqslant n$ 的多项式 $p_n(x) \in P_n$ 满足
线性插值也称为一次插值。已知函数的两个点 $(x_0,f_0)$,$(x_1,f_1)$,必存在唯一的次数 $\leqslant 1$ 的多项式 $L_1(x)$ 满足:
不难构造并验证所求的 $L_1(x)$ 就是
称上述等式为线性(一次)插值多项式。记
则称 $l_0(x)$,$l_1(x)$ 为线性插值基函数。于是,可知线性插值函数是线性插值基函数 $l_0(x)$,$l_1(x)$ 与函数值 $f_0$,$f_1$ 的线性组合。
二次插值也称为抛物线插值。已知函数的三个点 $(x_0,f_0)$,$(x_1,f_1)$,$(x_2,f_2)$,根据定理,必存在唯一的次数 $\leqslant 2$ 的插值多项式 $L_2(x)$ 满足:
采用基函数方法,仿照线性插值,作三个二次插值基函数:
同样,注意到它们的构造规律,并可验证它们具有下列性质:
于是,通过验证插值条件,可知所求二次插值多项式为:
称为二次(抛物线)插值多项式。以下还可将线性插值和二次插值推广到一般情形。
已知函数的 $n+1$ 个点 $(x_i,f_i)(i=0,1,\cdots,n)$,根据定理,必存在唯一的次数 $\leqslant n$的多项式 $L_n(x)$ 满足:
仍仿照上述基函数方法,由 $n+1$ 个节点 $x_i(i=0,1,\cdots,n)$ 作 $n+1$ 个 $n$ 次插值基函数:
容易验证,它们具有下列性质:
或采用所谓 $Kronecker$(克罗内克尔)符号
基函数的性质可表示为:
于是,所求的插值多项式为:
或写成紧凑格式:
这种由插值基函数 $l_i(x)$ 和函数值样本 $f_i \ (i=0,1,\cdots,n)$ 构造的插值函数,便称为 $n$ 次 $Lagrange$ 插值函数,或称为插值多项式的 $Lagrange$ 形式。
理论分析中为了简化形式,常引用记号
并由对数求导法可推导出
于是,基函数表示成
利用插值多项式 $L_n(x)$ 作为 $f(x)$ 的近似函数,在 $[a,b]$ 上有误差(截断误差):
称为插值多项式的余项。其中,当 $x=x_i \ (i=0,1,\cdots,n)$ 时,$R(x_i)=0$。对余项的估计有一个理论的结果:
设 $f \in C^n[a,b]$,且 $f^{(n+1)}$ 在 $(a,b)$ 内存在,则 $f$ 的 $n$ 次插值多项式 $L_n$ 对任何 $x\in[a,b]$,有插值余项
其中,$\xi \in (a,b)$ 且与 $x$ 有关,$\omega_{n+1}(x)=\prod \limits_{i=0}^n(x-x_i)$
如果不仅已知插值节点处的函数值,而且还掌握插值节点处的导数值(1 阶甚至高阶);或者说,不仅要求在节点处插值多项式与被插函数的值相等(插值条件),而且还要求相应阶的导数值也相等(相切),这就是带导插值,也称 $Hermite$(埃尔米特)插值。
设已知 $f$ 在 $[a,b]$ 上 $n+1$ 个互异节点 $x_i \in [a,b]$ 处的函数值 $f_i = f(x_i)$ 和 1 阶导数值 $f{}’_i = f{}’(x_i) \ (i=0,1,\cdots,n)$,或记为离散数据
求作一个次数尽可能低的多项式 $H(x)$,满足插值条件:
这样的多项式 $H(x)$ 就被称为 $f$ 的带导插值多项式,或称为带导插值多项式的 $Hermite$ 形式。
对已知数据 $(2.3.1)$,存在唯一的次数 $\leqslant 2n+1$ 的多项式 $H_{2n+1}(x) \in P_{2n+1}$,满足插值条件:
插值基函数
仿照 $Lagrange$ 插值多项式的做法,用基函数的方法来求插值多项式 $H_{2n+1}(x)$。如果能够由已知插值节点 $x_i \ (i=0,1,\cdots,n)$ 作出 $2n+2$ 个 $2n+1$ 次插值基函数
且它们具有下列性质:
则容易验证,满足插值条件的 $2n+1$ 次 $Hermite$ 插值多项式为:
其中插值基函数为:
余项公式
设函数 $f \in C^{2n+1}[a,b]$,且 $f^{(2n+2)}$ 在 $(a,b)$ 内存在,则 $f$ 的 $Hermite$ 插值多项式 $H_{2n+1}(x)$ 的余项公式为:
其中,$\xi = \xi(x) \in (a,b)$,$\omega_{n+1}(x)=\prod \limits_{i=0}^n(x-x_i)$。
两个节点 $x_0,x_1$(即 $n=1$) 的三次 $Hermite$ 插值多项式 $H_3(x)$ 是应用中最基本的情形。这时,基函数为:
三次 $Hermite$ 插值多项式 为:
插值余项为:
]]>行内代码 inline
yum list installed | grep openssh-server
something
cd /usr/local/etccp php.ini php.ini.bakvi php.ini/usr/local/etcuname -a # comment
.example-gradient { background: -moz-linear-gradient(left, #cb60b3 0%, #c146a1 50%, #a80077 51%, #db36a4 100%); /* FF3.6+ */ background: -webkit-linear-gradient(left, #cb60b3 0%,#c146a1 50%,#a80077 51%,#db36a4 100%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(left, #cb60b3 0%,#c146a1 50%,#a80077 51%,#db36a4 100%); /* Opera 11.10+ */ background: -ms-linear-gradient(left, #cb60b3 0%,#c146a1 50%,#a80077 51%,#db36a4 100%); /* IE10+ */ background: linear-gradient(to right, #cb60b3 0%,#c146a1 50%,#a80077 51%,#db36a4 100%); /* W3C */}.example-angle { transform: rotate(10deg);}.example-color { color: rgba(255, 0, 0, 0.2); background: purple; border: 1px solid hsl(100,70%,40%);}.example-easing { transition-timing-function: linear;}.example-time { transition-duration: 3s;}
/usr/sbin/sestatus -vSELinux status: disabledgetenforceDisabled
使用下列命令设置 SELinux 为 permissive
模式:
setenforce 0 # setenforce 1 设置 SELinux 为 enforcing 模式
永久关闭 SELinux 需要修改配置文件并重启机器。
首先编辑 /etc/selinux/config
文件,将 SELINUX=enforcing
改为 SELINUX=disabled
:
# This file controls the state of SELinux on the system.# SELINUX= can take one of these three values:# enforcing - SELinux security policy is enforced.# permissive - SELinux prints warnings instead of enforcing.# disabled - No SELinux policy is loaded.SELINUX=disabled# SELINUXTYPE= can take one of three two values:# targeted - Targeted processes are protected,# minimum - Modification of targeted policy. Only selected processes are protected.# mls - Multi Level Security protection.SELINUXTYPE=targeted
之后重启机器,即可关闭 SELinux。
]]>相关资料
镜像内部结构、构建镜像、镜像命名、Registry
docker pull hello-worlddocker imagesdocker run hello-worldHello from Docker!This message shows that your installation appears to be working correctly.
hello-world
的 Dockerfile 内容如下:
FROM scratchCOPY hello /CMD ["/hello"]
FROM scratch
:此镜像是从白手起家,从 0 开始构建COPY hello /
:将文件 hello
复制到镜像的根目录CMD ["/hello"]
:容器启动时,执行 /hello
hello-world
虽然是一个完整的镜像,但它并没有什么实际用途。通常来说,我们希望镜像能提供一个基本的操作系统环境,用户可以根据需要安装和配置软件。这样的镜像我们称作 base 镜像。
base 镜像有两层含义:
scratch
构建所以,能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。
使用 docker pull centos
命令下载 centos
镜像并查看其镜像信息,发现大小仅为 200 MB
,为什么会这么小?
Linux 操作系统由内核空间和用户空间组成,如下图所示:
内核空间是 Kernel,Linux 刚启动时会加载 bootfs 文件系统,之后 bootfs 会被卸载掉。
用户空间的文件系统是 rootfs,包含我们熟悉的 /dev
, /proc
, /bin
等目录。
对于 base 镜像而言,底层直接用 Host 的 kernel,自己只需提供 rootfs。
而对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了。
CentOS 镜像的 Dockerfile 文件内容如下:
FROM scratchADD centos-7-docker.tar.xz /CMD ["/bin/bash"]
第二行 ADD
指令添加到镜像的 tar 包就是 CentOS 7 的 rootfs。在制作镜像时,这个 tar 包会自动解压到 /
目录下,生成 /dev
, /porc
, /bin
等目录。
不同 Linux 发行版的区别主要就是 rootfs。
比如 Ubuntu 14.04 使用 upstart
管理服务,apt
管理软件包;而 CentOS 7 使用 systemd
和 yum
。这些都是用户空间上的区别,Linux kernel 差别不大。
所以 Docker 可以同时支持多种 Linux 镜像,模拟出多种操作系统环境。
上图 Debian 和 BusyBox(一种嵌入式 Linux)上层提供各自的 rootfs,底层共用 Docker Host 的 kernel。
需要说明的是:
Docker 支持通过扩展现有镜像,创建新的镜像。
实际上,Docker Hub 中 99% 的镜像都是通过在 base 镜像中安装和配置需要的软件构建出来的。例如我们现在构建一个新的镜像,Dockerfile 如下:
FROM debianRUN apt-get install emacsRUN apt-get install apache2CMD ["/bin/bash"]
构建过程如下图所示:
可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。
Docker 镜像采用这种分层结构最大的好处就是 共享资源。
比如:有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享,我们将在后面更深入地讨论这个特性。
如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc
目录下的文件时,修改会被限制在单个容器内,这就是容器的 Copy-on-Write 特性。
当容器启动时,一个新的可写层会被加载到镜像的顶部。这一层通常被称作容器层,容器层之下的都叫镜像层。
所有对容器的改动——无论是添加、删除还是修改文件,都只会发生在容器层中。
并且,只有容器层是可写的,容器层下面的所有镜像层都是只读的。
镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。在容器层中,用户看到的是一个叠加之后的文件系统。
只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。
这样就解释了之前的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改。所以镜像可以被多个容器共享。
对于 Docker 用户来说,最好的情况是不需要自己创建镜像。使用现成镜像的好处除了省去自己做镜像的工作量外,更重要的是可以利用前人的经验。
当然,某些情况下我们也不得不自己构建镜像,比如:
ssh
Docker 提供了两种构建镜像的方法:
docker commit 命令是创建新镜像最直观的方法,其过程包含三个步骤:
(1) 运行容器
docker run -it ubuntu
(2) 安装 vi
vimbash: vim: command not foundapt-get install vimReading package lists... DoneBuilding dependency treeReading state information... DoneThe following additional packages will be installed: file libexpat1 libgpm2 libmagic-mgc libmagic1 libmpdec2 libpython3.6...
(3) 保存为新镜像
在新窗口中查看当前运行的容器:
docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES1abe6e7341ca ubuntu "/bin/bash" 8 minutes ago Up 8 minutes laughing_leavitt
laughing_leavitt
是 Docker 为我们的容器随机分配的名字。
执行 docker commit
命令将容器保存为镜像:
docker commit laughing_leavitt ubuntu-with-visha256:9d2fac08719de640df6a923bd6c1dc82d73817d29e9c287d024b6cd2a7235683
查看新镜像 ubuntu-with-vi
的属性:
docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEubuntu-with-vi latest 9d2fac08719d About a minute ago 169MBubuntu latest ea4c82dcd15a 2 weeks ago 85.8MB
从 SIZE 属性看到镜像因为安装了软件而变大了。从新镜像启动容器,验证 vi
已经可以使用:
which vim/usr/bin/vim
以上演示了如何通过 docker commit
创建新镜像。然而,Docker 并不建议用户通过这种方式构建镜像。原因如下:
Dockerfile 是一个文本文件,记录了镜像构建的所有步骤。
用 Dockerfile 创建上节的 ubuntu-with-vi
,其内容为:
FROM ubuntuRUN apt-get update && apt-get install -y vim
下面运行 docker build
命令构建镜像,并分析其细节:
pwd (1)/root ls (2) Dockerfile docker build -t ubuntu-with-vi-dockerfile . (3) Sending build context to Docker daemon 32.26 kB (4) Step 1 : FROM ubuntu (5) ---> f753707788c5 Step 2 : RUN apt-get update && apt-get install -y vim (6) ---> Running in 9f4d4166f7e3 (7) ...... Setting up vim (2:7.4.1689-3ubuntu1.1) ... ---> 35ca89798937 (8) Removing intermediate container 9f4d4166f7e3 (9) Successfully built 35ca89798937 (10)
(1) 当前目录为 /root
。
(2) Dockerfile 准备就绪。
(3) 运行 docker build
命令,-t
命名新镜像,末尾的 .
表明 build context 为当前目录。Docker 默认会从 build context 中查找 Dockerfile 文件,也可以通过 -f
参数指定 Dockerfile 的位置。
(4) 从这步开始就是镜像真正的构建过程。首先 Docker 将 build context 中的所有文件发送给 Docker daemon。build context 为镜像构建提供所需要的文件或目录。
Dockerfile 中的 ADD
、COPY
等命令可以将 build context 中的文件添加到镜像。此例中,build context 为当前目录 /root
,该目录下的所有文件和子目录都会被发送给 Docker daemon。
注意不要将多余的文件放到 build context 中,特别不要把
/
、/usr
等目录作为 build context,否则构建过程会相当缓慢甚至失败。
(5) Step 1:执行 FROM
,将 Ubuntu 作为 base 镜像。
(6) Step 2:执行 RUN
,安装 vi,具体步骤为 (7) (8) (9)。
(7) 启动 ID 为 9f4d4166f7e3
的临时容器,在容器中通过 apt-get
安装 vim。
(8) 安装成功后,将容器保存为镜像,其 ID 为 35ca89798937
。这一步底层使用的是类似 docker commit
的命令。
(9) 删除临时容器 9f4d4166f7e3
。
(10) 镜像构建成功。
docker history
会显示镜像的构建历史,也就是 Dockerfile 的执行过程。
Docker 会缓存已有镜像的镜像层,构建新镜像时,如果某镜像层已经存在,就直接使用,无需重新创建。
如果 Dockerfile 由于某种原因执行到某个指令失败了,我们也将能够得到前一个指令成功执行构建出的镜像,这对调试 Dockerfile 非常有帮助。可以通过docker run -it
启动镜像的一个容器,手动执行目标命令,查看错误信息。
COPY src dest
或COPY ["src", "dest"]
ENV MY_VERSION 1.3RUN apt-get install -y mypackage=$MY_VERSION...
docker run
之后的参数替换。docker run
之后的参数会被当做参数传递给 ENTRYPOINT。下面是一个较为全面的 Dockerfile:
# my dockerfileFROM busyboxMAINTAINER abelsu7@gmail.comWORKDIR /testdirRUN touch tmpfile1COPY ["tmpfile2", "."]ADD ["bunch.tar.gz", "."]ENV WELCOME "You are in my container, welcome!"
运行容器,验证镜像内容:
> docker run -it my-image/testdir > lsbunch tmpfile1 tmpfile2/testdit > echo $WELCOMEYou are in my container, welcome!
这三个 Dockerfile 指令看上去很类似,但也有不同之处。简单来说:
docker run
后面跟的命令行参数替换。Shell 和 Exec 格式
可以用两种方式指定 RUN、CMD 和 ENTRYPOINT 要运行的命令:Shell 格式和 Exec 格式。
Shell 格式:
<instruction> <command>RUN apt-get install python3CMD echo "Hello World"ENTRYPOINT echo "Hello World"
当指令执行时,shell 格式底层会调用/bin/sh -c <command>
。
Exec 格式:
<instruction> ["executable", "param1", "param2", ...]RUN ["apt-get", "install", "python3"]CMD ["/bin/echo", "Hello world"]ENTRYPOINT ["/bin/ehco", "Hello world"]
当指令执行时,会直接调用<command>
,不会被 Shell 解析。
CMD 和 ENTRYPOINT 推荐使用 Exec 格式,因为指令可读性更强,更容易理解。RUN 则两种格式都可以。
RUN
RUN 指令通常用于安装应用和软件包。
RUN 在当前镜像的顶部执行命令,并创建新的镜像层。Dockerfile 中常常包含多个 RUN 指令。
注意:
apt-get update
和apt-get install
被放在一个 RUN 指令中执行,这样能够保证每次安装的是最新的包。如果apt-get install
在单独的 RUN 中执行,则会使用apt-get update
创建的镜像层,而这一层可能是很久之前缓存的。
CMD
CMD 指令允许用户指定容器默认执行的命令。
此命令会在容器启动且docker run
没有指定其他命令时运行。
docker run
指定了其他命令,则 CMD 指定的默认命令将被忽略ENTRYPOINT
ENTRYPOINT 指令可让容器以应用程序或服务的形式运行。
ENTRYPOINT 看上去与 CMD 很像,它们都可以指定要执行的命令及其参数。不同的地方在于 ENTRYPOINT 不会被忽略,一定会被执行,即使运行 docker run 时指定了其他命令。
ENTRYPOINT 的 Exec 格式用于设置要执行的命令及其参数,同时可通过 CMD 提供额外的参数。
例如下面的 Dockerfile 片段:
ENTRYPOINT ["/bin/echo", "Hello"]CMD ["world"]
当容器通过docker run -it [image]
启动时,输出为:Hello world
。
当容器通过docker run -it [image] CloudMan
启动时,输出为:Hello CloudMan
ENTRYPOINT 的 Shell 格式会忽略任何 CMD 或
docker run
提供的参数。
最佳实践
docker run
命令行替换默认参数。docker run
命令行中替换此默认命令。一个特定镜像的名字由两部分组成:repository 和 tag:
[image name] = [repository]:[tag]
如果执行docker build
时没有指定 tag,则会使用默认值 latest,其效果相当于:
docker build -t ubuntu-with-vi:latest
Docker Hub 是 Docker 公司维护的公共 Registry。用户可以将自己的镜像保存到 Docker Hub 免费的 repository 中。如果不希望别人访问自己的镜像,也可以购买私有 repository。
除了 Docker Hub,quay.io 是另一个公共 Registry,提供与 Docker Hub 类似的服务。
通过 Docker Hub 存取镜像的步骤如下:
docker login -u [username]
[username]/xxx:tag
。通过docker tag
命令重命名镜像:docker tag httpd cloudman6/httpd:v1
docker push
将镜像上传到 Docker Hub:docker push cloudman6/httpd:v1
。省略 tag 部分即可上传同一 repository 中的所有镜像docker pull
命令下载使用该镜像Docker Hub 虽然非常方便,但还是有些限制:
解决方案就是搭建本地的 Registry。
Docker 已经将 Registry 开源了,同时在 Docker Hub 上也有官方的镜像 Registry。
1) 启动 registry 容器:
docker run -d -p 5000:5000 -v /myregistry:/var/lib/registry registry:2
2) 通过docker tag
重命名镜像,使之与 registry 匹配:
docker tag cloudman6/httpd:v1 registry.example.net:5000/cloudman6:httpd:v1
只有 Docker Hub 上的镜像可以省略
[registry-host]:[port]
3) 通过docker push
上传镜像:
docker push registry.example.net:5000/cloudman6/httpd:v1
4) 现在就可以通过docker pull
从本地 registry 下载镜像了:
docker pull registry.example.net:5000/cloudman6/httpd:v1
本地 registry 也支持认证、HTTPS 安全传输等特性,具体可参考官方文档。
镜像的常用操作子命令如下:
]]>参考文章
误差分析、最值、谱半径、范数
如果有 ,则称 $x^*$ 有 $n$ 位有效数字。
(1) 如果 $x^*$ 有 $n$ 位有效数字,则
(2) 如果下式成立,则 $x^*$ 至少有 $n$ 位有效数字:
条件数
病态/良态
所谓严重降低计算精确度,确切地说,设由且仅由 $\varepsilon$ 引起的、$n$ 步运算之后的误差为 $e_n$:
对于数值算法设计来说,主要强调以下两方面:
其中,$\xi$ 在 $x_0$ 与 $x$ 之间,前 $n+1$ 项称为 $n$ 次 $Taylor$ 多项式,最后一项称为 $n$ 次 $Taylor$ 多项式的余项(即截断误差)。为了方便作误差估计,有时还假定 $f$ 的 $n+1$ 阶导数连续。
定义
大 $O$ 记号是为表示近似值而允许我们用 “$=$” 号替代 “$\approx$” 的方便符号。
设变量 $X, Y$(其中 $X \neq 0$),如果在变化过程的某一时刻以后,有:
($M$ 为大于 $0$ 的常数)便记成 $Y=O(X)$。
性质
运算法则
最大值/最小值
设 $S$ 是一个非空的实数集,$S \subset \mathbf{R}$($\mathbf{R} \equiv \mathbf{R}^1 = (-\infty,+\infty)$ 是实数的全体),
最小上界公理
上确界/下确界
设 $S \subset \mathbf{R}$,
设数列 $\left \{ x_n \right \}$,如果存在常数 $a$,对任意给定的正数 $\varepsilon$(无论它多小),总存在正整数 $N$,使得当 $n > N$ 时,有
则称 $a$ 为数列 $\left \{ x_n \right \}$ 的极限,记为 $\lim \limits_{n \to \infty} x_n = a$,或称为数列 $\left \{ x_n \right \}$ 收敛于 $a$。
定义
设 $\pmb{A} = (a_{ij}) \in \mathbf{R^{n \times n}}$
(1) 如果 $\pmb{A}^T = \pmb{A}$,即 $a^{ij} = a^{ji}$,则称 $\pmb{A}$ 为对称矩阵
(2) 如果对称矩阵 $\pmb{A}$ 满足对于 $\forall x \neq 0, x \in \mathbf{R}^{n \times n}$,有
则称 $\pmb{A}$ 为对称正定矩阵
性质
对称矩阵和对称正定矩阵有如下性质:
正交矩阵定义
设 $\pmb{A} = (a_{ij}) \in \mathbf{R^{n \times n}}$ 且成立 $\pmb{A}^T \pmb{A} = I$,则称 $\pmb{A}$ 为正交矩阵。
定义式 $\pmb{A}^T \pmb{A} = I$ 可以写成:
其中,
称为 $Kronecker$(克罗内克尔)符号。
正交矩阵性质
正交矩阵有如下性质:
相似矩阵定义
设 $\pmb{A}$ 与 $\pmb{B}$ 为 $n$ 阶方阵,如果有非奇异的 $n$ 阶方阵 $\pmb{S}$,使得
则称 $\pmb{A}$ 与 $\pmb{B}$ 相似,记作 $\pmb{A} \sim \pmb{B}$
相似矩阵性质
矩阵相似关系有如下三个性质:
应用中,常常不是判断两个矩阵是否相似,而是对给定的矩阵 $\pmb{B}$,寻找合适的可逆矩阵 $\pmb{S}$ 按 $\pmb{A} = \pmb{S}^{-1}\pmb{B}\pmb{S}$ 来产生一个矩阵 $\pmb{A}$。
初等矩阵
下面三种形式的 $n$ 阶矩阵称为初等矩阵:
初等矩阵性质
矩阵特征值定义
设 $\pmb{A}=(a_{ij}) \in \mathbf{R}^{n \times n}$。若存在一个数 $\lambda$(实数或复数)和非零向量 $\pmb{x}=(x_1,x_2,\cdots,x_n)^T \in \mathbf{R}^n$,使得:
则称 $\lambda$ 为 $\pmb{A}$ 的特征值,$\pmb{x}$ 为 $\pmb{A}$ 对应 $\lambda$ 的特征向量。
矩阵特征值求解
应用中主要对给定的 $\pmb{A}$ 求其特征值 $\lambda$,为此将定义式改写为齐次方程 $(\lambda\pmb{I} - \pmb{A})\pmb{x}=0$,即
矩阵谱半径定义
设 $\pmb{A} \in \mathbf{R}^{n \times n}$,$\pmb{A}$ 的特征值 $\lambda_1,\lambda_2,\cdots,\lambda_n$,则有:
矩阵特征值性质
线性相关
对数域 $K$ 上的线性空间 $X$,有 $u_1, u_2, \cdots, u_n \in X$,若存在不全为零的数 $\alpha_1, \alpha_2, \cdots, \alpha_n \in K$,使得下列等式成立:
则称 $u_1, u_2, \cdots, u_n$ 是线性相关的,否则,等式只对 $\alpha_1 = \alpha_2 = \cdots = \alpha_n = 0$ 才能成立,则称 $u_1, u_2, \cdots, u_n$ 是线性无关的。
基、维数、坐标
称 $S$ 是由线性空间 $X$ 中的 $n$ 个线性无关元素 $u_1, u_2, \cdots, u_n \in X$生成的,即 $\forall s \in S$,都有:
设 $X$ 是数域 $K$ 上的线性空间,若 $\forall u \in X$,存在唯一的实数 ,满足条件:
则称 为线性空间 $X$ 上的范数,并且称 $X$ 为赋范线性空间,仍记为 $X$
对于连续函数空间 $C[a,b]$,可以定义 $f \in C[a,b]$ 的如下两种范数:
以及稍后在内积空间中还要定义:
内积:线性空间 $\pmb{R}^n$ 中,任意两个向量 $\pmb{x},\pmb{y}$ 的数量积,记为 $(\pmb{x},\pmb{y})$:
内积空间:设 $X$ 是数域 $K$(如实数域 $\pmb{R}$ 或复数域 $\pmb{C}$)上的线性空间,若对 $\forall u,v \in X$,有 $K$ 中一个数与之对应,记为 $(u,v)$,且满足条件:
则 $(u,v)$ 称为 $u$ 与 $v$ 的内积,定义了内积的线性空间 $X$ 称为 内积空间
正交:设 $X$ 为内积空间,若对任意的 $u,v \in X$,有:
则称 $u$ 与 $v$ 正交。
设向量 $\pmb{x} \in \pmb{R}^n$,若与 $\pmb{x}$ 对应的一个实值函数 满足:
则称 为 $\pmb{R}^n$ 上 $\pmb{x}$ 的一个向量范数。
3 种常用的向量范数
或一般地定义
向量范数基本性质
设矩阵 $\pmb{A} \in \pmb{R}^{n \times n}$,若与 $\pmb{A}$ 对应的一个实值函数 满足:
则称 为 $\pmb{R}^{n \times n}$ 上 $\pmb{A}$ 的一个矩阵范数。
几种常用的矩阵范数
当 $\pmb{A}$ 是对称矩阵时,有:
]]>参考文章
例如e_r(x^{*})=\frac{x-x^*}{x^*}
,讲道理它应该长成下面的样子:
然而实际上它是这个样子:$e_r(x^{})=\frac{x-x^}{x^*}$
又或者f_n=f_{n-1}+f_{n-2}
,讲道理它应该生的和下边一样俊俏:
然而也不幸长残了:$f_n=f_{n-1}+f_{n-2}$
What are you 弄啥嘞?😡
注:针对两个单独的
_
的语义冲突已在后文中修复,因此上面的行内公式显示正常。未修复之前,Markdown 渲染器仍然会将两个单独的_
之间的内容渲染为<em>
标签,显示效果为:$fn=f{n-1}+f_{n-2}$
不难发现,上边既然有成功的渲染,就说明 MathJax 本身没有罢工。而且,仔细观察还会发现,第一个公式中最开始两个*
中间的字体变成了斜体;第二个公式中最开始两个_
也是同样的情况。审查元素发现,第一个公式中的斜体部分被渲染成了<em>
标签:
<em>})=\frac{x-x^</em>
这样来看答案就很清楚了:这个错误是由 Markdown 渲染器(默认的是 hexo-renderer-marked )引起的。Markdown 本身并不支持 Latex,在渲染时正则匹配到两个_
或*
就会把下划线替换成了<em>
,于是到了 MathJax 渲染公式时就彻底懵了。
解决办法也很简单:使用 hexo-renderer-kramed 替换 Hexo 默认的渲染器 hexo-renderer-marked。
hexo-renderer-kramed 是 hexo-renderer-marked 的 Fork 修改版,仅针对 MathJax 渲染的语义冲突问题进行了修改,因此可以放心使用。在 Hexo 根目录下执行以下命令替换默认渲染引擎:
npm uninstall hexo-renderer-marked --savenpm install hexo-renderer-kramed --save
更换渲染引擎后,整行公式就可以正常显示了,然而行内公式还是会遇到<em>
标签语义冲突的问题。在 Markdown 语法中,用$$
包括起来的内容表示整行公式,用$
包括起来的内容表示行内公式。之所以行内公式的渲染依然存在问题,是因为 hexo-renderer-kramed 引擎同样存在语义冲突的问题。
在博客根目录下,找到node_modules/kramed/lib/rules/inline.js
文件,在inline
变量中做出如下修改:
var inline = { // escape: /^\\([\\`*{}\[\]()#$+\-.!_>])/, 第 11 行, 将其修改为 escape: /^\\([`*\[\]()#$+\-.!_>])/, autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, url: noop, html: /^<!--[\s\S]*?-->|^<(\w+(?!:\/|[^\w\s@]*@)\b)*?(?:"[^"]*"|'[^']*'|[^'">])*?>([\s\S]*?)?<\/\1>|^<(\w+(?!:\/|[^\w\s@]*@)\b)(?:"[^"]*"|'[^']*'|[^'">])*?>/, link: /^!?\[(inside)\]\(href\)/, reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, reffn: /^!?\[\^(inside)\]/, strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, // em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, 第 20 行,将其修改为 em: /^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, br: /^ {2,}\n(?!\s*$)/, del: noop, text: /^[\s\S]+?(?=[\\<!\[_*`$]| {2,}\n|$)/, math: /^\$\$\s*([\s\S]*?[^\$])\s*\$\$(?!\$)/,};
第 11 行的修改去掉了\\
和{}
,目的是在原基础上去掉对\
、{
、}
的转义 (escape)。
第 20 行的修改去掉了\b_((?:__|[\s\S])+?)_\b
,目的是去掉对两个_
之间内容的<em>
标签转义。
也就是说,依然可以在 Hexo 中使用*
表示斜体,但用_
表示_斜体_就不会生效了。
另外在行内公式中,针对两个*
的语义冲突依旧存在,目前来看没什么比较好的解决办法(摊手)。
hexo-theme-indigo 默认集成了 MathJax,然而只在主题配置文件中定义了 MathJax 的开关。这样就会造成一个问题:
只要
theme.mathjax
为true
,所有文章页面都会引入 MathJax.js,在不需要使用 MathJax 的页面中会带来毫无必要的时间和资源开销。
因此需要修改主题模板文件,使其按需加载 MathJax.js。
还是以 hexo-theme-indigo 为例,首先在主题配置文件theme/_config.yaml
中,将MathJax
设置为true
:
mathjax: true
随后修改主题模板文件中的判定条件。以 hexo-theme-indigo 为例,其判定是否引入MathJax.js
的代码在layout/_partial/plugins/mathjax.ejs
文件中:
<% if (theme.mathjax){ %><!-- mathjax config similar to math.stackexchange --><script type="text/x-mathjax-config">MathJax.Hub.Config({ tex2jax: { inlineMath: [ ['$','$'], ["\\(","\\)"] ], processEscapes: true, skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'] }});MathJax.Hub.Queue(function() { var all = MathJax.Hub.getAllJax(), i; for(i=0; i < all.length; i += 1) { all[i].SourceElement().parentNode.className += ' has-jax'; }});</script><script async src="//cdn.bootcss.com/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML" async></script><% } %>
显然,只需修改第一行的判定条件为双重判定:
<% if ( theme.mathjax && page.mathjax ){ %>
最后在需要使用 MathJax 文章的 Front-matter 中,将Mathjax
设置为true
,即可在该页面中引入 MathJax.js 而不影响其他页面:
---title: 测试Mathjaxcategory: - 前端tags: - Hexo - MathJaxdate: 2018-10-29 19:58:35mathjax: true---
这里还有一个小问题:博客的首页也可能会调用 MathJax.js 渲染公式,而按照以上设置,非文章页面是不会引入 MathJax.js 的。这里给出两种解决办法:
<!-- more -->
标签位置,确保首页不会展示公式。mathjax.ejs
的判定条件如下,首页同样引入 MathJax.js:<% if ( theme.mathjax && ( page.mathjax || is_home() ) ){ %>
问题解决~最后来测试一下😎
]]>参考文章
- 常用数学符号的 LaTex 表示方法
- 一份不太简短的 LaTex 2 介绍.pdf
- Online LaTex Equation Editor | CODECOGS
- 在 Hexo 中渲染 MathJax 数学公式 | 码迷
- Hexo 博客 MathJax 公式渲染问题 | 博客园
- Hexo 博客 MathJax 公式渲染问题 | 衡仔的技术小窝
- 如何处理 Hexo 和 MathJax 的兼容问题 | 林肯先生的 Blog
- 在 Hexo 中渲染 MathJax 数学公式 | 简书
- 在 Hexo 博客中使用 MathJax 写 LaTex 数学公式 | CSDN
- Hexo 中插入数学公式 | Steven’s Space
- 前端整合 MathjaxJS 配置笔记 | 博客园
- hexo-renderer-kramed | Github
- hexo-renderer-marked | Github
- MathJax.org
- The LaTex project
]]>参考文章
点击查看我的豆列 Java 语言学习推荐书单,欢迎留言补充。
Java 作为目前全球最流行的高级语言,在 TIOBE 常年霸榜。Write Once, Run Anywhere.
下面推荐几本 Java 语言的经典著作,学海无涯,与君共勉。
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Kathy Sierra / [美]Bert Bates | 中国电力出版社 | 2007-2 | 8.7 |
Head First 系列,轻松入门 Java。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Cay S. Horstmann / [美]Gary Cornell | 机械工业出版社 | 2013-11 | 8.3 |
经典著作《Core Java》基础篇。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Cay S. Horstmann / [美]Gary Cornell | 机械工业出版社 | 2014-3 | 8.5 |
经典著作《Core Java》进阶篇。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Y.Daniel Liang (梁勇) | 机械工业出版社 | 2011-6 | 8.7 |
Java入门基础篇,大学教材,相比而言更加推荐《Core Java》系列。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Y.Daniel Liang (梁勇) | 机械工业出版社 | 2011-6 | 8.2 |
Java入门进阶篇,大学教材,相比而言更加推荐《Core Java》系列。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Mark Allen Weiss | 机械工业出版社 | 2009-1 | 8.6 |
最好的 Java 数据结构与算法分析入门教程,兼顾广度和深度。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
周志明 | 机械工业出版社 | 2013-9 | 8.9 |
Java 进阶必看,可能是最好的 JVM 中文书籍之一。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Joshua Bloch | 机械工业出版社 | 2009-1 | 9.0 |
经典进阶著作,有条件的推荐去看英文原版。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Bruce Eckel | 机械工业出版社 | 2007-6 | 9.1 |
与《Core Java》齐名的《Thinking in Java》。需要具备一定的 Java 基础,Java 进阶必备。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
翟陆续 (加多) / 薛宾田 | 电子工业出版社 | 2018-11 | 暂无 |
深度剖析 Java 并发编程原理。作者加多,淘宝高级开发。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Brian Goetz / [美]Tim Peierls 等 | 机械工业出版社 | 2012-2 | 9.0 |
]]>Java 并发编程必读,条理清晰,偏工程实践性质。豆瓣传送门
点击查看我的豆列 C 语言学习推荐书单,欢迎留言补充。
Steve Jobs 和 Dennis Ritchie 是在同年同月离世的。之后每年的这段时间,很多媒体都会纪念 Jobs,但很少会提到 Dennis Ritchie。
如果没有丹尼斯·里奇( Dennis Ritchie ),就不会有我们现在所熟知的现代计算。他是 C 语言之父和 UNIX 操作系统的联合发明人。
C 语言是里奇在 1969-1973 年间开发的,他被认为是第一个真正意义上可移植的现代编程语言。自它诞生差不多 45 年以来,它已经被移植到几乎每一个出现过的系统架构和操作系统上。
另外,现在常年霸占 TIOBE 榜单前三甲的正是Java、C、C++这三种语言。除了 C 语言本身以外,另外两种语言 Java 和 C++ 正是在 C 语言的基础之上发展而来。因此对于现代软件工程师而言,学好 C 语言是非常重要的。下面推荐几本 C 语言的经典著作,学海无涯,与君共勉。
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Brian W. Kernighan / [美]Dennis M. Ritchie | 机械工业出版社 | 2004-1 | 9.4 |
C 语言设计者的权威经典著作,K&R C。最后包括 C 语言参考手册及标准库的详细介绍,推荐配合习题解答同步学习。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[日]柴田望洋 | 人民邮电出版社 | 2013-5 | 8.8 |
对于初学编程的非 CS 专业的读者而言,会是本不错的入门书。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Mark Allen Weiss | 机械工业出版社 | 2004-1 | 8.9 |
数据结构与算法入门经典。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Kenneth A·Reek | 人民邮电出版社 | 2008-4 | 9.0 |
C 语言进阶三部曲之一。深入理解 C 指针的运作原理,全面而不失细致。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[荷]Peter van der Linden | 人民邮电出版社 | 2008-2 | 9.2 |
C 语言进阶三部曲之一。讲解 C 编程的高级技巧,并简单介绍 C++ 的特性。豆瓣传送门
作者 | 出版社 | 出版时间 | 豆瓣评分 |
---|---|---|---|
[美]Andrew Koenig | 人民邮电出版社 | 2008-2 | 8.9 |
]]>C 语言进阶三部曲之一。出版于 ANSI C 规范制定之前,因此某些书中提到的缺陷已经不复存在了。豆瓣传送门
Updating…
温故而知新,可以为师矣。
READ
或WRITE
语句,也没有内置的文件访问方法ANSI C
#define
指令可以把符号名(或称为符号常量)定义为一个特定的字符串:
#include <stdio.h>#define LOWER 0 /* 表的下限 */#define UPPER 300 /* 表的上限 */#define STEP 20 /* 步长 */
EOF
定义在头文件<stdio.h>
中,是一个整型数。其具体数值是什么并不重要,只要它与任何char
类型的值都不相同即可。
在 C 语言中,传递给被调用函数的参数值存放在临时变量中,而不是存放在原来的变量中。被调用函数不能直接修改主调函数中变量的值,而只能修改其私有的临时副本的值。
getline
函数把字符\0
(即空字符,其值为 0 )插入到它创建的数组的末尾,以标记字符串的结束。这一约定已被 C 语言采用:当在 C 语言程序中出现类似于"hello\n"
的字符串常量时,它将以字符数组的形式存储。
h | e | l | l | 0 | \n | \0 |
---|---|---|---|---|---|---|
换行符 | 空字符 |
除自动变量外,还可以定义位于所有函数外部的变量。
外部变量必须定义在所有函数之外,且只能定义一次,定义后编译程序将为它分配存储单元。在每个需要访问外部变量的函数中,必须声明相应的外部变量,此时说明其类型。声明时可以用extern
语句显式声明。
#include <stdio.h>#define MAXLINE 1000int max;char line[MAXLINE];char longest[MAXLINE];int getline(void);void copy(void);main() { int len; extern int max; extern char longest[]; ...}
某些情况下可以省略
extern
声明:在源文件中,如果外部变量的定义出现在使用它的函数之前,那么在那个函数中就没有必要使用extern
声明。
_
开头,因此变量名不要以_
开头数据类型 | 说明 |
---|---|
char | 字符型,占用一个字节,可以存放本地字符集中的一个字符 |
int | 整型,通常反映了所用机器中整数的最自然长度 |
float | 单精度浮点类型 |
double | 双精度浮点类型 |
short
和long
两个限定符用于限定整型。short
类型通常为 16 位,long
类型通常为 32 位,而int
类型通常为 16 位或 32 位。
short
与int
类型至少为 16 位long
类型至少为 32 位short
类型不得长于int
类型int
类型不得长于long
类型]]>类型限定符
signed
与unsigned
可用于限定char
类型或任何整型。unsigned
类型的数总是正值或 0。例如,如果
char
对象占用 8 位,那么unsigned char
类型变量的取值范围为 0~255,而signed char
类型变量的取值范围为 -128~127(在采用对二的补码的机器上)。
在 HTTP 协议中有可能存在信息窃听或身份伪装等安全问题。使用 HTTPS 通信机制可以有效地防止这些问题。
HTTP 主要有这些不足,列举如下,
这些问题不仅在 HTTP 上出现,其他未加密的协议中也会存在这类问题。
由于 HTTP 本身不具备加密的功能,所以也无法做到对通信整体(使用 HTTP 协议通信的请求和响应的内容)进行加密。即,HTTP 报文使用明文方式发送。
TCP/IP 是可能被窃听的网络
如果要问为什么通信时不加密是一个缺点,这是因为,按 TCP/IP 协议族的工作机制,通信内容在所有的通信线路上都有可能遭到窥视。
即使已经过加密处理的通信,也会被窥视到通信内容,这点和未加密的通信是相同的。只是说如果通信经过加密,就有可能让人无法破解报文信息的含义,但加密处理后的报文信息本身还是会被看到的。
加密处理防止被窃听
如何防止窃听保护信息的几种对策中,最为普及的就是加密技术。加密的对象可以有这么几个。
一种方式就是将通信加密。HTTP 协议中没有加密机制,但可以通过和 SSL(Secure Socket Layer,安全套接层)或 TLS(Transport Layer Security,安全传输层协议)的组合使用,加密 HTTP 的通信内容。
用 SSL 建立安全通信线路之后,就可以在这条线路上进行 HTTP 通信了。与 SSL 组合使用的 HTTP 被称为 HTTPS(HTTP Secure,超文本传输安全协议)或 HTTP over SSL。
还有一种将参与通信的内容本身加密的方式。由于 HTTP 协议中没有加密机制,那么就对 HTTP 协议传输的内容本身加密。即把 HTTP 报文里所含的内容进行加密处理。
在这种情况下,客户端需要对 HTTP 报文进行加密处理后再发送请求。
诚然,为了做到有效的内容加密,前提是要求客户端和服务器同时具备加密和解密机制。主要应用在 Web 服务中。有一点必须引起注意,由于该方式不同于 SSL 或 TLS 将整个通信线路加密处理,所以内容仍有被篡改的风险。
HTTP 协议中的请求和响应不会对通信方进行确认。
任何人都可发起请求
在 HTTP 协议通信时,由于不存在确认通信方的处理步骤,任何人都可以发起请求。另外,服务器只要接收到请求,不管对方是谁都会返回一个响应。
• 无法确定请求发送至目标的 Web 服务器是否是按真实意图返回响应的那台服务器。有可能是已伪装的 Web 服务器。
• 无法确定响应返回到的客户端是否是按真实意图接收响应的那个客户端。有可能是已伪装的客户端。
• 无法确定正在通信的对方是否具备访问权限。因为某些 Web 服务器上保存着重要的信息,只想发给特定用户通信的权限。
• 无法判定请求是来自何方、出自谁手。
• 即使是无意义的请求也会照单全收。无法阻止海量请求下的 DoS 攻击(Denial of Service,拒绝服务攻击)。
查明对手的证书
虽然使用 HTTP 协议无法确定通信方,但如果使用 SSL 则可以。SSL 不仅提供加密处理,而且还使用了一种被称为证书的手段,可用于确定方。
证书由值得信任的第三方机构颁发,用以证明服务器和客户端是实际存在的。另外,伪造证书从技术角度来说是异常困难的一件事。所以只要能够确认通信方(服务器或客户端)持有的证书,即可判断通信方的真实意图。这对使用者个人来讲,也减少了个人信息泄露的危险性。
接收到的内容可能有误
由于 HTTP 协议无法证明通信的报文完整性,因此,在请求或响应送出之后直到对方接收之前的这段时间内,即使请求或响应的内容遭到篡改,也没有办法获悉。
像这样,请求或响应在传输途中,遭攻击者拦截并篡改内容的攻击称为中间人攻击(Man-in-the-Middle attack,MITM)。
如何防止篡改
虽然有使用 HTTP 协议确定报文完整性的方法,但事实上并不便捷、可靠。其中常用的是 MD5 和 SHA-1 等散列值校验的方法,以及用来确认文件的数字签名方法。
提供文件下载服务的 Web 网站也会提供相应的以 PGP(Pretty Good Privacy,完美隐私)创建的数字签名及 MD5 算法生成的散列值。PGP 是用来证明创建文件的数字签名,MD5 是由单向函数生成的散列值。不论使用哪一种方法,都需要操纵客户端的用户本人亲自检查验证下载的文件是否就是原来服务器上的文件。浏览器无法自动帮用户检查。
可惜的是,用这些方法也依然无法百分百保证确认结果正确。因为 PGP 和 MD5 本身被改写的话,用户是没有办法意识到的。
为了防止通信线路遭到窃听导致数据泄露,需要在 HTTP 上再加入加密处理和认证等机制。我们把添加了加密及认证机制的 HTTP 称为 HTTPS(HTTP Secure)。
HTTPS 并非是应用层的一种新协议。只是 HTTP 通信接口部分用 SSL(Secure Socket Layer)和 TLS(Transport Layer Security)协议代替而已。
通常,HTTP 直接和 TCP 通信。当使用 SSL 时,则演变成先和 SSL 通信,再由 SSL 和 TCP 通信了。简言之,所谓 HTTPS,其实就是身披 SSL 协议这层外壳的 HTTP。
在采用 SSL 后,HTTP 就拥有了 HTTPS 的加密、证书和完整性保护这些功能。
SSL 是独立于 HTTP 的协议,所以不光是 HTTP 协议,其他运行在应用层的 SMTP 和 Telnet 等协议均可配合 SSL 协议使用。可以说 SSL 是当今世界上应用最为广泛的网络安全技术。
SSL 采用一种叫做公开密钥加密(Public-key cryptography)的加密处理方式。
近代的加密方法中加密算法是公开的,而密钥却是保密的。通过这种方式得以保持加密方法的安全性。
加密和解密都会用到密钥。没有密钥就无法对密码解密,反过来说,任何人只要持有密钥就能解密了。如果密钥被攻击者获得,那加密也就失去了意义。
共享密钥加密的困境
加密和解密同用一个密钥的方式称为共享密钥加密(Common key crypto system),也被叫做对称密钥加密。
以共享密钥方式加密时必须将密钥也发给对方。可究竟怎样才能安全地转交?在互联网上转发密钥时,如果通信被监听那么密钥就可会落入攻击者之手,同时也就失去了加密的意义。另外还得设法安全地保管接收到的密钥。
发送密钥就有被窃听的风险,但不发送,对方就不能解密。再说,若密钥能够安全发送,那数据也应该能安全送达。
使用两把密钥的公开密钥加密
公开密钥加密方式很好地解决了共享密钥加密的困难。
公开密钥加密使用一对非对称的密钥。一把叫做私有密钥(private key),另一把叫做公开密钥(public key)。顾名思义,私有密钥不能让其他任何人知道,而公开密钥则可以随意发布,任何人都可以获得。
使用公开密钥加密方式,发送密文的一方使用对方的公开密钥进行加密处理,对方收到被加密的信息后,再使用自己的私有密钥进行解密。利用这种方式,不需要发送用来解密的私有密钥,也不必担心密钥被攻击者窃听而盗走。
另外,要想根据密文和公开密钥,恢复到信息原文是异常困难的,因为解密过程就是在对离散对数进行求值,这并非轻而易举就能办到。退一步讲,如果能对一个非常大的整数做到快速地因式分解,那么密码破解还是存在希望的。但就目前的技术来看是不太现实的。
HTTPS 采用混合加密机制
HTTPS 采用共享密钥加密和公开密钥加密两者并用的混合加密机制。若密钥能够实现安全交换,那么有可能会考虑仅使用公开密钥加密来通信。但是公开密钥加密与共享密钥加密相比,其处理速度要慢。
所以应充分利用两者各自的优势,将多种方法组合起来用于通信。在交换密钥环节使用公开密钥加密方式,之后的建立通信交换报文阶段则使用共享密钥加密方式。
遗憾的是,公开密钥加密方式还是存在一些问题的。那就是无法证明公开密钥本身就是货真价实的公开密钥。
为了解决上述问题,可以使用由数字证书认证机构(CA,Certificate Authority)和其相关机关颁发的公开密钥证书。
认证机关的公开密钥必须安全地转交给客户端,然而如何安全转交是一件很困难的事。因此,多数浏览器开发商发布版本时,会事先在内部植入常用认证机关的公开密钥。
可证明组织真实性的 EV SSL 证书
证书的一个作用是用来证明作为通信一方的服务器是否规范,另外一个作用是可确认对方服务器背后运营的企业是否真实存在。拥有该特性的证书就是 EV SSL 证书(Extended Validation SSL Certificate)。
EV SSL 证书是基于国际标准的认证指导方针颁发的证书。其严格规定了对运营组织是否真实的确认方针,因此,通过认证的 Web 网站能够获得更高的认可度。
用以确认客户端的客户端证书
HTTPS 中还可以使用客户端证书。以客户端证书进行客户端认证,证明服务器正在通信的对方始终是预料之内的客户端,其作用跟服务器证书如出一辙。
但客户端证书仍存在几处问题点。其中的一个问题点是证书的获取及发布。
现状是,安全性极高的认证机构可颁发客户端证书但仅用于特殊用途的业务。例如网上银行就采用了客户端证书,在登录网银时不仅要求用户确认输入 ID 和密码,还会要求用户的客户端证书,以确认用户是否从特定的终端访问网银。
Client Hello
报文开始 SSL 通信。报文中包含客户端支持的 SSL 的指定版本、加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等)。Server Hello
报文作为应答。和客户端一样,在报文中包含 SSL 版本以及加密组件。服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的。Certificate
报文。报文中包含公开密钥证书。Server Hello Done
报文通知客户端,最初阶段的 SSL 握手协商部分结束。Client Key Exchange
报文作为回应。报文中包含通信加密中使用的一种被称为 Pre-master secret 的随机密码串。该报文已用步骤 3 中的公开密钥进行加密。Change Cipher Spec
报文。该报文会提示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密。Finished
报文。该报文包含连接至今全部报文的整体校验值。这次握手协商是否能够成功,要以服务器是否能够正确解密该报文作为判定标准。Change Cipher Spec
报文。Finished
报文。Finished
报文交换完毕之后,SSL 连接就算建立完成。当然,通信会受到 SSL 的保护。从此处开始进行应用层协议的通信,即发送 HTTP 请求。close_notify
报文,这步之后再发送TCP FIN
报文来关闭与 TCP 的通信。在以上流程中,应用层发送数据时会附加一种叫做 MAC(Message Authentication Code)的报文摘要。MAC 能够查知报文是否遭到篡改,从而保护报文的完整性。
下面是对整个流程的图解。图中说明了从仅使用服务器端的公开密钥证书(服务器证书)建立 HTTPS 通信的整个过程。
SSL 和 TLS
HTTPS 使用 SSL(Secure Socket Layer) 和 TLS(Transport Layer Security)这两个协议。
SSL 技术最初是由浏览器开发商网景通信公司(Netscape)率先倡导的,开发过 SSL3.0 之前的版本。目前主导权已转移到 IETF(Internet Engineering Task Force,Internet 工程任务组)的手中。
IETF 以 SSL3.0 为基准,后又制定了 TLS1.0、TLS1.1 和 TLS1.2。TSL 是以 SSL 为原型开发的协议,有时会统一称该协议为 SSL。当前主流的版本是 SSL3.0 和 TLS1.0。
由于 SSL1.0 协议在设计之初被发现出了问题,就没有实际投入使用。SSL2.0 也被发现存在问题,所以很多浏览器直接废除了该协议版本。
SSL 速度慢吗
HTTPS 也存在一些问题,那就是当使用 SSL 时,它的处理速度会变慢。
SSL 的慢分两种:
和使用 HTTP 相比,网络负载可能会变慢 2 到 100 倍。除去和 TCP 连接、发送 HTTP 请求 • 响应以外,还必须进行 SSL 通信,因此整体上处理通信量不可避免会增加。
SSL 必须进行加密处理。在服务器和客户端都需要进行加密和解密的运算处理。因此从结果上讲,比起 HTTP 会更多地消耗服务器和客户端的硬件资源,导致负载增加。
为什么不一直使用 HTTPS
与纯文本通信相比,加密通信会消耗更多的 CPU 及内存资源。如果每次通信都加密,会消耗相当多的资源,平摊到一台计算机上时,能够处理的请求数量必定也会随之减少。
因此,如果是非敏感信息则使用 HTTP 通信,只有在包含个人信息等敏感数据时,才利用 HTTPS 加密通信。
某些 Web 页面只想让特定的人浏览,或者干脆仅本人可见。为达到这个目标,必不可少的就是认证功能。
计算机本身无法判断坐在显示器前的使用者的身份。为确认正在访问服务器的对方是否真的具有访问系统的权限,就需要核对登陆者本人才知道、才会有的信息。
核对的信息通常是指以下这些:
HTTP 使用的认证方式
HTTP/1.1 使用的认证方式如下所示:
BASIC 认证(基本认证)是从 HTTP/1.0 就定义的认证方式。即便是现在仍有一部分的网站会使用这种认证方式。是 Web 服务器与通信客户端之间进行的认证方式。
401 Authorization Required
,返回带WWW-Authenticate
首部字段的响应。该字段内包含认证的方式(BASIC) 及Request-URI
安全域字符串(realm)。:
连接后,再经过 Base64 编码处理。Authorization
请求的服务器,会对认证信息的正确性进行验证。如验证通过,则返回一条包含Request-URI
资源的响应。BASIC 认证虽然采用 Base64 编码方式,但这不是加密处理。不需要任何附加信息即可对其解码。
另外,除此之外想再进行一次 BASIC 认证时,一般的浏览器却无法实现认证注销操作,这也是问题之一。
BASIC 认证使用上不够便捷灵活,且达不到多数 Web 网站期望的安全性等级,因此它并不常用。
为弥补 BASIC 认证存在的弱点,从 HTTP/1.1 起就有了 DIGEST 认证。 DIGEST 认证同样使用质询 / 响应的方式(challenge/response),但不会像 BASIC 认证那样直接发送明文密码。
所谓质询响应方式是指,一开始一方会先发送认证要求给另一方,接着使用从另一方那接收到的质询码计算生成响应码。最后将响应码返回给对方进行认证的方式。
401 Authorization Required
,返回带WWW-Authenticate
首部字段的响应。该字段内包含质问响应方式认证所需的临时质询码(随机数,nonce)。Authorization
信息。首部字段Authorization
内必须包含username
、realm
、nonce
、uri
和 response
的字段信息。其中,realm
和nonce
就是之前从服务器接收到的响应中的字段。Authorization
请求的服务器,会确认认证信息的正确性。认证通过后则返回包含Request-URI
资源的响应,并且会在首部字段Authentication-Info
写入一些认证成功的相关信息。DIGEST 认证提供了高于 BASIC 认证的安全等级,但是和 HTTPS 的客户端认证相比仍旧很弱。DIGEST 认证提供防止密码被窃听的保护机制,但并不存在防止用户伪装的保护机制。
DIGEST 认证和 BASIC 认证一样,使用上不那么便捷灵活,且仍达不到多数 Web 网站对高度安全等级的追求标准。因此它的适用范围也有所受限。
SSL 客户端认证是借由 HTTPS 的客户端证书完成认证的方式。凭借客户端证书认证,服务器可确认访问是否来自已登录的客户端。
为达到 SSL 客户端认证的目的,需要事先将客户端证书分发给客户端,且客户端必须安装此证书。
Certificate Request
报文,要求客户端提供客户端证书。Client Certificate
报文方式发送给服务器。在多数情况下,SSL 客户端认证不会仅依靠证书完成认证,一般会和基于表单认证组合形成一种双因素认证(Two-factor authentication)来使用。
所谓双因素认证就是指,认证过程中不仅需要密码这一个因素,还需要申请认证者提供其他持有信息,从而作为另一个因素,与其组合使用的认证方式。
基于表单的认证方法并不是在 HTTP 协议中定义的。客户端会向服务器上的 Web 应用程序发送登录信息(Credential),按登录信息的验证结果认证。
由于使用上的便利性及安全性问题,HTTP 协议标准提供的 BASIC 认证和 DIGEST 认证几乎不怎么使用。另外,SSL 客户端认证虽然具有高度的安全等级,但因为导入及维持费用等问题,还尚未普及。
不具备共同标准规范的表单认证,在每个 Web 网站上都会有各不相同的实现方式。如果是全面考虑过安全性能而实现的表单认证,那么就能够具备高度的安全等级。但在表单认证的实现中存在问题的 Web 网站也是屡见不鲜。
基于表单认证的标准规范尚未有定论,一般会使用 Cookie 来管理 Session(会话)。
基于表单认证本身是通过服务器端的 Web 应用,将客户端发送过来的用户 ID 和密码与之前登录过的信息做匹配来进行认证的。
但鉴于 HTTP 是无状态协议,之前已认证成功的用户状态无法通过协议层面保存下来。即,无法实现状态管理,因此即使当该用户下一次继续访问,也无法区分他与其他的用户。于是我们会使用 Cookie 来管理 Session,以弥补 HTTP 协议中不存在的状态管理功能。
Session ID
。通过验证从客户端发送过来的登录信息进行身份认证,然后把用户的认证状态与Session ID
绑定后记录在服务器端。向客户端返回响应时,会在首部字段Set-Cookie
内写入Session ID
。Session ID
后,会将其作为 Cookie 保存在本地。下次向服务器发送请求时,浏览器会自动发送 Cookie,所以Session ID
也随之发送到服务器。服务器端可通过验证接收到的Session ID
识别用户和其认证状态。一台 Web 服务器可搭建多个独立域名的 Web 网站,也可作为通信路径上的中转服务器提升传输效率。
HTTP/1.1 规范允许一台 HTTP 服务器搭建多个 Web 站点。比如,提供 Web 托管服务(Web Hosting Service)的供应商,可以用一台服务器为多位客户服务,也可以以每位客户持有的域名运行各自不同的网站。这是因为利用了虚拟主机(Virtual Host,又称虚拟服务器)的功能。
在互联网上,域名通过 DNS 服务映射到 IP 地址(域名解析)之后访问目标网站。可见,当请求发送到服务器时,已经是以 IP 地址形式访问了。
所以,如果一台服务器内托管了www.tricorder.jp
和www.hackr.jp
这两个域名,当收到请求时就需要弄清楚究竟要访问哪个域名。
在相同的 IP 地址下,由于虚拟主机可以寄存多个不同主机名和域名的 Web 网站,因此在发送 HTTP 请求时,必须在 Host 首部内完整指定主机名或域名的 URI。
HTTP 通信时,除客户端和服务器以外,还有一些用于通信数据转发的应用程序,例如代理、网关和隧道。它们可以配合服务器工作。
这些应用程序和服务器可以将请求转发给通信线路上的下一站服务器,并且能接收从那台服务器发送的响应再转发给客户端。
代理是一种有转发功能的应用程序,它扮演了位于服务器和客户端“中间人”的角色,接收由客户端发送的请求并转发给服务器,同时也接收服务器返回的响应并转发给客户端。
网关是转发其他服务器通信数据的服务器,接收从客户端发送来的请求时,它就像自己拥有资源的源服务器一样对请求进行处理。有时客户端可能都不会察觉,自己的通信目标是一个网关。
隧道是在相隔甚远的客户端和服务器两者之间进行中转,并保持双方通信连接的应用程序。
代理服务器的基本行为就是接收客户端发送的请求后转发给其他服务器。代理不改变请求 URI,会直接发送给前方持有资源的目标服务器。
持有资源实体的服务器被称为源服务器。从源服务器返回的响应经过代理服务器后再传给客户端。
在 HTTP 通信过程中,可级联多台代理服务器。请求和响应的转发会经过数台类似锁链一样连接起来的代理服务器。转发时,需要附加Via
首部字段以标记出经过的主机信息。
使用代理服务器的理由有:利用缓存技术减少网络带宽的流量,组织内部针对特定网站的访问控制,以获取访问日志为主要目的,等等。
代理有多种使用方法,按两种基准分类。一种是是否使用缓存,另一种是是否会修改报文。
代理转发响应时,缓存代理(Caching Proxy)会预先将资源的副本(缓存)保存在代理服务器上。当代理再次接收到对相同资源的请求时,就可以不从源服务器那里获取资源,而是将之前缓存的资源作为响应返回。
转发请求或响应时,不对报文做任何加工的代理类型被称为透明代理(Transparent Proxy)。反之,对报文内容进行加工的代理被称为非透明代理。
网关的工作机制和代理十分相似。而网关能使通信线路上的服务器提供非 HTTP 协议服务。
利用网关能提高通信的安全性,因为可以在客户端与网关之间的通信线路上加密以确保连接的安全。比如,网关可以连接数据库,使用 SQL 语句查询数据。另外,在 Web 购物网站上进行信用卡结算时,网关可以和信用卡结算系统联动。
隧道可按要求建立起一条与其他服务器的通信线路,届时使用 SSL 等加密手段进行通信。隧道的目的是确保客户端能与服务器进行安全的通信。
隧道本身不会去解析 HTTP 请求。也就是说,请求保持原样中转给之后的服务器。隧道会在通信双方断开连接时结束。
缓存是指代理服务器或客户端本地磁盘内保存的资源副本。利用缓存可减少对源服务器的访问,因此也就节省了通信流量和通信时间。
缓存服务器是代理服务器的一种,并归类在缓存代理类型中。换句话说,当代理转发从服务器返回的响应时,代理服务器将会保存一份资源的副本。
缓存服务器的优势在于利用缓存可避免多次从源服务器转发资源。因此客户端可就近从缓存服务器上获取资源,而源服务器也不必多次处理相同的请求了。
即便缓存服务器内有缓存,也不能保证每次都会返回对同资源的请求。因为这关系到被缓存资源的有效性问题。
当遇上源服务器上的资源更新时,如果还是使用不变的缓存,那就会演变成返回更新前的“旧”资源了。
即使存在缓存,也会因为客户端的要求、缓存的有效期等因素,向源服务器确认资源的有效性。若判断缓存失效,缓存服务器将会再次从源服务器上获取“新”资源。
缓存不仅可以存在于缓存服务器内,还可以存在客户端浏览器中,客户端缓存称为临时网络文件(Temporary Internet File)。
浏览器缓存如果有效,就不必再向服务器请求相同的资源了,可以直接从本地磁盘内读取。
另外,和缓存服务器相同的一点是,当判定缓存过期后,会向源服务器确认资源的有效性。若判断浏览器缓存失效,浏览器会再次请求新资源。
HTTP 协议的请求和响应报文中必定包含 HTTP 首部。首部内容为客户端和服务器分别处理请求和响应提供所需要的信息。
HTTP 请求报文
在请求中,HTTP 报文由方法、URI、HTTP 版本、HTTP 首部字段等部分构成。
下面的示例是访问http://hackr.jp
时,请求报文的首部信息。
GET / HTTP/1.1Host: hackr.jpUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:13.0) Gecko/20100101 Firefox/13.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*; q=0.8Accept-Language: ja,en-us;q=0.7,en;q=0.3Accept-Encoding: gzip, deflateDNT: 1Connection: keep-aliveIf-Modified-Since: Fri, 31 Aug 2007 02:02:20 GMTIf-None-Match: "45bae1-16a-46d776ac"Cache-Control: max-age=0
HTTP 响应报文
在响应中,HTTP 报文由 HTTP 版本、状态码、HTTP 首部字段 3 部分构成。
以下示例是之前请求访问http://hackr.jp/
时,返回的响应报文的首部信息。
HTTP/1.1 304 Not ModifiedDate: Thu, 07 Jun 2012 07:21:36 GMTServer: ApacheConnection: closeEtag: "45bae1-16a-46d776ac"
HTTP 首部字段是构成 HTTP 报文的要素之一。在客户端与服务器之间以 HTTP 协议进行通信的过程中,无论是请求还是响应都会使用首部字段,它能起到传递额外重要信息的作用。
使用首部字段是为了给浏览器和服务器提供报文主体大小、所使用的语言、认证信息等内容。
首部字段名: 字段值
字段值对应单个 HTTP 首部字段可以有多个值
Content-Type: text/htmlKeep-Alive: timeout=15, max=100
HTTP 首部字段根据实际用途被分为以下 4 种类型。
请求报文和响应报文两方都会使用的首部。
从客户端向服务器端发送请求报文时使用的首部。补充了请求的附加内容、客户端信息、响应内容相关优先级等信息。
从服务器端向客户端返回响应报文时使用的首部。补充了响应的附加内容,也会要求客户端附加额外的内容信息。
针对请求报文和响应报文的实体部分使用的首部。补充了资源内容更新时间等与实体有关的信息。
HTTP/1.1 规范定义了如下 47 种首部字段。
在 HTTP 协议通信交互中使用到的首部字段,不限于 RFC2616 中定义的 47 种首部字段。还有Cookie、Set-Cookie、Content-Disposition等在其他 RFC 中定义的首部字段,它们的使用频率也很高。
这些非正式的首部字段统一归纳在RFC4229 HTTP Header Field Registrations中。
HTTP 首部字段将定义成缓存代理和非缓存代理的行为,分成 2 种类型。
端到端首部(End-to-end Header)
分在此类别中的首部会转发给请求 / 响应对应的最终接收目标,且必须保存在由缓存生成的响应中,另外规定它必须被转发。
逐跳首部(Hop-by-hop Header)
分在此类别中的首部只对单次转发有效,会因通过缓存或代理而不再转发。HTTP/1.1 和之后版本中,如果要使用 hop-by-hop 首部,需提供Connection
首部字段。
下面列举了 HTTP/1.1 中的逐跳首部字段。除这 8 个首部字段之外,其他所有字段都属于端到端首部。
通用首部字段是指,请求报文和响应报文双方都会使用的首部。
通过指定首部字段Cache-Control
的指令,就能操作缓存的工作机制。
指令的参数是可选的,多个指令之间通过,
分隔。首部字段Cache-Control
的指令可用于请求及响应时。
Cache-Control 指令一览
可用的指令按请求和响应分类如下所示。
表示是否能缓存的指令
Cache-Control: public
当指定使用public
指令时,则明确表明其他用户也可利用缓存。
Cache-Control: private
当指定private
指令后,响应只以特定的用户作为对象,这与public
指令的行为相反。
缓存服务器会对该特定用户提供资源缓存的服务,对于其他用户发送过来的请求,代理服务器则不会返回缓存。
no-cache 指令
Cache-Control: no-cache
使用no-cache
指令的目的是为了防止从缓存中返回过期的资源。
客户端发送的请求中如果包含no-cache
指令,则表示客户端将不会接收缓存过的响应。于是,“中间”的缓存服务器必须把客户端请求转发给源服务器。
如果服务器返回的响应中包含no-cache
指令,那么缓存服务器不能对资源进行缓存。源服务器以后也将不再对缓存服务器请求中提出的资源有效性进行确认,且禁止其对响应资源进行缓存操作。
Cache-Control: no-cache=Location
由服务器返回的响应中,若报文首部字段
Cache-Control
中对no-cache
字段名具体指定参数值,那么客户端在接收到这个被指定参数值的首部字段对应的响应报文后,就不能使用缓存。
控制可执行缓存的对象的指令
Cache-Control: no-store
当使用no-store
指令 时,暗示请求(和对应的响应)或响应中包含机密信息。因此,该指令规定缓存不能在本地存储请求或响应的任一部分。
指定缓存期限和认证的指令
Cache-Control: s-maxage=604800(单位 :秒)
s-maxage
指令的功能和max-age
指令的相同,它们的不同点是s-maxage
指令只适用于供多位用户使用的公共缓存服务器(一般指代理服务器)。也就是说,对于向同一用户重复返回响应的服务器来说,这个指令没有任何作用。
另外,当使用s-maxage
指令后,则直接忽略对Expires
首部字段及max-age
指令的处理。
Cache-Control: max-age=604800(单位:秒)
当客户端发送的请求中包含max-age
指令时,如果判定缓存资源的缓存时间数值比指定时间的数值更小,那么客户端就接收缓存的资源。另外,当指定max-age
值为 0,那么缓存服务器通常需要将请求转发给源服务器。
当服务器返回的响应中包含max-age
指令时,缓存服务器将不对资源的有效性再作确认,而max-age
数值代表资源保存为缓存的最长时间。
应用 HTTP/1.1 版本的缓存服务器遇到同时存在
Expires
首部字段的情况时,会优先处理max-age
指令,而忽略掉Expires
首部字段。而 HTTP/1.0 版本的缓存服务器的情况却相反,max-age
指令会被忽略掉。
Cache-Control: min-fresh=60(单位:秒)
min-fresh
指令要求缓存服务器返回至少还未过指定时间的缓存资源。比如,当指定min-fresh
为 60 秒后,过了 60 秒的资源都无法作为响应返回了。
Cache-Control: max-stale=3600(单位:秒)
使用max-stale
可指示缓存资源,即使过期也照常接收。
Cache-Control: only-if-cached
使用only-if-cached
指令表示客户端仅在缓存服务器本地缓存目标资源的情况下才会要求其返回。换言之,该指令要求缓存服务器不重新加载响应,也不会再次确认资源有效性。若发生请求缓存服务器的本地缓存无响应,则返回状态码504 Gateway Timeout
。
Cache-Control: must-revalidate
使用must-revalidate
指令,代理会向源服务器再次验证即将返回的响应缓存目前是否仍然有效。
若代理无法连通源服务器再次获取有效资源的话,缓存必须给客户端一条 504(Gateway Timeout)状态码。
另外,使用
must-revalidate
指令会忽略请求的max-stale
指令(即使已经在首部使用了max-stale
,也不会再有效果)。
Cache-Control: proxy-revalidate
proxy-revalidate
指令要求所有的缓存服务器在接收到客户端带有该指令的请求返回响应之前,必须再次验证缓存的有效性。
Cache-Control: no-transform
使用no-transform
指令规定无论是在请求还是响应中,缓存都不能改变实体主体的媒体类型。这样做可防止缓存或代理压缩图片等类似操作。
Connection
首部字段具备如下两个作用:
Connection: 不再转发的首部字段名
Connection: close
HTTP/1.1 版本的默认连接都是持久连接。为此,客户端会在持久连接上连续发送请求。当服务器端想明确断开连接时,则指定
Connection
首部字段的值为Close
。
Connection: Keep-Alive
HTTP/1.1 之前的 HTTP 版本的默认连接都是非持久连接。为此,如果想在旧版本的 HTTP 协议上维持持续连接,则需要指定
Connection
首部字段的值为Keep-Alive
。
首部字段Date
表明创建 HTTP 报文的日期和时间。
HTTP/1.1 协议使用在 RFC1123 中规定的日期时间的格式,如下示例:
Date: Tue, 03 Jul 2012 04:40:59 GMT
之前的 HTTP 协议版本中使用在 RFC850 中定义的格式,如下所示:
Date: Tue, 03-Jul-12 04:40:59 GMT
除此之外,还有一种格式。它与 C 标准库内的 asctime()
函数的输出格式一致:
Date: Tue Jul 03 04:40:59 2012
Pragma 是 HTTP/1.1 之前版本的历史遗留字段,仅作为与 HTTP/1.0 的向后兼容而定义。
Pragma: no-cache
所有的中间服务器如果都能以 HTTP/1.1 为基准,那直接采用
Cache-Control: no-cache
指定缓存的处理方式是最为理想的。但要整体掌握全部中间服务器使用的 HTTP 协议版本却是不现实的。因此,发送的请求会同时含有下面两个首部字段。
Cache-Control: no-cachePragma: no-cache
首部字段Trailer
会事先说明在报文主体后记录了哪些首部字段。该首部字段可应用在 HTTP/1.1 版本分块传输编码时。
HTTP/1.1 200 OKDate: Tue, 03 Jul 2012 04:40:56 GMTContent-Type: text/html...Transfer-Encoding: chunkedTrailer: Expires...(报文主体)...0Expires: Tue, 28 Sep 2004 23:59:59 GMT
以上用例中,指定首部字段Trailer
的值为Expires
,在报文主体之后(分块长度 0 之后)出现了首部字段Expires
。
首部字段Transfer-Encoding
规定了传输报文主体时采用的编码方式。HTTP/1.1 的传输编码方式仅对分块传输编码有效。
HTTP/1.1 200 OKDate: Tue, 03 Jul 2012 04:40:56 GMTCache-Control: public, max-age=604800Content-Type: text/javascript; charset=utf-8Expires: Tue, 10 Jul 2012 04:40:56 GMTX-Frame-Options: DENYX-XSS-Protection: 1; mode=blockContent-Encoding: gzipTransfer-Encoding: chunkedConnection: keep-alivecf0 ←16进制(10进制为3312)...3312字节分块数据...392 ←16进制(10进制为914)...914字节分块数据...0
以上用例中,正如在首部字段
Transfer-Encoding
中指定的那样,有效使用分块传输编码,且分别被分成 3312 字节和 914 字节大小的分块数据。
首部字段Upgrade
用于检测 HTTP 协议及其他协议是否可使用更高的版本进行通信,其参数值可以用来指定一个完全不同的通信协议。
上图用例中,首部字段Upgrade
指定的值为TLS/1.0
。请注意此处两个字段首部字段的对应关系,Connection
的值被指定为Upgrade
。Upgrade
首部字段产生作用的Upgrade
对象仅限于客户端和邻接服务器之间。因此,使用首部字段Upgrade
时,还需要额外指定Connection:Upgrade
。
对于附有首部字段
Upgrade
的请求,服务器可用101 Switching Protocols
状态码作为响应返回。
使用首部字段Via
是为了追踪客户端与服务器之间的请求和响应报文的传输路径。
首部字段Via
不仅用于追踪报文的转发,还可避免请求回环的发生。所以必须在经过代理时附加该首部字段内容。
HTTP/1.1 的Warning
首部是从 HTTP/1.0 的响应首部Retry-After
演变过来的。该首部通常会告知用户一些与缓存相关的问题的警告。
Warning: 113 gw.hackr.jp:8080 "Heuristic expiration" Tue, 03 Jul 2012 05:09:44 GMT
Warning
首部的格式如下。最后的日期时间部分可省略。
Warning: [警告码][警告的主机:端口号]“[警告内容]”([日期时间])
HTTP/1.1 中定义了 7 种警告。警告码对应的警告内容仅推荐参考。另外,警告码具备扩展性,今后有可能追加新的警告码。
请求首部字段是从客户端往服务器端发送请求报文中所使用的字段,用于补充请求的附加信息、客户端信息、对响应内容相关的优先级等内容。
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept
首部字段可通知服务器,用户代理能够处理的媒体类型及媒体类型的相对优先级。可使用type/subtype
这种形式,一次指定多种媒体类型。
当服务器提供多种内容时,将会首先返回权重值最高的媒体类型。
Accept-Charset: iso-8859-5, unicode-1-1;q=0.8
Accept-Charset
首部字段可用来通知服务器用户代理支持的字符集及字符集的相对优先顺序。另外,可一次性指定多种字符集。与首部字段Accept
相同的是可用权重q
值来表示相对优先级。
Accept-Encoding: gzip, deflate
Accept-Encoding
首部字段用来告知服务器用户代理支持的内容编码及内容编码的优先级顺序。可一次性指定多种内容编码。
Accept-Language: zh-cn,zh;q=0.7,en-us,en;q=0.3
Authorization: Basic dWVub3NlbjpwYXNzd29yZA==
首部字段Authorization
是用来告知服务器,用户代理的认证信息(证书值)。
Expect: 100-continue
客户端使用首部字段Expect
来告知服务器,期望出现的某种特定行为。因服务器无法理解客户端的期望作出回应而发生错误时,会返回状态码417 Expectation Failed
。
首部字段From
用来告知服务器使用用户代理的用户的电子邮件地址。通常,其使用目的就是为了显示搜索引擎等用户代理的负责人的电子邮件联系方式。
Host: www.hackr.jp
首部字段Host
会告知服务器,请求的资源所处的互联网主机名和端口号。Host
首部字段在 HTTP/1.1 规范内是唯一一个必须被包含在请求内的首部字段。
首部字段
Host
和以单台服务器分配多个域名的虚拟主机的工作机制有很密切的关联,这是其必须存在的意义。
若服务器未设定主机名,那直接发送一个空值即可。如下所示。
Host:
形如If-xxx
这种样式的请求首部字段,都可称为条件请求。服务器接收到附带条件的请求后,只有判断指定条件为真时,才会执行请求。
If-Match: "123456"
服务器会比对If-Match
的字段值和资源的 ETag 值,仅当两者一致时,才会执行请求。反之,则返回状态码412 Precondition Failed
的响应。
If-Modified-Since: Thu, 15 Apr 2004 00:00:00 GMT
If-Modified-Since
用于确认代理或客户端拥有的本地资源的有效性。获取资源的更新日期时间,可通过确认首部字段Last-Modified
来确定。
首部字段If-None-Match
属于附带条件之一。它和首部字段If-Match
作用相反。用于指定If-None-Match
字段值的实体标记(ETag)值与请求资源的 ETag 不一致时,它就告知服务器处理该请求。
在 GET 或 HEAD 方法中使用首部字段
If-None-Match
可获取最新的资源。因此,这与使用首部字段If-Modified-Since
时有些类似。
首部字段If-Range
属于附带条件之一。它告知服务器若指定的If-Range
字段值(ETag 值或者时间)和请求资源的 ETag 值或时间相一致时,则作为范围请求处理。反之,则返回全体资源。
If-Unmodified-Since: Thu, 03 Jul 2012 00:00:00 GMT
首部字段If-Unmodified-Since
和首部字段If-Modified-Since
的作用相反。它的作用的是告知服务器,指定的请求资源只有在字段值内指定的日期时间之后,未发生更新的情况下,才能处理请求。如果在指定日期时间后发生了更新,则以状态码412 Precondition Failed
作为响应返回。
Max-Forwards: 10
通过 TRACE 方法或 OPTIONS 方法,发送包含首部字段Max-Forwards
的请求时,该字段以十进制整数形式指定可经过的服务器最大数目。
Proxy-Authorization: Basic dGlwOjkpNLAGfFY5
接收到从代理服务器发来的认证质询时,客户端会发送包含首部字段Proxy-Authorization
的请求,以告知服务器认证所需要的信息。
Range: bytes=5001-10000
对于只需获取部分资源的范围请求,包含首部字段Range
即可告知服务器资源的指定范围。上面的示例表示请求获取从第 5001 字节至第 10000 字节的资源。
Referer: http://www.hackr.jp/index.htm
首部字段Referer
会告知服务器请求的原始资源的 URI。
客户端一般都会发送Referer
首部字段给服务器。但当直接在浏览器的地址栏输入 URI,或出于安全性的考虑时,也可以不发送该首部字段。
因为原始资源的 URI 中的查询字符串可能含有 ID 和密码等保密信息,要是写进 Referer 转发给其他服务器,则有可能导致保密信息的泄露。
TE: gzip, deflate;q=0.5
首部字段TE
会告知服务器客户端能够处理响应的传输编码方式及相对优先级。它和首部字段Accept-Encoding
的功能很相像,但是用于传输编码。
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:13.0) Gecko/20100101 Firefox/13.0.1
首部字段User-Agent
会将创建请求的浏览器和用户代理名称等信息传达给服务器。
响应首部字段是由服务器端向客户端返回响应报文中所使用的字段,用于补充响应的附加信息、服务器信息,以及对客户端的附加要求等信息。
Accept-Ranges: bytes
首部字段Accept-Ranges
是用来告知客户端服务器是否能处理范围请求,以指定获取服务器端某个部分的资源。
可指定的字段值有两种,可处理范围请求时指定其为bytes
,反之则指定其为none
。
Age: 600
首部字段Age
能告知客户端,源服务器在多久前创建了响应。字段值的单位为秒。
若创建该响应的服务器是缓存服务器,Age
值是指缓存后的响应再次发起认证到认证完成的时间值。
代理创建响应时必须加上首部字段Age
。
ETag: "82e22293907ce725faf67773957acd12"
首部字段 ETag 能告知客户端实体标识。它是一种可将资源以字符串形式做唯一性标识的方式。服务器会为每份资源分配对应的 ETag 值。
另外,当资源更新时,ETag 值也需要更新。生成 ETag 值时,并没有统一的算法规则,而仅仅是由服务器来分配。
强 ETag 值和弱 ETag 值
ETag: "usagi-1234"
W/
ETag: W/"usagi-1234"
Location: http://www.usagidesign.jp/sample.html
使用首部字段Location
可以将响应接收方引导至某个与请求 URI 位置不同的资源。
基本上,该字段会配合3xx :Redirection
的响应,提供重定向的 URI。
Proxy-Authenticate: Basic realm="Usagidesign Auth"
首部字段Proxy-Authenticate
会把由代理服务器所要求的认证信息发送给客户端。
Retry-After: 120
首部字段Retry-After
告知客户端应该在多久之后再次发送请求。主要配合状态码503 Service Unavailable
响应,或3xx Redirect
响应一起使用。
字段值可以指定为具体的日期时间(Wed, 04 Jul 2012 06:34:24 GMT 等格式),也可以是创建响应后的秒数。
Server: Apache/2.2.17 (Unix)
首部字段Server
告知客户端当前服务器上安装的 HTTP 服务器应用程序的信息。不单单会标出服务器上的软件应用名称,还有可能包括版本号和安装时启用的可选项。
Server: Apache/2.2.6 (Unix) PHP/5.2.5
Vary: Accept-Language
首部字段Vary
可对缓存进行控制。源服务器会向代理服务器传达关于本地缓存使用方法的命令。
从代理服务器接收到源服务器返回包含
Vary
指定项的响应之后,若再要进行缓存,仅对请求中含有相同Vary
指定首部字段的请求返回缓存。即使对相同资源发起请求,但由于Vary
指定的首部字段不相同,因此必须要从源服务器重新获取资源。
WWW-Authenticate: Basic realm="Usagidesign Auth"
首部字段WWW-Authenticate
用于 HTTP 访问认证。它会告知客户端适用于访问请求 URI 所指定资源的认证方案(Basic 或是 Digest)和带参数提示的质询(challenge)。状态码401 Unauthorized
响应中,肯定带有首部字段WWW-Authenticate
。
实体首部字段是包含在请求报文和响应报文中的实体部分所使用的首部,用于补充内容的更新时间等与实体相关的信息。
Allow: GET, HEAD
首部字段Allow
用于通知客户端能够支持 Request-URI 指定资源的所有 HTTP 方法。
当服务器接收到不支持的 HTTP 方法时,会以状态码405 Method Not Allowed
作为响应返回。与此同时,还会把所有能支持的 HTTP 方法写入首部字段Allow
后返回。
Content-Encoding: gzip
首部字段Content-Encoding
会告知客户端服务器对实体的主体部分选用的内容编码方式。内容编码是指在不丢失实体信息的前提下所进行的压缩。
主要采用以下四种内容编码的方式:
Content-Language: zh-CN
首部字段Content-Language
会告知客户端,实体主体使用的自然语言。
Content-Length: 15000
首部字段Content-Length
表明了实体主体部分的大小(单位是字节)。
对实体主体进行内容编码传输时,不能再使用Content-Length
首部字段。
Content-Location: http://www.hackr.jp/index-ja.html
首部字段Content-Location
给出与报文主体部分相对应的 URI。和首部字段Location
不同,Content-Location
表示的是报文主体返回资源对应的 URI。
Content-MD5: OGFkZDUwNGVhNGY3N2MxMDIwZmQ4NTBmY2IyTY==
首部字段Content-MD5
是一串由 MD5 算法生成的值,其目的在于检查报文主体在传输过程中是否保持完整,以及确认传输到达。
对报文主体执行 MD5 算法获得的 128 位二进制数,再通过 Base64 编码后将结果写入Content-MD5
字段值。由于 HTTP 首部无法记录二进制值,所以要通过 Base64 编码处理。为确保报文的有效性,作为接收方的客户端会对报文主体再执行一次相同的 MD5 算法。计算出的值与字段值作比较后,即可判断出报文主体的准确性。
采用这种方法,对内容上的偶发性改变是无从查证的,也无法检测出恶意篡改。其中一个原因在于,内容如果能够被篡改,那么同时意味着
Content-MD5
也可重新计算然后被篡改。所以处在接收阶段的客户端是无法意识到报文主体以及首部字段Content-MD5
是已经被篡改过的。
Content-Range: bytes 5001-10000/10000
针对范围请求,返回响应时使用的首部字段Content-Range
,能告知客户端作为响应返回的实体的哪个部分符合范围请求。字段值以字节为单位,表示当前发送部分及整个实体大小。
Content-Type: text/html; charset=UTF-8
首部字段Content-Type
说明了实体主体内对象的媒体类型。和首部字段Accept
一样,字段值用type/subtype
形式赋值。
参数charset
使用iso-8859-1
或euc-jp
等字符集进行赋值。
Expires: Wed, 04 Jul 2012 08:26:05 GMT
首部字段Expires
会将资源失效的日期告知客户端。缓存服务器在接收到含有首部字段Expires
的响应后,会以缓存来应答请求,在Expires
字段值指定的时间之前,响应的副本会一直被保存。当超过指定的时间后,缓存服务器在请求发送过来时,会转向源服务器请求资源。
源服务器不希望缓存服务器对资源缓存时,最好在Expires
字段内写入与首部字段Date
相同的时间值。
但是,当首部字段
Cache-Control
有指定max-age
指令时,比起首部字段Expires
,会优先处理max-age
指令。
Last-Modified: Wed, 23 May 2012 09:59:55 GMT
首部字段Last-Modified
指明资源最终修改的时间。
一般来说,这个值就是Request-URI
指定资源被修改的时间。但类似使用 CGI 脚本进行动态数据处理时,该值有可能会变成数据最终修改时的时间。
管理服务器与客户端之间状态的 Cookie,虽然没有被编入标准化 HTTP/1.1 的 RFC2616 中,但在 Web 网站方面得到了广泛的应用。
Cookie 的工作机制是用户识别及状态管理。Web 网站为了管理用户的状态会通过 Web 浏览器,把一些数据临时写入用户的计算机内。接着当用户访问该Web网站时,可通过通信方式取回之前发放的 Cookie。
调用 Cookie 时,由于可校验 Cookie 的有效期,以及发送方的域、路径、协议*等信息,所以正规发布的 Cookie 内的数据不会因来自其他 Web 站点和攻击者的攻击而泄露。
下面的表格列举了与 Cookie 有关的首部字段。
首部字段名 | 说明 | 首部类型 |
---|---|---|
Set-Cookie | 开始状态管理所使用的 Cookie 信息 | 响应首部字段 |
Cookie | 服务器接收到的 Cookie 信息 | 请求首部字段 |
Set-Cookie: status=enable; expires=Tue, 05 Jul 2011 07:26:31 GMT; path=/; domain=.hackr.jp;
当服务器准备开始管理客户端的状态时,会事先告知各种信息。
下面的表格列举了Set-Cookie
的字段值。
属性 | 说明 |
---|---|
NAME=VALUE | 赋予 Cookie 的名称和其值(必需项) |
expires=DATE | Cookie 的有效期 |
path=PATH | 将服务器上的文件目录作为 Cookie 的适用对象 |
domain=域名 | 作为 Cookie 适用对象的域名 |
Secure | 仅在 HTTPS 安全通信时才会发送 Cookie |
HttpOnly | 加以限制,使 Cookie 不能被 JavaScript 脚本访问 |
Cookie: status=enable
首部字段Cookie
会告知服务器,当客户端想获得 HTTP 状态管理支持时,就会在请求中包含从服务器接收到的Cookie
。
接收到多个Cookie
时,同样可以以多个Cookie
形式发送。
X-Frame-Options: DENY
首部字段X-Frame-Options
属于 HTTP 响应首部,用于控制网站内容在其他 Web 网站的 Frame 标签内的显示问题。其主要目的是为了防止点击劫持(clickjacking)攻击。
X-XSS-Protection: 1
首部字段X-XSS-Protection
属于 HTTP 响应首部,它是针对跨站脚本攻击(XSS)的一种对策,用于控制浏览器 XSS 防护机制的开关。
DNT: 1
首部字段DNT
属于 HTTP 请求首部,其中DNT
是 Do Not Track(请勿跟踪) 的简称,意为拒绝个人信息被收集,是表示拒绝被精准广告追踪的一种方法。
P3P: CP="CAO DSP LAW CURa ADMa DEVa TAIa PSAa PSDa IVAa IVDa OUR BUS IND UNI COM NAV INT"
首部字段P3P
属于 HTTP 响应首部,通过利用 P3P(The Platform for Privacy Preferences,在线隐私偏好平台)技术,可以让 Web 网站上的个人隐私变成一种仅供程序可理解的形式,以达到保护用户隐私的目的。
用于 HTTP 协议交互的信息被称为 HTTP 报文。请求端(客户端)的 HTTP 报文叫做请求报文,响应端(服务器端)的叫做响应报文。HTTP 报文本身是由多行(用 CR+LF 作换行符)数据构成的字符串文本。
HTTP 报文大致可分为报文首部和报文主体两块。两者由最初出现的空行(CR+LF)来划分。通常,并不一定要有报文主体。
请求报文和响应报文的首部内容由以下数据组成:
HTTP 在传输数据时可以按照数据原貌直接传输,但也可以在传输过程中通过编码提升传输速率。通过在传输时编码,能有效地处理大量的访问请求。但是,编码的操作需要计算机来完成,因此会消耗更多的 CPU 等资源。
是 HTTP 通信中的基本单位,由 8 位组字节流(octet sequence,其中 octet 为 8 个比特)组成,通过 HTTP 通信传输。
作为请求或响应的有效载荷数据(补充项)被传输,其内容由实体首部和实体主体组成。
HTTP 报文的主体用于传输请求或响应的实体主体。
通常,报文主体等于实体主体。只有当传输中进行编码操作时,实体主体的内容发生变化,才导致它和报文主体产生差异。
向待发送邮件内增加附件时,为了使邮件容量变小,我们会先用 ZIP 压缩文件之后再添加附件发送。HTTP 协议中有一种被称为内容编码的功能也能进行类似的操作。
内容编码指明应用在实体内容上的编码格式,并保持实体信息原样压缩。内容编码后的实体由客户端接收并负责解码。
常用的内容编码有以下几种:
在 HTTP 通信过程中,请求的编码实体资源尚未全部传输完成之前,浏览器无法显示请求页面。在传输大容量数据时,通过把数据分割成多块,能够让浏览器逐步显示页面。
这种把实体主体分块的功能称为分块传输编码(Chunked Transfer Coding)。
分块传输编码会将实体主体分成多个部分(块)。每一块都会用十六进制来标记块的大小,而实体主体的最后一块会使用0(CR+LF)
来标记。
使用分块传输编码的实体主体会由接收的客户端负责解码,恢复到编码前的实体主体。
HTTP/1.1 中存在一种称为传输编码(Transfer Coding)的机制,它可以在通信时按某种编码方式传输,但只定义作用于分块传输编码中。
发送邮件时,我们可以在邮件里写入文字并添加多份附件,这时因为采用了 MIME(Multipurpose Internet Mail Extensions,多用途因特网邮件扩展)机制,它允许邮件处理文本、图片、视频等多个不同类型的数据。
例如,图片等二进制数据以 ASCII 码字符串编码的方式指明,就是利用 MIME 来描述标记数据类型。而在 MIME 扩展中会使用一种称为多部分对象集合(Multipart)的方法,来容纳多份不同类型的数据。
相应地,HTTP 协议中也采纳了多部分对象集合,发送的一份报文主体内可含有多类型实体。通常是在图片或文本文件等上传时使用。
多部分对象集合包含的对象如下:
Content-Type: multipart/form-data; boundary=AaB03x --AaB03xContent-Disposition: form-data; name="field1" Joe Blow--AaB03xContent-Disposition: form-data; name="pics"; filename="file1.txt"Content-Type: text/plain ...(file1.txt的数据)...--AaB03x--
HTTP/1.1 206 Partial ContentDate: Fri, 13 Jul 2012 02:45:26 GMTLast-Modified: Fri, 31 Aug 2007 02:02:20 GMTContent-Type: multipart/byteranges; boundary=THIS_STRING_SEPARATES--THIS_STRING_SEPARATESContent-Type: application/pdfContent-Range: bytes 500-999/8000...(范围指定的数据)...--THIS_STRING_SEPARATESContent-Type: application/pdfContent-Range: bytes 7000-7999/8000...(范围指定的数据)...--THIS_STRING_SEPARATES--
在 HTTP 报文中使用多部分对象集合时,需要在首部字段里加上
Content-type
。
以前如果下载过程遇到网络中断的情况,必须重头开始。为了解决上述问题,需要一种可恢复的机制。所谓的恢复是指能从之前下载中断处恢复下载。
要实现该功能需要指定下载的实体范围。像这样,指定范围发送的请求叫做范围请求(Range Request)。
对一份 10000 字节大小的资源,如果使用范围请求,可以只请求 5001~10000
字节内的资源。
byte 范围的指定形式如下,
5001~10000
字节:Range: bytes=5001-10000
5001
字节之后全部的:Range: bytes=5001-
3000
字节和5000~7000
字节的多重范围:Range: bytes=-3000, 5000-7000
针对范围请求,响应会返回状态码为206 Partial Content
的响应报文。另外,对于多重范围的范围请求,响应会在首部字段 Content-Type 标明 multipart/byteranges 后返回响应报文。
如果服务器端无法响应范围请求,则会返回状态码 200 OK
和完整的实体内容。
当浏览器的默认语言为英语或中文,访问相同 URI 的 Web 页面时,则会显示对应的英语版或中文版的 Web 页面。这样的机制称为内容协商(Content Negotiation)。
内容协商机制是指客户端和服务器端就响应的资源内容进行交涉,然后提供给客户端最为适合的资源。内容协商会以响应资源的语言、字符集、编码方式等作为判断的基准。
包含在请求报文中的以下首部字段就是判断的基准:
Accept
Accept-Charset
Accept-Encoding
Accept-Language
Content-Language
内容协商技术有以下三种类型:
由服务器端进行内容协商。以请求的首部字段为参考,在服务器端自动处理。但对用户来说,以浏览器发送的信息作为判定的依据,并不一定能筛选出最优内容。
由客户端进行内容协商的方式。用户从浏览器显示的可选项列表中手动选择。还可以利用 JavaScript 脚本在 Web 页面上自动进行上述选择。比如按 OS 的类型或浏览器类型,自行切换成 PC 版页面或手机版页面。
是服务器驱动和客户端驱动的结合体,是由服务器端和客户端各自进行内容协商的一种方法。
状态码的职责是当客户端向服务器端发送请求时,描述返回的请求结果。借助状态码,用户可以知道服务器端是正常处理了请求,还是出现了错误。
状态码如200 OK
,以 3 位数字和原因短语组成。
数字中的第一位指定了响应类别,后两位无类别。响应类别有以下 5 种:
状态码 | 响应类别 | 原因短语 |
---|---|---|
1XX | Informational(信息性状态码) | 接收的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
只要遵守状态码类别的定义,即使改变 RFC2616 中定义的状态码,或服务器端自行创建状态码都没问题。
仅记录在 RFC2616 上的 HTTP 状态码就达 40 种,若再加上 WebDAV(Web-based Distributed Authoring and Versioning,基于万维网的分布式创作和版本控制)(RFC4918、5842) 和附加 HTTP 状态码(RFC6585)等扩展,数量就达 60 余种。别看种类繁多,实际上经常使用的大概只有 以下14 种。
2XX
的响应结果表示请求被正常处理了。
200 OK
表示从客户端发来的请求在服务器端被正常处理了。
在响应报文内,随状态码一起返回的信息会因方法的不同而发生改变。比如,使用
GET
方法时,对应请求资源的实体会作为响应返回;而使用HEAD
方法时,对应请求资源的实体首部不随报文主体作为响应返回(即在响应中只返回首部,不会返回实体的主体部分)。
204 No Content
代表服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分。另外,也不允许返回任何实体的主体。比如,当从浏览器发出请求处理后,返回204 No Content
响应,那么浏览器显示的页面不发生更新。
一般在只需要从客户端往服务器发送信息,而对客户端不需要发送新信息内容的情况下使用。
206 Partial Content
表示客户端进行了范围请求,而服务器成功执行了这部分的GET
请求。响应报文中包含由Content-Range
指定范围的实体内容。
3XX
响应结果表明浏览器需要执行某些特殊的处理以正确处理请求。
永久性重定向。该状态码表示请求的资源已被分配了新的 URI,以后应使用资源现在所指的 URI。
临时性重定向。该状态码表示请求的资源已被分配了新的 URI,希望用户(本次)能使用新的 URI 访问。
和
301 Moved Permanently
状态码相似,但 302 状态码代表的资源不是被永久移动,只是临时性质的。换句话说,已移动的资源对应的 URI 将来还有可能发生改变。比如,用户把 URI 保存成书签,但不会像 301 状态码出现时那样去更新书签,而是仍旧保留返回 302 状态码的页面对应的 URI。
303 See Other
表示由于请求对应的资源存在着另一个 URI,应使用GET
方法定向获取请求的资源。
303 See Other
状态码和302 Found
状态码有着相同的功能,但 303 状态码明确表示客户端应当采用 GET 方法获取资源,这点与 302 状态码有区别。
当 301、302、303 响应状态码返回时,几乎所有的浏览器都会把 POST 改成 GET,并删除请求报文内的主体,之后请求会自动再次发送。
301、302 标准是禁止将 POST 方法改变成 GET 方法的,但实际使用时大家都会这么做。
该状态码表示客户端发送附带条件的请求时,服务器端允许请求访问资源,但未满足条件的情况。
附带条件的请求是指采用
GET
方法的请求报文中包含If-Match、If-Modified-Since、If-None-Match、If-Range、If-Unmodified-Since中任一首部。
304 状态码返回时,不包含任何响应的主体部分。304 虽然被划分在 3XX 类别中,但是和重定向没有关系。
临时重定向。该状态码与302 Found
有着相同的含义。尽管 302 标准禁止POST
变换成GET
,但实际使用时大家并不遵守。
307 会遵照浏览器标准,不会从 POST 变成 GET。但是,对于处理响应时的行为,每种浏览器有可能出现不同的情况。
4XX
的响应结果表明客户端是发生错误的原因所在。
该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。另外,浏览器会像200 OK
一样对待该状态码。
该状态码表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息。另外若之前已进行过 1 次请求,则表示用 户认证失败。
该状态码表明对请求资源的访问被服务器拒绝了。服务器端没有必要给出拒绝的详细理由,但如果想作说明的话,可以在实体的主体部分对原因进行描述,这样就能让用户看到了。
未获得文件系统的访问授权,访问权限出现某些问题(从未授权的发送源 IP 地址试图访问)等列举的情况都可能是发生 403 的原因。
该状态码表明服务器上无法找到请求的资源。除此之外,也可以在服务器端拒绝请求且不想说明理由时使用。
5XX
的响应结果表明服务器本身发生错误。
该状态码表明服务器端在执行请求时发生了错误。也有可能是 Web 应用存在的 bug 或某些临时的故障。
该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入RetryAfter
首部字段再返回给客户端。
Web 使用一种名为HTTP(HyperText Transfer Protocol,超文本传输协议 )的协议作为规范,完成从客户端到服务器端等一系列运作流程。而协议是指规则的约定。可以说,Web 是建立在 HTTP 协议上通信的。
HTTP 于 1990 年问世。那时的 HTTP 并没有作为正式的标准被建立。现在的 HTTP 其实含有 HTTP1.0 之前版本的意思,因此被称为 HTTP/0.9。
HTTP 正式作为标准被公布是在 1996 年的 5 月,版本被命名为 HTTP/1.0,并记载于 RFC1945。虽说是初期标准,但该协议标准至今仍被广泛使用在服务器端。
1997 年 1 月公布的 HTTP/1.1 是目前主流的 HTTP 协议版本。当初的标准是 RFC2068,之后发布的修订版 RFC2616 就是当前的最新版本。
像这样把与互联网相关联的协议集合起来总称为 TCP/IP。也有说法认为,TCP/IP 是指 TCP 和 IP 这两种协议。还有一种说法认为,TCP/ IP 是在 IP 协议的通信过程中,使用到的协议族的统称。
TCP/IP 协议族里重要的一点就是分层。TCP/IP 协议族按层次分别分为以下 4 层:应用层、传输层、网络层和数据链路层。
把 TCP/IP 层次化是有好处的。比如,如果互联网只由一个协议统筹,某个地方需要改变设计时,就必须把所有部分整体替换掉。而分层之后只需把变动的层替换掉即可。把各层之间的接口部分规划好之后,每个层次内部的设计就能够自由改动了。
TCP/IP 协议族内预存了各类通用的应用服务。比如,FTP(File Transfer Protocol,文件传输协议)和 DNS(Domain Name System,域名系统)服务就是其中两类。
HTTP 协议也处于该层。
在传输层有两个性质不同的协议:TCP(Transmission Control Protocol,传输控制协议)和 UDP(User Data Protocol,用户数据报协议)。
数据包是网络传输的最小数据单位。该层规定了通过怎样的路径(所谓的传输路线)到达对方计算机,并把数据包传送给对方。
数据包是网络传输的最小数据单位。该层规定了通过怎样的路径(所谓的传输路线)到达对方计算机,并把数据包传送给对方。
包括控制操作系统、硬件的设备驱动、NIC(Network Interface Card,网络适配器,即网卡),及光纤等物理可见部分(还包括连接器等一切传输媒介)。
硬件上的范畴均在链路层的作用范围之内。
发送端在层与层之间传输数据时,每经过一层时必定会被打上一个该层所属的首部信息。反之,接收端在层与层传输数据时,每经过一层时会把对应的首部消去。这种把数据信息包装起来的做法称为封装(encapsulate)。
IP 协议的作用是把各种数据包传送给对方。而要保证确实传送到对方那里,则需要满足各类条件。其中两个重要的条件是 IP 地址和 MAC 地址(Media Access Control Address)。
IP 地址指明了节点被分配到的地址,MAC 地址是指网卡所属的固定地址。IP 地址可以和 MAC 地址进行配对。IP 地址可变换,但 MAC 地址基本上不会更改。
使用 ARP 协议凭借 MAC 地址进行通信
IP 间的通信依赖 MAC 地址。在网络上,通信的双方在同一局域网(LAN)内的情况是很少的,通常是经过多台计算机和网络设备中转才能连接到对方。而在进行中转时,会利用下一站中转设备的 MAC 地址来搜索下一个中转目标。这时,会采用 ARP 协议(Address Resolution Protocol)。ARP 是一种用以解析地址的协议,根据通信方的 IP 地址就可以反查出对应的 MAC 地址。
所谓的字节流服务(Byte Stream Service)是指,为了方便传输,将大块数据分割成以报文段(segment)为单位的数据包进行管理。
而可靠的传输服务是指,能够把数据准确可靠地传给对方。
一言以蔽之,TCP 协议为了更容易传送大数据才把数据分割,而且 TCP 协议能够确认数据最终是否送达到对方。
确保数据能到达目标
发送端首先发送一个带 SYN 标志的数据包给对方。接收端收到后,回传一个带有 SYN/ACK 标志的数据包以示传达确认信息。最后,发送端再回传一个带 ACK 标志的数据包,代表“握手”结束。
若在握手过程中某个阶段莫名中断,TCP 协议会再次以相同的顺序发送相同的数据包。
URI 是 Uniform Resource Identifier 的缩写,RFC2396 分别对这 3 个单词进行了如下定义。
规定统一的格式可方便处理多种不同类型的资源,而不用根据上下文环境来识别资源指定的访问方式。
另外,加入新增的协议方案(如 http: 或 ftp:)也更容易。
资源的定义是“可标识的任何东西”。除了文档文件、图像或服务(例如当天的天气预报)等能够区别于其他类型的,全都可作为资源。
另外,资源不仅可以是单一的,也可以是多数的集合体。
表示可标识的对象。也称为标识符。
综上所述,URI 就是由某个协议方案表示的资源的定位标识符。协议方案是指访问资源所使用的协议类型名称。
“RFC3986:统一资源标识符(URI)通用语法”中列举了几种 URI 例子,如下所示。
ftp://ftp.is.co.za/rfc/rfc1808.txthttp://www.ietf.org/rfc/rfc2396.txtldap://[2001:db8::7]/c=GB?objectClass?onemailto:John.Doe@example.comnews:comp.infosystems.www.servers.unixtel:+1-816-555-1212telnet://192.0.2.16:80/urn:oasis:names:specification:docbook:dtd:xml:4.1.2
表示指定的 URI,要使用涵盖全部必要信息的绝对 URI、绝对 URL 以及相对 URL。相对 URL,是指从浏览器中基本 URI 处指定的 URL,形如 /image/logo.gif
。
指定用户名和密码作为从服务器端获取资源时必要的登录信息(身份认证)。此项是可选项。
使用绝对 URI 必须指定待访问的服务器地址。地址可以是类似 hackr.jp 这种 DNS 可解析的域名,或是 192.168.1.1 这类 IPv4 地址 名,还可以是 [0:0:0:0:0:0:0:1] 这样用方括号括起来的 IPv6 地址名。
指定服务器连接的网络端口号。此项也是可选项,若用户省略则自动使用默认端口号。
指定服务器上的文件路径来定位特指的资源。这与 UNIX 系统的文件目录结构相似。
针对已指定的文件路径内的资源,可以使用查询字符串传入任意参数。此项可选。
使用片段标识符通常可标记出已获取资源中的子资源(文档内的某个位置)。但在 RFC 中并没有明确规定其使用方法。该项也为可选项。
请求访问文本或图像等资源的一端称为客户端,而提供资源响应的一端称为服务器端。
在两台计算机之间使用 HTTP 协议通信时,在一条通信线路上必定有一端是客户端,另一端则是服务器端。
有时候,按实际情况,两台计算机作为客户端和服务器端的角色有可能会互换。但就仅从一条通信路线来说,服务器端和客户端的角色是确定的,而用 HTTP 协议能够明确区分哪端是客户端,哪端是服务器端。
HTTP 协议规定,请求从客户端发出,最后服务器端响应该请求并返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有接收到请求之前不会发送响应。
下面是从客户端发送给某个 HTTP 服务器端的请求报文中的内容。
GET /index.htm HTTP/1.1Host: hackr.jp
/index.htm
指明了请求访问的资源对象,称为请求URI(request-URI)综合来看,这段请求内容的意思是:请求访问某台 HTTP 服务器上的 /index.htm 页面资源。
请求报文是由请求方法、请求 URI、协议版本、可选的请求首部字段和内容实体构成的。
接收到请求的服务器,会将请求内容的处理结果以响应的形式返回。
HTTP/1.1 200 OKDate: Tue, 10 Jul 2012 06:50:15 GMTContent-Length: 362Content-Type: text/html<html>……
响应报文基本上由协议版本、状态码、用以解释状态码的原因短语、可选的响应首部字段以及实体主体构成。
HTTP 是一种不保存状态,即无状态(stateless)协议。HTTP 协议自身不对请求和响应之间的通信状态进行保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。
HTTP/1.1 虽然是无状态协议,但为了实现期望的保持状态功能,于是引入了 Cookie 技术。有了 Cookie 再用 HTTP 协议通信,就可以管理状态了。
HTTP 协议使用 URI 定位互联网上的资源。正是因为 URI 的特定功能,在互联网上任意位置的资源都能访问到。
当客户端请求访问资源而发送请求时,URI 需要将作为请求报文中的请求 URI 包含在内。指定请求 URI 的方式有很多。
除此之外,如果不是访问特定资源而是对服务器本身发起请求,可以用一个 *
来代替请求 URI。下面这个例子是查询 HTTP 服务器端支持 的 HTTP 方法种类。
OPTIONS * HTTP/1.1
下面介绍 HTTP/1.1 中可使用的方法。
GET 方法用来请求访问已被 URI 识别的资源,指定的资源经服务器端解析后返回响应内容。
虽然用 GET 方法也可以传输实体的主体,但一般不用 GET 方法进行传输,而是用 POST 方法。
虽说 POST 的功能与 GET 很相似,但 POST 的主要目的并不是获取响应的主体内容。
PUT 方法用来传输文件。就像 FTP 协议的文件上传一样,要求在请求报文的主体中包含文件内容,然后保存到请求 URI 指定的位置。
但是,鉴于 HTTP/1.1 的 PUT 方法自身不带验证机制,任何人都可以上传文件 , 存在安全性问题,因此一般的 Web 网站不使用该方法。
若配合 Web 应用程序的验证机制,或架构设计采用 REST(REpresentational State Transfer,表征状态转移)标准的同类 Web 网站,就可能会开放使用 PUT 方法。
HEAD 方法和 GET 方法一样,只是不返回报文主体部分。用于确认 URI 的有效性及资源更新的日期时间等。
DELETE 方法用来删除文件,是与 PUT 相反的方法。DELETE 方法按请求 URI 删除指定的资源。
但是,HTTP/1.1 的 DELETE 方法本身和 PUT 方法一样不带验证机制,所以一般的 Web 网站也不使用 DELETE 方法。当配合 Web 应用程序的验证机制,或遵守 REST 标准时还是有可能会开放使用的。
OPTIONS 方法用来查询针对请求 URI 指定的资源支持的方法。
TRACE 方法是让 Web 服务器端将之前的请求通信环回给客户端的方法。
发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器端就将该数字减 1,当数值刚好减到 0 时,就停止继续传输,最后接收到请求的服务器端则返回状态码 200 OK 的响应。
客户端通过 TRACE 方法可以查询发送出去的请求是怎样被加工修改 / 篡改的。这是因为,请求想要连接到源目标服务器可能会通过代理中转,TRACE 方法就是用来确认连接过程中发生的一系列操作。
但是,TRACE 方法本来就不怎么常用,再加上它容易引发 XST(Cross-Site Tracing,跨站追踪)攻击,通常就更不会用到了。
CONNECT 方法要求在与代理服务器通信时建立隧道,实现用隧道协议进行 TCP 通信。主要使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加 密后经网络隧道传输。
向请求 URI 指定的资源发送请求报文时,采用称为方法的命令。
方法的作用在于,可以指定请求的资源按期望产生某种行为。方法中有 GET、POST 和 HEAD 等。
下表列出了 HTTP/1.0 和 HTTP/1.1 支持的方法。另外,方法名区分大小写,注意要用大写字母。
方法 | 说明 | 支持的 HTTP 协议版本 |
---|---|---|
GET | 获取资源 | 1.0、1.1 |
POST | 传输实体主体 | 1.0、1.1 |
PUT | 传输文件 | 1.0、1.1 |
HEAD | 获得报文首部 | 1.0、1.1 |
DELETE | 删除文件 | 1.0、1.1 |
OPTIONS | 询问支持的方法 | 1.1 |
TRACE | 追踪路径 | 1.1 |
CONNECT | 要求用隧道协议连接代理 | 1.1 |
LINK | 建立和资源之间的联系 | 1.1 |
UNLINE | 断开连接关系 | 1.1 |
LINK 和 UNLINK 已被 HTTP/1.1 废弃,不再支持。
HTTP 协议的初始版本中,每进行一次 HTTP 通信就要断开一次 TCP 连接。
以当年的通信情况来说,因为都是些容量很小的文本传输,所以即使这样也没有多大问题。可随着 HTTP 的普及,文档中包含大量图片的情况多了起来。
比如,使用浏览器浏览一个包含多张图片的 HTML 页面时,在发送请求访问 HTML 页面资源的同时,也会请求该 HTML 页面里包含的其他资源。因此,每次的请求都会造成无谓的 TCP 连接建立和断开,增加通信量的开销。
为解决上述 TCP 连接的问题,HTTP/1.1 和一部分的 HTTP/1.0 想出了持久连接(HTTP Persistent Connections,也称为 HTTP keep-alive 或 HTTP connection reuse)的方法。持久连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。
持久连接的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。另外,减少开销的那部分时间,使 HTTP 请求和响应能够更早地结束,这样 Web 页面的显示速度也就相应提高了。
持久连接使得多数请求以管线化(pipelining)方式发送成为可能。从前发送请求后需等待并收到响应,才能发送下一个请求。管线化技术出现后,不用等待响应亦可直接发送下一个请求。
这样就能够做到同时并行发送多个请求,而不需要一个接一个地等待响应了。
比如,当请求一个包含 10 张图片的 HTML Web 页面,与挨个连接相比,用持久连接可以让请求更快结束。而管线化技术则比持久连接还要快。请求数越多,时间差就越明显。
HTTP 是无状态协议,它不对之前发生过的请求和响应的状态进行管理。也就是说,无法根据之前的状态进行本次的请求处理。
保留无状态协议这个特征的同时又要解决类似的矛盾问题,于是引入了 Cookie 技术。Cookie 技术通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态。
Cookie 会根据从服务器端发送的响应报文内的一个叫做
Set-Cookie
的首部字段信息,通知客户端保存 Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入 Cookie 值后发送出去。
服务器端发现客户端发送过来的 Cookie 后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到之前的状态信息。
在发生 Cookie 交互的场景中,HTTP 请求报文和响应报文的内容如下。
GET /reader/ HTTP/1.1Host: hackr.jp*首部字段内没有Cookie的相关信息
HTTP/1.1 200 OKDate: Thu, 12 Jul 2012 07:12:20 GMTServer: Apache<Set-Cookie: sid=1342077140226724; path=/; expires=Wed,10-Oct-12 07:12:20 GMT>Content-Type: text/plain; charset=UTF-8
GET /image/ HTTP/1.1Host: hackr.jpCookie: sid=1342077140226724
]]>所谓程序员,是指那些能够创造、编写计算机程序的人。不论一个人是什么样的程序员,或多或少,他都在为我们这个社会贡献着什么东西。然而,有些程序员的贡献却超过了一个普通人一辈子能奉献的力量。这些程序员是先驱,受人尊重,他们贡献的东西改变了我们人类的整个文明进程。下面就让我们看看历史上12位伟大的程序员。
Ada Lovelace,原名August Ada Byron,数学爱好者,被后人公认为第一位计算机程序员。
在1842年至1843年期间,Ada花了9个月时间翻译了意大利数学家Luigi Federico Menabrea讲述Charles Babbage计算机分析机(Analytical Engine)的论文。在译文后面,她增加了许多注记,详细说明用该机器计算伯努利数(Bernoulli number)的方法,被认为是世界上第一个计算机程序。因此,Ada也被认为是世界上第一位程序员。
Linus Benedict Torvalds,著名的电脑程序员、黑客,Linux内核的发明人及该计划的合作者。Linux利用个人时间创造出了这套当今全球最流行的操作系统内核之一。他还发起了Git这个开源项目并成为主要开发者。
因为成功开发了Linux内核而荣获2014年计算机先驱奖。他的获奖创造了计算机先驱奖历史上的多个第一:第一次授予一位芬兰人;第一次授予一位“60后”(其实只差3天就是“70后”);获奖成果是在学生时期取得的。
Linus在网上邮件列表中也以脾气火爆而著称。例如,有一次在和人争论Git为何不使用C++开发时,与对方用“bullshit”互骂。他更曾以“OpenBSD crowd is a bunch of masturbating monkeys”来称呼OpenBSD团队。
Niklaus Emil Wirth生于瑞士温特图尔,瑞士计算机科学家。
在1963年到1967期间,他担任斯坦福大学的计算机科学部助理教授,之后又在苏黎世大学担任相同的职位。1968年,他担任苏黎世联邦理工学院的信息学教授,又往施乐帕洛阿尔托研究中心进修了两年。
他是好几种编程语言的主设计师,包括Algol W,Modula,Pascal,Modula-2,Oberon等。
他亦是Euler语言的发明者之一。1984年,他因发展了这些语言而获图灵奖。此外他还是Lilith电脑和Oberon系统的设计和运行队伍的重要成员。
Stephen Gary Wozniak,美国电脑工程师,曾与Steve Jobs合伙创立苹果公司。
Wozniak在1970年代中期创造出苹果一号和苹果二号,苹果二号风靡普及后成为1970年代及1980年代初期销量最佳的个人电脑,他本人也被誉为是使电脑从“旧时王谢堂前燕”到“飞入寻常百姓家”的工程师。
James Gosling,出生于加拿大,软件专家,Java编程语言的共同创始人之一,被公认为“Java之父”。
在12岁时,Gosling已经能设计电子游戏机,帮忙邻居修理收割机。1981年开发在Unix上运行的类Emacs编辑器Gosling Emacs(以C语言编写,使用Mocklisp作为扩展语言)。1983年获得卡耐基·梅隆大学计算机科学博士学位。毕业后到IBM工作,设计IBM第一代工作站NeWS系统,但不受重视,后来转投Sun公司。1990年,与Patrick Naughton和Mike Sheridan等人合作“绿色计划”,开发了一套语言Oak,后改名为Java。1994年底,James Gosling在硅谷召开的大会上展示Java程序。2000年,Java成为世界上最流行的电脑语言。
Ken Thompson生于美国新奥尔良,计算机科学学者与软件工程师。他与Dennis Ritchie一同设计了B语言、C语言,并创建了Unix和Plan 9操作系统。Thompson也是编程语言Go的共同作者,与Dennis Ritchie同为1983年图灵奖得主。
Ken Thompson的贡献还包括发明正则表达式,开发早期的电脑文字编辑器QED与ed,定义UTF-8编码,以及开发电脑象棋。
Rasmus Lerdorf出生于加拿大,并在早年搬到丹麦。1994年,Rasmus开发了PHP,刚开始只是一个简单的用Perl语言编写的程序,用来统计他自己网站的访问者。后来又用C语言重新编写,并可以访问数据库。
在1995年以Personal Home Page Tools(PHP Tools)开始对外发表第一个版本,Lerdorf写了一些介绍此程序的文档,并且发布了PHP1.0。在这早期的版本中,提供了访客留言本、访客计数器等简单的功能。以后越来越多的网站使用了PHP,并且强烈要求增加一些特性,比如循环语句和数组变量等等。
在新的成员加入开发行列之后,在1995年中,PHP2.0发布了。第二版定名为PHP/FI(Form Interpreter)。PHP/FI加入了对MySQL的支持,从此建立了PHP在动态网页开发上的地位。
Brian Wilson Kernighan是一位加拿大计算机科学家。在贝尔实验室,他与Unix的创造者Thompson以及C语言之父Dennis Ritchie一起工作,同时他也是开发Unix的主要贡献者。他是AWK和AMPL编程语言的作者之一,AWK中的K说的就是Kernighan。同时,它也是《C程序设计语言》的作者之一,他与C语言的发明人Dennis Ritchie共同合作了这本书,该书被很多人简称为“K&R C”,K&R就是两人名字的缩写。Brian Kernighan现在是普林斯顿大学计算机学院的教授,同时也是本科学部的代表。
松本行弘,日本计算机科学家、软件工程师,筑波大学毕业,在1995年首次发布Ruby脚本语言的第一个版本。
Ruby是一种功能强大的面向对象的脚本语言,它综合了Perl,Python,Java等语言的特点写成,有强大的文字处理能力,简单的语法,完全的面向对象。同时,Ruby是解释型语言,不需编译即可快捷地编程,擅长于文本处理、系统管理等任务。
Bjarne Stroustrup生于1950年,丹麦计算机科学家,最著名的便是创造并开发了如今被广泛使用的C++编程语言。Bjarne是哥伦比亚大学的客座教授,目前在摩根士丹利工作。
用他自己的话来说,Bjarne“发明了C++,写下了它的早期定义并做出了首个实现……选择制定了C++的设计标准,设计了C++主要的辅助支持环境,并负责处理C++标准委员会的扩展提案。”此外,他还写了一本《C++程序设计语言》,被许多人认为是C++的范本经典,最新的第四版于2013年出版,并囊括了C++ 11所引进的一些新特性。
Steve Jobs和Dennis Ritchie是在同年同月离世的。之后每年的这段时间,很多媒体都会纪念Jobs,但很少会提到Dennis Ritchie。
如果没有丹尼斯·里奇(Dennis Ritchie),就不会有我们现在所熟知的现代计算。他是C语言之父和UNIX操作系统的联合发明人。
不可否认,乔布斯带给我们世上从未见过的创新和标志性的产品,还有一大批对他顶礼膜拜的狂热消费者和终端用户。诸如此类的事情可能再也看不到了。
但是苹果和乔布斯以及很多其他公司所创造的“神奇的”产品,和所有现在我们了解和写在现代计算里的东西,都要归功于丹尼斯·里奇,他于2011年10月12号离开人世,享年70岁。
C语言是里奇在1969-1973年间开发的,他被认为是第一个真正意义上可移植的现代编程语言。自它诞生差不多45年以来,它已经被移植到几乎每一个出现过的系统架构和操作系统上。
除此之外,里奇还是UNIX操作系统的联合发明人。当然UNIX的原型是用汇编语言编写的,到七十年代早期就完全用C重写了。看下面这张图,可以更好的理解“Unix家族”。
关于Dennis Ritchie的其他成就及贡献,推荐阅读以下两篇文章:
最后,用Ritchie在贝尔实验室的同事兼好友Brian Kernighan的评价做个总结:“牛顿说他是站在巨人的肩膀上,如今,我们都站在里奇的肩膀上。”
这句话,应该是对Dennis Ritchie的一生最有力也是最中肯的评价。
Guido van Rossum是一名荷兰的计算机程序员,于1982年获得了阿姆斯特丹大学的数学和计算机科学的硕士学位,并于同年加入一个多媒体组织CWI,做调研员。他作为Python编程语言的作者而为人熟知。在Python社区,Guido被公认为终身仁慈独裁者(Benevolent Dictator For Life,BDFL),意思是他仍然关注Python的开发进程,并在必要的时刻做出决定。
1991年初,Python发布了第一个公开发行版。Guido原居荷兰,1995年移居到美国,并遇到了他现在的妻子。在2003年初,Guido和他的家人,包括他2001年出生的儿子Orlijn一直居住在华盛顿州北弗吉尼亚的郊区,随后他们搬迁到硅谷。从2005年开始Guido就职于Google,其中有一半时间是花在Python上。而现在Guido在为Dropbox工作。
关于Guido还有一个著名的段子:Guido van Rossum 去 Google 应聘,简历只写了三个词「I wrote Python」。当然事后证明这只是为了调侃Google面试流程冗长复杂,事实上在他2005年加入Google时,Google内部已经有相当一部分工程师在使用Guido发明的Python了,而Google请Guido就是冲着Python去的——条件是允许他用一半的工作时间来维护Python, 版权归他自己。
另外Google +上Guido自己也发帖称别再找我应聘Python开发,也是很搞笑了……
Notes: To Do
参考文章
- 历史上最伟大的12位程序员 | Python之禅
- Ada Lovelace | 维基百科
- Ada Lovelace:19世纪的数学奇女子——计算机之母 | 电子技术设计
- Ada Lovelace, the First Tech Visionar | The New Yorker
- Ada Byron, Lady Lovelace (1815-1852) | Yale CS
- 苹果联合创始人沃兹尼亚克的那些成就 | 腾讯科技
- 对Unix40岁的一些感想 | 阮一峰的网络日志
- Unix英烈传:图文细数十五位计算先驱 | Linux公社
- 丹尼斯·里奇,那个给乔布斯提供肩膀的巨人 | 果壳网
- 纪念C语言之父丹尼斯·里奇离世 6 周年 | 开源中国
- 世界十大黑客 | 百度百科
- 务实至上:“PHP之父”Rasmus Lerdorf访谈录 | ITeye
- C/Unix思想后隐藏的巨人——Brian Kernighan | 图灵社区
- [英]Brian W. Kernighan:我与CS的半个世纪(图灵访谈)| 图灵社区
- 真相暴露帖:本人采访Ruby语言创始人松本行弘(Matz)先生 | 果壳日志
- Bjarne Stroustrup | 维基百科
- Bjarne Stroustrup’s homepage
- Guido van Rossum - Personal Home Page
- 代码世界值得你珍藏的 72 张面孔 | 阿里巴巴中间件
Python中sorted
函数用于对集合进行排序,它的功能非常强大,今天来介绍一下sorted
的各种使用场景。
这里说的集合是对可迭代对象的一个统称,他们可以是列表、字典、set、甚至是字符串。
1、默认情况,sorted
函数将按列表升序进行排序,并返回一个新列表对象,原列表保持不变,最简单的排序。
>>> nums = [3,4,5,2,1]>>> sorted(nums)[1, 2, 3, 4, 5]
2、降序排序。如果要按照降序排列,只需指定参数reverse=True
即可。
>>> sorted(nums, reverse=True)[5, 4, 3, 2, 1]
3、如果要按照某个规则排序,则需指定参数key
。key
是一个函数对象,例如对字符串构成的列表进行排序,想要按照字符串长度排序的话:
>>> chars = ['Andrew', 'This', 'a', 'from', 'is', 'string', 'test']>>> sorted(chars, key=len)['a', 'is', 'from', 'test', 'This', 'Andrew', 'string']
len
是内建函数,sorted
函数在排序的时候会用len
去获取每个字符串的长度来排序。
4、如果是一个复合的列表结构,例如由元组构成的列表,要按照元组中的第二个元素排序,那么可以用lambda定义一个匿名函数。
>>> students = [('zhang', 'A'), ('li', 'D'), ('wang', 'C')]>>> sorted(students, key=lambda x: x[1])[('zhang', 'A'), ('wang', 'C'), ('li', 'D')]
这里将按照字母A-C-D
的顺序排列。
5、如果要排序的元素是自定义类,例如Student
类按照年龄来排序,则可以写成:
>>> class Student: def __init__(self, name, grade, age): self.name = name self.grade = grade self.age = age def __repr__(self): return repr((self.name, self.grade, self.age))>>> student_objects = [ Student('john', 'A', 15), Student('jane', 'B', 12), Student('lily', 'A', 12), Student('dave', 'B', 10), ]>>> sorted(student_objects, key=lambda t:t.age)[('dave', 'B', 10), ('jane', 'B', 12), ('lily', 'A', 12), ('john', 'A', 15)]
6、和数据库的排序一样,sorted
也可以根据多个字段来排序。例如要先根据age
排序,如果age
相同则根据grade
排序,则可以使用元组:
>>> sorted(student_objects, key=lambda t:(t.age, t.grade))[('dave', 'B', 10), ('lily', 'A', 12), ('jane', 'B', 12), ('john', 'A', 15)]
7、前面碰到的排序场景都是建立在两个元素可以互相比较的前提下,例如数值按大小比较,字母按顺序比较。如果遇到本身是不可比较的,需要我们自己来定义比较规则的情况如何处理呢?
举个简单的例子:
>>> nums = [2, 1.5, 2.5, '2', '2.5']>>> sorted(nums)TypeError: '<' not supported between instances of 'str' and 'int'
一个整数列表中,可能有数字、字符串,在Python 3中,字符串和数值是不能比较的,而Python 2中任何类型都可以比较。这是两个版本中一个很大的区别。
# python 2.7>>> "2.5" > 2True# python 3.6>>> "2.5" > 2TypeError: '>' not supported between instances of 'str' and 'int'
我们需要使用functools
模块中的cmp_to_key
来指定比较函数是什么。
import functoolsdef compare(x1, x2): if isinstance(x1, str): x1 = float(x1) if isinstance(x2, str): x2 = float(x2) return x1 - x2>>> sorted(nums, key=functools.cmp_to_key(compare))[1.5, 2, '2', 2.5, '2.5']
8、关于sorted
函数,Python 2和Python 3之间的区别是Python 2中的sorted
可以指定cmp
关键字参数。就是说当遇到需要自定义比较操作的数据时,可以通过cmp=compare
来实现,不需要像Python 3一样还要导入functools.cmp_to_key
实现。
nums = [2, 1.5, 2.5, '2', '2.5']def compare(x1, x2): if isinstance(x1, str): x1 = float(x1) if isinstance(x2, str): x2 = float(x2) return 1 if x1 - x2 > 0 else -1 if x1 - x2 < 0 else 0>>> sorted(nums, cmp=compare)[1.5, 2, '2', 2.5, '2.5']
其实,在Python 2中,上面这种情况就算不指定
cmp
,默认也会按照这种方式排序。需要记住的是,在Python 2中,任何类型都可以比较,而Python 3只有同类型数据可以比较。
9、对于集合构成的列表,有一种更高效的方法指定这个key
:
>>> from operator import itemgetter>>> sorted(students, key=itemgetter(1))[('zhang', 'A'), ('wang', 'C'), ('li', 'D')]
10、同样的,对于自定义类,也有一种更高效的方法指定key
:
>>> from operator import attrgetter>>> sorted(student_objects, key=attrgetter('age'))[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
如果参与排序的字段有两个怎么办?可以这样操作:
>>> sorted(student_objects, key=attrgetter('grade', 'age'))[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
]]>参考文章
Updating…
Linux下的压缩包,最常见的格式是.tar.gz
以及.tar.bz2
。而Linux平台最常用的压缩解压命令就是tar
命令,相信不少人都有面对不同格式压缩包的各种参数抓狂的经历,今天就来总结学习一下tar
命令的基本操作。
tar -zxvf ***.tar.gztar -xvf ***.gz# tar.xzxz -d ***.tar.xztar -xvf ***.tar# 或直接tar -xvJf ***.tar.xz
]]>参考文章
更新中…
MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可 MariaDB的目的是完全兼容MySQL,包括API和命令行,使之能轻松成为MySQL的代替品。在存储引擎方面,使用XtraDB来代替MySQL的InnoDB。
MariaDB由MySQL的创始人Michael Widenius主导开发,他早前曾以10亿美元的价格,将自己创建的公司MySQL AB卖给了SUN,此后,随着SUN被甲骨文收购,MySQL的所有权也落入Oracle的手中。MariaDB名称来自Michael Widenius的女儿Maria的名字。
比较不错的几篇
容器云平台
公有云
文章教程
徽章
名称 | 博主 | 备注 |
---|---|---|
张砷镓 | 张砷镓 | 14岁大学毕业,Web开发 |
陈沙克日志 | 陈沙克 | 招银,容器,OpenShift |
酷壳 COOLSHELL | 陈皓 | 20年软件开发经验,底层技术平台 |
Some 云计算 Links | ||
metaboy’s blog | wangyuxiong | 阿里云工程师 |
笑遍世界 | 任永杰 | 《KVM虚拟化技术:实战与原理解析》作者,华工09届校友 |
Deserts | Deserts | Valine-Admin作者 |
Hux Blog | 黄玄 | 前端 |
Hollis | HollisChuang | 《成神之路系列文章》,阿里资深工程师 |
1 Byte | 江宏 | LeanCloud创始人,CEO |
crossoverJie’s Blog | crossoverJie | JCSprout发起者,JVM、并发、分布式 |
RUO DOJO | 潘小鶸(潘家邦) | 云数据库平台技术专家,阿里ApsaraDB |
阮一峰的网络日志 | 阮一峰 | 上财世界经济博士,支付宝前端 |
廖雪峰的官方网站 | 廖雪峰 | 技术作家,JS、Python、Git教程 |
纯洁的微笑 | 一线技术总监 | Java后端、微服务 |
小弟调调 | jaywcjlove | 前端、各类awesome项目 |
鸟窝 | colobu | 《Scala集合技术手册》作者,中科大毕业,现在微博做架构和开发 |
CyC2018 | 郑永川 | 中山大学 |
klion’s blog | klionsec | 网络安全,博客已停止更新 |
Sean’s Notes | seanlook | 腾讯互娱,游戏DBA |
Jimmy Song | 宋净超 | 蚂蚁金服,Cloud Native,ServiceMesh |
在前端开发中,我们经常需要获取网页中滚动条滚过的长度。获取该值的方法一般是通过scrollTop属性,如document.body.scrollTop
,但同时也有document.documentElement.scrollTop
。这两者都是经常用来获取文档滚动条滚过距离的方式,它们又有什么区别呢?
DTD(Document Type Definition,文档类型定义 )是一套为了进行程序间的数据交换而建立的关于标记符的语法规则,它使用一系列的合法元素来定义文档结构。 DTD告诉浏览器当前文档用的是什么标记语言,之后浏览器才能正确的根据W3C标准解析文档代码。
目前HTML DTD共有三种类型:
HTML文档就是通过<!DOCTYPE ...>
定义的。下面是一个HTML4.0的过渡DTD HTML文档:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html> <head> <title></title> </head> <body> </body></html>
或在HTML5中:
<!doctype html><html> <head> <title></title> </head> <body> </body></html>
document.documentElement
是整个文档节点树的根节点,在网页中即html
标签元素body
节点,在网页中即body
标签元素我们常看见如下写法来获取页面滚动条滚动的长度,
var top = document.documentElement.scrollTop || document.body.scrollTop;// 或者var top = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop;
document.body.scrollTop
的值为0,此时需要使用document.documentElement.scrollTop
来获取滚动条滚过的长度document.body.scrollTop
来获取滚动条滚过的长度// 网页可见区域宽var clientWidth = document.body.clientWidth; // 网页可见区域高var clientHeight = document.body.clientHeight;// 网页可见区域宽(包括边线的宽)var offsetWidth = document.body.offsetWidth;// 网页可见区域高(包括边线的高)var offsetHeight = document.body.offsetHeight;// 网页正文全文宽var scrollWidth = document.body.scrollWidth;// 网页正文全文高var scrollHeight = document.body.scrollHeight;// 网页被卷去的高var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;// 网页被卷去的左var scrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
// 元素的实际高度var offsetHeight = document.getElementById("div").offsetHeight;// 元素的实际宽度var offsetWidth = document.getElementById("div").offsetWidth;// 元素距离左边界的距离var offsetLeft = document.getElementById("div").offsetLeft;// 元素距离上边界的距离var offsetTop = document.getElementById("div").offsetTop;
]]>参考文章
译自 Manage a Docker Cluster with Kubernetes | Linode
Kubernetes是一个来管理容器化应用程序的开源平台。如果您使用Docker将应用部署到多个服务器节点上,Kubernetes集群就可以管理您的服务器和应用,包括扩展、部署和滚动更新等操作。
Kubernetes集群由至少一个主节点和多个工作节点组成。主节点运行API服务器、调度程序和控制器管理器,并在集群中动态部署应用程序。
要完成本指南的操作,您需要三台运行Ubuntu 16.04 LTS的服务器,每台服务器内存需在4GB以上。
本文需要您首先完成如何在Kubernetes集群上安装,配置和部署NGINX指南的相关操作,并按照其中的步骤配置一个主节点和两个工作节点。
设置三台服务器主机名如下:
kube-master
kube-worker-1
kube-worker-2
除非另有说明,否则以下的所有命令都将在kube-master
节点上执行。
每个Pod由一个或多个紧密耦合的容器组成,这些容器共享存储和网络等资源。Pod中的容器以Pod为单位启动、停止或复制。
部署(Deployments)是可以管理Pod创建的高级对象,并支持声明性扩展和滚动升级等功能。
1.在文本编辑器中,创建nginx.yaml
配置文件并添加以下内容。
~/nginx.yaml
:
apiVersion: apps/v1kind: Deploymentmetadata: name: nginx-server labels: app: nginxspec: replicas: 1 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.13-alpine ports: - containerPort: 80
该文件包含了定义一个部署所需的所有必要信息,包括要使用的Docker镜像、副本数量以及容器端口。要了解关于配置部署的更多信息,请参阅官方文档。
2.创建您的第一个部署:
kubectl create -f nginx.yaml --record
3.查看部署列表:
kubectl get deploymentsNAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGEnginx-server 1 1 1 1 13s
4.检查Pod状态:
kubectl get podsNAME READY STATUS RESTARTS AGEnginx-server-b9bc6c6b5-d2gqv 1/1 Running 0 58
5.要查看部署的创建节点,请添加-o wide
参数:
kubectl get pods -o wideNAME READY STATUS RESTARTS AGE IP NODEnginx-server-b9bc6c6b5-d2gqv 1/1 Running 0 1m 192.168.255.197 kube-worker-02
Kubernetes可以轻松扩展部署以添加或删除副本。
1.将副本的数量增加到8:
kubectl scale deployment nginx-server --replicas=8
2.检查新副本的可用性:
kubectl get pods -o wideNAME READY STATUS RESTARTS AGE IP NODEnginx-server-b9bc6c6b5-4mdf6 1/1 Running 0 41s 192.168.180.10 kube-worker-1nginx-server-b9bc6c6b5-8mvrd 1/1 Running 0 3m 192.168.180.9 kube-worker-1nginx-server-b9bc6c6b5-b99pt 1/1 Running 0 40s 192.168.180.12 kube-worker-1nginx-server-b9bc6c6b5-fjg2c 1/1 Running 0 40s 192.168.127.12 kube-worker-2nginx-server-b9bc6c6b5-kgdq5 1/1 Running 0 41s 192.168.127.11 kube-worker-2nginx-server-b9bc6c6b5-mhb7s 1/1 Running 0 40s 192.168.180.11 kube-worker-1nginx-server-b9bc6c6b5-rlf9w 1/1 Running 0 41s 192.168.127.10 kube-worker-2nginx-server-b9bc6c6b5-scwgj 1/1 Running 0 40s 192.168.127.13 kube-worker-2
3.可以使用同样的命令减少副本的数量:
kubectl scale deployment nginx-server --replicas=3
通过部署来管理Pod允许您使用滚动更新(Rolling Upgrades)的功能。滚动更新是一种允许您在不停机的情况下更新应用程序版本的机制。Kubernetes确保至少有25%的Pod可随时提供服务,并会在删除旧Pod之前先创建新的Pod。
1.将容器的NGINX版本从 1.13 升级到 1.13.8:
kubectl set image deployment/nginx-server nginx=nginx:1.13.8-alpine
与扩展过程类似,set
命令使用声明性方法:您只需指定所需的目标状态,控制器会管理完成该目标所需的所有任务。
2.检查更新状态:
kubectl rollout status deployment/nginx-serverWaiting for rollout to finish: 1 out of 3 new replicas have been updated...Waiting for rollout to finish: 1 out of 3 new replicas have been updated...Waiting for rollout to finish: 1 out of 3 new replicas have been updated...Waiting for rollout to finish: 2 out of 3 new replicas have been updated...Waiting for rollout to finish: 2 out of 3 new replicas have been updated...Waiting for rollout to finish: 2 out of 3 new replicas have been updated...Waiting for rollout to finish: 1 old replicas are pending termination...Waiting for rollout to finish: 1 old replicas are pending termination...deployment "nginx-server" successfully rolled out
3.你可以使用describe
命令手动检查应用程序版本:
kubectl describe pod <pod-name>
4.如果发生错误,回滚(Rollout)将被挂起,系统将强制要求用户输入 CTRL + C 以取消更新。通过设置无效的NGINX版本来测试:
kubectl set image deployment/nginx-server nginx=nginx:1.18.
5.查看当前Pod状态:
kubectl get pods -o wideNAME READY STATUS RESTARTS AGE IP NODEnginx-server-76976d4555-7nv6z 1/1 Running 0 3m 192.168.127.15 kube-worker-2nginx-server-76976d4555-wg785 1/1 Running 0 3m 192.168.180.13 kube-worker-1nginx-server-76976d4555-ws4vf 1/1 Running 0 3m 192.168.127.14 kube-worker-2nginx-server-7ddd985dd6-mpn9h 0/1 ImagePullBackOff 0 2m 192.168.180.16 kube-worker-1
可以看到名为nginx-server-7ddd985dd6-mpn9h
的Pod正在试图将NGINX更新到一个不存在的版本。
6.检查此Pod以获取该错误的更多详细信息:
kubectl describe pod nginx-server-7ddd985dd6-mpn9h
7.由于在创建部署时使用了--record
参数,您可以通过以下命令检索完整的历史记录:
kubectl rollout history deployment/nginx-serverREVISION CHANGE-CAUSE1 kubectl scale deployment nginx-server --replicas=32 kubectl set image deployment/nginx-server nginx=nginx:1.13.8-alpine3 kubectl set image deployment/nginx-server nginx=nginx:1.18
8.您可以使用undo
命令回滚到之前的工作版本:
kubectl rollout undo deployment/nginx-server
9.要回滚到特定的版本,请使用--to-revision
选项以指定要回滚的目标版本:
kubectl rollout undo deployment/nginx-server --to-revision=1
您现在已经有了一个运行三个Pod的部署,每个Pod都运行了一个NGINX应用。要将Pod发布到互联网,您需要创建一个 服务。在Kubernetes中,服务是一种抽象,允许随时访问Pod。服务会自动处理IP更改,更新以及扩展,因此在启用该服务后,只要运行的Pod保持活动状态,就可通过互联网访问您的应用程序。
1.配置一个测试服务。
~/nginx-service.yaml
:
apiVersion: v1kind: Servicemetadata: name: nginx-service labels: run: nginxspec: type: NodePort ports: - port: 80 targetPort: 80 protocol: TCP name: http selector: app: nginx
2.创建服务:
kubectl create -f nginx-service.yaml
3.检查新服务的状态:
kubectl get servicesNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEkubernetes ClusterIP 10.96.0.1 443/TCP 2dnginx-service NodePort 10.97.41.31 80:31738/TCP 38m
服务正在运行并接受31738
端口上的连接。
4.测试服务:
curl <MASTER_LINODE_PUBLIC_IP_ADDRESS>:<PORT(S)>
5.使用describe
命令查看此服务的其他信息:
kubectl describe service nginx-serviceName: nginx-serviceNamespace: defaultLabels: run=nginxAnnotations: Selector: app=nginxType: NodePortIP: 10.97.41.31Port: http 80/TCPTargetPort: 80/TCPNodePort: http 31738/TCPEndpoints: 192.168.127.14:80,192.168.127.15:80,192.168.180.13:80Session Affinity: NoneExternal Traffic Policy: ClusterEvents:
命名空间是是一个逻辑环境,可以灵活的在多个团队或用户之间划分集群资源。
1.查看可用的命名空间:
kubectl get namespacesdefault Active 7hkube-public Active 7hkube-system Active 7h
顾名思义,如果未指定其他的命名空间,则您的部署将会放置在default
命名空间下。kube-system
为Kubernetes创建的对象保留,而kube-public
则对所有用户可用。命名空间可以通过.json
文件创建,也可以直接在命令行创建。
2.为 development 环境新建名为dev-namespace.json
的文件。
~/home/dev-namespace.json
:
{ "kind": "Namespace", "apiVersion": "v1", "metadata": { "name": "development", "labels": { "name": "development" } }}
3.在集群中创建命名空间:
kubectl create -f dev-namespace.json
4.再次查看命名空间:
kubectl get namespaces
要使用命名空间,您需要定义使用命名空间的 上下文(Context)。Kubernetes上下文保存在kubectl
配置文件中。
1.查看当前的配置:
kubectl config view
2.检查您当前正在使用的上下文:
kubectl config current-context
3.使用以下命令添加dev
上下文:
kubectl config set-context dev --namespace=development \--cluster=kubernetes \--user=kubernetes-admin
4.切换至dev
上下文/命名空间:
kubectl config use-context dev
5.验证更改是否生效:
kubectl config current-context
6.查看新的配置:
kubectl config view
7.命名空间中的Pod对其他命名空间不可见。列出您的Pod来检查该特性:
kubectl get pods
系统提示“No resources found”,是因为您未在此命名空间中创建Pod或部署,不过您仍然可以添加--all-namespaces
参数来查看这些对象:
kubectl get services --all-namespaces
Kubernetes中的任何对象都可以添加标签。标签是一组键值对,可以帮助用户基于各种特征更加轻松的组织、过滤并选择对象。
1.在此命名空间中创建一个测试部署,此部署将包含nginx
标签。
~/my-app.yaml
:
apiVersion: apps/v1kind: Deploymentmetadata: name: my-app labels: app: my-appspec: replicas: 4 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.12-alpine ports: - containerPort: 80
2.创建部署:
kubectl create -f my-app.yaml --record
3.如果您只需在集群中查找特定的Pod,而不是列出所有Pod,那么在命令中添加-l
选项以按标签搜索通常更有效率:
kubectl get pods --all-namespaces -l app=nginx
这里仅列出了default
和development
命名空间中的Pod,因为它们的定义中包含nginx
标签。
Kubernetes节点可以是物理机或虚拟机。可以将节点视为Kubernetes抽象模型中的最高级别。
1.列出您当前的节点:
kubectl get nodesNAME STATUS ROLES AGE VERSIONkube-master Ready master 21h v1.9.2kube-worker-1 Ready 19h v1.9.2kube-worker-2 Ready 17h v1.9.2
2.要查看更多信息,添加-o
参数:
kubectl get nodes -o wide
3.显示的信息大部分是自解释的,对于检查全部节点是否准备就绪而言非常有用。您可以使用describe
命令以获取特定节点的详细信息:
kubectl describe node kube-worker-1
Kubernetes提供了一种非常直接的办法使节点安全离线。
1.返回您正在运行NGINX服务的默认命名空间:
kubectl config use-context kubernetes-admin@kubernetes
2.检查您的Pod:
kubectl get pods -o wide
3.在kube-worker-2
节点上禁止新Pod的创建:
kubectl cordon kube-worker-2
4.检查您的节点状态:
kubectl get nodesNAME STATUS ROLES AGE VERSIONkube-master Ready master 4h v1.9.2kube-worker-1 Ready 4h v1.9.2kube-worker-2 Ready,SchedulingDisabled 4h v1.9.2
5.要测试Kubernetes控制器和调度程序,请扩展您的部署:
kubectl scale deployment nginx-server --replicas=10
6.再次查看您的Pod:
kubectl get pods -o wideNAME READY STATUS RESTARTS AGE IP NODEnginx-server-b9bc6c6b5-2pnbk 1/1 Running 0 11s 192.168.188.146 kube-worker-1nginx-server-b9bc6c6b5-4cls5 1/1 Running 0 11s 192.168.188.148 kube-worker-1nginx-server-b9bc6c6b5-7nw5m 1/1 Running 0 3d 192.168.255.220 kube-worker-2nginx-server-b9bc6c6b5-7s7w5 1/1 Running 0 44s 192.168.188.143 kube-worker-1nginx-server-b9bc6c6b5-88dvp 1/1 Running 0 11s 192.168.188.145 kube-worker-1nginx-server-b9bc6c6b5-95jgr 1/1 Running 0 3d 192.168.255.221 kube-worker-2nginx-server-b9bc6c6b5-md4qd 1/1 Running 0 3d 192.168.188.139 kube-worker-1nginx-server-b9bc6c6b5-r5krq 1/1 Running 0 11s 192.168.188.144 kube-worker-1nginx-server-b9bc6c6b5-r5nd6 1/1 Running 0 44s 192.168.188.142 kube-worker-1nginx-server-b9bc6c6b5-ztgmr 1/1 Running 0 11s 192.168.188.147 kube-worker-1
现在总共有10个Pod,但新Pod只在第一个节点中创建。
7.通知kube-worker-2
节点停止其运行的Pod:
kubectl drain kube-worker-2 --ignore-daemonsetsnode "kube-worker-2" already cordonedWARNING: Ignoring DaemonSet-managed pods: calico-node-9mgc6, kube-proxy-2v8rwpod "my-app-68845b9f68-wcqsb" evictedpod "nginx-server-b9bc6c6b5-7nw5m" evictedpod "nginx-server-b9bc6c6b5-95jgr" evictedpod "my-app-68845b9f68-n5kpt" evictednode "kube-worker-2" drained
8.检查上述命令对您Pod产生的效果:
kubectl get pods -o wideNAME READY STATUS RESTARTS AGE IP NODEnginx-server-b9bc6c6b5-2pnbk 1/1 Running 0 9m 192.168.188.146 kube-worker-1nginx-server-b9bc6c6b5-4cls5 1/1 Running 0 9m 192.168.188.148 kube-worker-1nginx-server-b9bc6c6b5-6zbv6 1/1 Running 0 3m 192.168.188.152 kube-worker-1nginx-server-b9bc6c6b5-7s7w5 1/1 Running 0 9m 192.168.188.143 kube-worker-1nginx-server-b9bc6c6b5-88dvp 1/1 Running 0 9m 192.168.188.145 kube-worker-1nginx-server-b9bc6c6b5-c2c5c 1/1 Running 0 3m 192.168.188.150 kube-worker-1nginx-server-b9bc6c6b5-md4qd 1/1 Running 0 3d 192.168.188.139 kube-worker-1nginx-server-b9bc6c6b5-r5krq 1/1 Running 0 9m 192.168.188.144 kube-worker-1nginx-server-b9bc6c6b5-r5nd6 1/1 Running 0 9m 192.168.188.142 kube-worker-1nginx-server-b9bc6c6b5-ztgmr 1/1 Running 0 9m 192.168.188.147 kube-worker-1
9.您现在已经可以在不中断服务的情况下安全关闭kube-worker-2
节点。
10.完成维护后,通知控制器此节点可以再次进行调度:
kubectl uncordon kube-worker-2
]]>参考资料
Apache Guacamole是一款HTML5应用程序,可通过RDP,VNC和其他协议访问远程桌面。您可以创建一个虚拟云桌面,用户通过Web浏览器即可访问。本指南将介绍如何通过Docker安装Apache Guacamole,并借助其访问托管在Linode上的远程桌面。
这里介绍的方法将安装最新版本的Docker。如需安装特定版本Docker,或需要Docker EE环境,请参阅官方文档寻求帮助。
以下步骤将使用Ubuntu官方软件库安装Docker社区版(Community Edition,CE)。如需在其他Linux发行版上安装,请参阅官网的安装说明。
1.卸载系统上可能存在的旧版本Docker:
sudo apt remove docker docker-engine docker.io
2.确保您已安装了使用Docker仓库所需的如下依赖:
sudo apt install apt-transport-https ca-certificates curl software-properties-common
3.添加Docker的GPG密钥:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
4.验证GPG密钥指纹:
sudo apt-key fingerprint 0EBFCD88
您应该看到类似以下内容的输出:
pub 4096R/0EBFCD88 2017-02-22 Key fingerprint = 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88uid Docker Release (CE deb) <docker@docker.com>sub 4096R/F273FCD8 2017-02-22
5.添加Dockerstable
仓库:
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
6.更新软件包索引并安装Docker社区版:
sudo apt updatesudo apt install docker-ce
7.将受限的Linux用户账户添加到docker
用户组:
sudo usermod -aG docker exampleuser
您需要重启Shell会话才能使此更改生效。
8.运行内置的“Hello World”程序以检查Docker是否成功安装:
docker run hello-world
本指南将使用MySQL作为参考,但PostgreSQL以及MariaDB也同样适用。
1.拉取Guacamole服务器、Guacamole客户端和MySQL的Docker镜像:
docker pull guacamole/guacamoledocker pull guacamole/guacddocker pull mysql/mysql-server
2.创建数据库初始化脚本以创建用于验证身份的数据表:
docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --mysql > initdb.sql
3.为MySQL的root用户生成一次性密码,可在日志中查看:
docker run --name example-mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_ONETIME_PASSWORD=yes -d mysql/mysql-serverdocker logs example-mysql
Docker日志会在终端中打印密码:
[Entrypoint] Database initialized[Entrypoint] GENERATED ROOT PASSWORD: <password>
4.重命名并将initdb.sql
移动到MySQL容器中:
docker cp initdb.sql example-mysql:/guac_db.sql
5.在MySQL的Docker容器中打开bash终端:
docker exec -it example-mysql bash
6.使用一次性密码登录。在重新设定root
用户密码之前,终端不会接受任何命令。创建一个新的数据库和用户,如下所示:
bash-4.2# mysql -u root -pEnter password:Welcome to the MySQL monitor. Commands end with ; or \g.Your MySQL connection id is 11Server version: 5.7.20Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.Oracle is a registered trademark of Oracle Corporation and/or itsaffiliates. Other names may be trademarks of their respectiveowners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_root_password';Query OK, 0 rows affected (0.00 sec)mysql> CREATE DATABASE guacamole_db;Query OK, 1 row affected (0.00 sec)mysql> CREATE USER 'guacamole_user'@'%' IDENTIFIED BY 'guacamole_user_password';Query OK, 0 rows affected (0.00 sec)mysql> GRANT SELECT,INSERT,UPDATE,DELETE ON guacamole_db.* TO 'guacamole_user'@'%';Query OK, 0 rows affected (0.00 sec)mysql> FLUSH PRIVILEGES;Query OK, 0 rows affected (0.00 sec)mysql> quitBye
7.在bash终端中,使用初始化脚本为新数据库创建数据表:
cat guac_db.sql | mysql -u root -p guacamole_db
验证数据表是否已成功添加。如果guacamole
数据库中不存在新建的表,请再次确认之前的步骤均已正确执行。
mysql> USE guacamole_db;Reading table information for completion of table and column namesYou can turn off this feature to get a quicker startup with -ADatabase changedmysql> SHOW TABLES;+---------------------------------------+| Tables_in_guacamole_db |+---------------------------------------+| guacamole_connection || guacamole_connection_group || guacamole_connection_group_permission || guacamole_connection_history || guacamole_connection_parameter || guacamole_connection_permission || guacamole_sharing_profile || guacamole_sharing_profile_parameter || guacamole_sharing_profile_permission || guacamole_system_permission || guacamole_user || guacamole_user_password_history || guacamole_user_permission |+---------------------------------------+13 rows in set (0.00 sec)
退出bash终端:
exit
1.在Docker中启动guacd:
docker run --name example-guacd -d guacamole/guacd
2.连接容器,以便Guacamole验证存储在MySQL数据库中的凭证:
docker run --name example-guacamole --link example-guacd:guacd --link example-mysql:mysql -e MYSQL_DATABASE='guacamole_db' -e MYSQL_USER='guacamole_user' -e MYSQL_PASSWORD='guacamole_user_password' -d -p 127.0.0.1:8080:8080 guacamole/guacamole
注意
可通过以下命令查看所有正在运行和未运行的Docker容器:
docker ps -a
3.example-guacamole
、example-guacd
和example-mysql
都已运行后,请在浏览器中访问localhost:8080/guacamole/
。默认的登录账户是guacadmin
,默认登录密码guacadmin
。登录后应尽快修改登录账户及密码。
在共享远程桌面之前,必须在Linode上安装桌面环境以及VNC服务器。本指南将使用Xfce桌面,因为Xfce是轻量级的,不会过度消耗系统资源。
1.在Linode上安装Xfce:
sudo apt install xfce4 xfce4-goodies
如果系统资源的限制较少,则可使用Unity桌面作为替代:
sudo apt install --no-install-recommends ubuntu-desktop gnome-panel gnome-settings-daemon metacity nautilus gnome-terminal
2.安装VNC服务器。启动VNC服务器时,系统将提示用户输入密码:
sudo apt install tightvncservervncserver
除了提示输入密码外,系统还会提供“仅可查看”的选项。密码最大长度为8位字符。对于需要更高安全性的设置,我们强烈建议您将Guacamole部署为使用SSL加密的反向代理。
You will require a password to access your desktops.Password:Verify:Would you like to enter a view-only password (y/n)?
3.确保在.vnc/xstartup
的最后启动桌面环境,否则只会显示灰色屏幕:
echo 'startxfce4 &' | tee -a .vnc/xstartup
若使用Unity桌面作为替代,则配置示例如下:
#!/bin/shxrdb $HOME/.Xresourcesxsetroot -solid grey#x-terminal-emulator -geometry 80x24+10+10 -ls -title "$VNCDESKTOP Desktop" &#x-window-manager &# Fix to make GNOME workexport XKL_XMODMAP_DISABLE=1/etc/X11/Xsessiongnome-panel &gnome-settings-daemon &metacity &nautilus &
Guacamole支持VNC,RDP,SSH和Telnet协议。本章节将介绍如何在浏览器界面中添加新的连接。
1.在连接到VNC服务器之前,创建一个SSH隧道,并将user
和example.com
替换为Linode的用户名和公网IP:
ssh -L 5901:localhost:5901 -N -f -l user example.com
2.在Guacamole控制面板中,点击右上角的下拉菜单,然后选择 Settings 。在 Connections 选项卡中,点击 New Connection 按钮。
3.在 Edit Connection 设置中,输入连接名。在 Parameters 设置中,主机名即为Linode的公网IP地址。端口号为5900 + 显示编号——这里以5901为例。最后输入8位密码。
官方文档详细描述了所有参数的具体含义。
注意
如果您在同一Linode服务器上有多个VNC连接,请增加连接所用的端口号:5902,5903……以此类推。如果您的远程连接托管在不同的Linode服务器上,则仍应继续使用5901端口。
4.在右上角的下拉菜单中,点击 Home。新建的连接现在应该已经可以使用。
使用快捷键 CTRL + ALT + SHIFT 可以打开剪贴板、键盘/鼠标设置以及导航菜单。
5.点击浏览器的后退按钮,回到 Home 菜单。
6.可以连接至其他桌面,并且可在新的浏览器选项卡中同时连接多个远程桌面。
本指南旨在通过Docker简化安装过程,并演示如何使用Apache Guacamole快速连接至远程桌面。除此之外Apache Guacamole还提供了许多功能,如屏幕录制、Duo双重身份认证、SFTP文件传输等。Guacamole作为Apache的孵化项目,我们期待在不久的将来看到其进一步的发展。
]]>参考文章
@Deserts在此基础上对Valine进行了二次开发,并利用LeanCloud搭建简易评论管理后台。
]]>参考文章
本指南基于Debian 7或Ubuntu 14.04编写。要完成该指南,请确保您的账户中至少存在两个Linode节点和一个NodeBalancer。两个Linode节点都需要私有IP地址。同时还要确保已经在Linode节点上配置了SSH密钥,并且还需将另一台Linode主机的SSH密钥添加在本机的/.ssh/authorized_keys
文件中。
注意
本指南是为非root用户编写的,会在需要提升权限的命令之前加上
sudo
。如果您不熟悉sudo
命令,请参阅Linux用户和用户组指南。
使用以下命令在每个Linode节点上安装Apache,PHP和MySQL:
sudo apt-get updatesudo apt-get upgrade -ysudo apt-get install apache2 php5 php5-mysql mysql-server mysql-client
1.编辑每个Linode节点上的/etc/mysql/my.cnf
配置文件,添加或修改以下值:
Server 1:
server_id = 1log_bin = /var/log/mysql/mysql-bin.loglog_bin_index = /var/log/mysql/mysql-bin.log.indexrelay_log = /var/log/mysql/mysql-relay-binrelay_log_index = /var/log/mysql/mysql-relay-bin.indexexpire_logs_days = 10max_binlog_size = 100Mlog_slave_updates = 1auto-increment-increment = 2auto-increment-offset = 1
Server 2:
server_id = 2log_bin = /var/log/mysql/mysql-bin.loglog_bin_index = /var/log/mysql/mysql-bin.log.indexrelay_log = /var/log/mysql/mysql-relay-binrelay_log_index = /var/log/mysql/mysql-relay-bin.indexexpire_logs_days = 10max_binlog_size = 100Mlog_slave_updates = 1auto-increment-increment = 2auto-increment-offset = 2
2.对于每个Linode节点,编辑bind-address
配置以使用私有IP地址:
bind-address = x.x.x.x
3.修改完配置文件后,重启MySQL应用:
sudo service mysql restart
1.在每台Linode节点上登录MySQL:
mysql -u root -p
2.在每台Linode节点上配置复制用户。将x.x.x.x
替换为另一台Linode节点的私有IP地址,并将password
修改为强密码:
GRANT REPLICATION SLAVE ON *.* TO 'replication'@'x.x.x.x' IDENTIFIED BY 'password';
3.返回终端,运行以下命令以测试配置。使用另一台Linode节点的私有IP地址:
mysql -ureplication -p -h x.x.x.x -P 3306
此时您应该可以通过以上命令连接到远程服务器的MySQL实例。
1.在第一台服务器上登录MySQL,查询主节点状态:
SHOW MASTER STATUS;
请注意显示的文件名和所在位置:
mysql> SHOW MASTER STATUS;+------------------+----------+--------------+------------------+| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |+------------------+----------+--------------+------------------+| mysql-bin.000001 | 277 | | |+------------------+----------+--------------+------------------+1 row in set (0.00 sec)
2.在第二台服务器上,根据MySQL的提示,为该数据库设置从属功能。将x.x.x.x
替换为第一台服务器的私有IP。还要将master_log_file
和master_log_pos
替换为上一步中对应的值:
SLAVE STOP;CHANGE MASTER TO master_host='x.x.x.x', master_port=3306, master_user='replication', master_password='password', master_log_file='mysql-bin.000001', master_log_pos=277;SLAVE START;
3.在第二台服务器上,查询主节点状态。注意文件名及其所在位置:
SHOW MASTER STATUS;
4.在第一台服务器上设置从属数据库状态,重复步骤2,并将需要修改的值替换为第一台服务器上相对应的值:
SLAVE STOP;CHANGE MASTER TO master_host='x.x.x.x', master_port=3306, master_user='replication', master_password='password', master_log_file='mysql-bin.000001', master_log_pos=277;SLAVE START;
5.在两台Linode节点上退出MySQL:
exit
两台Linode服务器均需要执行本章节的以下步骤。
注意
请将之后出现的
example.com
替换为您的域名。
1.输入以下命令禁用默认的Apache虚拟主机:
sudo a2dissite *default
2.切换至/var/www
目录:
cd /var/www
3.输入以下命令,创建用来保存网站的文件夹:
sudo mkdir example.com
4.在您刚刚创建的文件夹中创建一组文件夹,以存储您网站的文件、日志和备份:
sudo mkdir example.com/public_htmlsudo mkdir example.com/log
5.为网站创建虚拟主机文件:
/etc/apache2/sites-available/example.com.conf:
# domain: example.com# 域名: example.com# public: /var/www/example.com/public_html/# 网站根目录: /var/www/example.com/public_html/<VirtualHost *:80> # Admin email, Server Name (domain name), and any aliases # 管理员邮箱地址, 服务器名 (域名), 服务器别名 ServerAdmin webmaster@example.com ServerName www.example.com ServerAlias example.com # Index file and Document Root (where the public files are located) # 索引文件以及根目录 (网站页面文件存放位置) DirectoryIndex index.html index.php DocumentRoot /var/www/example.com/public_html # Log file locations # 日志文件存放位置 LogLevel warn ErrorLog /var/www/example.com/log/error.log CustomLog /var/www/example.com/log/access.log combined</VirtualHost>
警告
在Apache 2.4(Ubuntu 14.04使用的版本)及之后的版本中,虚拟主机文件必须以
.conf
扩展名作为结尾。.conf
扩展名在之前版本的Apache兼容。
6.输入以下命令启用新网站:
sudo a2ensite example.com.conf
7.重启Apache:
sudo service apache2 restart
1.在Linode主节点上,下载并安装最新版本的WordPress。请根据您的实际配置替换以下命令中列出的所有路径:
cd /var/wwwwget https://wordpress.org/latest.tar.gztar -xvf latest.tar.gzcp -R wordpress/* /var/www/example.com/public_html
2.配置MySQL数据库以安装WordPress。您需要将wordpressuser
和password
替换为您自己的设置:
mysql -u root -pCREATE DATABASE wordpress;GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpressuser'@'localhost' IDENTIFIED BY 'password';FLUSH PRIVILEGES;EXIT
3.设置网站根目录权限以确保WordPress能够完成其配置步骤:
chmod 777 /var/www/example.com/public_html/
4.使用Web浏览器访问您Linode的IP地址,并完成配置步骤以安装全功能的WordPress。
警告
为了确保每个WordPress实例都能定位到本地数据库,您需要将此步骤中的 Database Host(数据库主机)值设置为
localhost
。这也是WordPress的默认值。
5.通过WordPress管理界面中的 General Settings(常规设置)配置WordPress URL和网站地址,并确保在两个字段中都配置了您的域。
注意
完成WordPress安装步骤并首次登录后,应重置网站根目录的权限以确保安全。您可以使用以下命令来重置根目录权限:
chmod 755 /var/www/example.com/public_html/
6.完成WordPress安装步骤后,将配置文件复制到另一台Linode节点。将x.x.x.x
替换为另一台Linode节点的IP地址:
rsync -r /var/www/* x.x.x.x:/var/www/.
7.登录另一台Linode节点并重启Apache:
sudo service apache2 restart
1.在Linode集群主节点安装Lsyncd:
sudo apt-get install lsyncd
2.创建配置文件以执行同步操作。将x.x.x.x
替换为集群中另一台Linode节点的私有IP地址:
settings = { logfile = "/var/log/lsyncd.log", statusFile = "/var/log/lsyncd-status.log"}sync { default.rsyncssh, delete = false, insist source="/var/www", host="x.x.x.x", targetdir="/var/www", rsync = { archive = true, perms = true, owner = true, _extra = {"-a"}, }, delay = 5, maxProcesses = 4, ssh = { port = 22 }}
3.启动Lsyncd进程:
service lsyncd start
4.测试Lsyncd是否已经成功启动:
service lsyncd status
如果此命令返回的结果不是lsyncd is running
,请仔细检查lsyncd.conf.lua
配置文件,并确保RSA公钥位于从属服务器的正确位置。
5.通过在主Linode节点的/var/www
文件夹中创建文件来测试同步复制是否生效。几秒钟后您应该能够在从属Linode节点上的相同路径下看到该文件。
1.打开Linode管理界面中的NodeBalancer选项卡。
2.如果您之前尚未配置,请添加NodeBalancer,并确保它与后端Linode服务器位于同一数据中心。
3.选择您新添加的NodeBalancer并点击“Create Configuration”。按如下所示,编辑配置文件:
Port: 80Protocol: HTTPAlgorithm: Least ConnectionsSession Stickiness: TableHealth Check Type: HTTP Valid Status
4.点击“Save Changes”按钮后,系统将提示您添加Linode节点。为每台节点提供唯一的标签,并在每个节点的地址字段中输入私有网络地址和端口号。
5.添加完两个节点后,确保节点运行状况检查功能处于启用状态。等到两个节点都显示为启动状态,返回NodeBalancer主页并记录列出的IP地址。您现在应该能访问该IP地址并查看您的网站。
为了测试高可用性,可以在其中一个节点上停止Apache2/MySQL服务,或者关闭其中一个节点。即使其中一个节点被标记为关闭状态,您的网站仍可以继续提供服务而不会出现问题。
恭喜,您现在已经成功搭建了高可用的WordPress网站!
]]>参考文章
本指南将介绍如何在运行Ubuntu 16.04的服务器上安装图形桌面环境,以及如何使用VNC从本地计算机连接至该桌面。
sudo apt-get update && sudo apt-get upgrade
注意
本指南是为非root用户编写的,会在需要提升权限的命令之前加上
sudo
。如果您不熟悉sudo
命令,请参阅Linux用户和用户组指南。
1.Ubuntu的软件库中有多个可用的桌面环境。以下命令将会安装Ubuntu系统的默认桌面Unity,以及图形界面正常工作所需的依赖项:
sudo apt-get install ubuntu-desktop gnome-panel gnome-settings-daemon metacity nautilus gnome-terminal
注意
这将安装完整的Ubuntu桌面环境,包括办公软件和Web浏览器等工具。要只安装桌面而不安装这些软件包的话,请运行以下命令:
sudo apt-get install --no-install-recommends ubuntu-desktop gnome-panel gnome-settings-daemon metacity nautilus gnome-terminal
在安装过程中,系统会询问您是否将系统文件更新为新版本:
Configuration file '/etc/init/tty1.conf' ==> File on system created by you or by a script. ==> File also in package provided by package maintainer. What would you like to do about it ? Your options are: Y or I : install the package maintainer's version N or O : keep your currently-installed version D : show the differences between the versions Z : start a shell to examine the situation The default action is to keep your current version.*** tty1.conf (Y/I/N/O/D/Z) [default=N] ?
输入 y 或 回车 确认更新。
2.安装VNC服务器:
sudo apt-get install vnc4server
VNC服务器生成 display (图形输出)编号,该编号在服务器启动时定义。如果未定义display编号,服务器将使用最小的可用编号。VNC连接使用的端口号是5900 + display
。本指南将使用1作为display编号;因此,您将连接至远程的5901端口来使用VNC。
默认的VNC连接是非加密的。为了保护您密码和数据的安全,您需要借助SSH隧道将流量传输至本地端口。可以使用相同的本地端口来保持一致性。
1.在您的桌面环境下,通过以下命令连接至Linode。请务必将user@example.com
替换为您的用户名、Linode主机名或IP地址:
ssh -L 5901:127.0.0.1:5901 user@example.com
2.在您的Linode上启动VNC服务器并测试连接。系统将提示您设置密码:
vncserver :1
3.根据从您的桌面连接至VNC章节的步骤初始化连接。
1.打开PuTTY并导航至菜单中SSH
下的Tunnels
。按照下图所示新建一个转发端口,并将example.com
替换为您Linode的IP地址或主机名:
2.点击 Add,之后返回Session(会话)界面。输入您Linode的主机名或IP地址,以及会话的标题。点击 Save 保存设置以供将来使用,之后点击 Open 初始化SSH隧道。
3.启动VNC服务器并测试连接。系统将提示您设置密码:
vncserver :1
4.根据从您的桌面连接至VNC章节的步骤初始化连接。
在本章节中,您将使用VNC客户端或 查看器 连接至远程服务器。查看器是绘制VNC服务器生成的图形界面并在本地计算机输出显示的软件。
在OS X和Windows上有很多查看器的选择,本指南将使用RealVNC Viewer。
1.安装并打开VNC Viewer后,通过VNC客户端连接至本地主机。VNC服务器地址格式为localhost:#
,其中#
代表我们在保护VNC连接安全章节中使用的display编号:
2.系统会警告您连接未加密,但如果您已按照上述步骤确保了VNC连接的安全,则会话将安全的通过SSH隧道连接至您的Linode。点击 Continue 以继续:
3.系统将提示您输入首次启动VNC服务器时设定的密码。如果您尚未在Linode上启动VNC服务器,请参阅保护VNC连接安全章节。
连接后,您将看到一个空白的灰色屏幕,这是因为服务器的桌面进程尚未启动。在下一章节,我们将配置您的Linode以启动完整的桌面环境。
Ubuntu桌面环境下有多款可用的VNC客户端。您可以在这里找到可供Ubuntu使用的VNC客户端列表。本指南将使用Ubuntu默认安装的Remmina。
1.打开Remmina。
2.点击Create a new remote desktop profile
按钮,新建一个远程桌面配置文件。为您的配置文件命名,指定VNC协议,并在服务器字段中输入localhost:1
。服务器字段中的:1
和display编号相对应。在密码设置中填写您在保护VNC连接安全章节中设定的密码:
3.点击 Connect。
连接后,您将看到一个空白的灰色屏幕,这是因为服务器的桌面进程尚未启动。在下一章节,我们将配置您的Linode以启动完整的桌面环境。
本章节将配置VNC,使其在启动时启动完整的Unity桌面。
1.成功连接之后,再退出该连接。关闭VNC服务器:
vncserver -kill :1
2.根据以下配置编辑~/.vnc/xstartup
文件的末尾部分。这将在启动VNC服务器时以后台进程的方式启动桌面依赖项:
#!/bin/sh# 在常规模式下运行桌面时请取消掉以下两行的注释:# unset SESSION_MANAGER# exec /etc/X11/xinit/xinitrc[ -x /etc/vnc/xstartup ] && exec /etc/vnc/xstartup[ -r $HOME/.Xresources ] && xrdb $HOME/.Xresourcesxsetroot -solid greyvncconfig -iconic &x-terminal-emulator -geometry 80x24+10+10 -ls -title "$VNCDESKTOP Desktop" &x-window-manager &gnome-panel &gnome-settings-daemon &metacity &nautilus &
3.保存并退出文件。重新启动VNC会话:
vncserver :1
4.按照之前章节的相同步骤从您本地的VNC客户端连接至VNC服务器。现在您应该可以看见完整的Ubuntu桌面:
此部分是可选操作。请按以下步骤配置VNC服务器,使其在系统重启后可以自动启动。
1.启动您的crontab。如果您之前从未编辑过crontab配置文件,系统会提示您从可用的文本编辑器中选择一个对该文件进行编辑:
crontab -eno crontab for user - using an empty oneSelect an editor. To change later, run 'select-editor'. 1. /bin/ed 2. /bin/nano 3. /usr/bin/vim.basic 4. /usr/bin/vim.tinyChoose 1-4 [2]:
2.在文件的最后添加@reboot /usr/bin/vncserver :1
。您的crontab配置文件应该与以下内容类似:
# Edit this file to introduce tasks to be run by cron.## Each task to run has to be defined through a single line# indicating with different fields when the task will be run# and what command to run for the task## To define the time you can provide concrete values for# minute (m), hour (h), day of month (dom), month (mon),# and day of week (dow) or use '*' in these fields (for 'any').## Notice that tasks will be started based on the cron's system# daemon's notion of time and timezones.## Output of the crontab jobs (including errors) is sent through# email to the user the crontab file belongs to (unless redirected).## For example, you can run a backup of all your user accounts# at 5 a.m every week with:# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/## For more information see the manual pages of crontab(5) and cron(8)## m h dom mon dow command@reboot /usr/bin/vncserver :1
3.保存并退出文件。您可以通过重启Linode服务器并连接VNC服务器来验证上述配置是否生效。
]]>参考文章
UFW(Uncomplicated Firewall)是Arch Linux、Debian或Ubuntu中管理防火墙规则的前端工具。UFW通常在命令行环境下使用(尽管UFW也提供了图形界面),目的是让配置防火墙变得简单(或者说,没那么复杂)。
sudo
权限。请阅读文档中保护您的服务器安全章节部分,以创建标准用户账号,加强SSH访问并移除不必要的网络服务。请不要遵循 创建防火墙 章节的指引——本指南将介绍如何使用UFW来控制防火墙,这是iptables命令之外另一种控制防火墙的方法。Arch Linux
sudo pacman -Syu
Debian / Ubuntu
sudo apt-get update && sudo apt-get upgrade
UFW默认包含在Ubuntu中,但在Arch Linux及Debian中必须手动安装。Debian会自动启动UFW的systemd单元并使其在重启时启动,但Arch Linux并不会这样做。这与告诉UFW启用防火墙规则不同,因为通过systemd或upstart启动UFW只会告诉系统初始化程序启动UFW守护进程。
默认情况下,UFW的规则集为空——因此即使守护进程正在运行,也不会启用任何防火墙规则。启用防火墙规则集的内容将在页面下方进行介绍。
安装UFW:
sudo pacman -S ufw
启动并启用UFW的systemd单元:
sudo systemctl start ufwsudo systemctl enable ufw
安装UFW
sudo apt-get install ufw
大部分系统只需要一小部分端口为传入连接打开,其余所有端口均关闭。先从简单的基础规则开始,ufw default
命令可用来设置对传入和传出连接的默认响应。要想拒绝所有传入连接并允许所有传出连接,请运行:
sudo ufw default allow outgoingsudo ufw default deny incoming
ufw default
命令还允许使用reject
参数。
警告
除非有明确的允许规则,否则配置默认拒绝或拒绝规则可能会阻止您退出Linode。在应用默认拒绝或拒绝规则之前,请确保已按照以下部分为SSH和其他关键服务配置了允许规则。
可以通过两种方式添加防火墙规则:声明端口号或声明服务名称。
例如,要允许22端口上的传入和传出连接用于SSH,您可以运行:
sudo ufw allow ssh
您还可以运行:
sudo ufw allow 22
同样的,要拒绝某个端口上的流量(本例中为111端口),您只需运行:
sudo ufw deny 111
要进一步微调规则,您还可以允许基于TCP或UDP的数据包通过。以下命令将允许80端口上的TCP数据包通过:
sudo ufw allow 80/tcpsudo ufw allow http/tcp
而以下命令将允许1725端口上的UDP数据包通过:
sudo ufw allow 1725/ufw
除了仅通过指定端口来添加允许或拒绝规则之外,UFW还可让您允许/阻止来自指定IP地址、子网或特定IP地址/子网/端口组合的连接。
允许来自指定IP地址的连接:
sudo ufw allow from 123.45.67.89
允许来自指定子网的连接:
sudo ufw allow from 123.45.67.89/24
允许来自指定IP地址/端口组合的连接:
sudo ufw allow from 123.45.67.89 to any port 22 proto tcp
可根据您的实际需求删除proto tcp
参数或替换为proto udp
,并且如有需要,所有示例中的allow
都可替换为deny
。
要想删除规则,请在规则语句前添加delete
。如果您不在希望允许HTTP流量通过,则可运行:
sudo ufw delete allow 80
还可以通过指定服务名称来删除规则。
虽然可以通过命令行添加简单的规则,但有些时候也需要添加或删除更加高级或特定的防火墙规则。在运行通过终端输入的规则之前,UFW会首先运行before.rules
文件中的规则,该文件允许本地环回(loopback)、ping以及DHCP通过防火墙。要对这些规则添加修改,请编辑/etc/ufw/before.rules
文件。在相同目录下同样存在一个名为before6.rules
的文件用来对IPv6的规则进行配置。
同样的,还存在after.rule
和after6.rule
文件,用来添加在UFW运行从命令行输入的规则后需要添加的任何规则。
另一个配置文件位于/etc/default/ufw
。在该配置文件中可以禁用或启用IPv6,设置默认规则,还可以设置UFW来管理内置的防火墙链。
您可随时使用sudo ufw status
命令查看UFW的状态信息。这将以列表的形式打印出所有的规则信息,并显示UFW是否处于活跃状态:
sudo ufw statusStatus: To Action From-- ------ ----22 ALLOW Anywhere80/tcp ALLOW Anywhere443 ALLOW Anywhere22 (v6) ALLOW Anywhere (v6)80/tcp (v6) ALLOW Anywhere (v6)443 (v6) ALLOW Anywhere (v6)
根据您设置的规则,首次运行ufw status
可能会输出Status: inactive
。可通过以下命令启用UFW并强制执行防火墙规则:
sudo ufw enable active
同样的,可通过以下命令禁用防火墙规则:
sudo ufw disable
注意
系统重启后UFW服务仍会启动并运行。
您可使用以下命令启用UFW日志记录:
sudo ufw logging on
日志级别可通过sudo ufw logging low|medium|high
设置,low
、medium
、high
分别对应从低到高的级别,默认级别为low
。
一条正常的日志记录与以下内容类似,它位于/var/logs/ufw
:
Sep 16 15:08:14 <hostname> kernel: [UFW BLOCK] IN=eth0 OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:00:00 SRC=123.45.67.89 DST=987.65.43.21 LEN=40 TOS=0x00 PREC=0x00 TTL=249 ID=8475 PROTO=TCP SPT=48247 DPT=22 WINDOW=1024 RES=0x00 SYN URGP=0
最开始的值分别表示日期,时间以及您的主机名。其他重要的字段包括:
0
表示不需要]]>参考文章
Seafile有两个版本:免费的开源社区版和付费的专业版。虽然专业版最多可供3位用户免费使用,本教程还是将使用Seafile的社区版本,使用Nginx作为服务器提供HTTPS连接,后端使用MySQL数据库。
注意
本指南是为非root用户编写的,会在需要提升权限的命令之前加上
sudo
。如果您不熟悉sudo
命令,请参阅Linux用户和用户组指南。
升级系统:
apt update && apt upgrade
使用root权限创建标准用户账户。本例中,我们将创建一个名为 sfadmin 的用户:
adduser sfadminadduser sfadmin sudo
注销您已登录Linode的root账户,然后以 sfadmin 的身份重新登录:
exitssh sfadmin@<your_linode's_ip>
现在您应该已经以 sfadmin 的身份登录到您的Linode服务器。请参考保护您的服务器安全指南以提高SSH访问的安全性。
设置UFW防火墙规则。UFW是Ubuntu的防火墙控制器,它让设置防火墙规则变得更加简单。有关UFW的更多信息,请参阅使用UFW配置防火墙指南。使用以下命令允许SSH和HTTP(S)通过防火墙:
sudo ufw allow sshsudo ufw allow httpsudo ufw allow httpssudo ufw enable
之后检查防火墙规则的状态,并以标号列表的形式列出:
sudo ufw status numbered
输出应与下面的示例相似:
Status: activeTo Action From-- ------ ----[ 1] 22 ALLOW IN Anywhere[ 2] 80 ALLOW IN Anywhere[ 3] 443 ALLOW IN Anywhere[ 4] 22 (v6) ALLOW IN Anywhere (v6)[ 5] 80 (v6) ALLOW IN Anywhere (v6)[ 6] 443 (v6) ALLOW IN Anywhere (v6)
注意
如果不希望UFW在22端口上允许来自IPv4与IPv6的SSH连接,您可以删除对应的规则。例如,您可以运行
sudo ufw delete 4
命令来删除允许来自IPv6的SSH连接通过的规则。
设置Linode主机名,这里我们设置为 seafile :
sudo hostnamectl set-hostname seafile
在/etc/hosts
中添加新主机名。该文件的第二行应该类似下面的示例:
127.0.1.1 members.linode.com seafile
首次启动时,您的Linode服务器时区会被设置为UTC(Coordinated Universal Time,世界协调时间)。更改时区是可选项,如果您希望这么做,请运行以下命令:
sudo dpkg-reconfigure tzdata
安装程序将要求您为MySQL的root用户设置密码。请确保您安装的是mysql-server-5.7
,而不是mysql-server
。这是因为如果您通过mysql-server
包安装MySQL,一个来自上游的问题将导致MySQL服务在启动时出现错误。
sudo apt install mysql-server-5.7
运行 mysql_secure_installation 脚本:
sudo mysql_secure_installation
有关MySQL的更多信息,请参阅在Ubuntu上安装MySQL指南。
如果您还没有SSL/TLS证书,可以现在创建一个。这是一个自签名证书,并让Web浏览器拒绝未经认证的连接。您应该验证浏览器证书的SHA256指纹与服务器证书的SHA256指纹是否相同,并在浏览器中添加例外以永久信任该证书。
切换到证书文件存储的路径,并使用密钥创建服务器证书:
cd /etc/sslsudo openssl genrsa -out privkey.pem 4096sudo openssl req -new -x509 -key privkey.pem -out cacert.pem
通过Ubuntu的软件库安装Nginx:
sudo apt install nginx
创建站点配置文件。您唯一需要修改的一行是server_name
。有关HTTPS的更多配置选项,请参阅Nginx的TLS最佳实践指南。
server{ listen 80; server_name example.com; rewrite ^ https://$http_host$request_uri? permanent; proxy_set_header X-Forwarded-For $remote_addr; } server { listen 443 ssl http2; ssl on; ssl_certificate /etc/ssl/cacert.pem; ssl_certificate_key /etc/ssl/privkey.pem; server_name example.com; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY; ssl_session_cache shared:SSL:10m; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH !RC4"; ssl_prefer_server_ciphers on; fastcgi_param HTTPS on; fastcgi_param HTTP_SCHEME https; location / { fastcgi_pass 127.0.0.1:8000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_script_name; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; fastcgi_param REMOTE_ADDR $remote_addr; access_log /var/log/nginx/seahub.access.log; error_log /var/log/nginx/seahub.error.log; fastcgi_read_timeout 36000; client_max_body_size 0; } location /seafhttp { rewrite ^/seafhttp(.*)$ $1 break; proxy_pass http://127.0.0.1:8082; client_max_body_size 0; proxy_connect_timeout 36000s; proxy_read_timeout 36000s; proxy_send_timeout 36000s; send_timeout 36000s; proxy_request_buffering off; } location /media { root /home/sfadmin/sfroot/seafile-server-latest/seahub; } }
禁用默认的站点配置并启用刚刚创建的站点配置:
sudo rm /etc/nginx/sites-enabled/defaultsudo ln -s /etc/nginx/sites-available/seafile.conf /etc/nginx/sites-enabled/seafile.conf
运行Nginx配置测试并重启Web服务器。如果测试失败,终端会显示简要的错误描述信息,以便您能借此解决问题。
sudo nginx -tsudo systemctl restart nginx
Seafile手册建议使用特定的目录结构来简化日后的升级过程。在这里我们也会这样做,只不过我们把在sfadmin
家目录下创建的目录命名为sfroot
,而不是Seafile手册示例中的haiwen
。
mkdir ~/sfroot && cd ~/sfroot
下载Seafile CE Linux服务端安装文件的64位版本。您需要从Seafile官网获取对应的下载链接。取得下载URL后,使用wget
命令将其下载至~/sfadmin/sfroot
。
wget <link>
解压tarball,并将压缩包移动到installed
目录:
tar -xzvf seafile-server*.tar.gzmkdir installed && mv seafile-server*.tar.gz installed
安装Seafile的依赖包:
sudo apt install python2.7 libpython2.7 python-setuptools python-imaging python-ldap python-mysqldb python-memcache python-urllib3
运行安装脚本:
cd seafile-server-* && ./setup-seafile-mysql.sh
启动服务端程序。
./seafile.sh start./seahub.sh start-fastcgi
seahub.sh
脚本将创建用于登录Seafile的管理员用户账户。系统会要求您输入登录用的电子邮件账户并创建密码。
现在可以通过您Linode服务器的IP地址,或是之前在Nginx的seafile.conf
配置文件中设置的server_name
,在Web浏览器中访问Seafile。如之前所说,Nginx将重定向至HTTPS连接,由于您创建了自签名证书,因此您的浏览器将警告该HTTPS连接不是私有的。忽略浏览器警告并继续访问该网址,您将看到Seafile的登录界面。
seafile.sh
与seahub.sh
脚本并不会自动在您的Linode服务器重启后运行,需要我们手动进行设置。
创建systemd单元文件:
/etc/systemd/system/seafile.service
:
[Unit]Description=Seafile ServerAfter=network.target mysql.service[Service]Type=oneshotExecStart=/home/sfadmin/sfroot/seafile-server-latest/seafile.sh startExecStop=/home/sfadmin/sfroot/seafile-server-latest/seafile.sh stopRemainAfterExit=yesUser=sfadminGroup=sfadmin[Install]WantedBy=multi-user.target
/etc/systemd/system/seahub.service
:
[Unit]Description=Seafile HubAfter=network.target seafile.service[Service]Type=oneshotExecStart=/home/sfadmin/sfroot/seafile-server-latest/seahub.sh start-fastcgiExecStop=/home/sfadmin/sfroot/seafile-server-latest/seahub.sh stopRemainAfterExit=yesUser=sfadminGroup=sfadmin[Install]WantedBy=multi-user.target
之后启动服务:
sudo systemctl enable seafilesudo systemctl enable seahub
您可以使用以下命令验证服务是否成功启动:
sudo systemctl status seafilesudo systemctl status seahub
重新启动Linode服务器验证自启动脚本是否生效。服务器启动后,当运行上一步中的验证命令时,Seafile和Seahub都应处于活跃状态。同样的,此时您应该也可以在浏览器中访问Seafile服务。
有多种方法可供您升级Seafile。请参阅Seafile手册以了解最适合您需求的升级说明。
]]>参考文章
容器生态系统及 Docker 核心概念
大致看来,容器生态系统包含核心技术、平台技术和支持技术。
容器核心技术是指能够让 Container 在 host 上运行的那些技术:
容器不光是 Docker,还有其他容器,比如 CoreOS 的 rkt。为了保证容器生态的健康发展,保证不同容器之间能够兼容,包含 Docker、CoreOS、Google在内的若干公司共同成立了一个叫 Open Container Initiative(OCI) 的组织,其目的是制定开放的容器规范。
目前 OCI 发布了两个规范:
runtime 是容器真正运行的地方。runtime 需要跟操作系统 kernel 紧密协作,为容器提供运行环境。
目前主流的三种容器 runtime:
光有 runtime 还不够,用户得有工具来管理容器。容器管理工具对内与 runtime 交互,对外为用户提供 interface,比如 CLI。这就好比除了 JVM,还得提供 java
命令让用户能够启停应用。
容器定义工具允许用户定义容器的内容和属性,这样容器就能够被保存、共享和创建。
容器是通过 image 创建的,需要有一个仓库来统一存放 image,这个仓库就叫做 Registry。
容器 OS 是专门运行容器的操作系统。与常规 OS 相比,容器 OS 通常体积更小,启动更快。因为是为容器定制的 OS,通常它们运行容器的效率会更高。
目前已经存在不少容器 OS:
容器核心技术使得容器能够在单个 Host 上运行。而容器平台技术能够让容器作为集群在分布式环境中运行。
容器平台技术包括容器编排引擎、容器管理平台和基于容器的 PaaS。
基于容器的应用一般会采用微服务架构。在这种架构下,应用被划分为不同的组件,并以服务的形式运行在各自的容器中,通过 API 对外提供服务。为了保证应用的高可用,每个组件都可能会运行多个相同的容器。这些容器会组成集群,集群中的容器会根据业务需要被动态地创建、迁移和销毁。
所谓编排(orchestration),通常包括容器管理、调度、集群定义和服务发现等。通过容器编排引擎,容器被有机的组合成微服务应用,实现业务需求。
以上三者是当前主流的容器编排引擎。
容器管理平台是架构在容器编排引擎之上的一个更为通用的平台。通常容器管理平台能够支持多种编排引擎,抽象了编排引擎的底层实现细节,为用户提供更方便的功能,比如 application catalog 和一键应用部署等。
基于容器的 PaaS 为微服务应用开发人员和公司提供了开发、部署和管理应用的平台,使用户不必关心底层基础设施而专注于应用的开发。以下是开源容器 PaaS 的代表:
下面这些技术被用于支持基于容器的基础设施:
容器的出现使网络拓扑变得更加动态和复杂。用户需要专门的解决方案来管理容器与容器,容器与其他实体之间的连通性和隔离性。
docker network 是 Docker 原生的网络解决方案。除此之外,我们还可以采用第三方开源解决方案,例如 flannel、weave 和 calico。
动态变化是微服务应用的一大特点。当负载增加时,集群会自动创建新的容器;负载减小,多余的容器会被销毁。容器也会根据 host 的资源使用情况在不同 host 中迁移,容器的 IP 和端口也会随之发生变化。
在这种动态的环境下,必须要有一种机制让 client 能够知道如何访问容器提供的服务。这就是服务发现技术要完成的工作。
服务发现会保存容器集群中所有微服务最新的信息,比如 IP 和端口,并对外提供 API,提供服务查询功能。
etcd、consul 和 zookeeper 是服务发现的典型解决方案。
容器的动态特征对监控提出更多挑战。针对容器环境,已经涌现出很多监控工具和方案:
ps
/top
/stats
docker ps
/top
/stats
是 Docker 原生的命令行监控工具。除了命令行,Docker 也提供了 stats API,用户可以通过 HTTP 请求获取容器的状态信息。
sysdig、cAdvisor/Heapster 和 Weave Scope 是其他开源的容器监控方案。
容器经常会在不同的 host 之间迁移,如何保证持久化数据也能够动态迁移,是 Rex-Ray 这类数据管理工具提供的能力。
日志为问题排查和事件管理提供了重要依据。
logs
:Docker 原生的日志工具OpenSCAP 能够对容器镜像进行扫描,发现潜在的漏洞。
如图所示,由于所有的容器共享同一个 Host OS,这使得容器在体积上要比虚拟机小很多。
另外,启动容器不需要启动整个操作系统,所以容器部署和启动速度更快,开销更小,也更容易迁移。
Docker 的核心组件包括:
Docker 采用的是 C/S 架构。客户端向服务器发送请求,服务器负责构建、运行和分发容器。
客户端和服务器可以运行在同一个 Host 上,客户端也可以通过 Socket 或 REST API 与远程的服务器通信。
最常用的 Docker 客户端是 docker
命令。通过 docker
我们可以方便地在 Host 上构建和运行容器。
除了 docker 命令行工具,用户也可以通过 REST API 与服务器通信。
Docker daemon 是服务器组件,以 Linux 后台服务的方式运行。
Docker daemon 运行在 Docker host 上,负责创建、运行、监控容器,构建、存储镜像。
默认配置下,Docker daemon 只能响应来自本地 Host 的客户端请求。如果要允许远程客户端请求,需要在配置文件中打开 TCP 监听,步骤如下:
(1) 编辑配置文件 /etc/systemd/system/multi-user.target.wants/docker.service
,在环境变量 ExecStart
后面添加 -H tcp://0.0.0.0
,允许来自任意 IP 的客户端连接。
[Service]Type=notify# the default is not to use systemd for cgroups because the delegate issues still# exists and systemd currently does not support the cgroup feature set required# for containers run by dockerExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0ExecReload=/bin/kill -s HUP $MAINPID
注:以上配置文件 Ubuntu 18.04 为例,在其他操作系统中配置文件可能并不相同。
(2) 重启 Docker daemon
systemctl daemon-reloadsystemctl restart docker.service
(3) 假设服务器 IP 为 192.168.56.102
,客户端在命令行里加上 -H
参数,即可与远程服务器通信。
docker -H 192.168.56.102 infoContainers: 1 Running: 0 Paused: 0 Stopped: 1Images: 1Server Version: 18.06.1-ce...
可将 Docker 镜像看作只读模板,通过它可以创建 Docker 容器。
镜像有多种生成方法:
我们可以将镜像的内容和创建步骤描述在一个文本文件中,这个文件被称作 Dockerfile,通过执行 docker build <docker-file>
命令可以构建出 Docker 镜像。
Docker 容器就是 Docker 镜像的运行实例。用户可以通过 CLI(docker)或是 API 启动、停止、移动或删除容器。
可以这么认为,对于应用软件,镜像是软件生命周期的构建和打包阶段,而容器则是启动和运行阶段。
Registry 是存放 Docker 镜像的仓库,分私有和公有两种。
Docker Hub 是默认的 Registry,由 Docker 公司维护,上面有数以万计的镜像,用户可以自由下载和使用。
出于对速度或安全的考虑,用户也可以创建自己的私有 Registry。
docker pull
命令可以从 Registry 下载镜像docker run
命令则是先下载镜像(如果本地没有),然后再启动容器一个容器启动过程的示例如下:
docker run
命令另外,使用 docker images
命令可以查看本地已下载的镜像。
使用 docker ps
或 docker container ls
命令显示正在运行的容器及相关信息。
]]>参考文章
]]>参考文章
- rsync基础及基本使用 | 51CTO博客
- 译:Google的大规模集群管理工具Borg(一)——— 用户视角的Borg特性 | cnblogs
- 译:Google的大规模集群管理工具Borg(二)——— Borg架构 | cnblogs
- 全球发布!阿里云Serverless Kubernetes全球免费公测 | 阿里云
- 阿里混部技术最佳实践 | InfoQ
- 阿里混部技术最佳实践 | MySlide
- 解密阿里“双11”超级工程,混部技术亮了 | CSDN
- 阿里决战双11核心技术揭秘——混部调度助力云化战略再次突破 | 雷锋网
- 谷歌Borg论文阅读笔记(一)——分布式架构 | 云栖社区
- 谷歌Borg论文阅读笔记(二)——任务混部的解决 | 云栖社区
Java中关于字符串有三个相关的类:String,StringBuilder和StringBuffer,那么这三个类之间有什么区别呢?cnblogs博主酥风对此做了整理记录,摘抄过来仅供参考。
简单来说,这三个类之间的区别主要是在两个方面:运行速度和线程安全。
首先来看运行速度,StringBuilder > StringBuffer > String 。
String最慢的原因在于:String为字符串常量,即String对象一旦创建之后是不可以更改的,但后两者的对象是变量,是可以更改的。以下面的一段代码为例,
String str = "abc";System.out.println(str);str = str + "de";System.out.println(str);
如果运行这段代码会发现先输出abc
,然后又输出abcde
,好像是str
这个对象被更改了。但其实,这只是一种假象罢了。
JVM对于这几行代码是这样处理的:
str
,并把"abc"
赋值给str
str
str
的值和"de"
加起来再赋值给新的str
str
最后被JVM的GC机制回收因此,str
实际上并没有被改变,也就是前面提到的:String对象一旦创建之后就不可更改了。换言之,Java中对String对象的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。
而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。
另外,有时候我们会这样对字符串进行赋值,
String str = "abc" + "de";StringBuilder stringBuilder = new StringBuilder().append("abc").append("de");System.out.println(str);System.out.println(stringBuilder.toString());
这样输出的结果也是abcde
和abcde
,但是String的速度却比StringBuilder的反应速度快很多,这是因为第1行中的操作和String str = "abcde";
是完全一样的,所以会很快。如果写成下面的形式,
String str1 = "abc";String str2 = "de";String str = str1 + str2;
那么JVM就会像之前所提到的,不断的创建、回收对象来进行这个操作,速度就会很慢。
从线程的角度来看,StringBuilder是线程不安全的,而StringBuffer是线程安全的。
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized
关键字,所以可以保证线程是安全的;而StringBuilder的方法则没有synchronized
关键字,所以不能保证线程安全。
多线程的操作时,需要使用StringBuffer
单线程的情况下,建议使用StringBuilder,速度较快
]]>参考文章
str
是否为空有以下几种方法,str == null; // 1. 判断字符串对象是否实例化"".equals(str); // 2. 判断空字符串是否与被检验字符串相等str.length <= 0; // 3. 判断字符串长度是否大于0str.isEmpty(;) // 4. 调用String类型的isEmpty()方法
需要注意的是,
length
是属性,也是一般集合类对象都拥有的属性,取值为集合的大小;而length()
是方法,调用后取得集合的长度。""
是对象,所以null没有分配空间,而""
分配了空间。]]>参考文章
static TaskHomeActivity instance; // 在被finish掉的activity中定义instance = this; // 在被finish掉的activity的onCreate方法中定义TaskHomeActivity.instance.finish(); // 在其他Activity里调用
]]>参考文章
开源下载工具 Aria2 相关资料整理
Start.bat
:
@echo off & title Aria2aria2c.exe --conf-path=aria2.conf
Start.vbs
:
CreateObject("WScript.Shell").Run "aria2c.exe --conf-path=aria2.conf",0
Status.bat
:
@echo off & title Aria2 StatusTaskList /FI "IMAGENAME eq aria2c.exe" /FO LISTpause > nul
Restart.bat
:
Taskkill /F /IM aria2c.exestart Start.vbs
Stop.bat
:
@echo off & title Aria2 StopTaskkill /F /IM aria2c.exepause > nul
Boot.bat
:
@echo off & title Aria2 开机启动echo 1.将 Aria2 设为开机启动echo 2.取消 Aria2 开机启动set /p aria2= 请输入对应的序号:IF %aria2% EQU 1 (REG ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\ /v Aria2 /t REG_SZ /d %cd%\Start.vbs /f)IF %aria2% EQU 2 (REG DELETE HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\ /v Aria2 /f)pause > nul
- aria2 | Github
- aria2.github.io
- 下载工具系列——Aria2(几乎全能的下载神器)| Senraの小窝
- Aria2 + Web UI,迅雷倒下之后的替代品 | 白化火柴的博客
- PDM - 真·零配置的aria2下载器 | 小众软件
- 替代迅雷!小白都会用的免配置 Aria2 图形界面版免费开源下载软件 PDM | 异次元
- 「下载神器」aria2 懒人安装教程 | 小众软件
- Mac下使用Aria2实现迅雷离线和百度云下载 | 知乎
- Mac下使用Aria2实现迅雷离线和百度云下载 | ChenTalk
- Mac配置Aria2,高速下载百度云 | YALV的博客
- Mac 上使用百度网盘很烦躁?花点时间配置 aria2 吧 | 少数派
]]>
sudo
命令时终端报错:xxx is not in the sudoers file. This incident will be reported.
CentOS默认创建的用户并没有sudo
命令的执行权限,而且CentOS中也并不存在sudo
用户组。
不同于CentOS,Ubuntu在安装后默认创建的用户属于sudo
用户组,因此可以使用sudo
命令。
错误信息中提到的sudoers file位于/etc/sudoers
,root
用户使用visudo
命令可对其进行查看,最开始的文件介绍内容如下:
## Sudoers allows particular users to run various commands as## the root user, without needing the root password.#### Examples are provided at the bottom of the file for collections## of related commands, which can then be delegated out to particular## users or groups.## ## This file must be edited with the `visudo` command.
概括来说,sudoers
文件允许指定用户在运行命令时获取root权限而无需输入root密码。
根据最后一行,必须通过visudo
命令来对/etc/sudoers
文件进行编辑,该命令需要root权限。
sudoers
文件可对多种类型的命令权限及用户组权限进行授权,预设的命令包括网络管理、软件安装与管理、服务管理、数据库升级、存储管理、权限代理、进程管理、驱动管理等,预设的命令如下:
## Networking# Cmnd_Alias NETWORKING = /sbin/route, /sbin/ifconfig, /bin/ping, /sbin/dhclient, /usr/bin/net, /sbin/iptables, /usr/bin/rfcomm, /usr/bin/wvdial, /sbin/iwconfig, /sbin/mii-tool## Installation and management of software# Cmnd_Alias SOFTWARE = /bin/rom, /usr/bin/up2date, /usr/bin/yum## Services# Cmnd_Alias SERVICES = /sbin/service, /sbin/chkconfig## Updating the locate database# Cmnd_Alias LOCATE = /usr/bin/updatedb## Storage# Cmnd_Alias STORAGE = /sbin/fdisk, /sbin/sfdisk, /sbin/parted, /sbin/partprobe, /bin/mount, /bin/umount## Delegating permissions# Cmnd_Alias DELEGATING = /usr/sbin/visudo, /bin/chown, /bin/chmod, /bin/chgrp## Processes# Cmnd_Alias PROCESSES = /bin/nice, /bin/kill, /usr/bin/kill, /usr/bin/killall## Drivers# Cmnd_Alias DRIVERS = /sbin/modprobe
首先使用root
用户运行visudo
命令,打开/etc/sudoers
文件,找到如下所示的片段:
## Next comes the main part: which users can run what software on## which machines (the sudoers file can be shared between multiple systems).## Syntax:#### user MACHINE=COMMANDS#### The COMMANDS section may have other options added to it.#### Allow root to run any commands anywhereroot ALL=(ALL) ALL
以及
## Allow people in group wheel to run all commands# %wheel ALL=(ALL) ALL## Same thing without a password# %wheel ALL=(ALL) NOPASSWD: ALL
可知有两种方法可让指定用户获取sudo
权限。
查阅网上相关博客,大多是基于此方法。例如用户名为test
,直接给test
用户授权,只需在root
用户的授权定义下添加相同的授权定义,将用户名改为test
:
## Allow root to run any commands anywhereroot ALL=(ALL) ALL## Allow test to run any commands anywheretest ALL=(ALL) ALL
这种方法虽然简单却也比较极端:例如将test
用户删除后忘记删除sudoers
中的授权,之后再次新建同名账户的话,test
用户就直接获得了sudo
权限,存在安全隐患;而且每次新建用户后都需要再次添加授权定义,操作很麻烦,因此推荐使用下面的方法。
可以注意到,在sudoers
文件中可对wheel
用户组整体授权,因此可先将用户test
加入用户组wheel
中:
su - rootusermod -G wheel test
之后通过visudo
命令在sudoers
文件中对wheel
用户组进行授权,分为需要密码和无需密码两种方式,取消掉任意一种授权前面的注释即可:
## Allow people in group wheel to run all commands%wheel ALL=(ALL) ALL## Same thing without a password# %wheel ALL=(ALL) NOPASSWD: ALL
保存文件并退出,问题解决。
首先创建用户test
,并设置用户密码:
useradd testid testuid=502(test) gid=502(test) groups=502(test)groups testtest : testpasswd test
切换至test
用户,尝试运行sudo visudo
命令,提示无sudo
权限:
[root@centos ~]$ su - test[test@centos ~]$ sudo visudotest is not in the sudoers file. This incident will be reported.
切换回root
用户,将test
加入wheel
用户组,再次尝试使用test
用户运行sudo visudo
命令,成功执行,问题解决!
[test@centos ~]$ su - root[root@centos ~]$ usermod -G wheel test[root@centos ~]$ id testuid=502(test) gid=502(test) groups=502(test),10(wheel)[root@centos ~]$ groups testtest : test wheel [root@centos ~]$ su - test[test@centos ~]$ sudo visudo
]]>参考文章
# 使用ifconfig查看IP地址、广播地址和掩码等ifconfig <接口>
1. 通过命令修改(重启后失效)
# ifconfig 网口 [参数]ifconfig eth3 192.168.100.128 broadcast 192.168.100.255 netmask 255.255.255.0
2. 修改网络配置文件(重启后依然有效)
/etc/sysconfig/network/ifcfg-[网口]
ifup
命令,启动网口vi ifcfg-eth4ifup ifcfg-eth4
使用route
命令查询本机路由表
routeKernel IP routing tableDestination Gateway Genmask Flags Metric Ref Use Ifacedefault 172.16.0.1 0.0.0.0 UG 0 0 0 eth0172.16.0.0 * 255.255.240.0 U 0 0 0 eth0
Destination
表示目的网关或目的主机,如果值为default
,表示这是一条默认的路由Gateway
表示网关Genmask
表示网段掩码Flags
标记为U
,表示这条路由状态是UP
,该路由可用Flags
标记为G
,表示需要通过网关转发Flags
标记为H
,表示目的地址是一个主机Iface
表示该路由的网络出口1. 通过命令修改(重启后失效)
# route add [-net|-host] [netmask Nm] [gw Gw] [[dev] If]route add -net 192.168.101.0 netmask 255.255.255.0 dev eth3route add -host 192.168.101.100 dev eth1``
2. 修改路由配置文件(重启后依然有效)
/etc/sysconfig/network/routes
ping命令
# ping [参数] 目的地址ping -c 5 10.77.215.5
-c
:后接执行ping的次数traceroute命令
# traceroute <地址 or 主机名>traceroute 10.77.215.5
配置FTP服务
YaST是SUSE Linux中自带的图形化工具,用来设置软件、硬件系统和网络服务
yast
命令Services > Network Services(xinetd)
,启动vsftpd
服务/etc/vsftpd.conf
配置文件,取消下边列出的注释/etc/ftpusers
配置文件,在root前添加注释#
xinetd
服务:/etc/init.d/xinetd restart
ascii_upload_enable 上传权限ascii_download_enable 下载权限local_enable 本地系统用户FTP权限write_enable 用户写权限设置 anonymous_enable=NO设置 listen=NO
配置Telnet服务
YaST
命令Network Services > Network Services(xinetd)
,开启Telnet
服务vi
修改/etc/pam.d/login
,在auth required pam_securetty.so
前添加注释#
LVM是Logical Volume Manager的简写,是建立在硬盘和分区之间的逻辑层,用来提高磁盘分区管理的灵活性
LVM设计的主要目标是实现文件系统存储容量的可扩展性,使对容量的调整更为简易
LVM是通过交换PE的方式来达到弹性变更文件系统大小的功能
/dev/sda2
分区类似,是能够用来格式化的单位当对LV进行写入操作时,LVM定位相应的LE,通过PV头部的映射表将数据写入到相应的PE上
LV实现的关键在于PE与LE间建立的映射关系,不同的映射规则就决定了不同的LVM存储模型
fdisk
将System ID修改为LVM标记(8e)pvcreate
、pvdisplay
将Linux分区处理成物理卷PVvgcreate
、vgdisplay
将创建好的物理卷PV处理成卷组VGlvcreate
将卷组分成若干个逻辑卷LVmkfs
将LV格式化,最后通过fdisk
、mount
挂载格式化后的LV到文件系统命令 | 功能 |
---|---|
pvcreate | 创建物理卷 |
pvscan | 查看物理卷信息 |
pvdisplay | 查看各个物理卷的详细参数 |
pvremove | 删除物理卷 |
pvcreate
# 将普通的分区加上PV属性# 例如:将分区/dev/sda6创建为物理卷pvcreate /dev/sda6
pvremove
# 删除分区的PV属性# 例如:删除分区/dev/sda6的物理卷属性pvremove /dev/sda6
pvscan、pvdisplay
pvdisplay
更为详细命令 | 功能 |
---|---|
vgcreate | 创建卷组 |
vgscan | 查看卷组信息 |
vgdisplay | 查看卷组的详细参数 |
vgreduce | 缩小卷组,把物理卷从卷组中删除 |
vgextend | 扩展卷组,把某个物理卷添加到卷组中 |
vgremove | 删除卷组 |
命令 | 功能 |
---|---|
lvcreate | 创建逻辑卷 |
lvscan | 查看逻辑卷信息 |
lvdisplay | 查看逻辑卷的具体参数 |
lvextend | 增大逻辑卷大小 |
lvreduce | 减小逻辑卷大小 |
lvremove | 删除逻辑卷 |
1. 增大文件系统空间
vgextend
,lvextend
等命令增大LV的空间resize2fs
将逻辑卷容量增加2. 缩小文件系统空间
resize2fs
将逻辑卷容量减小lvreduce
等命令减小LV的空间]]>参考文章
4月16日,腾讯云与CODING宣布达成战略合作,共同发布以腾讯云云服务器为基础的国内第一款完全基于云端的IDE工具:Cloud Studio的beta版本。
有别于Heroku这样的PaaS云计算平台,根据两家在微信推送中的表述,Cloud Studio更接近于SaaS的概念——本质上是一款在线云端开发工具,减少用户安装IDE的成本,并与腾讯云IaaS/PaaS深度结合,从而提供代码编写、调试、上线一站式闭环体验。
Cloud Studio的前身正是CODING自主研发的Coding WebIDE,CODING的老用户应该会比较熟悉。在Cloud Studio的登录界面仍然保留了旧版WebIDE的访问入口提示,方便老用户继续访问。
值得注意的是,WebIDE的首页明确提到,其底层基于容器技术,可以让系统的预热时间从分钟级降到秒级,配置好的开发环境也可以快捷的保存与分享。
而源于Coding WebIDE的Cloud Studio同样采用了容器技术,这点可以在腾讯云发布的微信推送中得到印证,以下为部分内容摘抄。
“软件研发效率在不断提升,开发工具也需要同步更新迭代,这就对计算资源提出了更高要求。每台 Cloud Studio 的背后,都有腾讯云云服务器、容器服务等服务在提供计算支持,帮助用户升级开发模式、变更应用交付、重构数据管理方式,提速企业应用部署。依托腾讯云的强大弹性能力,还能够做到资源快速伸、容灾等。开发者使用Cloud Studio 时登录浏览器即可进行编程,提供完整的 Linux 环境,并且支持自定义域名指向、动态计算资源调整,可以完成各种应用的开发编译与部署。另外,每个 Cloud Studio 拥有独立的计算资源,支持多环境快速切换、协同编辑、全功能 Terminal 等功能。据悉,下一步,Cloud Studio 将开放调配资源、在线 Terminal 操作云资源等功能。”
话不多说,现在就来初探Cloud Studio吧~
Cloud Studio是由CODING和腾讯云共同提供的服务,自然需要我们注册这两家的账号。访问https://studio.coding.net,随即跳转至CODING账号登录界面,因为我之前就是CODING的用户,直接登录,进入下一步。
登录CODING账户之后,系统会首先检测是否已有云主机。首次登录可以申请30天的免费试用。按照官方的说法,到期之后可按低至9.9元/月的价格续费主机,可以说是很划算了。
该界面还有产品介绍和帮助文档的访问链接,正式进入Cloud Studio之前不妨先去逛一逛。
重点提及的功能包括多环境切换、协同编辑以及全功能Terminal,终端默认使用oh-my-zsh,好评~
回到正题,继续我们的Cloud Studio的体验之旅。
申请Free Trial试用后,系统会自动申请一台1核1GB,10G空间的腾讯云主机作为Cloud Studio的后端服务器,如果之前没有绑定腾讯云的账号,此时会跳转至腾讯云的授权页面,点击授权即可。如无意外,就会进入Cloud Studio的主界面中。
Cloud Studio有着广阔的使用场景。在其官方介绍中,将开发微信小程序作为示例场景进行展示。
另外Cloud Studio还支持协同编辑和聊天的功能,以官方介绍图为例。
而用户初次进入Cloud Studio会创建默认的workspace,也可以创建空项目或从CODING导入已有项目。可以看到IDE的风格和IntelliJ IDEA很相似。
Cloud Studio预设了包括Node.js、Jekyll、Hexo、PHP、Ruby、Java、Python、.Net、Machine Learning(是的,你没有看错)等多种开发环境,用户可在Environments选项卡中快速切换。
在General Setting中,可对界面显示语言及文件树隐藏文件进行设置。
在Appearance Setting中,可切换亮/暗主题,并设置代码高亮配色,默认为material
。
在Editor Setting中,可设置缩进风格与缩进宽度。
在Keymap Setting中,可设置快捷键风格,预设包括Default
、Sublime
、Vim
和Emacs
。
在Extension Setting中,可搜索并安装各类插件,目前插件数量十分有限,相信日后会逐渐提高数量与质量。
右上角的Environments选项卡中列出了腾讯云专用主机的公网IP地址及硬件参数,点击查看我的专用主机即可跳转至腾讯云主机列表。
点击该主机查看详细信息,发现其位于成都机房,剩余30天有效期。
返回Cloud Studio,继续体验之旅。
接下来通过Cloud Studio中的集成终端来对这台云主机一探究竟,可以看到配色还是比较舒服的。
使用df
及uname
命令,发现该云主机根目录挂载了40G存储空间,操作系统为Ubuntu 16.04.4 LTS。
点击终端右上角的图标,可以快速切换终端运行环境。使用htop
命令发现该云主机为1核CPU、内存1G。
由于用户未设置密码,使用su
命令可直接获取root权限。
可通过ifconfig
命令查看网卡信息,但与硬件相关的命令均无法调用。Java版本为1.8.0_161
,Python2版本为2.7.12
,Python3版本为3.5.2
。
体验完强大的Terminal之后,就来试跑一下官方提供的Demo吧~
在默认的Workspace中,CODING准备了Java、Python、PHP三种语言的小示例帮助用户体验Cloud Studio的基本功能。
Python 2的Demo功能很简单:获取当前时间与IP,hello.py
代码如下。
#!/usr/bin/env python# -*- coding: utf-8 -*-import socketimport timedef get_time(): return time.strftime('%Y-%m-%d',time.localtime(time.time()))def get_host_ip(): try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(('8.8.8.8', 80)) ip = s.getsockname()[0] finally: s.close() return ip print "您好,欢迎来到 Cloud Studio"print "当前时间是:" + get_time()print "您的IP是:" + get_host_ip()
进入python
目录,运行python hello.py
即可。
Python 3的Demo要更有趣一些:来自Github上的开源项目Cursed Snake,这是一个由borisuvarov开发、基于Python 3的控制台贪吃蛇游戏,snake.py
代码如下。
#!/usr/local/bin/python3# -*- coding: utf-8 -*-"""Simple Snake console game for Python 3.From https://github.com/borisuvarov/cursed_snakeUse it as introduction to curses module.Warning: curses module available only in Unix.On Windows use UniCurses (https://pypi.python.org/pypi/UniCurses).UniCurses is not installed by default."""import curses # https://docs.python.org/3/library/curses.htmlimport timeimport randomdef redraw(): # Redraws game field and it's content after every turn # win.erase() win.clear() draw_food() # Draws food on the game field draw_snake() # Draws snake draw_menu() win.refresh()def draw_menu(): win.addstr(0,0, "Score: " + str(len(snake) - 2) + " Press 'q' to quit", curses.color_pair(5))def draw_snake(): try: n = 0 # There can be only one head for pos in snake: # Snake is the list of [y, x], so we swap them below if n == 0: win.addstr(pos[1], pos[0], "@", curses.color_pair(ex_foodcolor)) # Draws head else: win.addstr(pos[1], pos[0], "#", curses.color_pair(ex_foodcolor)) # Draws segment of the body n += 1 except Exception as drawingerror: print(drawingerror, str(cols), str(rows))def draw_food(): for pos in food: win.addstr(pos[1], pos[0], "+", curses.color_pair(foodcolor))def drop_food(): x = random.randint(1, cols - 2) y = random.randint(1, rows - 2) for pos in snake: # Do not drop food on snake if pos == [x, y]: drop_food() food.append([x, y])def move_snake(): global snake # List global grow_snake # Boolean global cols, rows # Integers head = snake[0] # Head is the first element of "snake list" if not grow_snake: # Remove tail if food was not eaten on this turn snake.pop() else: # If food was eaten on this turn, we don't pop last item of list, grow_snake = False # but we restore default state of grow_snake if direction == DIR_UP: # Calculate the position of the head head = [head[0], head[1] - 1] # We will swap x and y in draw_snake() if head[1] == 0: head[1] = rows - 2 # Snake passes through the border elif direction == DIR_DOWN: head = [head[0], head[1] + 1] if head[1] == rows - 1: head[1] = 1 elif direction == DIR_LEFT: head = [head[0] - 1, head[1]] if head[0] == 0: head[0] = cols - 2 elif direction == DIR_RIGHT: head = [head[0] + 1, head[1]] if head[0] == cols - 1: head[0] = 1 snake.insert(0, head) # Insert new headdef is_food_collision(): for pos in food: if pos == snake[0]: food.remove(pos) global foodcolor global ex_foodcolor ex_foodcolor = foodcolor foodcolor = random.randint(1, 6) # Pick random color of the next food return True return Falsedef game_over(): global is_game_over is_game_over = True win.erase() win.addstr(10, 20, "Game over! Your score is " + str(len(snake)) + " Press 'q' to quit", curses.color_pair(1))def is_suicide(): # If snake collides with itself, game is over for i in range(1, len(snake)): if snake[i] == snake[0]: return True return Falsedef end_game(): curses.nocbreak() win.keypad(0) curses.echo() curses.endwin()# Initialisation starts --------------------------------------------DIR_UP = 0 # Snake's directions, values are not important,DIR_RIGHT = 1 # they сan be "a", "b", "c", "d" or something elseDIR_DOWN = 2DIR_LEFT = 3is_game_over = Falsegrow_snake = Falsesnake = [[10, 5], [9, 5]] # Set snake size and positiondirection = DIR_RIGHTfood = []foodcolor = 2ex_foodcolor = 3win = curses.initscr() # Game field in console initialised with curses modulecurses.start_color() # Enables colorscurses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK)curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_BLACK)curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK)curses.init_pair(4, curses.COLOR_MAGENTA, curses.COLOR_BLACK)curses.init_pair(5, curses.COLOR_RED, curses.COLOR_BLACK)curses.init_pair(6, curses.COLOR_YELLOW, curses.COLOR_BLACK)win.keypad(1) # Enable arrow keyswin.nodelay(1) # Do not wait for keypresscurses.curs_set(0) # Hide cursorcurses.cbreak() # Read keys instantaneouslycurses.noecho() # Do not print stuff when keys are pressedrows, cols = win.getmaxyx() # Get terminal window size# Initialisation ends ---------------------------------------------# Main loop starts ------------------------------------------------drop_food()redraw()while True: if is_game_over is False: redraw() key = win.getch() # Returns a key, if pressed time.sleep(0.1) # Speed of the game if key != -1: # win.getch returns -1 if no key is pressed if key == curses.KEY_UP: if direction != DIR_DOWN: # Snake can't go up if she goes down direction = DIR_UP elif key == curses.KEY_RIGHT: if direction != DIR_LEFT: direction = DIR_RIGHT elif key == curses.KEY_DOWN: if direction != DIR_UP: direction = DIR_DOWN elif key == curses.KEY_LEFT: if direction != DIR_RIGHT: direction = DIR_LEFT elif chr(key) == "q": break if is_game_over is False: move_snake() if is_suicide(): game_over() if is_food_collision(): drop_food() grow_snake = Trueend_game()# Main loop ends --------------------------------------------------
真的可以玩哦!不过貌似在Cloud Studio上有延时(毕竟要与服务器交互),感兴趣的不妨在本地跑一跑哈哈~
一个很简单的PHP Web Demo,配合Cloud Studio中的Access URL选项卡使用,可将来自公网的访问重定向至云主机PHP Web Server的监听端口。这里提示找不到favico.ico
,页面图标无法加载,公网通过重定向链接可访问PHP服务。
官方提供的Java Demo是一个基于Maven构建的Spring Boot项目,StudioDemoApplication.java
代码如下。
package com.coding.studiodemo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.stereotype.Controller;import org.springframework.ui.ModelMap;@SpringBootApplication@Controllerpublic class StudioDemoApplication { @RequestMapping("/") public String greeting(ModelMap map) { String jreVersion = System.getProperty("java.specification.version"); map.addAttribute("jreVersion", "v" + jreVersion); return "index"; } public static void main(String[] args) { SpringApplication.run(StudioDemoApplication.class, args); }}
配置文件pom.xml
代码如下
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.coding</groupId> <artifactId>studio-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>studio-demo</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>
查看Maven版本为3.3.9
,直接运行mvn spring-boot:run
启动服务,由于是第一次运行,需要等待一段时间来下载依赖。
依赖下载完成后,服务启动成功,创建Access URL供公网访问。
最后访问该链接,成功访问Java Web Demo Page,Cloud Studio初体验结束~
和传统的云主机相比,基于容器技术的Cloud Studio更加轻量快捷,可视化IDE加持大大提升了开发效率,应用场景也更有针对性。如果只是希望在预搭建的环境中跑一些服务或进行一些实验,Cloud Studio会是一个不错的选择。
但是,Free Trial版本中Access URL的有效期仅为1个小时(解除有效期限制须升级CODING钻石会员),并且无法通过公网IP访问腾讯云专用主机,因此如果需要在公网中提供服务又对图形界面没有太大执念的话,各家的云主机仍是开发的第一选择。
]]>参考文章
虽然设置自动登录方便了很多,但也存在极大的安全隐患,不需要密码就可以进入你的系统,想想就是件可怕的事。还请务必根据个人需要谨慎开启。
首先按下Win + R
组合键,输入netplwiz
或control userpasswords2
并运行,打开用户账户界面。
之后取消勾选要使用本计算机,用户必须输入用户名和密码,并点击应用。
在弹出窗口中输入选中用户的密码并保存,这样就设置完成了。
另外,有用户在设置自动登录后,遇到开机提示用户名或密码不正确、点击OK后出现两个相同账户的情况,
出现上述问题的原因可能是你在设置了开机自动登录之后,又更改了计算机的名称。此时需要以任意用户身份登录系统,取消自动登录后再重新设置,问题即可解决。
]]>参考文章
Updating…
公共DNS:
1.1.1.1
- Cloudflare8.8.8.8
- Google9.9.9.9
- IBM参考文章
《死亡诗社》引用了多首诗作推动剧情发展,美国著名诗人沃尔特·惠特曼(Walt Whitman)的代表作《O Captain! My Captain!》便是其中之一。虽然影片中只引用了诗名,但却丝毫不妨碍其贯穿全片,见证着Keating对学生们春风化雨般的教育以及润物无声的人格影响。下面就来一起欣赏这首诗。
O Captain! my Captain! our fearful trip is done,
啊,船长,船长!我的船长!可怕的航程已完成,The ship has weather’d every rack, the prize we sought is won,
这船历尽风险,企求的目标已完成,The port is near, the bells I hear, the people all exulting,
港口在望,钟声响,人们在欢欣,While follow eyes the steady keel, the vessel grim and daring;
千万双眼镜注视着船——平稳,勇敢,坚定;But O heart! heart! heart!
但是痛心啊!痛心!痛心!O the bleeding drops of red,
瞧一滴滴鲜红的血,Where on the deck my Captain lies,
甲板上躺着我的船长,Fallen cold and dead.
他倒下去,冰冷,永别。O Captain! my Captain! rise up and hear the bells;
啊,船长!我的船长!起来吧,倾听钟声;Rise up—for you the flag is flung—for you the bugle trills,
起来吧,号角为您长鸣,旌旗为您高悬;For you bouquets and ribbon’d wreaths—for you the shores a-crowding,
迎着您,多少花束花圈——候着您,千万人蜂拥岸边,For you they call, the swaying mass, their eager faces turning;
他们向您高呼,拥来挤去,扬起殷切的脸;Here Captain! dear father!
啊,船长!亲爱的父亲!This arm beneath your head!
我的手臂托着您的头!It is some dream that on the deck,
莫非是一场梦——在甲板上,You’ve fallen cold and dead.
您倒下去,冰冷,永别。My Captain does not answer, his lips are pale and still,
我的船长不做声,嘴唇惨白,毫不动弹,My father does not feel my arm, he has no pulse nor will,
我的父亲没感觉到我的手臂,没有脉搏,没有遗愿,The ship is anchor’d safe and sound, its voyage closed and done,
船舶抛锚停下,平安抵达,航程终了,From fearful trip the victor ship comes in with object won;
历经艰险返航,夺得胜利目标;Exult O shores, and ring O bells!
啊,岸上钟声齐鸣,啊,人们一片欢腾!But I with mournful tread,
但是,我在甲板上,在船长身旁,Walk the deck my Captain lies,
心悲切,步履沉重,Fallen cold and dead.
因为他倒下去,冰冷,永别。
沃尔特·惠特曼(Walt Whitman,1819年5月31日—1892年3月26日)出生于纽约州长岛,美国著名诗人、人文主义者,创造了诗歌的自由体(Free Verse),其代表作品是诗集《草叶集》(Leaves of Grass)。
《O Captain! My Captain!》是沃尔特·惠特曼于1865年林肯(Lincoln)总统遇刺之后写下的一首隐喻诗,也可被归为纪念林肯总统的挽歌。船长即象征遭暗杀去世的林肯,而船则象征着刚刚结束南北战争的美国。
这首诗在1867年惠特曼的代表诗集《草叶集》第四次发行时被收录其中,表达了作者强烈的悲痛与哀伤以及美国民众对于林肯遇刺的震惊与伤感的心情。
《O Captain! My Captain!》在影片中的首次出现,始于Keating老师的第一堂课:Keating将学生们带到了陈列室,提到了惠特曼的诗作,并告诉学生可以称呼他为O Captain! My Captain!。此外,Keating还教给学生们怎样去聆听逝者的声音——Carpe Diem,及时行乐,不负芳华。
此后,这个称呼就自然而然的贯穿于学生们与Keating之间的对话中。而O Captain! My Captain!的最后一次出现,则是在片尾Keating离开学校的一幕——也是全片最为感人的场景。
即使是在Neil开枪自杀后,校方和Neil的父亲也并没有意识到自己的过错。他们将一切罪责都归咎于Keating的教育方式,逼迫学生在指认Keating的文件上签名,Keating成了整件事的替罪羊。
临别之际,在重新回归教条死板的课堂上,总是畏惧发声的Todd不再沉默——就像Keating曾经教过的那样,他站在课桌上高呼”O Captain! My Captain!”,向自己的人生导师致以最崇高的敬意。而古诗人社成员集体站在书桌上高呼”O Captain! My Captain!”的这一幕,必将成为电影史上的经典画面。
2014年8月11日,罗宾·威廉姆斯永远离开了我们。
他是《死亡诗社》中的Keating老师,《心灵捕手》中的Sean医生,一个全情投入的伟大演员,一个无与伦比的喜剧天才。
在2014年8月被证实在家中自杀去世后,Robin的粉丝在社交媒体上通过上传模仿《死亡诗社》中”O Captain! My Captain!”的照片或视频的方式,来向The Captain致敬。
《死亡诗社》的结局是开放的——也许学生们会因为站上桌子被罚,也许“地狱学院”的教育会一如既往,也许会有第二个表演天赋超群却不被家庭支持的Neil,也许Dead Poets Society从此成为禁忌而被逐渐遗忘……
但对Keating来说,有一件事是确定的——他会永远记得离开学校的那天,书桌上站立的学生们异口同声说出的那句:
O Captain! My Captain!
参考文章
一个Shell脚本是一个文本文件,包含一个或多个命令。作为系统管理员,我们经常需要使用多个命令来完成一项任务,我们可以在一个文本文件(Shell)脚本中添加所有这些命令来完成日常工作任务。
在Linux操作系统中,/bin/bash
是默认登录Shell,在创建用户时分配。使用chsh
命令可以改变默认的Shell,如下所示:
chsh <用户名> -s <新shell>chsh abelsu7 -s /bin/sh
在Shell脚本中,我们可以使用两种类型的变量:系统定义变量和用户定义变量。
系统变量是由系统自己创建的,这些变量通常由大写字母组成,可通过set
命令查看。
用户变量是由系统用户生成并定义的,变量的值可以通过命令echo $<变量名>
查看。
这里有两个方法可以实现:
# 方法一 2>&1ls /usr/share/doc > out.txt 2>&1# 方法二 &>ls /usr/share/doc &> out.txt
基础语法如下:
if [ 条件 ] then 命令1 命令2 ...else if [ 条件 ] then 命令1 命令2 ... else 命令1 命令2 ... fifi
在编写Shell脚本时,如果你想要检查上一条命令是否执行成功,在if
条件中使用$?
可以来检查上一条命令的结束状态。简单的例子如下:
ls /usr/bin/shuf/usr/bin/shufecho $?0
如果结束状态是0
,说明上一条命令执行成功。
ls /usr/bin/sharels: cannot access /usr/bin/share: No such file or directoryecho $?2
如果结束状态不是0
,说明执行失败。
在if-then
中使用测试命令如-gt
来比较两个数字:
#!/bin/bashx=10y=20if [ $x -gt $y ] then echo "x is greater than y" else echo "y is greater than x"fi
break
命令一个简单的用途是退出执行中的循环,我们可以在while
和until
循环中使用break
命令跳出循环。
continue
命令不同于break
命令,它只跳出当前循环的迭代,而不是整个循环。continue
命令很多时候是非常有用的——例如当有错误发生,但我们依然希望执行上层循环时。
基础语法如下:
case 变量 in 值1) 命令1 命令2 ... 最后命令 !! 值2) 命令1 命令2 ... 最后命令 ;;esac
基础语法如下:
for 变量 in 循环列表 do 命令1 命令2 ... 最后命令done
如同for
循环,while
循环只要条件成立就会重复执行命令块。不同于for
循环的是,while
循环会不断迭代,直到循环条件不为真。
while [ 条件 ] do 命令...done
例如:
COUNTER=0while [ $COUNTER -lt 5 ]do COUNTER='expr $COUNTER+1' echo $COUNTERdone
do-while
语句类似于while
语句,但检查条件语句之前先执行命令(意即至少执行一次):
do { 命令} while (条件)
使用chmod
命令为赋予脚本可执行权限:
chmod a+x myscript.sh
#!/bin/bash
是Shell脚本的第一行,称为释伴(shebang)行。这里#
符号叫做hash,而!
叫做bang。它的意思是命令通过/bin/bash
来执行。
使用-x
参数可以调试Shell脚本:
sh -x myscript.sh
另一种方法是使用-nv
参数:
sh -nv myscript.sh
test
命令可以用来比较字符串,测试命令会通过比较字符串中的每一个字符来进行比较。
下表列出了bash
中为命令行设置的特殊变量:
内建变量 | 解释 |
---|---|
$0 | 命令行中脚本名称 |
$1 | 第一个命令行参数 |
$2 | 第二个命令行参数 |
… | … |
$9 | 第九个命令行参数 |
$# | 命令行参数的数量 |
$* | 所有命令行参数,以空格隔开 |
test
命令也可以用来测试文件。下表列出了基础用法:
test | 用法 |
---|---|
-e <文件名> | 如果文件存在,返回true |
-d <文件名> | 如果文件存在并且是目录,返回true |
-f <文件名> | 如果文件存在并且是普通文件,返回true |
-s <文件名> | 如果文件存在并且不为空,返回true |
-r <文件名> | 如果文件存在并可读,返回true |
-w <文件名> | 如果文件存在并可写,返回true |
-x <文件名> | 如果文件存在并可执行,返回true |
注释可以用来描述一个脚本可以做什么和它是如何工作的,每一行注释以#
开头:
#!/bin/bashecho "I am logged in as $USER" # This is a command
read
命令可以读取来自终端(使用键盘输入)的数据。read
命令得到用户的输入并置于给定的变量中:
vi /tmp/test.sh#!/bin/bashecho "Please enter your name"read nameecho "My Name is $name"./test.shPlease enter your nameabelsu7My Name is abelsu7
unset
命令可用于取消变量或取消变量赋值:
unset <变量名>
可以使用expr
命令:
expr 16 + 420
或使用$[ 表达式 ]
:
test = $[ 16 + 4 ]echo $test20
在Shell中可以通过以下两种语法来定义函数,分别如下:
function_name (){ statement1 statement2 .... statementn}
或者
function function_name(){ statement1 statement2 .... statementn}
当函数定义好了以后,用户就可以通过函数名来调用该函数,函数调用的基本语法如下:
function_name param1 param2 ...
下面定义了一个sayhello()
方法,并调用:
#!/bin/bashfunction sayhello(){ echo "Hello,World"}sayhello
代码调用结果
sh ./hello.shHello,World
在Shell脚本中使用bc
的语法如下:
var = `echo "options; expression" | bc`
例如下面计算1+2+3+...+10
的计算结果
echo $(seq -s "+" 10)=`seq -s "+" 10 | bc`1+2+3+4+5+6+7+8+9+10=55
]]>参考文章
词云就是一种常见的数据可视化格式,它将词语彼此排列堆叠,以词云(又称标签云)的形式将数据直观的呈现给用户。作为开发者,在日常也会有大量制作词云图片的需求。
目前业界已经有ECharts和wordcloud2.js两大利器支持词云组件的编写。前者是百度出品的可视化图表库,词云只是其中的一类图表,相信大部分开发者已经体验过Echarts的魅力,本文不再赘述。今天就来尝试一下专注于词云的wordcloud2.js。
首先页面必须是以HTML5规范编写。以下是在VSCode中以Emmet语句html:5
展开得到的页面示例
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title></head><body></body></html>
随后需要引入jQuery和wordcloud2.js
<script src="https://cdn.bootcss.com/wordcloud2.js/1.1.0/wordcloud2.js"></script><script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
在body
中定义一个canvas
容器用来显示词云
<div id="canvas-container" align="center"> <canvas id="canvas" width="600px" height="400px"></canvas></div>
wordcloud2.js提供了验证是否可被当前浏览器支持的API。若当前浏览器不支持运行,则以下语句将返回false
> WordCloud.isSupportedtrue
具体API可参考wordcloud2.js官方文档
<script> var options = eval({ "list": [ ['Google', 10], ['Tencent', 9], ['Alibaba', 7], ['Baidu', 6], ['NetEase', 4], ['JD', 5], ['Youku', 4], ['Meituan', 3], ['Douban', 3] ], "gridSize": 16, // size of the grid in pixels "weightFactor": 10, // number to multiply for size of each word in the list "fontWeight": 'normal', // 'normal', 'bold' or a callback "fontFamily": 'Times, serif', // font to use "color": 'random-light', // 'random-dark' or 'random-light' "backgroundColor": '#333', // the color of canvas "rotateRatio": 1 // probability for the word to rotate. 1 means always rotate }); var canvas = document.getElementById('canvas'); WordCloud(canvas, options);</script>
至此,一个美观大方的词云就制作完成了。Just enjoy it!
]]>参考文章
目前在
Windows 10
环境下,迅雷极速版最好用的版本是1.0.35.366
,可以在迅雷极速版吧找到下载地址。
当然,迅雷9这种居然以浏览器为主打功能设计的产品,打死都不会再去用。好在可以通过修改hosts
文件的方法绕过迅雷的解析服务器,继续使用极速版。
首先编辑C:\Windows\System32\drivers\etc\hosts
,在其中添加规则:
# ThunderSpeed DNS Verification Cheat 127.0.0.1 hub5btmain.sandai.net127.0.0.1 hub5emu.sandai.net127.0.0.1 upgrade.xl9.xunlei.com
保存并关闭hosts
文件,退出迅雷极速版并清掉后台残余进程。
最后使用快捷键win+R
并输入cmd
回车,在命令行中执行ipconfig /flushdns
刷新DNS解析缓存,提示成功后重新打开迅雷,问题解决!
]]>参考文章
atom.xml
文件均会报错:原因在于内容中存在不完整的UTF-8
字符,导致XML
解析报错。
为了防止再次出现编码问题,应当避免从Word中直接复制内容到文件中
根据错误信息中的行列号定位到有问题的内容位置,重新以UTF-8
编码格式输入,再次部署,问题得以解决。
]]>参考文章
- Fix WordPress Feed Error - Input is not proper UTF-8, indicate encoding | WPTF
- WordPress RSS Feed Error: Input is not proper UTF-8, indicate encoding | Shout Me Loud
- RSS出现“Input is not proper UTF-8, indicate encoding !”的解决方法 | 枫芸志
- wordpress的RSS提示错误:Input is not proper UTF-8, indicate encoding | Jianchihu
- RSS报错“Input is not proper UTF-8, indicate encoding !”的解决方法 | 搞么子
- xml 浏览器打开报错Input is not proper UTF-8, indicate encoding ! | CSDN
这里是VS Code的相关介绍,VS Code快捷键文档
Keys | Function |
---|---|
Ctrl+Shift+P, F1 | 命令面板 |
Ctrl+P | 快速打开,转到文件 |
Alt+Up/Down | 移动当前行 |
Shift+Alt+Up/Down | 复制当前行 |
Ctrl+Shift+K | 删除当前行 |
Ctrl+Shift+L | Select all occurrences of current selection |
RSS(Really Simple Syndication)是一种描述和同步网站内容的格式,是使用最广泛的XML应用。
RSS目前广泛用于网上新闻频道,博客和Wiki,主要的版本有
0.91
,1.0
,2.0
。使用RSS订阅能更快地获取信息,网络用户可以在客户端借助于支持RSS的聚合工具软件,在不打开网站内容页面的情况下阅读支持RSS输出的网站内容。
下面将介绍如何安装针对Hexo博客的RSS插件,并借助生成atom.xml
文件提供RSS订阅服务。
Atom是一种订阅网志的格式,一种Web feed,与RSS类似但有更大的弹性。值得一提的是,Blogger和Gmail这两个由Google提供的服务都在使用Atom。
首先打开终端,进入本地Hexo根目录,通过npm
安装RSS插件:
npm install hexo-generator-feed
编辑本地Hexo根目录下的_config.yml
文件,添加以下配置:
# Extensions## Plugins: https://hexo.io/plugins/# RSS订阅plugin: - hexo-generator-feed# Feed Atomfeed: type: atom path: atom.xml limit: 20
在重新执行hexo generate
命令渲染Markdown
文件后,根目录下的public/
目录中已经生成了所需的atom.xml
文件。虽然文件已经存在,还需要在页面上添加订阅RSS的按钮。
不同的Hexo主题对RSS的实现方式不尽相同。博主采用的主题是Indigo
,可编辑主题目录下的_config.yml
文件,添加新菜单项,链接设为/atom.xml
即可(对应你atom.xml
文件的绝对路径 )。如希望在新页面中打开链接,则需要将target
属性设置为_blank
:
# 添加新菜单项遵循以下规则# menu:# link: fontawesome图标,省略前缀,本主题前缀为 icon-,必须# text: About 菜单显示的文字,如果省略即默认与图标一致,首字母会转大写# url: /about 链接,绝对或相对路径,必须。# target: _blank 是否跳出,省略则在当前页面打开menu: rss: text: RSS url: /atom.xml target: _blank
最后,清空已有的静态文件,重新渲染Markdown
文件,部署到服务器或pages
服务上,大功告成!
hexo cleanhexo generate # or hexo ghexo deploy # or hexo d
]]>
Atwood’s Law: “Any application that can be written in JavaScript, will be written in JavaScript.”
著名的Atwood’s Law是Jeff Atwood在2007年提出的。通俗来说就是:任何可以使用JavaScript来实现的应用,最终都会使用JavaScript来实现。
这句在程序员之间广为流传的经典名言最初翻译自Bruce Eckel的一句话
Bruce Eckel: “Life is short, you need Python.”
但 人生苦短,我用Python 这句中文版见于大牛Guido Van Rossum穿的T恤上印着的话,有待查证:
Keep it simple, stupid.
Do one thing, and do it well.
Linus: “Talk is cheap. Show me the code.”
来自LKML
]]>参考文章
#!
这个符号并不陌生,尤其是在初学bash
的时候,第一行一定要加上#!/bin/bash
待更新…
]]>参考文章
以太坊是一个开源的有智能合约功能的公共区块链平台,通过提供专用加密货币以太币(Ether)以及去中心化的以太坊虚拟机(EVM)来处理点对点合约。
以太坊是一个可编程的区块链,它并没有给用户提供一组预定义的操作(如比特币交易),而是允许用户创建他们自己的操作,这些操作可以任意复杂。这样一来,以太坊就成为了多种不同类型去中心化区块链的平台,包括且不仅限于密码学货币。
以太坊平台对底层区块链技术进行了封装,让区块链应用开发者可以直接基于以太坊平台进行开发。这样一来开发者只需要专注于应用本身的开发,从而大大降低了开发的难度。
狭义上来说,以太坊是一套可以实现分布式应用的平台协议,它的核心是可以执行任意复杂度算法的以太坊虚拟机(EVM)。
和其他区块链一样,以太坊也包含一套P2P协议。以太坊区块链数据由网络上的节点进行维护和更新,网络上的每一个节点都运行着EVM并执行相同的指令。因此,以太坊经常被描述为“世界电脑”。
以太坊网络中大规模的并行计算并不是为了计算更高效——事实上,这个过程让计算速度比传统的计算更为低效。另一方面,以太坊中每一个节点都运行着EVM的目的是保持整个区块链的共识。分散式的共识机制给予了以太坊极高的容错能力,确保零宕机,并且使存储于区块链的数据永远不可更改并且可被审查。
以太坊本质上就是一个保存数字交易永久记录的公共数据库。重要的是,这个数据库不需要任何中央权威机构来维持和保护,这是一个个体在不需要信任任何第三方或对方的情况下进行P2P交易的架构。
以太坊上的程序 被称为 智能合约,它是 代码和数据(状态)的集合。
比特币的交易是可以编程的,但是比特币脚本有很多的限制,能够编写的程序也有限。而以太坊则是图灵完备的,可以让我们像使用任何高级语言一样来编写几乎可以做任何事情的程序(即智能合约)。
智能合约非常适合对信任、安全和持久性要求较高的应用场景,比如:数字货币、数字资产、投票、保险、金融应用、预测市场、产权所有权管理、物联网、点对点交易等等。但目前除了数字货币之外,真正落地的应用还并不多。
EVM有自己内含的语言——EVM操作码。类似其他高级语言,EVM也有自己的高级语言,当下流行的是 Solidity和 Viper,编译后可在EVM中执行。
智能合约的默认编程语言是Solidity,文件扩展名以
.sol
结尾。Browser-Solidity Web IDE是一个基于Web的Solidity IDE。
Solidity是和JavaScript相似的语言,用来开发智能合约并将其编译成以太坊虚拟机字节代码。
EVM ( Ethereum Virtual Machine ) 即 以太坊虚拟机 是以太坊中智能合约的运行环境。而EVM运行在以太坊节点上,当我们把合约部署到以太坊网络上之后,合约就可以在以太坊网络中运行了。
Gas
的概念,所以原则上在EVM中可执行的计算总量受Gas
总量限制。EVM采用了基于栈(Stack)的架构,也就是后进先出(LIFO)的方式。
在以太坊设计原理中描述了EVM的设计目标:
同时EVM也有如下特殊设计:
像之前定义的那样,EVM是图灵完备虚拟机器,而EVM本质上是被Gas束缚,因此可以完成的计算总量本质上是被提供的Gas总量限制的。
此外,EVM具有基于堆栈的架构。每个堆栈顶的大小为256位,堆栈有一个最大的大小,为1024位。
EVM有内存(Memory),项目按照可寻址字节数组来存储。内存是易失的,也就是说数据是不持久的。EVM还有一个存储器(Storage),与内存不同,存储器是非易失的,并作为系统状态的一部分进行维护。EVM分开保存程序代码,在虚拟ROM中只能通过特殊指令来访问。
EVM上运行的是合约的字节码形式,需要我们在部署之前先对合约进行编译。可以选择Browser-Solidity Web IDE或者solc编译器。
在以太坊上开发应用时,常常要使用到 以太坊客户端(钱包)。其实我们可以把以太坊客户端理解为一个开发者工具,它提供 账户管理、挖矿、转账、智能合约的部署和执行 等功能,EVM也是由以太坊客户端提供的。
Geth 是典型的开发以太坊时使用的客户端,基于Go语言开发。Geth提供了一个交互式命令控制台,其中包含了以太坊的各种功能(API)。
Geth控制台和 Chrome浏览器开发者工具里的控制台是类似的,不过是跑在终端里。
相对于Geth,Mist则是图形化操作界面的以太坊客户端。
智能合约的部署是指把合约字节码发布到区块链上,并使用一个特定的地址来表示这个合约,这个地址称为合约账户。
以太坊中有两种不同类型但是共享同一地址空间的账户:
Key-Value
类型的存储,并将256位的Key
映射到256位的Value
上合约部署后,当需要调用这个智能合约的方法时,只需要向这个合约账户发送消息(交易)即可。通过交易消息触发后,智能合约的代码就会在EVM中执行了。
和云计算类似,占用区块链的资源需要付出相应的费用。
以太坊上用Gas机制来计费,可以将其看作一个工作量单位。智能合约越复杂(计算步骤的数量和类型、占用的内存等),用来完成运行就需要越多的Gas
。
任何特定的合约所需的运行合约的Gas数量是固定的,由合约的复杂度决定。而 Gas价格由运行合约的人在提交运行合约请求的时候规定,以确定他愿意为这次交易付出的费用:
Gas
价格 ×Gas
数量。
Gas
的目的是限制执行交易所需的工作量,同时为执行支付费用。
如果没有
Gas
机制,就会有人写出无法停止(如死循环)的合约来阻塞网络。
当EVM执行交易时,Gas
将按照特定规则被逐渐消耗,无论执行到什么位置,一旦Gas
被耗尽,将会触发异常,当前调用帧所做的所有状态修改都将被回滚。如果执行结束还有Gas
剩余,这些Gas
将被返还给发送账户。
要进行智能合约的开发,需要有以太币,可以选择以下方式:
在该测试网络中,很容易就可以获得免费的以太币,缺点是需要花很长时间对节点进行初始化。
创建自己的以太币私有测试网络,通常也成为私有链,我们可以用它来作为一个测试环境来开发、调试和测试智能合约。
通过之前提到的Geth
很容易就可以创建一个属于自己的测试网络,以太币想挖多少挖多少,也免去了同步正式网络整个区块链数据所耗费的时间。
相比私有链,开发者网络下会自动分配一个有大量余额的开发者账户供我们使用。
另一个创建测试网络的方法是使用testrpc
,testrpc
是在本地使用内存模拟的一个以太坊环境,对于开发调试来说,更方便快捷。而且testrpc
可以在启动时帮我们创建10个存有资金的测试账户。
进行合约开发时,可以先在testrpc
中测试通过后,再部署到Geth
节点中去。
testrpc
现在已经并入到Truffle
开发框架中,现在的名字是Ganache CLI
。
以太坊社区把基于智能合约的应用称为去中心化的应用程序(Decentralized App)。
如果我们把区块链理解为一个不可篡改的数据库,把智能合约理解为和数据库打交道的程序,那就很容易理解Dapp了。
以太坊是平台,它让我们方便的使用区块链技术开发去中心化的应用。
在这个应用中,使用Solidity
来编写和区块链交互的智能合约。
合约编写好之后,我们需要用以太坊客户端和一个有余额的账户去部署及运行合约。
使用
Truffle
框架可以更好的帮助我们做这些事情。
为了开发方便,我们可以用Geth或testrpc来搭建一个测试网络。
目前大多数的处理器主要由以下4种选择来实现快速的数学运算:
虽然在一些情况下32bit比16bit快,并且在x86架构中8bit数学运算并不是完全支持,但基本上如果你采用以上的任意一种,都可以保证数学运算在若干个时钟周期内完成,并且这个过程非常迅速,往往是纳秒级的。因此,可以说这些位长的整数是目前主流处理器能够原生支持且不需要额外操作的。
EVM处于所谓运算速度和效率方面考虑,采用了非主流的256bit整数。x86汇编码运算的比较实验,证明了采用256bit整数远比采用处理器原生支持的整数长度要复杂,即EVM的运算效率并不高。
在开发Solidity智能合约时就会碰到这个问题,因为Solidity中根本没有标准库。目前的情况是,人们只能不断的从一些开源软件中复制粘贴代码。首先这些代码的安全性无法保证,再加上人们会为了更小的Gas消耗而不断修改代码,这就有可能对他们的合约引入更严重的安全性问题。
这个问题不仅仅是EVM的设计缺陷,也和其实现方式有关。EVM唯一能抛出的异常就是OutOfGas
,并且没有调试日志,也无法调用外部代码。同时,以太坊本身很难生成一条测试网络的私链,即使成功,私链的参数和行为也与公链不同。
浮点数有很多应用实例,比如风险建模、科学计算,以及其他一些范围和近似值比准确值更加重要的情况。EVM将浮点数排除在外的做法有潜在的局限性。
智能合约在设计时需要考虑的重要问题之一就是是可升级性,因为合约的升级是必然的。
在EVM中代码是完全不可修改的,并且由于其采用哈佛计算机结构,也就不可能将代码在内存中加载并执行,代码和数据是被完全分离的。
目前只能够通过部署新的合约来达到升级的目的,这可能需要复制原合约中的所有代码,并将老的合约重定向到新的合约地址。给合约打补丁或是部分升级合约代码在EVM中是完全不可能的。
]]>
- 以太坊设计原理 | ETHFANS
- 深入理解以太坊系列(8):以太坊虚拟机EVM
- 以太坊虚拟机EVM是什么 | 金色百科
- 以太坊虚拟机 | Oriovo的博客
- 详解以太坊的工作原理 | CSDN
- 以太坊源码分析 Ⅰ. 区块和交易,合约和虚拟机 | CSDN
- 【区块链】以太坊源码学习-EVM | CSDN
- Solidity中文文档——1.3 以太坊虚拟机
- Diving Into The Ethereum VM Part One | Qtum’s Blog
- 深入了解以太坊虚拟机 | 简书
- What is the Ethereum Virtual Machine? | The Merkle
- What is Ethereum? | Ethereum Docs
- 以太坊是什么?| CnBlogs 深入浅出区块链
- 以太坊是什么?| 以太坊开发入门指南
- 智能合约开发环境搭建及Hello World合约 | 深入浅出区块链
- 也来谈一谈以太坊虚拟机EVM的缺陷和不足 | BITKAN
- How Ethereum Works? | coindesk
- Optimizing the Ethereum Virtual Machine | Medium.com
hexo new "My New Post"hexo ghexo d
More info: Writing
hexo server
More info: Server
hexo generate
More info: Generating
hexo deploy
More info: Deployment
hexo s --draft
hexo publish [layout] <title>
hexo g -d # or hexo d -g
]]>参考文章
Ubuntu
文件管理器卡死时,首先运行ps -A | grep nautilus
查找文件管理器进程对应的pid
,或者直接运行
killall nautilus
即可杀死文件管理器进程。之后点击任意文件夹,即可重新运行该进程,文件管理器可正常打开。
]]>参考文章