Compare commits

..

No commits in common. "222" and "master" have entirely different histories.
222 ... master

237 changed files with 43950 additions and 3244 deletions

38
.devops/测试.yml Normal file
View File

@ -0,0 +1,38 @@
version: 2
name: 测试
description: ""
global:
concurrent: 1
cache: repository_dir
workflow:
- ref: start
name: 开始
task: start
- ref: end
name: 结束
task: end
needs:
- shell_0
- ref: git_clone_0
name: git clone
task: git_clone@1.2.6
input:
username: ((gitlink.username))
password: ((gitlink.password))
remote_url: '"https://gitlink.org.cn/yystopf/gitstats.git"'
ref: '"refs/heads/master"'
commit_id: '""'
depth: 1
needs:
- start
- ref: shell_0
name: shell
image: docker.jianmuhub.com/library/alpine:3.17.0
env:
REPOPATH: git_clone_0.git_path
script:
- cd $REPOPATH
- ls -la
needs:
- git_clone_0

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
_site
_posts/_draft
_posts/_archive
_archive
node_modules
.vscode
*.log
*.lock
*.sh
.DS_Store
.jekyll-cache
*/.DS_Store
*/*/.DS_Store

16
404.html Normal file
View File

@ -0,0 +1,16 @@
---
layout: default
title: 404
hide-in-nav: true
description: "你来到了没有知识的荒原 :("
header-img: "img/404-bg.jpg"
permalink: /404.html
---
<!-- Page Header -->
{% include intro-header.html type="page" short='true' %}
<script>
document.body.classList.add('page-fullscreen');
</script>

1
CNAME Normal file
View File

@ -0,0 +1 @@
huangxuan.me

5
Gemfile Normal file
View File

@ -0,0 +1,5 @@
source 'https://rubygems.org'
gem 'jekyll-paginate'
gem "jekyll", "~> 4.0"
gem "rake"

73
Gruntfile.js Normal file
View File

@ -0,0 +1,73 @@
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
uglify: {
main: {
src: 'js/<%= pkg.name %>.js',
dest: 'js/<%= pkg.name %>.min.js'
}
},
less: {
expanded: {
options: {
paths: ["css"]
},
files: {
"css/<%= pkg.name %>.css": "less/<%= pkg.name %>.less"
}
},
minified: {
options: {
paths: ["css"],
cleancss: true
},
files: {
"css/<%= pkg.name %>.min.css": "less/<%= pkg.name %>.less"
}
}
},
banner: '/*!\n' +
' * <%= pkg.title %> v<%= pkg.version %> (<%= pkg.homepage %>)\n' +
' * Copyright <%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
' */\n',
usebanner: {
dist: {
options: {
position: 'top',
banner: '<%= banner %>'
},
files: {
src: ['css/<%= pkg.name %>.css', 'css/<%= pkg.name %>.min.css', 'js/<%= pkg.name %>.min.js']
}
}
},
watch: {
scripts: {
files: ['js/<%= pkg.name %>.js'],
tasks: ['uglify'],
options: {
spawn: false,
},
},
less: {
files: ['less/*.less'],
tasks: ['less'],
options: {
spawn: false,
}
},
},
});
// Load the plugins.
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-banner');
grunt.loadNpmTasks('grunt-contrib-watch');
// Default task(s).
grunt.registerTask('default', ['uglify', 'less', 'usebanner']);
};

216
LICENSE Normal file
View File

@ -0,0 +1,216 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015-2016 Huxpro
https://github.com/Huxpro/
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
=======================================================================
Hux Blog Subcomponents:
The Hux Blog project contains subcomponents with separate copyright
notices and license terms. Your use of the source code for the these
subcomponents is subject to the terms and conditions of the following
licenses.
(MIT License) Clean Blog Jekyll Theme: https://github.com/BlackrockDigital/startbootstrap-clean-blog-jekyll/
https://github.com/BlackrockDigital/startbootstrap-clean-blog-jekyll/blob/master/LICENSE
Copyright (c) 2013-2016 Blackrock Digital LLC.

View File

@ -1,34 +0,0 @@
PREFIX=/usr/local
BINDIR=$(PREFIX)/bin
RESOURCEDIR=$(PREFIX)/share/gitstats
RESOURCES=gitstats.css sortable.js *.gif
BINARIES=gitstats
VERSION=$(shell git describe 2>/dev/null || git rev-parse --short HEAD 2>/dev/null || date +%Y-%m-%d)
SEDVERSION=perl -pi -e 's/VERSION = 0/VERSION = "$(VERSION)"/' --
all: help
help:
@echo "Usage:"
@echo
@echo "make install # install to ${PREFIX}"
@echo "make install PREFIX=~ # install to ~"
@echo "make release [VERSION=foo] # make a release tarball"
@echo
install:
install -d $(BINDIR) $(RESOURCEDIR)
install -v $(BINARIES) $(BINDIR)
install -v -m 644 $(RESOURCES) $(RESOURCEDIR)
$(SEDVERSION) $(BINDIR)/gitstats
release:
@cp gitstats gitstats.tmp
@$(SEDVERSION) gitstats.tmp
@tar --owner=0 --group=0 --transform 's!^!gitstats/!' --transform 's!gitstats.tmp!gitstats!' -zcf gitstats-$(VERSION).tar.gz gitstats.tmp $(RESOURCES) doc/ Makefile
@$(RM) gitstats.tmp
man:
pod2man --center "User Commands" -r $(VERSION) doc/gitstats.pod > doc/gitstats.1
.PHONY: all help install release

63
README.md Normal file
View File

@ -0,0 +1,63 @@
[Hux Blog](https://huangxuan.me)
================================
> I never expected this to become popular 🤯 .
![](http://huangxuan.me/img/blog-desktop.jpg)
[User Manual 👉](_doc/Manual.md)
--------------------------------------------------
### Getting Started
1. You will need [Ruby](https://www.ruby-lang.org/en/) and [Bundler](https://bundler.io/) to use [Jekyll](https://jekyllrb.com/). Following [Using Jekyll with Bundler](https://jekyllrb.com/tutorials/using-jekyll-with-bundler/) to fullfill the enviromental requirement.
2. Installed dependencies in the `Gemfile`:
```sh
$ bundle install
```
3. Serve the website (`localhost:4000` by default):
```sh
$ bundle exec jekyll serve # alternatively, npm start
```
### Development (Build From Source)
To modify the theme, you will need [Grunt](https://gruntjs.com/). There are numbers of tasks you can find in the `Gruntfile.js`, includes minifing JavaScript, compiling `.less` to `.css`, adding banners to keep the Apache 2.0 license intact, watching for changes, etc.
Yes, they were inherited and are extremely old-fashioned. There is no modularization and transpilation, etc.
Critical Jekyll-related code are located in `_include/` and `_layouts/`. Most of them are [Liquid](https://github.com/Shopify/liquid/wiki) templates.
This theme uses the default code syntax highlighter of jekyll, [Rouge](http://rouge.jneen.net/), which is compatible with Pygments theme so just pick any pygments theme css (e.g. from [here](http://jwarby.github.io/jekyll-pygments-themes/languages/javascript.html) and replace the content of `highlight.less`.
### Interesting to know more? Checkout the [full user manual](_doc/Manual.md)!
Other Resources
---------------
Ports
- [**Hexo**](https://github.com/Kaijun/hexo-theme-huxblog) by @kaijun
- [**React-SSR**](https://github.com/LucasIcarus/huxpro.github.io/tree/ssr) by @LucasIcarus
[Starter/Boilerplate](https://github.com/huxpro/huxblog-boilerplate)
- Out of date. Helps wanted for updating it on par with the main repo
Translation
- [🇨🇳 中文文档(有点过时)](https://github.com/Huxpro/huxpro.github.io/blob/master/_doc/README.zh.md)
License
-------
Apache License 2.0.
Copyright (c) 2015-present Huxpro
Hux Blog is derived from [Clean Blog Jekyll Theme (MIT License)](https://github.com/BlackrockDigital/startbootstrap-clean-blog-jekyll/)
Copyright (c) 2013-2016 Blackrock Digital LLC.

54
Rakefile Normal file
View File

@ -0,0 +1,54 @@
require "rubygems"
require 'rake'
require 'yaml'
require 'time'
SOURCE = "."
CONFIG = {
'version' => "12.3.2",
'themes' => File.join(SOURCE, "_includes", "themes"),
'layouts' => File.join(SOURCE, "_layouts"),
'posts' => File.join(SOURCE, "_posts"),
'post_ext' => "md",
'theme_package_version' => "0.1.0"
}
# Usage: rake post title="A Title" subtitle="A sub title"
desc "Begin a new post in #{CONFIG['posts']}"
task :post do
abort("rake aborted: '#{CONFIG['posts']}' directory not found.") unless FileTest.directory?(CONFIG['posts'])
title = ENV["title"] || "new-post"
subtitle = ENV["subtitle"] || "This is a subtitle"
slug = title.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
begin
date = (ENV['date'] ? Time.parse(ENV['date']) : Time.now).strftime('%Y-%m-%d')
rescue Exception => e
puts "Error - date format must be YYYY-MM-DD, please check you typed it correctly!"
exit -1
end
filename = File.join(CONFIG['posts'], "#{date}-#{slug}.#{CONFIG['post_ext']}")
if File.exist?(filename)
abort("rake aborted!") if ask("#{filename} already exists. Do you want to overwrite?", ['y', 'n']) == 'n'
end
puts "Creating new post: #{filename}"
open(filename, 'w') do |post|
post.puts "---"
post.puts "layout: post"
post.puts "title: \"#{title.gsub(/-/,' ')}\""
post.puts "subtitle: \"#{subtitle.gsub(/-/,' ')}\""
post.puts "date: #{date}"
post.puts "author: \"Hux\""
post.puts "header-img: \"img/post-bg-2015.jpg\""
post.puts "tags: []"
post.puts "---"
end
end # task :post
desc "Launch preview environment"
task :preview do
system "jekyll --auto --server"
end # task :preview
#Load custom rake scripts
Dir['_rake/*.rake'].each { |r| load r }

133
_config.yml Normal file
View File

@ -0,0 +1,133 @@
# Site settings
title: Viletyy Blog
SEOTitle: Viletyy Blog
header-img: img/home-bg.jpg
email: yystopf@gmail.com
description: "代码的世界 | ViletyyWeb Software Engineer | 这里是 @viletyy 的个人博客。"
keyword: "Viletyy, @ViletyyWeb, 博客, 个人网站, 互联网, Go, Ruby, JavaScript, 前端, 后端"
url: "https://github.com/viletyy" # your host, for absolute URL
baseurl: "" # for example, '/blog' if your blog hosted on 'host/blog'
# Publish posts or collection documents with a future date.
future: true
# SNS settings
RSS: false
weibo_username: 3800502403
zhihu_username: viletyy
github_username: viletyy
# twitter_username: viletyy
#facebook_username: huxpro
#linkedin_username: firstname-lastname-idxxxx
# Build settings
# from 2016, 'pygments' is unsupported on GitHub Pages. Use 'rouge' for highlighting instead.
highlighter: rouge
permalink: pretty
paginate: 10
exclude: ["less","node_modules","Gruntfile.js","package.json","README.md","README.zh.md"]
anchorjs: true # if you want to customize anchor. check out line:181 of `post.html`
# If you have timezone issue (e.g. #68) in China, uncomment to use this:
#timezone: CN
# Gems
# from PR#40, to support local preview for Jekyll 3.0
# make sure you have this gem installed
# `$ gem install jekyll-paginate`
plugins: [jekyll-paginate]
# Markdown settings
# replace redcarpet to kramdown,
# although redcarpet can auto highlight code, the lack of header-id make the catalog impossible, so I switch to kramdown
# document: http://jekyllrb.com/docs/configuration/#kramdown
markdown: kramdown
kramdown:
input: GFM # use Github Flavored Markdown !important
syntax_highlighter_opts:
span:
line_numbers: false
block:
line_numbers: true
start_line: 1
# Disqus settings
disqus_username: viletyy
# Netease settings
netease_comment: false
# Analytics settings
# Baidu Analytics
ba_track_id: c431e7431606bb802dcb4bfd6863375a
# Google Analytics
# ga_track_id: 'UA-49627206-1' # Format: UA-xxxxxx-xx
# ga_domain: huangxuan.me
# Sidebar settings
sidebar: true # whether or not using Sidebar.
sidebar-about-description: "viletyy, 全栈工程师"
sidebar-avatar: /img/image.jpeg # use absolute URL, seeing it's used in both `/` and `/about/`
# Featured Tags
featured-tags: true # whether or not using Feature-Tags
featured-condition-size: 1 # A tag will be featured if the size of it is more than this condition value
# Progressive Web Apps
chrome-tab-theme-color: "#000000"
service-worker: true
# MathJax rendering for layout:page (e.g. post preview)
page-mathjax: false
# Friends
# friends: [
# {
# title: "Su",
# href: "http://su.gallery/"
# },{
# title: "乱序Midare",
# href: "http://mida.re/"
# },{
# title: "Sherry Woo",
# href: "https://sherrywoo.me/"
# },{
# title: "Kun Qian",
# href: "http://kunq.me"
# },{
# title: "Ebn Zhang",
# href: "http://ebnbin.com/"
# },{
# title: "SmdCn",
# href: "http://blog.smdcn.net"
# },{
# title: "JiyinYiyong",
# href: "http://tiye.me/"
# },{
# title: "DHong Say",
# href: "http://dhong.co"
# },{
# title: "尹峰以为",
# href: "http://ingf.github.io/"
# }
# ]

342
_doc/Manual.md Normal file
View File

@ -0,0 +1,342 @@
Hux Blog User Manual
====================
* Basics
* [Getting Started](#getting-started)
* [Development](#development)
* [Config](#configs)
* [Posts](#posts)
* Components
* [SideBar](#sidebar)
* [Mini About Me](#mini-about-me)
* [Featured Tags](#featured-tags)
* [Friends](#friends)
* [Keynote Layout](#keynote-layout)
* Misc
* [Comment](#comment)
* [Analytics](#analytics)
* [SEO Title](#seo-title)
* [FAQ](#faq)
* [Releases](#releases)
### Getting Started
1. You will need [Ruby](https://www.ruby-lang.org/en/) and [Bundler](https://bundler.io/) to use [Jekyll](https://jekyllrb.com/). Following [Using Jekyll with Bundler](https://jekyllrb.com/tutorials/using-jekyll-with-bundler/) to fullfill the enviromental requirement.
2. Installed dependencies in the `Gemfile`:
```sh
$ bundle install
```
3. Serve the website (`localhost:4000` by default):
```sh
$ bundle exec jekyll serve # alternatively, npm start
```
### Development
To modify the theme, you will need [Grunt](https://gruntjs.com/). There are numbers of tasks you can find in the `Gruntfile.js`, includes minifing JavaScript, compiling `.less` to `.css`, adding banners to keep the Apache 2.0 license intact, watching for changes, etc.
Yes, they were inherited and are extremely old-fashioned. There is no modularization and transpilation, etc.
Critical Jekyll-related code are located in `_include/` and `_layouts/`. Most of them are [Liquid](https://github.com/Shopify/liquid/wiki) templates.
This theme uses the default code syntax highlighter of jekyll, [Rouge](http://rouge.jneen.net/), which is compatible with Pygments theme so just pick any pygments theme css (e.g. from [here](http://jwarby.github.io/jekyll-pygments-themes/languages/javascript.html) and replace the content of `highlight.less`.
### Configs
You can easily customize the blog by modifying `_config.yml`:
```yml
# Site settings
title: Hux Blog # title of your website
SEOTitle: Hux Blog # check out docs for more detail
description: "Cool Blog" # ...
# SNS settings
github_username: huxpro # modify this account to yours
weibo_username: huxpro # the footer woule be auto-updated.
# Build settings
paginate: 10 # nums of posts in one page
```
For more options, please check out [Jekyll - Official Site](http://jekyllrb.com/).
Most of them are very descriptive so feel brave to dive into code directly as well.
### Posts
Posts are simply just Markdown files in the `_posts/`.
Metadata of posts are listed in a YAML style _front-matter_.
For instance, [Hello 2015])(https://huangxuan.me/2015/01/29/hello-2015/) has the front-matter of this:
```yml
---
layout: post
title: "Hello 2015"
subtitle: " \"Hello World, Hello Blog\""
date: 2015-01-29 12:00:00
author: "Hux"
header-img: "img/post-bg-2015.jpg"
catalog: true
tags:
- Life
- Meta
---
```
> Note: `tags` section can also be written as `tags: [Life, Meta]`.
After [Rake](https://github.com/ruby/rake) is introduced, we can use the command below to simplify the post creation:
```
rake post title="Hello 2015" subtitle="Hello World, Hello Blog"
```
This command will automatially generate a sample post similar as above under the `_posts/` folder.
There are a bunch of _advanced_ configs:
1. a _text style_ header like [this](https://huangxuan.me/2019/09/08/spacemacs-workflow/) with
```yml
header-style: text
```
2. Turning on Latex support:
```yml
mathjax: true
```
3. Adding a mask to the header picture:
```yml
header-mask: 0.3
```
Etc.
### SideBar
![](http://huangxuan.me/img/blog-sidebar.jpg)
**SideBar** provides possible modules to show off more personal information.
```yml
# Sidebar settings
sidebar: true # default true
sidebar-about-description: "your description here"
sidebar-avatar: /img/avatar-viletyy.jpg # use absolute URL.
```
Modules *[Featured Tags](#featured-tags)*, *[Mini About Me](#mini-about-me)* and *[Friends](#friends)* are turned on by default and you can add your own. The sidebar is naturally responsive, i.e. be pushed to bottom in a smaller screen (`<= 992px`, according to [Bootstarp Grid System](http://getbootstrap.com/css/#grid))
### Mini About Me
**Mini-About-Me** displays your avatar, description and all SNS buttons if `sidebar-avatar` and `sidebar-about-description` variables are set.
It would be hidden in a smaller screen when the entire sidebar are pushed to bottom. Since there is already SNS portion there in the footer.
### Featured Tags
**Featured-Tags** is similar to any cool tag features in website like [Medium](http://medium.com).
Started from V1.4, this module can be used even when sidebar is off and displayed always in the bottom.
```yml
# Featured Tags
featured-tags: true
featured-condition-size: 1 # A tag will be featured if the size of it is more than this condition value
```
The only thing need to be paid attention to is `featured-condition-size`, which indicate a criteria that tags need to have to be able to "featured". Internally, a condition `{% if tag[1].size > {{site.featured-condition-size}} %}` are made.
### Friends
Friends is a common feature of any blog. It helps with SEO if you have a bi-directional hyperlinks with your friends sites.
This module can live when sidebar is off as well.
Friends information is configured as a JSON string in `_config.yml`
```yml
# Friends
friends: [
{
title: "Foo Blog",
href: "http://foo.github.io/"
},
{
title: "Bar Blog",
href: "http://bar.github.io"
}
]
```
### Keynote Layout
![](http://huangxuan.me/img/blog-keynote.jpg)
There is a increased trend to use Open Web technology for keynotes and presentations via Reveal.js, Impress.js, Slides, Prezi etc. I consider a modern blog should have first-class support to embed these HTML based presentation so **Keynote layout** are made.
To use, in the **front-matter**:
```yml
---
layout: keynote
iframe: "http://huangxuan.me/js-module-7day/"
---
```
The `iframe` element will be automatically resized to adapt different form factors and device orientation.
Because most of the keynote framework prevent the browser default scroll behavior. A bottom-padding is set to help user and imply user that more content could be presented below.
### Comment
> Help Wanted: Moving to a Github-based solution.
Currently, [Disqus](http://disqus.com) <del> and [Duoshuo](http://duoshuo.com)</del> are supported as third party discussion system.
First of all, you need to sign up and get your own account. **Repeat, DO NOT use mine!** (I have set Trusted Domains) It is deathly simple to sign up and you will get the full power of management system. Please give it a try!
Second, from V1.5, you can easily complete your comment configuration by just adding your **short name** into `_config.yml`:
```yml
duoshuo_username: _your_duoshuo_short_name_
# OR
disqus_username: _your_disqus_short_name_
```
**To the old version user**, it's better that you pull the new version, otherwise you have to replace code in `post.html`, `keynote.html` and `about.html` on your own.
<del>Furthermore, Duoshuo support Sharing. if you only wanna use Duoshuo comment without sharing, you can set `duoshuo_share: false`. </del>
### Analytics
From V1.5, Google Analytics and Baidu Tongji are supported with a simple config away:
```yml
# Baidu Analytics
ba_track_id: 4cc1f2d8f3067386cc5cdb626a202900
# Google Analytics
ga_track_id: 'UA-49627206-1' # Format: UA-xxxxxx-xx
ga_domain: huangxuan.me
```
Just checkout the code offered by Google/Baidu, and copy paste here, all the rest is already done for you.
(Google might ask for meta tag `google-site-verification`)
### SEO Title
Before V1.4, site setting `title` is not only used for displayed in Home Page and Navbar, but also used to generate the `<title>` in HTML.
It's possible that you want the two things different. For me, my site-title is **“Hux Blog”** but I want the title shows in search engine is **“黄玄的博客 | Hux Blog”** which is multi-language.
So, the SEO Title is introduced to solve this problem, you can set `SEOTitle` different from `title`, and it would be only used to generate HTML `<title>` and setting DuoShuo Sharing.
FAQ
---
#### cannot load such file -- jekyll-paginate
This blog started in Jekyll 2 time when `jekyll-paginate` is standard. With Jekyll 3, it's a plugin we included in `_config.yml`.
Make sure you installed it via plain `gem` CLI or Bundler.
Releases
--------
#### V1.8.2
- Merged #333, #336 from @JinsYin.
- Add `Gemfile` due to increasing Bundler using.
- TODO: `multilingual` could be more automative via configurations and convention.
- Drop the entire `portfolio` page until a big rewrite of a better `project` page.
#### V1.8.1
- Improve multi-lingual implementation, see `about.html` or `_posts/2017-07-12-upgrading-eleme-to-pwa.markdown` for a example of uses.
#### V1.8
- Brand new [Archive](https://huangxuan.me/archive/) page! It combines previous Archive and Tag page and it's backward-cmpatible.
Shout out to [@kitian616/jekyll-TeXt-theme](https://github.com/kitian616/jekyll-TeXt-theme) of bringing this idea.
- Improve engineering by extracting duplicated liquid templates into reuseable includes. This was proposed in #74 by @Kaijun but postponed for entirely 2.5 years! I wasn't able to merge his PR directly because of long-time divegence but the credit is belonging to @Kaijun.
- Improved code block. Long-wanted line number are supported out of the box (thanks for @SmilingParadise's help from Sina Weibo), the default theme is updated to Atom One Dark as well (checkout FQA for how to change to your farovite theme)
- MathJax support by @Voleking in #80. I choose to use the SVG renderer though. See [Mathjax, kramdown and Octopress](https://www.lucypark.kr/blog/2013/02/25/mathjax-kramdown-and-octopress/) for writing and escaping details.
- Open Graph Protocol support by @Android-KitKat in #253
- `header-img-credit` and `header-img-credit-href`
- `nav-style: invert` and `header-style: text`
#### V1.7
- PWA / Service Worker support.
#### v1.6
- Change cdn to cdnjs for better HTTPS support
#### V1.5.2
- Feeling annoyed to delete my blog post after clone or pull? Try **Boilerplate (Beta)** to help you get started quickly and easily merge update.
- `-apple-system` is added in font rule, which display beautiful new font **San Francisco** in iOS 9 by default.
- Fixed [issue#15](https://github.com/Huxpro/huxpro.github.io/issues/15) about code wrap.
#### V1.5.1
- **[Comment](#comment)** support [**Disqus**](http://disqus.com) officially, thanks to @rpsh.
#### V1.5
- **[Comment](#comment)** and **[Analytics](#analytics)** is configurable now! We also add **Google Analytics support** and drop tencents. Both documents is updated.
#### V1.4
- **[Featured Tags](#featured-tags)** is now independent of [SideBar](#sidebar). Both documents is updated.
- New **[SEO Title](#seo-title)** for SEO usage which is differ from the site title
#### V1.3.1
- Support **PingFang (苹方)**, the new Chinese font presented by [OS X El Capitan](http://www.apple.com/cn/osx/whats-new/)
#### V1.3
- Big Improvement to the **Navigation Menu** *(especially in Android)*: Dropping the old, stuttering, low-performance [Bootstrap collapse.js](http://getbootstrap.com/javascript/#collapse), replaced with an own wrote, [jank free](http://jankfree.org/) navbar menu in a pretty high-performance implementation of [Google Material Design](https://www.google.com/design/spec/material-design/introduction.html).
<img src="http://huangxuan.me/img/blog-md-navbar.gif" width="320" />
#### V1.2
- Brand new **[Keynote Layout](#keynote-layout)** is provided for easily posting beautiful HTML presentations you have created with this blog
#### V1.1
- We now support a clean and gorgeous **[SideBar](#sidebar)** for displaying more info
- **[Friends](#friends)** is also added as a common feature of blog help you do SEO
#### V1.0
- Full-feature **Tag** support
- **Mobile first** user experience optimization
- **Typographic optimization** for Chinese Fonts
- **Network optimizaition** for China, dropping Google webfont, using local CDN
- Using [Github Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/)
- Using Baidu, Tencent/QQ analytics
- Using [DuoShuo](http://duoshuo.com/) as the Disqus-like third party discussion system

266
_doc/README.zh.md Normal file
View File

@ -0,0 +1,266 @@
# Hux blog 模板
### [我的博客在这里 &rarr;](http://huxpro.github.io)
### 关于收到"Page Build Warning"的email
由于jekyll升级到3.0.x,对原来的pygments代码高亮不再支持现只支持一种-rouge所以你需要在 `_config.yml`文件中修改`highlighter: rouge`.另外还需要在`_config.yml`文件中加上`gems: [jekyll-paginate]`.
同时,你需要更新你的本地jekyll环境.
使用`jekyll server`的同学需要这样:
1. `gem update jekyll` # 更新jekyll
2. `gem update github-pages` #更新依赖的包
使用`bundle exec jekyll server`的同学在更新jekyll后需要输入`bundle update`来更新依赖的包.
参考文档:[using jekyll with pages](https://help.github.com/articles/using-jekyll-with-pages/) & [Upgrading from 2.x to 3.x](http://jekyllrb.com/docs/upgrading/2-to-3/)
## 关于模板(beta)
我的博客仓库——`huxpro.github.io`是经常修改的而且还会有人乱提交代码因此给大家做了一个稳定版的模板。大家可以直接fork模板——`huxblog-boilerplate`,要改的地方我都说明了。或者可以直接下载zip到本地自己去修改。
```
$ git clone git@github.com:Huxpro/huxblog-boilerplate.git
```
**[在这里预览模板 &rarr;](http://huangxuan.me/huxblog-boilerplate/)**
## 各版本特性
##### New Feature (V1.5.2)
* 当你fork了我的仓库之后还要删掉里面的关于我的文档是不是感到略烦躁呢**Boilerplate** 模板将帮助你快速开始,方便合并与更新。
* `-apple-system`被添加到了字体规则里面了这套字体格式能将iOS9默认的新字体**San Francisco**表现的非常漂亮。
* 解决了代码过长自动换行的bug,替换为横向滚动条。详情请见[issue#15](https://github.com/Huxpro/huxpro.github.io/issues/15)
###### 其他历史版本个人觉得没有必要了解,看看英文就行了。
## 支持
* 你可以自由的fork。如果你能将主题作者和 github 的地址保留在你的页面底部,我将非常感谢你。
* 如果你喜欢我的这个博客模板,请在`huxpro.github.io`这个repository点个赞——右上角**star**一下。
## 说明文档
* 开始
* [环境要求](#environment)
* [开始](#get-started)
* [写一篇博文](#write-posts)
* 组件
* [侧边栏](#sidebar)
* [迷你关于我](#mini-about-me)
* [推荐标签](#featured-tags)
* [好友链接](#friends)
* [HTML5 演示文档布局](#keynote-layout)
* 评论与 Google/Baidu Analytics
* [评论](#comment)
* [网站分析](#analytics)
* 高级部分
* [自定义](#customization)
* [标题底图](#header-image)
* [搜索展示标题-头文件](#seo-title)
#### Environment
如果你安装了jekyll那你只需要在命令行输入`jekyll serve`就能在本地浏览器预览主题。你还可以输入`jekyll serve --watch`,这样可以边修改边自动运行修改后的文件。
经 [@BrucZhaoR](https://github.com/BruceZhaoR)的测试好像两个命令都是可以的自动运行修改后的文件的刷新后可以实时预览。官方文件是建议安装bundler这样你在本地的效果就跟在github上面是一样的。详情请见这里https://help.github.com/articles/using-jekyll-with-pages/#installing-jekyll
#### Get Started
你可以通用修改 `_config.yml`文件来轻松的开始搭建自己的博客:
```
# Site settings
title: Hux Blog # 你的博客网站标题
SEOTitle: Hux Blog # 在后面会详细谈到
description: "Cool Blog" # 随便说点,描述一下
# SNS settings
github_username: huxpro # 你的github账号
weibo_username: huxpro # 你的微博账号,底部链接会自动更新的。
# Build settings
# paginate: 10 # 一页你准备放几篇文章
```
Jekyll官方网站还有很多的参数可以调比如设置文章的链接形式...网址在这里:[Jekyll - Official Site](http://jekyllrb.com/) 中文版的在这里:[Jekyll中文](http://jekyllcn.com/).
#### write-posts
要发表的文章一般以markdown的格式放在这里`_posts/`,你只要看看这篇模板里的文章你就立刻明白该如何设置。
yaml 头文件长这样:
```
---
layout: post
title: "Hello 2015"
subtitle: "Hello World, Hello Blog"
date: 2015-01-29 12:00:00
author: "Hux"
header-img: "img/post-bg-2015.jpg"
tags:
- Life
---
```
在引入[Rake](https://github.com/ruby/rake)工具之后,我们可以使用命令:
```
rake post title="Hello 2015" subtitle="Hello World, Hello Blog"
```
来自动生成上面的文章模板。
#### SideBar
看右边:
![](http://huangxuan.me/img/blog-sidebar.jpg)
设置是在 `_config.yml`文件里面的`Sidebar settings`那块。
```
# Sidebar settings
sidebar: true #添加侧边栏
sidebar-about-description: "简单的描述一下你自己"
sidebar-avatar: /img/avatar-hux.jpg #你的大头贴,请使用绝对地址.
```
侧边栏是响应式布局的当屏幕尺寸小于992px的时候侧边栏就会移动到底部。具体请见bootstrap栅格系统 <http://v3.bootcss.com/css/>
#### Mini About Me
Mini-About-Me 这个模块将在你的头像下面,展示你所有的社交账号。这个也是响应式布局,当屏幕变小时候,会将其移动到页面底部,只不过会稍微有点小变化,具体请看代码。
#### Featured Tags
看到这个网站 [Medium](http://medium.com) 的推荐标签非常的炫酷,所以我将他加了进来。
这个模块现在是独立的,可以呈现在所有页面,包括主页和发表的每一篇文章标题的头上。
```
# Featured Tags
featured-tags: true
featured-condition-size: 1 # A tag will be featured if the size of it is more than this condition value
```
唯一需要注意的是`featured-condition-size`: 如果一个标签的 SIZE也就是使用该标签的文章数大于上面设定的条件值这个标签就会在首页上被推荐。
内部有一个条件模板 `{% if tag[1].size > {{site.featured-condition-size}} %}` 是用来做筛选过滤的.
#### Friends
好友链接部分。这会在全部页面显示。
设置是在 `_config.yml`文件里面的`Friends`那块,自己加吧。
```
# Friends
friends: [
{
title: "Foo Blog",
href: "http://foo.github.io/"
},
{
title: "Bar Blog",
href: "http://bar.github.io"
}
]
```
#### Keynote Layout
HTML5幻灯片的排版
![](http://huangxuan.me/img/blog-keynote.jpg)
这部分是用于占用html格式的幻灯片的一般用到的是 Reveal.js, Impress.js, Slides, Prezi 等等.我认为一个现代化的博客怎么能少了放html幻灯的功能呢~
其主要原理是添加一个 `iframe`在里面加入外部链接。你可以直接写到头文件里面去详情请见下面的yaml头文件的写法。
```
---
layout: keynote
iframe: "http://huangxuan.me/js-module-7day/"
---
```
iframe在不同的设备中将会自动的调整大小。保留内边距是为了让手机用户可以向下滑动以及添加更多的内容。
#### Comment
博客不仅支持多说[Duoshuo](http://duoshuo.com)评论系统,也支持[Disqus](http://disqus.com)评论系统。
`Disqus`优点是国际比较流行界面也很大气、简介如果有人评论还能实时通知直接回复通知的邮件就行了缺点是评论必须要去注册一个disqus账号分享一般只有Facebook和Twitter另外在墙内加载速度略慢了一点。想要知道长啥样可以看以前的版本点[这里](http://brucezhaor.github.io/about.html) 最下面就可以看到。
`多说` 优点是:支持国内各主流社交软件(微博微信豆瓣QQ空间 ...)一键分享按钮功能另外登陆比较方便管理界面也是纯中文的相对于disqus全英文的要容易操作一些缺点是就是界面丑了一点。
当然你是可以自定义界面的css的详情请看多说开发者文档 http://dev.duoshuo.com/docs/5003ecd94cab3e7250000008 。
**首先**你需要去注册一个账号不管是disqus还是多说的。**不要直接使用我的啊!**
**其次**你只需要在下面的yaml头文件中设置一下就可以了。
```
duoshuo_username: _你的用户名_
# 或者
disqus_username: _你的用户名_
```
**最后**多说是支持分享的,如果你不想分享,请这样设置:`duoshuo_share: false`。你可以同时使用两个评论系统,不过个人感觉怪怪的。
#### Analytics
网站分析现在支持百度统计和Google Analytics。需要去官方网站注册一下然后将返回的code贴在下面
```
# Baidu Analytics
ba_track_id: 4cc1f2d8f3067386cc5cdb626a202900
# Google Analytics
ga_track_id: 'UA-49627206-1' # 你用Google账号去注册一个就会给你一个这样的id
ga_domain: huangxuan.me # 默认的是 auto, 这里我是自定义了的域名你如果没有自己的域名需要改成auto。
```
#### Customization
如果你喜欢折腾,你可以去自定义我的这个模板的 code[Grunt](gruntjs.com)已经为你准备好了。(感谢 Clean Blog
JavaScript 的压缩混淆、Less 的编译、Apache 2.0 许可通告的添加与 watch 代码改动,这些任务都揽括其中。简单的在命令行中输入 `grunt` 就可以执行默认任务来帮你构建文件了。如果你想搞一搞 JavaScript 或 Less 的话,`grunt watch` 会帮助到你的。
**如果你可以理解 `_include/``_layouts/`文件夹下的代码(这里是整个界面布局的地方),你就可以使用 Jekyll 使用的模版引擎 [Liquid](https://github.com/Shopify/liquid/wiki)的语法直接修改/添加代码,来进行更有创意的自定义界面啦!**
#### Header Image
标题底图是可以自己选的看看几篇示例post你就知道如何设置了。在
[issue #6 ](https://github.com/Huxpro/huxpro.github.io/issues/6) 中我被问到:怎么样才能让标题底图好看呢?
标题底图的选取完全是看个人的审美了,我也帮不了你。每一篇文章可以有不同的底图,你想放什么就放什么,最后宽度要够,大小不要太大,否则加载慢啊。
但是需要注意的是本模板的标题是**白色**的,所以背景色要设置为**灰色**或者**黑色**总之深色系就对了。当然你还可以自定义修改字体颜色总之用github pages就是可以完全的个性定制自己的博客。
#### SEO Title
我的博客标题是 **“Hux Blog”** 但是我想要在搜索的时候显示 **“黄玄的博客 | Hux Blog”** 这个就需要SEO Title来定义了。
其实这个SEO Title就是定义了<head><title>标题</title></head>这个里面的东西和多说分享的标题,你可以自行修改的。
## 致谢
1. 这个模板是从这里[IronSummitMedia/startbootstrap-clean-blog-jekyll](https://github.com/IronSummitMedia/startbootstrap-clean-blog-jekyll) fork 的。 感谢这个作者
2. 感谢[@BrucZhaoR](https://github.com/BruceZhaoR)的中文翻译
3. 感谢 Jekyll、Github Pages 和 Bootstrap!

5
_includes/about/en.md Normal file
View File

@ -0,0 +1,5 @@
Hi我是viletyy一名全栈工程师
目前主要在湖南长沙。[博客](https://viletyy.github.io/)、[微博](https://weibo.com/u/3800502403)、[Github](https://github.com/viletyy)

5
_includes/about/zh.md Normal file
View File

@ -0,0 +1,5 @@
Hi我是viletyy一名全栈工程师
目前主要在湖南长沙。[博客](https://viletyy.github.io/)、[微博](https://weibo.com/u/3800502403)、[Github](https://github.com/viletyy)

7
_includes/ads.html Normal file
View File

@ -0,0 +1,7 @@
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<!-- first shot -->
<ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-6487568398225121" data-ad-slot="4814308751"
data-ad-format="auto" data-full-width-responsive="true"></ins>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>

View File

@ -0,0 +1,28 @@
{% comment %}
@param {boolean} bottom - bottom will render <hr>
{% endcomment %}
{% if site.featured-tags %}
<section>
{% if include.bottom %}
<hr class="hidden-sm hidden-xs">
{% endif %}
<h5><a href="{{'/archive/' | prepend: site.baseurl }}">FEATURED TAGS</a></h5>
<div class="tags">
{% capture tags %}
{% comment %}
there must be no space between for and if otherwise this tricky sort won't work.
url_encode/decode is for escaping otherwise extra <a> will get generated
but it will break sort...
{% endcomment %}
{% for tag in site.tags %}{% if tag[1].size > site.featured-condition-size %}
<a data-sort="{{ site.posts.size | minus: tag[1].size | prepend: '0000' | slice: -4, 4 }}"
href="{{ site.baseurl }}/archive/?tag={{ tag[0] | url_encode }}"
title="{{ tag[0] }}"
rel="{{ tag[1].size }}">{{ tag[0] }}</a>__SEPARATOR__
{% endif %}{% endfor %}
{% endcapture %}
{{ tags | split:'__SEPARATOR__' | sort }}
</div>
</section>
{% endif %}

330
_includes/footer.html Normal file
View File

@ -0,0 +1,330 @@
<!-- Footer -->
<footer>
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<!-- SNS Link -->
{% include sns-links.html center=true %}
<p class="copyright text-muted">
Copyright &copy; {{ site.title }} {{ site.time | date: '%Y' }}
<br>
Powered by <a href="https://viletyy.github.io/">Viletyy Blog</a> |
<iframe style="margin-left: 2px; margin-bottom:-5px;" frameborder="0" scrolling="0" width="100px"
height="20px"
src="https://ghbtns.com/github-btn.html?user=viletyy&repo=viletyy.github.io&type=star&count=true">
</iframe>
</p>
</div>
</div>
</div>
</footer>
<!-- jQuery -->
<script src="{{ "/js/jquery.min.js " | prepend: site.baseurl }}"></script>
<!-- Bootstrap Core JavaScript -->
<!-- Currently, only navbar scroll-down effect at desktop still depends on this -->
<script src="{{ "/js/bootstrap.min.js " | prepend: site.baseurl }}"></script>
<!-- Custom Theme JavaScript -->
<script src="{{ "/js/hux-blog.min.js " | prepend: site.baseurl }}"></script>
<!-- Simple Jekyll Search -->
<script src="{{ "/js/simple-jekyll-search.min.js" | prepend: site.baseurl }}"></script>
<!-- Service Worker -->
{% if site.service-worker %}
<script src="{{ "/js/snackbar.js " | prepend: site.baseurl }}"></script>
<script src="{{ "/js/sw-registration.js " | prepend: site.baseurl }}"></script>
{% endif %}
<!-- async load function -->
<script>
function async(u, c) {
var d = document, t = 'script',
o = d.createElement(t),
s = d.getElementsByTagName(t)[0];
o.src = u;
if (c) { o.addEventListener('load', function (e) { c(null, e); }, false); }
s.parentNode.insertBefore(o, s);
}
</script>
<!--
Because of the native support for backtick-style fenced code blocks
right within the Markdown is landed in Github Pages,
From V1.6, There is no need for Highlight.js,
so Huxblog drops it officially.
- https://github.com/blog/2100-github-pages-now-faster-and-simpler-with-jekyll-3-0
- https://help.github.com/articles/creating-and-highlighting-code-blocks/
- https://github.com/jneen/rouge/wiki/list-of-supported-languages-and-lexers
-->
<!--
<script>
async("http://cdn.bootcss.com/highlight.js/8.6/highlight.min.js", function(){
hljs.initHighlightingOnLoad();
})
</script>
<link href="http://cdn.bootcss.com/highlight.js/8.6/styles/github.min.css" rel="stylesheet">
-->
{% if page.plchart %}
<!-- jquery.tagcloud.js -->
<script>
// https://stackoverflow.com/questions/9975810/make-iframe-automatically-adjust-height-according-to-the-contents-without-using
function resizeIframe(obj) {
obj.style.height = obj.contentWindow.document.body.scrollHeight + 'px';
}
$(document).ready(function () {
var $chart = document.querySelector("#chart");
$chart.onload = function () {
resizeIframe($chart)
}
window.addEventListener("resize", () => {
resizeIframe($chart)
});
})
</script>
{% endif %}
{% if page.title == 'Archive' %}
<!-- jquery.tagcloud.js -->
<script>
async('{{ "/js/jquery.tagcloud.js" | prepend: site.baseurl }}', function () {
$.fn.tagcloud.defaults = {
//size: {start: 1, end: 1, unit: 'em'},
color: { start: '#bbbbee', end: '#2f93b4' },
};
$('#tag_cloud a').tagcloud();
})
</script>
<script src='{{ "/js/archive.js " | prepend: site.baseurl }}'></script>
{% endif %}
<!--fastClick.js -->
<script>
async("//cdnjs.cloudflare.com/ajax/libs/fastclick/1.0.6/fastclick.min.js", function () {
var $nav = document.querySelector("nav");
if ($nav) FastClick.attach($nav);
})
</script>
<!-- Google Analytics -->
{% if site.ga_track_id %}
<script>
// dynamic User by Hux
var _gaId = '{{ site.ga_track_id }}';
var _gaDomain = '{{ site.ga_domain }}';
// Originial
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date(); a = s.createElement(o),
m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
ga('create', _gaId, _gaDomain);
ga('send', 'pageview');
</script>
{% endif %}
<!-- Baidu Tongji -->
{% if site.ba_track_id %}
<script>
// dynamic User by Hux
var _baId = '{{ site.ba_track_id }}';
// Originial
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "//hm.baidu.com/hm.js?" + _baId;
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
{% endif %}
<!-- Side Catalog -->
{% unless page.no-catalog %}
<script type="text/javascript">
function generateCatalog(selector) {
// interop with multilangual
if ('{{ page.multilingual }}' == 'true') {
_containerSelector = 'div.post-container.active'
} else {
_containerSelector = 'div.post-container'
}
// init
var P = $(_containerSelector), a, n, t, l, i, c;
a = P.find('h1,h2,h3,h4,h5,h6');
// clean
$(selector).html('')
// appending
a.each(function () {
n = $(this).prop('tagName').toLowerCase();
i = "#" + $(this).prop('id');
t = $(this).text();
c = $('<a href="' + i + '" rel="nofollow">' + t + '</a>');
l = $('<li class="' + n + '_nav"></li>').append(c);
$(selector).append(l);
});
return true;
}
generateCatalog(".catalog-body");
// toggle side catalog
$(".catalog-toggle").click((function (e) {
e.preventDefault();
$('.side-catalog').toggleClass("fold")
}))
/*
* Doc: https://github.com/davist11/jQuery-One-Page-Nav
* Fork by Hux to support padding
*/
async("{{ '/js/jquery.nav.js' | prepend: site.baseurl }}", function () {
$('.catalog-body').onePageNav({
currentClass: "active",
changeHash: !1,
easing: "swing",
filter: "",
scrollSpeed: 700,
scrollOffset: 0,
scrollThreshold: .2,
begin: null,
end: null,
scrollChange: null,
padding: 80
});
});
</script>
{% endunless %}
<!-- Multi-Lingual -->
{% if page.multilingual %}
<!-- Handle Language Change -->
<script type="text/javascript">
// get nodes
var $zh = document.querySelector(".zh");
var $en = document.querySelector(".en");
var $select = document.querySelector("select");
// Changes at v1.8.1: include lang flag as a url query. This interop well with catalog hash anchors.
function getLang() { return new URLSearchParams(document.location.search).get("lang") }
function setLang(newLang) {
var params = new URLSearchParams(document.location.search)
params.set("lang", newLang)
document.location.search = params.toString() // refresh.
}
// handle render
function _render() {
var lang = getLang()
// en
if (lang == "en") {
$select.selectedIndex = 1;
$en.style.display = "block";
$en.classList.add("active");
$zh.style.display = "none";
$zh.classList.remove("active");
// default to zh-cn
} else {
$select.selectedIndex = 0;
$zh.style.display = "block";
$zh.classList.add("active");
$en.style.display = "none";
$en.classList.remove("active");
}
// interop with catalog
generateCatalog(".catalog-body");
}
// handle select change
function onLanChange(index) {
if (index == 0) {
lang = "zh"
} else {
lang = "en"
}
setLang(lang)
}
// init
_render();
</script>
{% endif %}
<!-- Simple Jekyll Search -->
<script>
// https://stackoverflow.com/questions/1912501/unescape-html-entities-in-javascript
function htmlDecode(input) {
var e = document.createElement('textarea');
e.innerHTML = input;
// handle case of empty input
return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
}
SimpleJekyllSearch({
searchInput: document.getElementById('search-input'),
resultsContainer: document.getElementById('search-results'),
json: '/search.json',
searchResultTemplate: '<div class="post-preview item"><a href="{url}"><h2 class="post-title">{title}</h2><h3 class="post-subtitle">{subtitle}</h3><hr></a></div>',
noResultsText: 'No results',
limit: 50,
fuzzy: false,
// a hack to get escaped subtitle unescaped. for some reason,
// post.subtitle w/o escape filter nuke entire search.
templateMiddleware: function (prop, value, template) {
if (prop === 'subtitle' || prop === 'title') {
if (value.indexOf("code")) {
return htmlDecode(value);
} else {
return value;
}
}
}
});
$(document).ready(function () {
var $searchPage = $('.search-page');
var $searchOpen = $('.search-icon');
var $searchClose = $('.search-icon-close');
var $searchInput = $('#search-input');
var $body = $('body');
$searchOpen.on('click', function (e) {
e.preventDefault();
$searchPage.toggleClass('search-active');
var prevClasses = $body.attr('class') || '';
setTimeout(function () {
$body.addClass('no-scroll');
}, 400)
if ($searchPage.hasClass('search-active')) {
$searchClose.on('click', function (e) {
e.preventDefault();
$searchPage.removeClass('search-active');
$body.attr('class', prevClasses); // from closure
});
$searchInput.focus();
}
});
});
</script>

9
_includes/friends.html Normal file
View File

@ -0,0 +1,9 @@
{% if site.friends %}
<hr>
<h5>FRIENDS</h5>
<ul class="list-inline">
{% for friend in site.friends %}
<li><a href="{{friend.href}}">{{friend.title}}</a></li>
{% endfor %}
</ul>
{% endif %}

72
_includes/head.html Normal file
View File

@ -0,0 +1,72 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="google-site-verification" content="xBT4GhYoi5qRD5tr338pgPM5OWHHIDR6mNg1a3euekI" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="description" content="{{ site.description }}">
<meta name="keywords" content="{{ site.keyword }}">
<meta name="theme-color" content="{{ site.chrome-tab-theme-color }}">
<!-- Open Graph -->
<meta property="og:title"
content="{% if page.title %}{{ page.title }} - {{ site.SEOTitle }}{% else %}{{ site.SEOTitle }}{% endif %}">
{% case page.layout %}
{% when 'post' %}
<meta property="og:type" content="article">
<meta property="og:description" content="{{ page.excerpt | strip_html | truncate:200 }}">
{% if page.date %}
<meta property="article:published_time" content="{{ page.date | date: " %Y-%m-%dT%H:%M:%SZ" }}">
{% endif %}
{% if page.author %}
<meta property="article:author" content="{{ page.author }}">
{% endif %}
{% for tag in page.tags %}
<meta property="article:tag" content="{{ tag }}">
{% endfor %}
{% else %}
<meta property="og:type" content="website">
<meta property="og:description"
content="{% if page.description %}{{ page.description }}{% else %}{{ site.description }}{% endif %}">
{% endcase %}
<meta property="og:image" content="{{ site.url }}{{ site.sidebar-avatar }}">
<meta property="og:url" content="{{ site.url }}{{ page.url }}">
<meta property="og:site_name" content="{{ site.SEOTitle }}">
<title>{% if page.title %}{{ page.title }} - {{ site.SEOTitle }}{% else %}{{ site.SEOTitle }}{% endif %}</title>
<!-- Web App Manifest -->
<link rel="manifest" href="{{ site.baseurl }}/pwa/manifest.json">
<!-- Favicon -->
<link rel="shortcut icon" href="{{ site.baseurl }}/img/favicon.ico">
<!-- Canonical URL -->
<link rel="canonical" href="{{ page.url | replace:'index.html','' | prepend: site.baseurl | prepend: site.url }}">
<!-- Bootstrap Core CSS -->
<link rel="stylesheet" href="{{ " /css/bootstrap.min.css" | prepend: site.baseurl }}">
<!-- Custom CSS -->
<link rel="stylesheet" href="{{ " /css/hux-blog.min.css" | prepend: site.baseurl }}">
<!-- Custom Fonts -->
<!-- <link href="http://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> -->
<!-- Hux change font-awesome CDN to qiniu -->
<link href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet"
type="text/css">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
<!-- ga & ba script hoook -->
<script></script>
<!-- Google AdSense -->
<script data-ad-client="ca-pub-6487568398225121" async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
</head>

110
_includes/intro-header.html Normal file
View File

@ -0,0 +1,110 @@
{% comment %}
@param {string} type - 'page' | 'post' | 'keynote'
@param {boolean} short
{% endcomment %}
{% if include.type == 'post' %}
<style type="text/css">
header.intro-header{
position: relative;
background-image: url('{{ site.baseurl }}/{% if page.header-img %}{{ page.header-img }}{% else %}{{ site.header-img }}{% endif %}');
background: {{ page.header-bg-css }};
}
{% if page.header-mask %}
header.intro-header .header-mask{
width: 100%;
height: 100%;
position: absolute;
background: rgba(0,0,0, {{ page.header-mask }});
}
{% endif %}
</style>
{% if page.header-style == 'text' %}
<header class="intro-header style-text" >
{% else %}
<header class="intro-header" >
{% endif %}
<div class="header-mask"></div>
{% if page.header-img-credit %}
<div class="header-img-credit">
Image by <a href="//{{page.header-img-credit-href}}">{{page.header-img-credit}}</a>
</div>
{% endif %}
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<div class="post-heading">
<div class="tags">
{% for tag in page.tags %}
<a class="tag" href="{{ site.baseurl }}/archive/?tag={{ tag | url_encode }}" title="{{ tag }}">{{ tag }}</a>
{% endfor %}
</div>
<h1>{{ page.title }}</h1>
{% comment %} always create a h2 for keeping the margin {% endcomment %}
<h2 class="subheading">{{ page.subtitle }}</h2>
<span class="meta">Posted by {% if page.author %}{{ page.author }}{% else %}{{ site.title }}{% endif %} on {{ page.date | date: "%Y-%m-%d %H:%M" }}</span>
</div>
</div>
</div>
</div>
</header>
{% endif %}
{% if include.type == 'keynote' %}
<style type="text/css">
header.intro-header{
height: 500px;
overflow: hidden;
}
header.intro-header .container{
visibility: hidden;
}
header iframe{
width: 100%;
height: 100%;
border: 0;
}
</style>
<header class="intro-header" >
<iframe src="{{page.iframe}}"></iframe>
<!-- keep for SEO -->
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<div class="post-heading">
<div class="tags">
{% for tag in page.tags %}
<a class="tag" href="{{ site.baseurl }}/archive/?tag={{ tag | url_encode }}" title="{{ tag }}">{{ tag }}</a>
{% endfor %}
</div>
<h1>{{ page.title }}</h1>
{% comment %} always create a h2 for keeping the margin {% endcomment %}
<h2 class="subheading">{{ page.subtitle }}</h2>
<span class="meta">Posted by {% if page.author %}{{ page.author }}{% else %}{{ site.title }}{% endif %}
on {{ page.date | date: "%Y-%m-%d %H:%M" }}</span>
</div>
</div>
</div>
</div>
</header>
{% endif %}
{% if include.type == 'page' %}
<header class="intro-header" style="background-image: url('{{ site.baseurl }}/{% if page.header-img %}{{ page.header-img }}{% else %}{{ site.header-img }}{% endif %}')">
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
{% if include.short %}
<div class="site-heading" id="tag-heading">
{% else %}
<div class="site-heading">
{% endif %}
<h1>{% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %}</h1>
<span class="subheading">{{ page.description }}</span>
</div>
</div>
</div>
</div>
</header>
{% endif %}

View File

@ -0,0 +1,20 @@
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
TeX: {
equationNumbers: {
autoNumber: "AMS"
}
},
SVG: {
scale: 90
},
tex2jax: {
inlineMath: [ ['$','$'] ],
displayMath: [ ['$$','$$'] ],
processEscapes: true,
}
});
</script>
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_SVG">
</script>

View File

@ -0,0 +1,6 @@
<!-- Language Selector -->
<select class="sel-lang" onchange= "onLanChange(this.options[this.options.selectedIndex].value)">
<option value="0" selected> 中文 | Chinese </option>
<option value="1"> 英文 | English </option>
</select>

97
_includes/nav.html Normal file
View File

@ -0,0 +1,97 @@
<!-- Navigation -->
{% if page.nav-style == "invert" or page.header-style == "text" %}
<nav class="navbar navbar-default navbar-custom navbar-fixed-top invert">
{% else %}
<nav class="navbar navbar-default navbar-custom navbar-fixed-top">
{% endif %}
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header page-scroll">
<button type="button" class="navbar-toggle">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ site.baseurl }}/">{{ site.title }}</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div id="huxblog_navbar">
<div class="navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li>
<a href="{{ site.baseurl }}/">Home</a>
</li>
{% for page in site.pages %}
{% if page.title and page.hide-in-nav != true %}
<li>
<a href="{{ page.url | prepend: site.baseurl }}">{{ page.title }}</a>
</li>
{% endif %}
{% endfor %}
<li class="search-icon">
<a href="javascript:void(0)">
<i class="fa fa-search"></i>
</a>
</li>
</ul>
</div>
</div>
<!-- /.navbar-collapse -->
</div>
<!-- /.container -->
</nav>
<script>
// Drop Bootstarp low-performance Navbar
// Use customize navbar with high-quality material design animation
// in high-perf jank-free CSS3 implementation
var $body = document.body;
var $toggle = document.querySelector('.navbar-toggle');
var $navbar = document.querySelector('#huxblog_navbar');
var $collapse = document.querySelector('.navbar-collapse');
var __HuxNav__ = {
close: function () {
$navbar.className = " ";
// wait until animation end.
setTimeout(function () {
// prevent frequently toggle
if ($navbar.className.indexOf('in') < 0) {
$collapse.style.height = "0px"
}
}, 400)
},
open: function () {
$collapse.style.height = "auto"
$navbar.className += " in";
}
}
// Bind Event
$toggle.addEventListener('click', function (e) {
if ($navbar.className.indexOf('in') > 0) {
__HuxNav__.close()
} else {
__HuxNav__.open()
}
})
/**
* Since Fastclick is used to delegate 'touchstart' globally
* to hack 300ms delay in iOS by performing a fake 'click',
* Using 'e.stopPropagation' to stop 'touchstart' event from
* $toggle/$collapse will break global delegation.
*
* Instead, we use a 'e.target' filter to prevent handler
* added to document close HuxNav.
*
* Also, we use 'click' instead of 'touchstart' as compromise
*/
document.addEventListener('click', function (e) {
if (e.target == $toggle) return;
if (e.target.className == 'icon-bar') return;
__HuxNav__.close();
})
</script>

View File

@ -0,0 +1,242 @@
> Read at medium.com: [Upgrading Ele.me to Progressive Web Apps](https://medium.com/elemefe/upgrading-ele-me-to-progressive-web-app-2a446832e509)
Since the very first experiments that [@Vue.js tweeted][1], we at Ele.me (the biggest food ordering and delivering company in China) have been working on upgrading our mobile website to a [Progressive Web App][2]. Were proud to ship the world-first PWA exclusively for the Chinese market, but even prouder to collaborate with Google, UC and Tencent to push the boundary of web experience and browser supports in China.
## Multi-page, Vue, PWA?
There is a prevailing opinion that only structuring a web app as a Single Page App can we build PWAs that deliver app-like user experience. Popular reference examples including [Twitter Lite][3], [Flipkart Lite][4], [Housing Go][5] and [Polymer Shop][6] are all using the SPA model.
However at Ele.me, weve come to appreciate many advantages of a Multi-Page App model, and decided to refactor the mobile site from an Angular 1 SPA to a Multi-Paged app more than a year ago. The most important advantage we see is the isolation and decoupling between pages, which allows us to built different parts of the mobile site as “micro-services”. These services can then be independently iterated, embedded into 3rd-party apps, and even maintained by different teams.
Meanwhile, we still leverage [Vue.js](http://vuejs.org/) to boost our productivity. You may have heard of Vue as a rival of React or Angular, but Vue's lightweight and performance make it also a perfect replacement of traditional "jQuery/Zepto + template engine" stack when engineering a Multi-page app. We built every component as [Single File Components](http://vuejs.org/v2/guide/single-file-components.html) so they can be easily shareable between pages. The declarative-ness plus reactivity Vue offered help us manage both code and data flow. Oh, did I mention that [Vue is progressive](https://www.youtube.com/watch?v=pBBSp_iIiVM)? So things like Vuex or Vue-Router can be incrementally adopted if our site's complexity scales up, like...migrating to SPA again? (Who knows...)
In 2017, PWA seems to be all the rage, so we embark on exploring how far can our Vue-based Multi-page PWAs actually go.
## Implementing "PRPL" with MPA
I love [PRPL pattern][7] because it gives you a high-level abstraction of how to structure and design your own PWA systems. Since we are not rebuild everything from scratch, we decided taking implementing PRPL as our migration goal:
### 1. PUSH critical resources for initial URL route.
The key of pushing/preloading is to prioritize resources hidden in deep dependency graph and make browser's network stack busy ASAP. Let's say you have a SPA with code splitting by route, you can push/preload chunks for the current route before the "entry chunks" (e.g. webpack manifest, router) finish downloading and evaluating. So when the actual fetches happen, they might already be in caches.
Routes in MPAs naturally fetch code for that route only, and tend to have a flattening dependency graph. Most scripts depended by Ele.me are just `<script>` elements, so they can be found and fetched by [good old browser preloader][8] in early parsing phase without explicit `<link rel="preload">`.
![](/img/in-post/post-eleme-pwa/PUSH-link-rel-preload.jpg)
To take benefits from HTTP2 Multiplexing, we currently serve all critical resources under a single domain (no more domain sharding), and we are also experimenting on Server Push.
### 2. RENDER initial route & get it interactive ASAP
This one is essentially free (ridiculously obvious) in MPA since there's only one route at one time.
A straightforward rendering is critical for metrics such as First-Meaningful-Paint and Time-To-Interactive. MPAs gain it for free due to the simplicity of traditional HTML navigation they used.
### 3. **PRE-CACHE** remaining routes using Service Worker
This's the part [Service Worker][9] come to join the show. Service Worker is known as a client-side proxy enabling developers to intercept requests and serve responses from cache, but it can also perform initiative fetch to prefetch then precache future resources.
![](/img/in-post/post-eleme-pwa/PRECACHE-future-routes.jpg)
We already used [Webpack][10] in the build process to do `.vue` compilation and asset versioning, so we create a webpack plugin to help us collecting dependencies into a "precache manifest" and generating a new Service Worker file after each build. This is pretty much like [how SW-Precache works][11].
**In fact, we only collect dependencies of routes we flagged as "Critical Route".** You can think of them as ["App Shell"][12] or the "Installation Package" of our app. Once they are cached/installed successfully, our web app can boot up directly from cache and available offline. Routes that "not critical" would be incrementally cached at runtime during the first visit. Thanks to the LRU cache policies and TTL invalidation mechanisms provided by [SW-Toolbox][13], we have no worries of hitting the quota in a long run.
### 4. LAZY-load & instantiate remaining routes on demand
Lazy-loading and lazily instantiating remaining parts of the app is relatively challenging for SPA to achieve. It requires both code splitting and async importing. Fortunately, this is also a built-in feature of MPA model, in which routes are naturally separated.
Noticed that the lazy-loading can be done instantly if the requested route is already pre-cached in Service Worker cache, no matter whether SPA or MPA is used. #ServiceWorkerAwesomeness
---
Surprisingly, we found Multi-page PWA is kinda naturally "PRPL"! MPA has already provided built-in support for "PRL", and the second "P" involving Service Worker can be easily fulfilled in any PWA.
So what about the end result?
![](/img/in-post/post-eleme-pwa/Lighthouse-before.png)
**In [Lighthouse](https://developers.google.com/web/tools/lighthouse/) simulation (3G & 5x Slower CPU), we made Time-To-Interactive around 2 seconds,** and this was benchmarked on our HTTP1 test server.
The first visit is fast. The repeat visit with Service Worker is even faster. You can check out this video to see the huge difference between with or without Service Worker:
<iframe width="560" height="315" src="https://www.youtube.com/embed/mbi_WnunJa8" frameborder="0" allowfullscreen></iframe>
Did you see that? No, I mean the annoying blank screen. Even in the Service Worker one, the blank screen is still conspicuous during navigating. How can that be?
## Multi-page Pitfall: Redo Everything!
Unlike SPA, changing routes in MPA means actual browser navigation happens: The previous page is discarded completely and the browser need to redo everything for next route: re-download resources, re-parse HTML, re-evaluate JavaScript, re-decode image data, re-layout the page and re-paint the screen, even many of them could be shared across routes. All of these works combined requires significant computing and time.
So here is the profile (2x slower CPU simulated) of our entry page (most heavy one). Even we can make Time-To-Interactive around 1s in repeat visit, our users can still feel too slow for just "switching a tab".
![](/img/in-post/post-eleme-pwa/msite-Before-Optim.png)
### Huge JavaScript Re-Startup Cost
According to the profile, most of the time (900ms) before hitting the first paint is spent on evaluating JavaScript. Half is on dependencies including Vue Runtime, components, libraries etc., another half is on actual Vue starting-up and mounting. Because all UI rendering is depended on JavaScript/Vue, all of the critical scripts remain guiltily parser-blocking. I'm by no means blaming JavaScript or Vue's overheads here, It's just a tradeoff when we need this layer of abstraction in engineering.
**In SPA, JavaScript Start-up Cost is amortized during the whole lifecycle.** Parsing/Compiling for each script is only once, many heavy executing can be only once. The big JavaScript objects like Vue's ViewModels and Virtual DOM can be kept in memory and reused as much as you want. **This is not the case in MPA however.**
### Could Browser Caches Help?
Yes or no.
V8 introduced [code caching](http://v8project.blogspot.com/2015/07/code-caching.html), a way to store a local copy of compiled code so fetching, parsing and compilation could all be skipped next time. As @addyosmani mentioned in [JavaScript Start-up Performance](https://medium.com/reloading/javascript-start-up-performance-69200f43b201), scripts stored in Cache Storage via Service Worker could trigger code caching in just the first execution.
Another browser cache you might hear of is "Back-Forward Cache", or bfcache. The name varies, like Opera's "Fast History Navigation" or [WebKit's "Page Cache"](https://webkit.org/blog/427/webkit-page-cache-i-the-basics/). **The idea is that browsers can keep the previous page live in memory, i.e. DOM/JS states, instead of destroying everything.** In fact, this idea works very well for MPA. You can try every traditional Multi-page websites in iOS Safari and observe an instantaneously loading when back/forward. (With browser UI/Gesture or with hyperlink can have a slight difference though.)
Unfortunately, Chrome has no this kind of in-memory bfcache currently concerning to memory consumption and its multi-process architecture. It just leverages HTTP disk cache to simplify the loading pipeline, almost everything still needs to be redone. More details and discussions can be seen [here](https://docs.google.com/document/d/1o8KImLPrJQcMNqvd_a-1V8fEVgtVeEJww453ZQ1hGuo/edit#).
## Striving for Perceived Performance
Although the reality is dark, we don't want to give up so easily. One optimization we try to do is to render DOM nodes/create Virtual DOM nodes as less as possible to improve the Time-To-Interactive. While another opportunity we see is to play tricks on perceived performance.
@owencm have written a great post ["Reactive Web Design: The secret to building web apps that feel amazing"](https://medium.com/@owencm/reactive-web-design-the-secret-to-building-web-apps-that-feel-amazing-b5cbfe9b7c50) covering both "Instant loads with skeleton screens" and "Stable loads via predefined sizes on elements" to improve perceived performance and user experience. Yes, we actually used both.
What about we showing the end result after these optimizations first before entering technical nitty gritty? There you go!
<iframe width="560" height="315" src="https://www.youtube.com/embed/K5JBGnMYO1s" frameborder="0" allowfullscreen></iframe>
Too fast and can not see the pulsing Skeleton Screen clearly? Here is a version showing how it looks like under 10 times slower CPU.
<iframe width="560" height="315" src="https://www.youtube.com/embed/w1ZbNsHmRjs" frameborder="0" allowfullscreen></iframe>
This is a much better UX, right? Even we have slow navigation in slow devices, at least the UI is stable, consistent and always responding. So how we get there?
### Rendering Skeleton Screen with Vue at Build-Time
As you might have guessed, the Skeleton Screen that consists of markups, styles, and images is inlined into `*.html` of each route. So they can be cached by Service Worker, be loaded instantly, and be rendered independently with any JavaScript.
We don't want to manually craft each Skeleton Screen for each routes. It's a tedious job and we have to manually sync every change between Skeleton Screens and the actual UI components (Yes we treat every route as just a Vue component). But think about it, [Skeleton Screen is just a blank version of a page into which information is gradually loaded](https://www.lukew.com/ff/entry.asp?1797). What if we bake the Skeleton Screen into the actual UI component as just a loading state so we can render Skeleton Screen out directly from it without the issue of syncing?
Thanks to the versatility of Vue, we can actually realize it with [Vue.js Server-Side Rendering](https://ssr.vuejs.org/en/). Instead of using it on a real server, we use it at build time to render Vue components to strings and injected them into HTML templates.
### Fast Skeleton Painting...
Having markups in `*.html` doesn't mean that they will be painted fast, you have to make sure the [Critical Rendering Path](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/) is optimized for that. Many developers believed that putting script tags in the end of the body is sufficient for getting content painted before executing scripts. This might be true for browsers supporting rendering an incomplete DOM tree (e.g. streaming render), But browsers might not do that in mobile concerning slower hardwares, battery, and heats. **And even we are told that script tags with `async` or `defer` is not parser-blocking, it also doesn't mean we can get content painted before executing scripts in reality.**
![](https://html.spec.whatwg.org/images/asyncdefer.svg)
First I want to clarify it a little bit. According to the [Scripting section of HTML](https://www.w3.org/TR/html51/semantics-scripting.html#elementdef-script) (WHATWG living standard, the W3C's same here), `async` scripts would be evaluated as soon as it is available thus could potentially blocking parsing. Only `defer` (and not inlined) is specified to be never block parsing. That's why [Steve Souders](http://stevesouders.com/) ever posted ["Prefer DEFER Over ASYNC"](https://calendar.perfplanet.com/2016/prefer-defer-over-async/). (`defer` has its own issue and we will cover it later.)
**Then I want to say: A script not blocking parser could still block painting nonetheless.** So here is a reduced test I wrote named **"Minimal Multi-page PWA"**, or MMPWA, which basically render 1000 list items within an `async` (and truly not parser-blocking) script to see if we can get Skeleton Screen painted before scripts get executed. The profile below (over USB debugging on my real Nexus 5) shows my ignorance:
![](/img/in-post/post-eleme-pwa/thisTick-&-Load.png)
Yes, keep your mouth open. The first paint is blocked. I was also surprised here. The reason I guess is that **if we touch DOM so quickly that the browser has still NOT finished previous painting job, our dear browser has to abort every pixel it has drawn, and has to wait until current DOM manipulation task finished and redo the rendering pipeline again.** And this more often happens with a mobile device with a slower CPU/GPU.
### Fast Skeleton Painting with setTimeout Hack
We indeed encountered this problem when testing our new beautiful Skeleton Screen. Perhaps Vue finishes its job and start to mount nodes too fast ;). But anyway we have to make it slower, or rather lazier. So we try to put DOM manipulation things inside `setTimeout(callback, 0)`, and it works like a charm!
![](/img/in-post/post-eleme-pwa/nextTick-&-Load.png)
I think you may curious about how this change performs in the wild, so I have refined MMPWA by rendering 5000 list items rather 1000 to make the differences more obvious, and by designing it in an A/B testing manner. The code is on [Github](https://github.com/Huxpro/mmpwa) and the demo is live on [huangxuan.me/mmpwa/](https://huangxuan.me/mmpwa). Here is also a video for loungers.
<iframe width="560" height="315" src="https://www.youtube.com/embed/3Ws7XBHrPD8" frameborder="0" allowfullscreen></iframe>
This famous `setTimeout` hack (a.k.a. Zero Delays) looks quite magic, but it is science™. If you are familiar with **event loop**, it just prevents these code from executing in the current loop by putting everything to the **task queues** with the Timer Callback, so the browser could breath (update the rendering) in the main thread.
So we applied what we learned from MMPWA by putting `new Vue()` inside `setTimeout` and BOOM! We have Skeleton Screen painted consistently after every navigating! Here is the profile after all these optimizations.
![](/img/in-post/post-eleme-pwa/msite-After-Optim.png)
Huge improvements right? This time we hit First Paint (Skeleton Screen Paint) at 400ms and TTI at 600ms. You should really go back to have a before-after comparison in details.
### One more thing that I deferred
But wait, why is there still a bunch of guiltily parser-blocking scripts? Are them all `async`? OK, ok. For historical reasons, we do keep some parser-blocking scripts, like [lib-flexible](https://github.com/amfe/lib-flexible), we couldn't get rid of it without a huge refactoring. But most of these blocking scripts are in fact `defer`. We expected that they can be executed after parsing and in order, however the profile kinda slap on my face. :(
Remember I said I would talk about one issue of `defer` previously? Yes, that's it. I have had a [conversation](https://twitter.com/Huxpro/status/859842124849827841) with [Jake Archibald](https://twitter.com/jaffathecake) and it turns out it might be a bug of Chrome when the deferred scripts are fully cached. [Vote it at crbug](https://bugs.chromium.org/p/chromium/issues/detail?id=717979)!
Similar improvements can be seen from Lighthouse (Under same server and network environment). A Pro Tip is you should always use lighthouse in a variable controlling approach.
![](/img/in-post/post-eleme-pwa/Lighthouse-after.png)
### Performance In the Real World
[Alex Russell](https://medium.com/@slightlylate) has given [a very insightful talk on mobile web performance](https://youtu.be/4bZvq3nodf4?list=PLNYkxOF6rcIBTs2KPy1E6tIYaWoFcG3uj) at Chrome Dev Summit 2016, talking about how hard can we build performant web applications on mobile devices. Highly recommended.
Chinese users tend to have a pretty powerful phone. MI4 is shipped with snapdragon 801 (slightly out-performs Nexus 5) but only costs 100$. Its affordable by at least 80% of our users so we take it as a baseline.
Here is a video screen-recorded on my Nexus 5 showing switching between 4 tabs. The performance varies between tabs due to their variant scale. The heaviest one, entry page, take around 1s to hit real Time-To-Interactive on my Nexus 5.
FYI. This is surprisingly comparable to what I get from Chrome Simulation with 2x CPU throttling. With 5x throttling, this can spend 23s to get TTI, horribly. (To be honest, I found even under same throttling, the results can vary drastically depended on my Macbooks “mood”.)
<iframe width="700" height="525" src="https://www.youtube.com/embed/ZLc8jysMqaw?ecver=1" frameborder="0" allowfullscreen></iframe>
## Final Thoughts
This article is much longer than I could imagine. I am really appreciated if you could get here. So what can we learn from it?
### MPA still has some way to go
[Jake Archibald](https://twitter.com/jaffathecake) ever said that "PWA !== SPA" at [Chrome Dev Summit 2016](https://youtu.be/J2dOTKBoTL4?list=PLNYkxOF6rcIBTs2KPy1E6tIYaWoFcG3uj). But the sad truth is that even we have taken advantages of bleeding edge technologies such as "PRPL" pattern, Service Worker, App-Shell, Skeleton Screen, there is still a distance between us and many Single Page PWA just because we are Multi-page structured.
The web is extremely versatile. Static blogs, e-business sites, desktop-level software, all of them should be the first-class citizens of the web family. MPA might have things like "bfcache API", navigation transitions to catch up the SPA in the future, but it is not today certainly.
### PWA is Awesome No Matter What
Hey, I am not overblowing it. Even we as a Multi-page PWA couldn't be as stunning and app-like as many Single Page PWAs are. The idea and technologies behind PWA still help us deliver a much better experience to our users on the web that hasnt been possible before.
What PWA is trying to solve are some fundamental problems of current web application model such as its hard dependencies to network and browser UIs. That' why PWA can be always beneficial no matter what architecture or what framework you actually used. [Addy Osmani](https://medium.com/@addyosmani) would give a talk [Production Progressive Web Apps With JavaScript Frameworks](https://events.google.com/io/schedule/?section=may-19&sid=e8436b55-ea89-4243-a644-5ecb319d9ef0) at this year's I/O (and [I/O 16](https://youtu.be/srdKq0DckXQ?list=PLNYkxOF6rcIDz1TzmmMRBC-kd8zPRTQIP)). You wont want to miss it!
---
Finally, Id love to thank:
- my colleagues [YiSi Wang](https://github.com/YiSiWang), [GuangHui Ren](https://github.com/rguanghui), [JiyinYiyong](https://medium.com/@jiyinyiyong) from Eleme
- collaborator [Michael Yeung](https://medium.com/@micyeung), [Liam Spradlin](https://medium.com/@LiamSpradlin) and other collaborators from Google
- collaborators from UC/Tencent
And special thanks to 
- invited reviewer, [Evan You](https://medium.com/@youyuxi).
- Chrome “StackOverflow”, [Jake Archibald](https://twitter.com/jaffathecake).
Thank you all!
---
## Appendix. Architecture Diagram
![](/img/in-post/post-eleme-pwa/Architecture.png)
[1]: https://twitter.com/vuejs/status/834087199008239619
[2]: https://developers.google.com/web/progressive-web-apps/
[3]: https://blog.twitter.com/2017/how-we-built-twitter-lite
[4]: https://medium.com/progressive-web-apps/building-flipkart-lite-a-progressive-web-app-2c211e641883
[5]: https://medium.com/engineering-housing/progressing-mobile-web-fac3efb8b454
[6]: https://shop.polymer-project.org/
[7]: https://developers.google.com/web/fundamentals/performance/prpl-pattern/
[8]: https://calendar.perfplanet.com/2013/big-bad-preloader/
[9]: https://w3c.github.io/ServiceWorker/v1/
[10]: https://webpack.github.io/
[11]: https://medium.com/@Huxpro/how-does-sw-precache-works-2d99c3d3c725
[12]: https://developers.google.com/web/updates/2015/11/app-shell
[13]: https://googlechrome.github.io/sw-toolbox/

View File

@ -0,0 +1,207 @@
> 很荣幸在今年 2 月到 5 月的时间里,以顾问的身份加入饿了么,参与 PWA 的相关工作。这篇文章其实最初是在以英文写作发表在 medium 上的:[Upgrading Ele.me to Progressive Web Apps](https://medium.com/elemefe/upgrading-ele-me-to-progressive-web-app-2a446832e509),获得了一定的关注。所以也决定改写为中文版本再次分享出来,希望能对你有所帮助 ;) <br><br>
> 本文首发于 [CSDN](http://geek.csdn.net/news/detail/210535) 与《程序员》2017 年 7 月刊,同步发布于 [饿了么前端 - 知乎专栏](https://zhuanlan.zhihu.com/ElemeFE)、[Hux Blog](https://huangxuan.me),转载请保留链接。
自 Vue.js 官方推特第一次[公开][1]到现在,我们就一直在进行着将[饿了么移动端网站](https://h5.ele.me/msite/#pwa=true)升级为 [Progressive Web App][2] 的工作。直到近日在 Google I/O 2017 上[登台亮相](https://m.weibo.cn/status/4109332495285652),才终于算告一段落。我们非常荣幸能够发布全世界第一个专门面向国内用户的 PWA但更荣幸的是能与 Google、UC 以及腾讯合作,一起推动国内 web 与浏览器生态的发展。
## 多页应用、Vue、PWA
对于构建一个希望达到原生应用级别体验的 PWA目前社区里的主流做法都是采用 SPA即单页面应用模型Single-page App来组织整个 web 应用,业内最有名的几个 PWA 案例 [Twitter Lite][3]、 [Flipkart Lite][4]、[Housing Go][5] 与 [Polymer Shop][6] 无一例外。
然而饿了么与很多国内的电商网站一样青睐多页面应用模型MPAMulti-page App所能带来的一些好处也因此在一年多将移动站从基于 Angular.js 的单页应用重构为目前的多页应用模型。团队最看重的优点莫过于页面与页面之间的隔离与解耦,这使得我们可以将每个页面当做一个独立的“微服务”来看待,这些服务可以被独立迭代,独立提供给各种第三方的入口嵌入,甚至被不同的团队独立维护。而整个网站则只是各种服务的集合而非一个巨大的整体。
与此同时,我们仍然依赖 [Vue.js](http://vuejs.org/) 作为 JavaScript 框架。Vue 除了是 React/Angular 这种“重型武器”的竞争对手外,其轻量与高性能的优点使得它同样可以作为传统多页应用开发中流行的 “jQuery/Zepto/Kissy + 模板引擎” 技术栈的完美替代。Vue 提供的组件系统、声明式与响应式编程更是提升了代码组织、共享、数据流控制、渲染等各个环节的开发效率。[Vue 还是一个渐进式框架]((https://www.youtube.com/watch?v=pBBSp_iIiVM)),如果网站的复杂度继续提升,我们可以按需、增量地引入 Vuex 或 Vue-Router 这些模块。万一哪天又要改回单页呢?(谁知道呢……)
2017 年PWA 已经成为 web 应用新的风潮。我们决定试试以我们现有的“Vue + 多页”的架构,能在升级 PWA 的道路上走多远,达到怎样的效果。
## 实现 “PRPL” 模式
[“PRPL”][7](读作 “purple”是 Google 的工程师提出的一种 web 应用架构模式,它旨在利用现代 web 平台的新技术以大幅优化移动 web 的性能与体验,对如何组织与设计高性能的 PWA 系统提供了一种高层次的抽象。我们并不准备从头重构我们的 web 应用,不过我们可以把实现 “PRPL” 模式作为我们的迁移目标。“PRPL”实际上是 Push/Preload、Render、Precache、Lazy-Load 的缩写,我们会在下文中展开它们的具体含义。
### 1. PUSH/PRELOAD推送/预加载初始 URL 路由所需的关键资源。
无论是 HTTP2 Server Push 还是 `<link rel="preload">`其关键都在于我们希望提前请求一些隐藏在应用依赖关系Dependency Graph较深处的资源以节省 HTTP 往返、浏览器解析文档、或脚本执行的时间。比如说,对于一个基于路由进行 code splitting 的 SPA如果我们可以在 webpack 清单、路由等入口代码entry chunks被下载与运行之前就把初始 URL即用户访问的入口 URL 路由所依赖的代码用 Server Push 推送或 `<link rel="preload">` 进行提前加载。那么当这些资源被真正请求时,它们可能已经下载好并存在在缓存中了,这样就加快了初始路由所有依赖的就绪。
在多页应用中,每一个路由本来就只会请求这个路由所需要的资源,并且通常依赖也都比较扁平。饿了么移动站的大部分脚本依赖都是普通的 `<script>` 元素,因此他们可以在文档解析早期就被浏览器的 preloader 扫描出来并且开始请求,其效果其实与显式的 `<link rel="preload">` 是一致的。
![](/img/in-post/post-eleme-pwa/PUSH-link-rel-preload.jpg)
我们还将所有关键的静态资源都伺服在同一域名下(不再做域名散列),以更好的利用 HTTP2 带来的多路复用Multiplexing。同时我们也在进行着对 API 进行 Server Push 的[实验](https://zhuanlan.zhihu.com/p/26757514)。
### 2. RENDER渲染初始路由尽快让应用可被交互
既然所有初始路由的依赖都已经就绪,我们就可以尽快开始初始路由的渲染,这有助于提升应用诸如首次渲染时间、可交互时间等指标。多页应用并不使用基于 JavaScript 的路由,而是传统的 HTML 跳转机制,所以对于这一部分,多页应用其实不用额外做什么。
### 3. PRE-CACHE用 Service Worker 预缓存剩下的路由
这一部分就需要 [Service Worker][9] 的参与了Service Worker 是一个位于浏览器与网络之间的客户端代理,它以可拦截、处理、响应流经的 HTTP 请求,使得开发者得以从缓存中向 web 应用提供资源而闻名。不过Service Worker 其实也可以主动发起 HTTP 请求,在“后台” 预请求与预缓存我们未来所需要的资源。
![](/img/in-post/post-eleme-pwa/PRECACHE-future-routes.jpg)
我们已经使用 [Webpack][10] 在构建过程中进行 `.vue` 编译、文件名哈希等工作,于是我们编写了一个 webpack 插件来帮助我们收集需要缓存的依赖到一个“预缓存清单”中,并使用这个清单在每次构建时生成新的 Service Worker 文件。在新的 Service Worker 被激活时,清单里的资源就会被请求与缓存,这其实与 [SW-Precache 这个库的运行机制][11]非常接近。
**实际上,我们只对我们标记为“关键路由”的路由进行依赖收集。**你可以将这些“关键路由”的依赖理解为我们整个应用的 ["App Shell"][12] 或者说“安装包”。一旦它们都被缓存,或者说成功安装,无论用户是在线离线,我们的 web 应用都可以从缓存中直接启动。对于那些并不那么重要的路由,我们则采取在运行时增量缓存的方式。我们使用的 [SW-Toolbox][13] 提供了 LRU 替换策略与 TTL 失效机制,可以保证我们的应用不会超过浏览器的缓存配额。
### 4. LAZY-LOAD 按需懒加载、懒实例化剩下的路由
懒加载与懒实例化剩下的路由对于 SPA 是一件相对麻烦点儿的事情,你需要实现基于路由的 code splitting 与异步加载。幸运的是,这又是一件不需要多页应用担心的事情,多页应用中的各个路由天生就是分离的。
值得说明的是,无论单页还是多页应用,如果在上一步中,我们已经将这些路由的资源都预先下载与缓存好了,那么懒加载就几乎是瞬时完成的了,这时候我们就只需要付出实例化的代价。
---
这四句话即是 PRPL 的全部了。有趣的是,我们发现多页应用在实现 PRPL 这件事甚至比单页还要容易一些。那么结果如何呢?
![](/img/in-post/post-eleme-pwa/Lighthouse-before.png)
根据 Google 推出的 Web 性能分析工具 Lighthousev1.6),在模拟的 3G 网络下,用户的初次访问(无任何缓存)大约在 2 秒左右达到“可交互”,可以说非常不错。而对于再次访问,由于所有资源都直接来自于 Service Worker 缓存,页面可以在 1 秒左右就达到可交互的状态了。
但是,故事并不是这么简单得就结束了。在实际的体验中我们发现,**应用在页与页的切换时,仍然存在着非常明显的白屏空隙**,由于 PWA 是全屏运行的,白屏对用户体验所带来的负面影响甚至比以往在浏览器内更大。我们不是已经用 Service Worker 缓存了所有资源了吗,怎么还会这样呢?
![](/img/in-post/post-eleme-pwa/before-skeleton.jpg)
*从首页点击到发现页,跳转过程中的白屏*
## 多页应用的陷阱:重启开销
与 SPA 不同在多页应用中路由的切换是原生的浏览器文档跳转Navigating across documents这意味着之前的页面会被完全丢弃而浏览器需要为下一个路由的页面重新执行所有的启动步骤重新下载资源、重新解析 HTML、重新运行 JavaScript、重新解码图片、重新布局页面、重新绘制……即使其中的很多步骤本是可以在多个路由之间复用的。这些工作无疑将产生巨大的计算开销也因此需要付出相当的时间成本。
图中为我们的入口页(同时也是最重的页面)在 2 倍 CPU 节流模拟下的 profile 数据。即使我们可以将“可交互时间”控制在 1 秒左右,我们的用户仍然会觉得这对于“仅仅切换个标签”来说实在是太慢了。
![](/img/in-post/post-eleme-pwa/msite-Before-Optim.png)
### 巨大的 JavaScript 重启开销
根据 Profile我们发现在首次渲染First Paint发生之前大量的时间900 毫秒)都消耗在了 JavaScript 的运行上Evaluate Script。几乎所有脚本都是阻塞的Parser-blocking不过因为所有的 UI 都是由 JavaScript/Vue 驱动的,倒也不会有性能影响。这 900ms 中,约一半是消耗在包括 Vue 运行时、组件、库等依赖的运行上,而另一半则花在了业务组件实例化时 Vue 的启动与渲染上。从软件工程角度来说,我们需要这些抽象,所以这里并不是想责怪 JavaScript 或是 Vue 所带来的开销。
**但是,在 SPA 中JavaScript 的启动成本是均摊到整个生命周期的:** 每个脚本都只需要被解析与编译一次,诸如生成 Virtual DOM 等较重的任务可以只执行一次,像 Vue 的 ViewModel 或是 Virtual DOM 这样的大对象也可以被留在内存里复用。**可惜在多页应用里就不是这样了,我们每次切换页面都为 JavaScript 付出了巨大的重启代价。**
### 浏览器的缓存啊,能不能帮帮忙?
能,也不能。
V8 提供了[代码缓存code caching](http://v8project.blogspot.com/2015/07/code-caching.html),可以将编译后的机器码在本地拷贝一份,这样我们就可以在下次请求同一个脚本时一次省略掉请求、解析、编译的所有工作。而且,对于缓存在 Service Worker 配套的 Cache Storage 中的脚本,会在第一次执行后就触发 V8 的代码缓存,这对于我们的多页切换能提供不少帮助。
另外一个你或许听过的浏览器缓存叫做“进退缓存”Back-Forward Cache简称 bfcache。浏览器厂商对其的命名各异Opera 称之为 Fast History NavigationWebkit 称其为 Page Cache。但是思路都一样**就是我们可以让浏览器在跳转时把前一页留存在内存中,保留 JavaScript 与 DOM 的状态,而不是全都销毁掉。**你可以随便找个传统的多页网站在 iOS Safari 上试试,无论是通过浏览器的前进后退按钮、手势,还是通过超链接(会有一些不同),基本都可以看到瞬间加载的效果。
Bfcache 其实非常适合多页应用。但不幸的是Chrome 由于内存开销与其多进程架构等原因目前并不支持。Chrome 现阶段仅仅只是用了传统的 HTTP 磁盘缓存,来稍稍简化了一下加载过程而已。对于 Chromium 内核霸占的 Android 生态来说,我们没法指望了。
## 为“感知体验”奋斗
尽管多页应用面临着现实中的不少性能问题,我们并不想这么快就妥协。一方面,我们尝试尽可能减少在页面达到可交互时间前的代码执行量,比如减少/推迟一些依赖脚本的执行,还有减少初次渲染的 DOM 节点数以节省 Virtual DOM 的初始化开销。另一方面,我们也意识到应用在感知体验上还有更多的优化空间。
Chrome 产品经理 Owen 写过一篇 [Reactive Web Design: The secret to building web apps that feel amazing](https://medium.com/@owencm/reactive-web-design-the-secret-to-building-web-apps-that-feel-amazing-b5cbfe9b7c50)谈到两种改进感知体验的手段一是使用骨架屏Skeleton Screen来实现瞬间加载二是预先定义好元素的尺寸来保证加载的稳定。跟我们的做法可以说不谋而合。
为了消除白屏时间,我们同样引入了尺寸稳定的骨架屏来帮助我们实现瞬间的加载与占位。即使是在硬件很弱的设备上,我们也可以在点击切换标签后立刻渲染出目标路由的骨架屏,以保证 UI 是稳定、连续、有响应的。我录了[两个](https://youtu.be/K5JBGnMYO1s)[视频](https://youtu.be/w1ZbNsHmRjs)放在 Youtube 上,不过如果你是国内读者,你可以直接访问饿了么移动网站来体验实地的效果 ;) 最终效果如下图所示。
![](/img/in-post/post-eleme-pwa/after-skeleton.jpg)
*在添加骨架屏后,从发现页点回首页的效果*
这效果本该很轻松的就能实现,不过实际上我们还费了点功夫。
### 在构建时使用 Vue 预渲染骨架屏
你可能已经想到了,为了让骨架屏可以被 Service Worker 缓存,瞬间加载并独立于 JavaScript 渲染,我们需要把组成骨架屏的 HTML 标签、CSS 样式与图片资源一并内联至各个路由的静态 `*.html` 文件中。
不过,我们并不准备手动编写这些骨架屏。你想啊,如果每次真实组件有迭代(每一个路由对我们来说都是一个 Vue 组件)我们都需要手动去同步每一个变化到骨架屏的话,那实在是太繁琐且难以维护了。好在,[骨架屏不过是当数据还未加载进来前,页面的一个空白版本而已](https://www.lukew.com/ff/entry.asp?1797)。如果我们能将骨架屏实现为真实组件的一个特殊状态 —— “空状态”的话,我们理论上就可以从真实组件中直接渲染出骨架屏来。
而 Vue 的多才多艺就在这时体现出来了,我们真的可以用 [Vue.js 的服务端渲染模块](https://ssr.vuejs.org/en/) 来实现这个想法,不过不是用在真正的服务器上,而是在构建时用它把组件的空状态预先渲染成字符串并注入到 HTML 模板中。你需要调整你的 Vue 组件代码使得它可以在 Node 上执行,有些页面对 DOM/BOM 的依赖一时无法轻易去除得,我们目前只好额外编写一个 `*.shell.vue` 来暂时绕过这个问题。
### 关于浏览器的绘制Painting
HTML 文件中有标签并不意味着这些标签就能立刻被绘制到屏幕上,你必须保证页面的[关键渲染路径](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/)是为此优化的。很多开发者相信将 script 标签放在 body 的底部就足以保证内容能在脚本执行之前被绘制,这对于能渲染不完整 DOM 树的浏览器(比如桌面浏览器常见的流式渲染)来说可能是成立的。但移动端的浏览器很可能因为考虑到较慢的硬件、电量消耗等因素并不这么做。**不仅如此,即使你曾被告知设为 `async``defer` 的脚本就不会阻塞 HTML 解析了,但这可不意味着浏览器就一定会在执行它们之前进行渲染。**
![](https://html.spec.whatwg.org/images/asyncdefer.svg)
首先我想澄清的是,根据 [HTML 规范 Scripting 章节](https://html.spec.whatwg.org/multipage/scripting.html)`async` 脚本是在其请求完成后立刻运行的,因此它本来就可能阻塞到解析。只有 `defer`(且非内联)与最新的 `type=module` 被指定为“一定不会阻塞解析”。(不过 `defer` 目前也有点小问题……我们稍后会再提到)
**而更重要的是,一个不阻塞 HTML 解析的脚本仍然可能阻塞到绘制。**我做了一个简化的**“最小多页 PWA”**Minimal Multi-page PWA或 MMPWA来测试这个问题我们在一个 `async`(且确实不阻塞 HTML 解析)脚本中,生成并渲染 1000 个列表项,然后测试骨架屏能否在脚本执行之前渲染出来。下面是通过 USB Debugging 在我的 Nexus 5 真机上录制的 profile
![](/img/in-post/post-eleme-pwa/thisTick-&-Load.png)
是的,出乎意料吗?首次渲染确实被阻塞到脚本执行结束后才发生。究其原因,**如果我们在浏览器还未完成上一次绘制工作之前就过快得进行了 DOM 操作,我们亲爱的浏览器就只好抛弃所有它已经完成的像素,且一直要等待到 DOM 操作引起的所有工作结束之后才能重新进行下一次渲染。**而这种情况更容易在拥有较慢 CPU/GPU 的移动设备上出现。
### 黑魔法:利用 setTimeout() 让绘制提前
不难发现,骨架屏的绘制与脚本执行实际是一个竞态。大概是 Vue 太快了,我们的骨架屏还是有非常大的概率绘制不出来。于是我们想着如何能让脚本执行慢点,或者说,“懒”点。于是我们想到了一个经典的 Hack `setTimeout(callback, 0)`。我们试着把 MMPWA 中的 DOM 操作(渲染 1000 个列表)放进 `setTimeout(callback, 0)` 里……
![](/img/in-post/post-eleme-pwa/nextTick-&-Load.png)
当当!首次渲染瞬间就被提前了。如果你熟悉浏览器的**事件循环模型event loop**的话,这招 Hack 其实是通过 setTimeout 的回调把 DOM 操作放到了事件循环的任务队列中以避免它在当前循环执行,这样浏览器就得以在主线程空闲时喘息一下(更新一下渲染)了。如果你想亲手试试 MMPWA 的话,你可以访问 [github.com/Huxpro/mmpwa](https://github.com/Huxpro/mmpwa) 或 [huangxuan.me/mmpwa/](https://huangxuan.me/mmpwa) 访问代码与 Demo。我把 UI 设计为了 A/B Test 的形式并改为渲染 5000 个列表项来让效果更夸张一些。
回到饿了么 PWA 上,我们同样试着把 `new Vue()` 放到了 `setTimeout` 中。果然,黑魔法再次显灵,骨架屏在每次跳转后都能立刻被渲染。这时的 Profile 看起来是这样的:
![](/img/in-post/post-eleme-pwa/msite-After-Optim.png)
现在,我们在 400ms 时触发首次渲染(骨架屏),在 600ms 时完成真实 UI 的渲染并达到页面的可交互。你可以拉上去详细对比下优化前后 profile 的区别。
### 被我 “defer” 的有关 `defer` 的 Bug
不知道你发现没有,在上图的 Profile 中,我们仍然有不少脚本是阻塞了 HTML 解析的。好吧让我解释一下,由于历史原因,我们确实保留了一部分的阻塞脚本,比如侵入性很强的 [lib-flexible](https://github.com/amfe/lib-flexible)我们没法轻易去除它。不过profile 里的大部分阻塞脚本实际上都设置了 `defer`,我们本以为他们应该在 HTML 解析完成之后才被执行,结果被 profile 打了一脸。
我和 [Jake Archibald](https://twitter.com/jaffathecake) [聊了一下](https://twitter.com/Huxpro/status/859842124849827841),果然这是 Chrome 的 Bug`defer` 的脚本被完全缓存时并没有遵守规范等待解析结束反而阻塞了解析与渲染。Jake 已经提交在 [crbug](https://bugs.chromium.org/p/chromium/issues/detail?id=717979) 上了,一起给它投票吧~
最后,是优化后的 Lighthouse 跑分结果,同样可以看到明显的性能提升。需要说明的是,能影响 Lighthouse 跑分的因素有很多,所以我建议你以控制变量(跑分用的设备、跑分时的网络环境等)的方式来进行对照实验。
![](/img/in-post/post-eleme-pwa/Lighthouse-after.png)
最后附上一张图,这张图当时是做给 Addy Osmani 的 I/O 演讲用的,描述了饿了么 PWA 是如何结合 Vue 来实现多页应用的 PRPL 模式,可以作为一个架构的参考与示意图。
![](/img/in-post/post-eleme-pwa/Architecture.png)
## 一些感想
### 多页应用仍然有很长的路要走
Web 是一个极其多样化的平台。从静态的博客,到电商网站,再到桌面级的生产力软件,它们全都是 Web 这个大家庭的第一公民。而我们组织 web 应用的方式也同样只会更多而不会更少多页、单页、Universal JavaScript 应用、WebGL、以及可以预见的 Web Assembly。不同的技术之间没有贵贱但是适用场景的差距确是客观存在的。
[Jake](https://twitter.com/jaffathecake) 曾在 [Chrome Dev Summit 2016](https://youtu.be/J2dOTKBoTL4?list=PLNYkxOF6rcIBTs2KPy1E6tIYaWoFcG3uj) 上说过 "PWA !== SPA"。可是尽管我们已经用上了一系列最新的技术PRPL、Service Worker、App Shell……我们仍然因为多页应用模型本身的缺陷有着难以逾越的一些障碍。多页应用在未来可能会有“bfcache API”、Navigation Transition 等新的规范以缩小跟 SPA 的距离,不过我们也必须承认,时至今日,多页应用的局限性也是非常明显的。
### 而 PWA 终将带领 web 应用进入新的时代
即使我们的多页应用在升级 PWA 的路上不如单页的那些来得那么闪亮,但是 PWA 背后的想法与技术却实实在在的帮助我们在 web 平台上提供了更好的用户体验。
PWA 作为[下一代 Web 应用模型](https://zhuanlan.zhihu.com/p/25167289),其尝试解决的是 web 平台本身的根本性问题:对网络与浏览器 UI 的硬依赖。因此,任何 web 应用都可以从中获益,这与你是多页还是单页、面向桌面还是移动端、是用 React 还是 Vue 无关。或许,它还终将改变用户对移动 web 的期待。现如今,谁还觉得桌面端的 web 只是个看文档的地方呢?
还是那句老话:让我们的用户,也像我们这般热爱 web 吧。
---
最后感谢饿了么的王亦斯、任光辉、题叶Google 的 Michael Yeung、DevRel 团队, UC 浏览器团队,腾讯 X5 浏览器团队在这次项目中的合作。感谢尤雨溪、陈蒙迪和 Jake Archibald 在写作过程中给予我的帮助。
[1]: https://twitter.com/vuejs/status/834087199008239619
[2]: https://developers.google.com/web/progressive-web-apps/
[3]: https://blog.twitter.com/2017/how-we-built-twitter-lite
[4]: https://medium.com/progressive-web-apps/building-flipkart-lite-a-progressive-web-app-2c211e641883
[5]: https://medium.com/engineering-housing/progressing-mobile-web-fac3efb8b454
[6]: https://shop.polymer-project.org/
[7]: https://developers.google.com/web/fundamentals/performance/prpl-pattern/
[8]: https://calendar.perfplanet.com/2013/big-bad-preloader/
[9]: https://w3c.github.io/ServiceWorker/v1/
[10]: https://webpack.github.io/
[11]: https://medium.com/@Huxpro/how-does-sw-precache-works-2d99c3d3c725
[12]: https://developers.google.com/web/updates/2015/11/app-shell
[13]: https://googlechrome.github.io/sw-toolbox/

18
_includes/search.html Normal file
View File

@ -0,0 +1,18 @@
<!-- Search -->
<div class="search-page">
<div class="search-icon-close-container">
<span class="search-icon-close">
<i class="fa fa-chevron-down"></i>
</span>
</div>
<div class="search-main container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<form></form>
<input type="text" id="search-input" placeholder="$ grep...">
</form>
<div id="search-results" class="mini-post-list"></div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,14 @@
<section class="visible-md visible-lg">
<hr>
<h5><a href="{{'/about/' | prepend: site.baseurl }}">ABOUT ME</a></h5>
<div class="short-about">
{% if site.sidebar-avatar %}
<img src="{{site.sidebar-avatar}}" />
{% endif %}
{% if site.sidebar-about-description %}
<p>{{site.sidebar-about-description}}</p>
{% endif %}
<!-- SNS Link -->
{% include sns-links.html %}
</div>
</section>

81
_includes/sns-links.html Normal file
View File

@ -0,0 +1,81 @@
{% comment %}
@param {Boolean} center
{% endcomment %}
{% if include.center %}
<ul class="list-inline text-center">
{% else %}
<ul class="list-inline">
{% endif %}
{% if site.RSS %}
<li>
<a href="{{ "/feed.xml" | prepend: site.baseurl }}">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-rss fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
{% endif %}
{% if site.twitter_username %}
<li>
<a href="https://twitter.com/{{ site.twitter_username }}">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-twitter fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
{% endif %}
{% if site.zhihu_username %}
<li>
<a target="_blank" href="https://www.zhihu.com/people/{{ site.zhihu_username }}">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
{% endif %}
{% if site.weibo_username %}
<li>
<a target="_blank" href="http://weibo.com/{{ site.weibo_username }}">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-weibo fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
{% endif %}
{% if site.facebook_username %}
<li>
<a target="_blank" href="https://www.facebook.com/{{ site.facebook_username }}">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-facebook fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
{% endif %}
{% if site.github_username %}
<li>
<a target="_blank" href="https://github.com/{{ site.github_username }}">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-github fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
{% endif %}
{% if site.linkedin_username %}
<li>
<a target="_blank" href="https://www.linkedin.com/in/{{ site.linkedin_username }}">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-linkedin fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
{% endif %}
</ul>

23
_layouts/default.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
{% include head.html %}
<!-- hack iOS CSS :active style -->
<body ontouchstart="">
{% include nav.html %}
{% include search.html %}
{{ content }}
{% include footer.html %}
<!-- Image to hack wechat -->
<img src="/img/icon_wechat.png" width="0" height="0" />
<!-- Migrate from head to bottom, no longer block render and still work -->
</body>
</html>

150
_layouts/keynote.html Normal file
View File

@ -0,0 +1,150 @@
---
layout: default
---
<!-- Image to hack wechat -->
<!-- <img src="/img/icon_wechat.png" width="0" height="0"> -->
<!-- <img src="{{ site.baseurl }}/{% if page.header-img %}{{ page.header-img }}{% else %}{{ site.header-img }}{% endif %}" width="0" height="0"> -->
<!-- Post Header -->
{% include intro-header.html type='keynote' %}
<!-- Post Content -->
<article>
<div class="container">
<div class="row">
<!-- Post Container -->
<div class="post-container
col-lg-8 col-lg-offset-2
col-md-10 col-md-offset-1 ">
{{ content }}
<hr style="visibility: hidden;">
<ul class="pager">
{% if page.previous.url %}
<li class="previous">
<a href="{{ page.previous.url | prepend: site.baseurl | replace: '//', '/' }}" data-toggle="tooltip" data-placement="top" title="{{page.previous.title}}">
Previous<br>
<span>{{page.previous.title}}</span>
</a>
</li>
{% endif %}
{% if page.next.url %}
<li class="next">
<a href="{{ page.next.url | prepend: site.baseurl | replace: '//', '/' }}" data-toggle="tooltip" data-placement="top" title="{{page.next.title}}">
Next<br>
<span>{{page.next.title}}</span>
</a>
</li>
{% endif %}
</ul>
<hr style="visibility: hidden;">
{% if site.disqus_username %}
<!-- disqus 评论框 start -->
<div class="comment">
<div id="disqus_thread" class="disqus-thread">
</div>
</div>
<!-- disqus 评论框 end -->
{% endif %}
</div>
<!-- Sidebar Container -->
<div class="sidebar-container
col-lg-8 col-lg-offset-2
col-md-10 col-md-offset-1 ">
<!-- Featured Tags -->
{% include featured-tags.html %}
<!-- Friends Blog -->
{% include friends.html %}
</div>
</div>
</div>
</article>
<!-- resize header to fullscreen keynotes -->
<script>
var $header = document.getElementsByTagName("header")[0];
function resize(){
/*
* leave 85px to both
* - told/imply users that there has more content below
* - let user can scroll in mobile device, seeing the keynote-view is unscrollable
*/
$header.style.height = (window.innerHeight-85) + 'px';
}
document.addEventListener('DOMContentLoaded', function(){
resize();
})
window.addEventListener('load', function(){
resize();
})
window.addEventListener('resize', function(){
resize();
})
resize();
</script>
{% if site.disqus_username %}
<!-- disqus 公共JS代码 start (一个网页只需插入一次) -->
<script type="text/javascript">
/* * * CONFIGURATION VARIABLES * * */
var disqus_shortname = "{{site.disqus_username}}";
var disqus_identifier = "{{page.id}}";
var disqus_url = "{{site.url}}{{page.url}}";
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
</script>
<!-- disqus 公共JS代码 end -->
{% endif %}
{% if site.anchorjs %}
<!-- async load function -->
<script>
function async(u, c) {
var d = document, t = 'script',
o = d.createElement(t),
s = d.getElementsByTagName(t)[0];
o.src = u;
if (c) { o.addEventListener('load', function (e) { c(null, e); }, false); }
s.parentNode.insertBefore(o, s);
}
</script>
<!-- anchor-js, Doc:http://bryanbraun.github.io/anchorjs/ -->
<script>
async("//cdnjs.cloudflare.com/ajax/libs/anchor-js/1.1.1/anchor.min.js",function(){
anchors.options = {
visible: 'always',
placement: 'right',
icon: '#'
};
anchors.add().remove('.intro-header h1').remove('.subheading').remove('.sidebar-container h5');
})
</script>
<style>
/* place left on bigger screen */
@media all and (min-width: 800px) {
.anchorjs-link{
position: absolute;
left: -0.75em;
font-size: 1.1em;
margin-top : -0.1em;
}
}
</style>
{% endif %}

74
_layouts/page.html Normal file
View File

@ -0,0 +1,74 @@
---
layout: default
---
<!-- Page Header -->
{% include intro-header.html type='page' %}
<!-- Main Content -->
<div class="container">
<div class="row">
{% if site.sidebar == false %}
<!-- NO SIDEBAR -->
<!-- PostList Container -->
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1
postlist-container">
{{ content }}
</div>
<!-- Sidebar Container -->
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1
sidebar-container">
<!-- Featured Tags -->
{% include featured-tags.html %}
<!-- Friends Blog -->
{% include friends.html %}
</div>
{% else %}
<!-- USE SIDEBAR -->
<!-- PostList Container -->
<div class="col-lg-8 col-lg-offset-1 col-md-8 col-md-offset-1 col-sm-12
col-xs-12 postlist-container">
{{ content }}
</div>
<!-- Sidebar Container -->
<div class="col-lg-3 col-lg-offset-0 col-md-3 col-md-offset-0 col-sm-12
col-xs-12 sidebar-container">
<!-- Featured Tags -->
{% include featured-tags.html %}
<!-- Short About -->
{% include short-about.html %}
<!-- Friends Blog -->
{% include friends.html %}
<!-- Ads -->
{% include ads.html %}
</div>
{% endif %}
</div>
</div>
{% if site.page-mathjax %}
<!-- Add support for Mathjax by Voleking-->
<!-- If you want to see formulars well in post preview, Maybe you should add this.-->
<!-- However, most of the time formulars may not appear in the post preview, you can delete it.-->
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
TeX: {
equationNumbers: {
autoNumber: "AMS"
}
},
tex2jax: {
inlineMath: [ ['$','$'] ],
displayMath: [ ['$$','$$'] ],
processEscapes: true,
}
});
</script>
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{% endif %}

173
_layouts/post.html Normal file
View File

@ -0,0 +1,173 @@
---
layout: default
---
<!-- Image to hack wechat -->
<!-- <img src="/img/icon_wechat.png" width="0" height="0"> -->
<!-- <img src="{{ site.baseurl }}/{% if page.header-img %}{{ page.header-img }}{% else %}{{ site.header-img }}{% endif %}" width="0" height="0"> -->
<!-- Post Header -->
{% include intro-header.html type='post' %}
<!-- Post Content -->
<article>
<div class="container">
<div class="row">
<!-- Post Container -->
<div class="
col-lg-8 col-lg-offset-2
col-md-10 col-md-offset-1
post-container">
<!-- Multi-Lingual -->
{% if page.multilingual %}
{% include multilingual-sel.html %}
{% endif %}
{{ content }}
<hr style="visibility: hidden;">
<ul class="pager">
{% if page.previous.url %}
<li class="previous">
<a href="{{ page.previous.url | prepend: site.baseurl | replace: '//', '/' }}" data-toggle="tooltip" data-placement="top" title="{{page.previous.title}}">
Previous<br>
<span>{{page.previous.title}}</span>
</a>
</li>
{% endif %}
{% if page.next.url %}
<li class="next">
<a href="{{ page.next.url | prepend: site.baseurl | replace: '//', '/' }}" data-toggle="tooltip" data-placement="top" title="{{page.next.title}}">
Next<br>
<span>{{page.next.title}}</span>
</a>
</li>
{% endif %}
</ul>
<hr style="visibility: hidden;">
{% if site.disqus_username %}
<!-- disqus 评论框 start -->
<div class="comment">
<div id="disqus_thread" class="disqus-thread"></div>
</div>
<!-- disqus 评论框 end -->
{% endif %}
{% if site.netease_comment %}
<!-- 网易云跟帖 评论框 start -->
<div id="cloud-tie-wrapper" class="cloud-tie-wrapper"></div>
<!-- 网易云跟帖 评论框 end -->
{% endif %}
</div>
<!-- Side Catalog Container -->
{% unless page.no-catalog %}
<div class="
col-lg-2 col-lg-offset-0
visible-lg-block
sidebar-container
catalog-container">
<div class="side-catalog">
<hr class="hidden-sm hidden-xs">
<h5>
<a class="catalog-toggle" href="#">CATALOG</a>
</h5>
<ul class="catalog-body"></ul>
</div>
</div>
{% endunless %}
<!-- Sidebar Container -->
<div class="
col-lg-8 col-lg-offset-2
col-md-10 col-md-offset-1
sidebar-container">
<!-- Featured Tags -->
{% include featured-tags.html bottom=true %}
<!-- Friends Blog -->
{% include friends.html %}
</div>
</div>
</div>
</article>
<!-- add support for mathjax by voleking-->
{% if page.mathjax %}
{% include mathjax_support.html %}
{% endif %}
{% if site.netease_comment %}
<!-- 网易云跟帖JS代码 start -->
<script src="https://img1.cache.netease.com/f2e/tie/yun/sdk/loader.js"></script>
<script>
var cloudTieConfig = {
url: document.location.href,
sourceId: "",
productKey: "de25fc98a6fe48b3bc8a7ae765da99a0",
target: "cloud-tie-wrapper"
};
var yunManualLoad = true;
Tie.loader("aHR0cHM6Ly9hcGkuZ2VudGllLjE2My5jb20vcGMvbGl2ZXNjcmlwdC5odG1s", true);
</script>
<!-- 网易云跟帖JS代码 end -->
{% endif %}
{% if site.disqus_username %}
<!-- disqus 公共JS代码 start (一个网页只需插入一次) -->
<script type="text/javascript">
/* * * CONFIGURATION VARIABLES * * */
var disqus_shortname = "{{site.disqus_username}}";
var disqus_identifier = "{{page.id}}";
var disqus_url = "{{site.url}}{{page.url}}";
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
</script>
<!-- disqus 公共JS代码 end -->
{% endif %}
{% if site.anchorjs %}
<!-- async load function -->
<script>
function async(u, c) {
var d = document, t = 'script',
o = d.createElement(t),
s = d.getElementsByTagName(t)[0];
o.src = u;
if (c) { o.addEventListener('load', function (e) { c(null, e); }, false); }
s.parentNode.insertBefore(o, s);
}
</script>
<!-- anchor-js, Doc:http://bryanbraun.github.io/anchorjs/ -->
<script>
async("//cdnjs.cloudflare.com/ajax/libs/anchor-js/1.1.1/anchor.min.js",function(){
anchors.options = {
visible: 'hover',
placement: 'right',
// icon: '#'
};
anchors.add().remove('.intro-header h1').remove('.subheading').remove('.sidebar-container h5');
})
</script>
<style>
/* place left on bigger screen */
@media all and (min-width: 800px) {
.anchorjs-link{
position: absolute;
left: -0.75em;
font-size: 1.1em;
margin-top : -0.1em;
}
}
</style>
{% endif %}

View File

@ -0,0 +1,83 @@
---
layout: post
title: "Rails: Devise的使用"
date: 2018-03-07 10:28:00
author: "Viletyy"
header-style: text
tags:
- Rails
- 包
---
[Devise](https://github.com/heartcombo/devise)是一套使用者认证(Authentication)套件是Rails社群中最广为使用的一套。
#### Usage
> 在gemfile中加入
```ruby
gem 'devise'
```
> 在终端输入
```shell
$ bunlde install
$ rails generate devise:install
```
- 注确保登陆能正常跳转。在config/route.rb中加入root地址
> 在layouts加入提示信息
```html
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
```
> 生成views页面文件
```shell
$ rails g devise:views
```
> 生成使用devise的model
```shell
$ rails g devise user
$ rails db:migrate
```
#### 定义多个authentication_keys
> 在model加入以下代码
```ruby
attr_accessor :signin
def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
if signin = conditions.delete(:signin)
where(conditions.to_h).where(["lower(username) = :value OR lower(mobile) = :value", { :value => signin.downcase }]).first
elsif conditions.has_key?(:username)|| conditions.has_key?(:mobile)
where(conditions.to_h).first
end
end
```
> 在application_controller.rb中加入
```ruby
before_action :configure_permitted_parametersod_name, if: :devise_controller?
def configure_permitted_parametersod_name
devise_parameter_sanitizer.permit(:sign_in) {|u| u.permit(:signin,:username, :mobile, :password, :remember_me)}
devise_parameter_sanitizer.permit(:sign_up) {|u|
u.permit(:signin,:username, :mobile, :password, :password_confirmation)}
end
```
> 最后更改initialize/devise.rb
```ruby
config.authentication_keys = [:signin]
```

View File

@ -0,0 +1,106 @@
---
layout: post
title: "Git: 教程"
date: 2018-03-08 10:32:00
author: "Viletyy"
header-style: text
tags:
- Git
- Shell
---
## Git
### Git简介
git是一个很强大的分布式版本控制系统。它不但适用于管理大型开源软件的源代码管理私人的文档和源代码也有很多优势。
### 安装Git
#### 在Linux上安装Git
```shell
sudo apt-get install git
```
#### 在MacOS上安装Git
```shell
brew install git
```
#### 在Windows上安装Git
直接在官网下载。官网[下载地址](https://git-scm.com/downloads)
#### 安装完毕后的配置
```shell
git config --global user.name "Your Name"
git config --global user.email "email@example.com"
```
用了`--global`参数,表示这台机器上的所有仓库都会使用这个配置,也可以去掉该参数,对当前目录下的仓库指定用户名和邮箱。
### 创建版本库
版本库又叫仓库英文名是repository可以简单理解成一个目录这个目录里面的所有文件都可以被Git管理起来每个文件的修改、删除Git都能跟踪以便任何时刻都可以追踪历史或者在将来某个时刻来进行“还原”
创建版本库/仓库:
```shell
mkdir learngit
cd learngit
git init
```
其中`mkdir`命令用来创建目录,`cd`进入目录,`git init`来初始化仓库
版本库/仓库创建好了,目录下面会多出.git目录这个目录是Git来跟踪管理版本库的。
### 提交修改
添加readme.txt文件加入以下内容
```
Git is a distributed version control system.
Git is free software.
```
使用`git status`可以让我们时刻掌握仓库的状态。
使用`git diff {filename}` 可以查看文件做了什么修改。
使用`git add {filename}` 可以将文件修改加入暂存区。
使用`git commit -m #{comment}` 将暂存区修改提交到版本库其中comment为提交说明
### 版本回退
如果你不断对文件进行修改然后不断提交修改到版本库。Git里当我们觉得文件修改到一定程度的时候可以“保存一个快照”这个快照在Git中被称为`commit`。一旦我们把文件改乱了,或者误删了文件,还可以从最近的一个`commit`恢复。
使用`git log`可以查看我们提交修改的历史记录,`--pretty=oneline`参数可以精简输出。
我们可以在历史记录中看到一个SHA1计算出来的一个非常大的数字用十六进制表示每一个提交一个新版本Git就会把它们串成一条时间线并且赋予一个唯一的版本号。
使用`git reset --hard HEAD^`可以回退到上一个版本
使用`git reset --hard {commit_id}`可以回退到指定版本comment为版本号
使用`git reflog`查看你的每一次命令,可以通过该命令,获取操作前的版本号,然后回退到该版本
### 工作区和暂存区
#### 工作区
就是在电脑里能看到的目录。
#### 版本库
工作区有一个隐藏目录`.git`这个就是Git的版本库
Git的版本库里存了很多东西其中最重要的就是暂存区state)。还有Git为我们自动创建的第一个分支`master`,以及指向`master`的一个指针`HEAD`
![701e02bf0b2d71ab51b7e2b6aa973d52.jpeg](evernotecid://AC84C433-9407-403E-89A8-42F56C323EC9/appyinxiangcom/25115422/ENNote/p38?hash=701e02bf0b2d71ab51b7e2b6aa973d52)
前面讲了我们把文件往Git版本库里添加的时候是分两步执行的
第一步是用`git add`把文件添加进去,实际上就是把文件修改添加到暂存区;
第二步是用`git commit`提交更改,实际上就是把暂存区的所有内容提交到当前分支。
因为我们创建Git版本库时Git自动为我们创建了唯一一个`master`分支,所以,现在,`git commit`就是往`master`分支上提交更改。
你可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改。
我们修改readme.txt文件和添加LICENSE文件并且使用`git add`将文件添加到暂存区。暂存区的状态变成了这样。
![3140921b12cc7d9a949076b2bb80d637.jpeg](evernotecid://AC84C433-9407-403E-89A8-42F56C323EC9/appyinxiangcom/25115422/ENNote/p38?hash=3140921b12cc7d9a949076b2bb80d637)
然后使用`git commit`提交代码到版本库,版本库就成了这样。
![f6ddf40f921f9953b6f84d107a11f689.jpeg](evernotecid://AC84C433-9407-403E-89A8-42F56C323EC9/appyinxiangcom/25115422/ENNote/p38?hash=f6ddf40f921f9953b6f84d107a11f689)
### 管理修改
第一次修改 -> git add -> 第二次修改 -> git commit
Git管理的是修改当使用git add命令后在工作区的第一次修改被放入暂存区准备提交但是在工作区的第二次修改并没有放入暂存区所以git commit只负责把暂存区的修改提交了也就是第一次的修改被提交了第二次的修改不会被提交。
提交后,用`git diff HEAD -- readme.txt`命令可以查看工作区和版本库里面最新版本的区别

View File

@ -0,0 +1,58 @@
---
layout: post
title: "Git: 提交代码速度过慢解决方法(MacOS)"
date: 2018-04-11 10:28:00
author: "Viletyy"
header-style: text
tags:
- Git
- Shell
- MacOS
---
iterm2中ping下github.com和github.global.ssl.fastly.net
**ping github.com**
```shell
$ ping github.com
```
得到
```shell
PING github.com (192.30.253.112): 56 data bytes
64 bytes from 192.30.253.112: icmp_seq=0 ttl=47 time=245.930 ms
64 bytes from 192.30.253.112: icmp_seq=1 ttl=47 time=245.910 ms
```
记录ip 192.30.253.112
**ping github.global.ssl.fastly.net**
```shell
$ ping github.global.ssl.fastly.net
```
记录ip 151.101.113.194
#### 解决方法
编辑hosts文件
```shell
$ vim /etc/hosts
```
加入这两行代码
```shell
192.30.253.112 github.com
151.101.113.194 github.global.ssl.fastly.net
```
最后更新DNS缓存
```shell
$ sudo dscacheutil -flushcache
```

View File

@ -0,0 +1,50 @@
---
layout: post
title: "Rails: Sql注入"
date: 2018-12-18 10:32:00
author: "Viletyy"
header-img: "img/post-bg-rwd.jpg"
tags:
- Rails
- Sql
- 代码安全
---
所谓SQL注入就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串最终达到欺骗服务器执行恶意的SQL命令。
我们永远不要信任用户的输入,我们必须认定用户输入的数据都是不安全的,我们都需要对用户输入的数据进行过滤处理。
防止SQL注入我们需要注意以下几个要点
1. 永远不要信任用户的输入。对用户的输入进行校验,可以通过正则表达式,或限制长度;对单引号和 双"-"进行转换等。
2. 永远不要使用动态拼装sql可以使用参数化的sql或者直接使用存储过程进行数据查询存取。
3. 永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接。
4. 不要把机密信息直接存放加密或者hash掉密码和敏感的信息。
5. 应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装。
6. sql注入的检测方法一般采取辅助软件或网站平台来检测软件一般采用sql注入检测工具jsky网站平台就有亿思网站安全平台检测工具。MDCSOFT SCAN等。采用MDCSOFT-IPS可以有效的防御SQL注入XSS攻击等。
**示例**
`User.order("#{sort_by} #{sort_direction}")`
如果 查询的是:
`sort_by = "email; DELETE from users; *--"*`
则把user全都删除了
**解决方案**
```ruby
def index
@users = User.order(sort_by + "" + direction)
end
private
def sort_by
%w(email name).include?(params[:sort_by] ? params[:sort_by] : 'name')
end
def direction
%w(asc desc).include?(params[:direction]) ? params[:direction] : 'asc'
end
```

View File

@ -0,0 +1,123 @@
---
layout: post
title: "Rails: ORM实现union(联集)"
date: 2019-04-14 10:28:00
author: "Viletyy"
header-style: text
tags:
- Rails
---
##如何在rails中的ORM实现union(联集)
Rails 的 ORM 提供很便利的语法让工程师可以轻松地对资料库作查询,但是在某些场景里仍让工程师们感到就有些美中不足,例如: 联集(UNION)。
假定系统需要一个 my tracking list 的页面,这个页面必需列出 manager 和 tracking product并且针对它们去做更进一步的查询操作如列出 publish 的 product你会怎么设计?
似乎这一切听起来令人头疼,但看完后面的范例,你也能了解实现联集的作法。
> product.rb
```ruby
# name :string(255)
# description :text
# status :string(255)
# manager_id :integer
class Product < ActiveRecord::Base
belongs_to :manager, :foreign_key => :manager_id, :class_name => "User"
has_many :tracking_lists
has_many :tracking_users, :through => :tracking_lists, :source => :user
end
```
> user.rb
```ruby
class User < ActiveRecord::Base
has_many :products
has_many :tracking_lists
has_many :tracking_products, :through => :tracking_lists, :source => :product
end
```
> track_list.rb
```ruby
# id :integer
# user_id :integer
# product_id :integer
class TrackList < ActiveRecord::Base
belongs_to :user
belongs_to :product
end
```
### 起初我们会想到 Array 的简单作法
```ruby
@products = Set.new
manage_products = @user.products.is_publish
tracking_products = @user.tracking_products.is_publish
@products.merge(manage_products)
@products.merge(tracking_products)
```
维护性: 低
- (优) 写起来快,几乎不用思考就写完了...
- (劣) 需要下两道 query且无法合并一起查询
- (劣) Array 的 instance 无法做更进一步的查询操作,例: order(:created_at)...etc 。
- (劣) 速度比较慢 但我们始终觉得这样的做法效率太差,且维护性非常的低...
### 然后我们想到 find_by_sql + left join 的作法
```ruby
@products = Product.find_by_sql(["
SELECT p0.* FROM product as p0
LEFT JOIN track_lists as t1 on t1.product_id = p0.id AND t1.user_id = p0.manager_id
where p0.manager_id = ? AND p0.status = 'published';
", user.id])
```
维护性: 中
- (优) 只有一道 query
- (劣) 需以 product 的 query 条件为主,当 user 没有被 assigned manage 某样商品时,结果就会为空
- (劣) 由于 find_by_sql 出来的结果是 Array无法做更进一步的查询操作例: order(:created_at)...etc 。
- 然而这种解法还是不够方便,且当有不同的 query 需求,例如: is_draft?,我们就必须再重写一次 method。
### 最后我们发现可以这样做 from + union
```ruby
manage_products_sql = @user.products.to_sql
tracking_products_sql = @user.tracking_products.to_sql
@products = Product.from("( #{ manage_products_sql } UNION #{ tracking_products_sql } ) AS products").is_publish
```
维护性: 高
- (优) 只有一道 query
- (优) from 出来的结果是 ActiveRecord::Relation我们可以续用 QueryMethods 做更进一步的查询操作
- (优) 简单好维护 备注: to_sql 只会产生 query 的字串,并不会实际去下 query UNION 是 mysql 的联集语法,且预设的 distinct 只会取一次相同的资料

View File

@ -0,0 +1,39 @@
---
layout: post
title: "Linux: mysqldump用法"
date: 2019-05-14 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## Mysql导出表结构及表数据 mysqldump用法
命令行下具体用法如下: mysqldump -u用戶名 -p密码 -d 數據库名 表名 脚本名;
1、导出數據库為dbname的表结构其中用戶名為root,密码為dbpasswd,生成的脚本名為db.sql
```shell
mysqldump -uroot -pdbpasswd -d dbname >db.sql;
```
2、导出數據库為dbname某张表(test)结构
```shell
mysqldump -uroot -pdbpasswd -d dbname test>db.sql;
```
3、导出數據库為dbname所有表结构及表數據不加-d
```shell
mysqldump -uroot -pdbpasswd dbname >db.sql;
```
4、导出數據库為dbname某张表(test)结构及表數據(不加-d
```shell
mysqldump -uroot -pdbpasswd dbname test>db.sql;
```

View File

@ -0,0 +1,31 @@
---
layout: post
title: "Shell命令Linux中adduser和useradd的区别"
date: 2019-12-17 10:28:00
author: "Viletyy"
header-style: text
tags:
- Shell
- Linux
---
### 本质
useradd是一个linux命令但是它提供了很多参数在用户使用的时候根据自己的需要进行设置
adduser是一个perl 脚本在使用的时候会出现类似人机交互的界面提供选项让用户填写和选择这个命令比起useradd来说比较简单也比较傻瓜。
### 输入后,产生不同的效果
**useradd**只是创建了一个用户名。
**adduser**会在/home目录下创建同名文件夹然后利用passwd+用户名)为指定的用户名设置密码。
**adduser**会用类似底层命令的更友好的前端,进行以下操作:
1. 建立一个新目录作为家目录
2. 建立同名新租
3. 把用户的主要组设为该组
4. 从/etc/SKEL目录下拷贝文件到家目录完成初始化
5. 建立新用户的密码
6. 如果其存在的话,还会执行一个脚本

View File

@ -0,0 +1,43 @@
---
layout: post
title: "Golang环境搭建"
date: 2019-12-20 10:28:00
author: "Viletyy"
header-style: text
tags:
- Golang
- Shell
---
### MacOS
去[下载地址](https://golang.org/dl/)下载这里使用pkg包进行安装
在iterm中输入`go version`能获取到go到版本号则表示安装成功
### 配置环境变量
在iterm中查看是否存在 .bash_profile 或者 .zprofile
PS: 终端类型根据执行命令echo $SHELL显示的结果
/bin/bash => bash => .bash_profile
/bin/zsh => zsh => .zprofile
编辑profile文件添加以下代码
```shell
export GOROOT=/usr/local/go
export GOPATH=/Users/user/go_work
export GOBIN=$GOROOT/bin
export PATH=$PATH:$GOBIN
export GO111MODULE=on
export GOPROXY=https://goproxy.io,direct
```
输入`source ~/.bash_profile` 或者 `source ~/.zprofile` 使配置生效
输入`go env` 查看配置结果
### 一般项目启动命令
```shell
go mod tidy
go mod vendor
go run main.go
```

View File

@ -0,0 +1,41 @@
---
layout: post
title: "开源世界旅行手册GUI和CLI"
date: 2019-12-21 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## GUI和CLI
### 定义
**GUI**Graphical User Interface图形用户界面。用户界面的所有元素图形化主要使用鼠标作为输入工具点击图标执行程序使用按钮、菜单、对话框等进行交互追求易用看起来比较美。
**CUI**Commend User Interface命令行界面。用户界面字符化使用键盘作为输入工具输入命令、选项、参数执行程序追求高效看起来比较酷
### CUI的优点
对于Linux命令行不是必须的点点鼠标同样可以完成所有的事。
而GUI的易用使得一个没接触过Linux的初学者也可以通过点击鼠标作一些事情比如他可以点击文件夹启动文件管理器在文件上点击右键通过菜单对文件进行一些操作
这是不是意味着CLI就应该放到博物馆里供人凭吊
当然不是这样的实际上CLI在熟练用户中仍然大行其道因为它的效率高。
回忆一下在Windows系统中通过GUI启动“计算器”开始->所有程序->附件->计算器,其实这不算什么,只不过点了几次鼠标而已。
在这个计算器中,用鼠标点击上面的按键输入表达式,或者某些程序要求你输入密码的时候,弹出一个小键盘,要求你点击上面的数字。。。不得不说,这种设置十分的人性化,计算器看起来跟真的很像,到银行取钱,也用用于输入密码的小键盘。不过问题的关键在于,这中图形界面模拟的键盘是用手来按的,而不是用鼠标点的。
既然CLI下使用计算器只要输入`bc`就可以启动计算器,输入一个复杂的公式`1+1`,回车得到结果 **2**
好了,我必须承认,这个例子有点过分,因为算数不是计算机的主要用途。
------
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,294 @@
---
layout: post
title: "开源世界旅行手册Unix缩写风格"
date: 2019-12-22 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## Unix缩写风格
### 缩写习惯
构建于图形界面之上的操作系统使用鼠标作为主输入设备是否使用缩写并不重要。比如Windows系统中的目录几乎都是全称。点击两次鼠标进入文件夹**pf**并不意味着点击13次才能进入文件夹**Program Files**
而构建于命令行之上的操作系统如Linux只要3个字母以上的单词几乎都要缩写。例如`cd`命令是**Change Directory**的缩写。作为常用命令,如果使用它的全称**Change Directory**,绝对是无聊和乏味的工作。
最常见的缩写,取每个单词的首字母。如下
| 缩写 | 实际意义 |
| ---- | --------------------------- |
| cd | Change Directory |
| dd | Disk Dump |
| df | Disk Free |
| du | Disk Usage |
| pwd | Print Working Directory |
| ps | Processes Status |
| PS | Prompt Strings |
| su | Substitute User |
| rc | Run Command |
| Tcl | Tool Command Language |
| cups | Common Unix Printing System |
| apt | Advanced Packaging Tool |
| bg | BackGround |
| ping | Packet InterNet Grouper |
如果首字母后为“h”。通常保留
| 缩写 | 实际意义 |
| ----- | ------------------ |
| chsh | CHange SHell |
| chmod | CHange Mod |
| chown | CHange OWNer |
| chgrp | CHange GRouP |
| bash | Bourne Again SHell |
| zsh | Z SHell |
| ksh | Korn SHell |
| ssh | Secure SHell |
递归缩写定义中包含自身缩写GNUGNU's Not Unix使用这个定义来解释定义中的缩写
(GNU's Not Unix)'s Not Unix
这意味着它是可以无限递归的:
(((((GNU's Not Unix)'s Not Unix)'s Not Unix)'s Not Unix)'s Not Unix)'s Not Unix ……)也属于这一类,如
| 缩写 | 实际意义 |
| ---- | --------------------------- |
| GNU | GNU's Not Unix |
| PHP | PHP: Hypertext Proprocessor |
| RPM | RPM Package Manager |
| WINE | WINE Is Not an Emulator |
| PNG | PNG's Not GIF |
| nano | Nano's ANOther editor |
有些缩写可能有多种定义,如
**rpm**
- RPM Package Manager
- RedHat Package Manager
**bc**
- Basic Calculator
- Better Calculator
这方面Emacs可谓是独领风骚
**Emacs**
- Editor MACroS
- Emacs Makes A Computer Slow
- Escape Meta Alt Control Shift
- Emacs Makers Are Crazy Sickos
- Emacs Makes All Computing Simple
- Emacs Makefiles Annihilate C-Shells
- Emacs Manuals Always Cause Senility
- Emacs May Allow Customized Screwups
- Emacs Manuals Are Cryptic and Surreal
- Eventually Munches All Computer Storage
- Eight Megabytes And Constantly Swapping
- Elsewhere Maybe All Commands are Simple
- Excellent Manuals Are Clearly Suppressed
- Emacs May Alienate Clients and Supporters
- Except by Middle Aged Computer Scientists
- Extended Macros Are Considered Superfluous
- Every Mode Accelerates Creation of Software
- Each Manual's Audience is Completely Stupefied
- Exceptionally Mediocre Algorithm for Computer Scientists
- Easily Maintained with the Assistance of Chemical Solutions
- Eradication of Memory Accomplished with Complete Simplicity
如果只有一个单词,通常取每个音节的首字母:
| 缩写 | 实际意义 |
| ---- | -------- |
| cp | CoPy |
| ln | LiNk |
| ls | LiSt |
| mv | MoVe |
| rm | ReMove |
对于目录,通常使用前几个字母作为缩写:
| 缩写 | 实际意义 |
| ---- | --------------------- |
| bin | BINaries |
| dev | DEVices |
| etc | ETCetera |
| lib | LIBrary |
| var | VARiable |
| proc | PROCesses |
| sbin | Superuser BINaries |
| tmp | TeMPorary |
| usr | Unix Shared Resources |
这种缩写的其他情况
| 缩写 | 实际意义 |
| ------ | ------------------ |
| diff | DIFFerences |
| cal | CALendar |
| cat | CATenate |
| ed | EDitor |
| exec | EXECute |
| tab | TABle |
| regexp | REGular EXPression |
如果某种缩写比较深入人心例如“mesg”代表“message”在新的复合缩写中将沿用这种缩写方式
| 缩写 | 实际意义 |
| ------ | ------------------ |
| dmesg | Diagnostic MESsaGe |
| sed | Stream EDitor |
| stty | Set TTY |
| fstab | FileSystem TABle |
| passwd | PASSWorD |
有些缩写第一个字母“g”代表“GNU”
| 缩写 | 实际意义 |
| ----- | ---------------------------- |
| awk | Aho Weiberger and Kernighan |
| gawk | GNU AWK |
| gpg | GNU Privacy Guard |
| grep | GNU Regular Expression Print |
| egrep | Extended GREP |
### 命令选项从a到z
Linux命令的选项繁复庞杂让人眼花缭乱。不过这些选项往往具有相对固定的含义熟悉了它们记忆便不再困难。
**-a**
- all所有ls, lsattr, uname
- archive存档cprsync
- append附加tar -A 7z
**-b**
- blocksize块大小带参数dudf
- batch批处理模式交互模式的程序通常拥有此选项如top -b
**-c**
- commands执行命令带参数bashkshpython
- create创建tar
**-d**
- debug测试
- delete删除
- directory目录ls
**-e**
- execute执行带参数xtermperl
- edit编辑
- exclude排除
**-f**
- force强制不经确认cprmmv
- file文件带参数tar
- configuration file指定配置文件有些守护进程拥有此选项如sshlighttpd
**-g**
**-h**
- --help帮助
- human readable人性化显示lsdudf
- headers头部
**-i**
- interactive交互模式提示rmmv
- include包含
**-k**
- keep保留
- kill
**-l**
- long listing format长格式ls
- list列表
- load读取gccemacs
**-m**
- message消息cvs
- manual手册whereis
- create home创建home目录usermoduseradd
**-n**
- number行号、编号catheadtailpstreelspci
- nouseraddmake
**-o**
- output输出ccsort
- options选项mount
**-p**
- port端口带参数很多网络工具拥有此选项如sshlftp
- protocal协议带参数
- passwd密码带参数
**-q**
- queit静默
**-r**
- reverse反转
- recursive递归cprmchmod -R
**-s**
- silent安静
- size大小带参数
- subject
**-t**
- tag
- type类型mount
**-u**
- user用户名、UID带参数
**-v**
- verbose冗长
- version版本
**-w**
- width宽度
- warning警告
**-x**
- exclude排除tarzip
**-y**
- yes
**-z**
- zip启用压缩bziptarzcatzipcvs
------
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,85 @@
---
layout: post
title: "开源世界旅行手册:版本号的迷雾"
date: 2019-12-23 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 版本号的迷雾
有时候我会听到这种说法我安装了9.0版的Linux然后。。。
目前最新的内核为2.6.27版何来9.0版之说根据经验我大概可以揣测出9.0为某一发行版的版本。
需要指出的是Linux并不是一个完整的系统它只是内核。没有内核系统不能运行什么都干不了。但只有内核还是什么都干不了一个完整的系统是包含内核在内的一系列软件工具包。
开源系统由许多独立的软件项目构成,如果最终用户需要自行获取这些软件包,然后把它们组装起来,成为可以运行的系统,就会浪费很多时间;而且组装一个系统并不是一件容易的事情,恐怕大多数用户无法做到。
于是出现了一些组织或个人将内核和其他软件组装在一起作为一个完整的系统发布这就是发行版如ubuntu、archlinux就是通过安装程序将已经组装起来的系统安装到计算机上。
### 发行版
各种发行版都有自己的版本命名方式有些通过发布时间来命名像archlinux
```shel
archlinux 2008.06
```
表示2008年6月发布该版本。
很多发行版除了版本号还有发布代号。比如ubuntu选择一种动物作为吉祥物。
```shell
7.04 - Feisty Fawn
7.10 - Gutsy Gibbon
8.04 - Hardy Heron
8.10① - Intrepid Ibex②
9.04 - Jaunty Jackalope
```
① 发布时间为08年10月
②无畏的北山羊
ubuntu每6个月发布一次新版所以比较隆重而archlinux是滚动更新发布新版只是便于新用户安装。
一些历史悠久的发行版,倾向使用序数作版本号,例如:
```shell
Fedora Core 10
openSUSE 11
Debian 5.0
FreeBSD 8.0
```
严格的说FreeBSD并不是Linux发行版因为它使用的不是Linux内核
### 内核
Linux内核的开发在两个分支上同时进行稳定分支和实验分支。稳定分支相当健壮可用于生产环境而实验分支中包含一些新的特效还不够成熟待实验分支的代码经过充分测试被证明足够成熟便会被转移到稳定分支
这种开发模式既保证了有一个可靠的稳定版用于生产又保证了能够大胆的在内核中应用新技术大部分开源项目都使用此模式开发包括FreeBSD等
来看看内核版本号
```shell
2①.6②.27③-2④-i686⑤
```
① 主版本号。革命性改进,这个版本号在几年内应该不会升级
② 次版本号。重大改进,偶数为稳定分支,奇数为实验分支
③ 修正版本号。重大修正
④ 修补版本号。一些BUG的修补
⑤ 目标架构。i686表示intel奔腾Ⅱ或以上级别CPU
------
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,488 @@
---
layout: post
title: "开源世界旅行手册完全用GNU/Linux工作"
date: 2019-12-24 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 完全用GNU/Linux工作
我已经半年没有使用Windows的方式工作了。Linux高效的完成了我所有的工作。
GNU/Linux不是每个人都想用的。如果你只需要处理一般的事物打游戏那么你不需要了解下面这些。
我不是一个狂热的自由软件份子,虽然我很喜欢自由软件。这篇文章也不是用来推行自由软件运动的,虽然我觉得自由软件运动是非常好的。
这篇文章也不是用来比较Linux和Windows内核效率文件系统网络服务的。我现在是作为一个用户而不是一个开发者来说话的我们的讨论是基于操作应用层面的。是为了告诉大学还不了解或者不理解UNIX的科学工作者和大学生UNIX比Windows更适合于科学研究工作请大家理解UNIX的工作方式不要用Windows的标准来要求Linux而要用一个科学工作者的标准来要求自己用UNIX的思想来武装自己。
我显然是反对在大学特别是理工科专业推广Windows的。我也反对在对“娃娃”门的计算机启蒙教育中使用Windows。因为Windows不论从技术上经济上思想风格上都是与我们培养高科技人才的目标格格不入的。Windows的流行属于历史遗留问题爷爷一级的人当然已经不可救药其实爷爷一级的人也不上不可救药但是我们不应该让下一代继续走上歧途。
### UNIX不是计算机专家的专利
当我建议一些非计算机专业的人用Linux的时候很多人说“UNIX是计算机系的人用的我们不能理解。”“UNIX是男孩用的我们女孩不用。”
但是其实世界上的大多数科学家和工程师几乎用的都是UNIX作为他们的电脑工具。就因为它简单可靠稳定强大有趣。甚至很多时候UNIX就是唯一的选择。
你说“我们都会用UNIX的话你们计算机专业的人还用来干什么”很荣幸的告诉你计算机专业的有一部分人就是专门为你们提供这样强大而方便的计算机工具的。如果他们制造的工具只有自己会用的话那么这个工具有什么用
### 理解 GNU/Linux
不要用Windows的标准来要求Linux。
由于GNU/Linux这个词太长下面如果没有特别指明“Linux”就是指“GNU/Linux”。
在这个年代恐怕没有人需要我来介绍Linux是什么了吧如果你觉得“Linux只不过是和DOS差不多的东西”那请问问你旁边的Linux用户Linux到底是什么
那为什么我还要写一篇这样的文章因为我发现还有很多人不理解Linux和UNIX虽然他们也在用它但是他们有时会问“为什么Linux不能像Windows那样。。。“怎么Redhat Linux不能mount NTFS分区“Linux下用什么整理硬盘”“什么时候OpenOffice才能完全兼容Word文件啊“现在还有什么Windows能干的事情Linux干不了的”。。。。
他们有40G的硬盘却只为Linux分配了2G空间有时还抱怨“这个东西怎么占着么多硬盘似乎Windows该占用大部分硬盘。他们把重要的数据装在Windows的分区似乎信不过Linux。他们总是到处寻找新奇的好看GUI程序对命令行的东西一概不屑一顾。他们对Drag&Drop菜单配置自动升级非常感兴趣。他们如果找到一个很像Windows程序的Linux程序一定会高兴的说“哈哈Linux也能。。。了”如果Linux在某种测试中胜过Windows他们会高兴的跳起来。他们没办法用Linux解决问题的时候甚至用WineWine现在已经非常实用了来运行Windows程序。有时是在没办法只好重启到Windows或者干脆省的麻烦在Windows下装一个VMWare其实这没什么不好虚拟一个Linux玩。
你如果出现了上面的情况说明你的思想收到了Windows的某种潜移默化的影响和误导。你没有能够从本质上理解存在于Linux身上的UNIX思想。你支持Linux你喜欢Linux你能从中感觉到快乐这非常好。你现在只需要明白的是Linux从来就不是一个玩具它是天才UNIX的后代。UNIX是自晶体管发明以来最伟大的发明自图灵奖诞生以来其获得者一直都是计算机领域的科学家和学者而在所有这些届的图灵奖中只有唯一的一届有个例外那就是Ken Thompson与Dennis M. Ritchie他们都是计算机软件工程师。由于UNIX与C语言的深远影响1983年美国计算机协会将当年的图灵奖破例颁给了作为软件工程师的Ken和Dennis并在当年还决定新设立一个奖项——软件系统奖以奖励那些优秀的软件开发者当然首个软件系统奖也是非他们两人莫属了它从诞生那一天开始就比Windows的设计出色。
你要体会什么叫“设计”一个糟糕的设计并不是到后来缝缝补补就可以变好的而一个出色的设计不但可以以不变应万变而且可以影响到后来者。一个出色的设计配上一个出色的实现那就是非常出色的发明。Linux就是这样的一个出色的发明。Linux并不需要追赶Windows也不需要打垮微软。它的最终目标是改变整个计算机世界还人们自由给人们乐趣和方便。
UNIX是简单的你不需要成为一个天才也能理解这种简单。
UNIX的设计者Dennis Ritchie说“Unix is simple. It just takes a genius to understand its simplicity.”但我不这么认为因为我不是一个天才但是我却勇敢的把Windows完全删除掉遇到不明白的事情的时候努力用UNIX的方式去解决而不是寻求Windows的帮助。现在我体会到了UNIX的思想和好处我可以用比Windows高效几倍的效率工作。因为我相信这样的信念“Windows能办到的事Linux一定能办到而且办的更好。”
这小节的开头的话应该改成“UNIX是简单的你不需要成为一个天才或是计算机专家。但是在这个充斥着Windows错误观念的世界你需要信念和勇气才能理解它的简单。”我下面就告诉你一些我理解到的东西。首先你要知道的是微软在国际科学领域是根本没有地位的。
### 微软的地位
微软的名声在欧洲和美国的大学里,特别是在计算机系里之坏,大家可能有所耳闻。我认识的 MITStanford 的教授,贝尔实验室的专家,甚至一个欧洲小国的高中计算机老师都绝口不提微软的名字。在他们眼里,微软只是一个没有真技术,专靠在落后国家商业宣传和垄断经营的小公司。这个“小”并不是说它人少,钱少,而是说它先进技术少。
我上次和王益合作写了一个算法演示程序那个算法是贝尔实验室一位科学家Steven Fortune很天才的发明为了程序能够被身边大多数人使用我们选择了 VC+MFC 作为平台。我在分析算法时还得到 Fortune 很热情的鼓励,寄给我一份资料,还多次回信耐心的给我讲解了很多细节。但是程序完成之后,我把样品发给 Fortune他回信说“对不起。我机器上没有 MFC。” 话说的很客气,但是我已经感觉到了他对 Windows 的不屑。然后我把 MFC 静态编译进程序再发给他,他就没有再回信了。他显然不是瞧不起我,而是确实有难处。
你能感觉到这位科学家对微软和 Windows 是什么态度了吧?不是反感,而是他心里根本没有 Windows 这个东西!微软在高科技领域没有发展,那么它怎么生存呢?到发展中国家去发展一下,他们的人民还对电脑一无所知,我说不定甚至可以打入大学的计算机系呢。我送他们软件,我捐钱盖大楼,我出钱找图灵奖获得者来演讲,让他们觉得我们都是科学家!
好了,现在全国的大学包括清华,几乎所有人机器必装盗版 Win2000Office XP学校的选课系统是非IE不能正确浏览论文用 Word 编辑演示用ppt做email 的通知附件是 doc 文件,你不用 Word 打不开,连 863 项目都用 VC 写程序了。我很久以前就看到一份报纸说,“微软为什么不严厉打击盗版?” 这篇文章说,微软非但不打击中国的盗版行为,而且有放任之趋势。放长线吊大鱼,“以后我要你们加倍的来还我!” 确实如此,它的目的快实现了(黑屏事件)。
#### Windows笼罩下的中国计算机教育
说句丢脸的话,比尔盖茨很久以前是我的偶像……
在中国,比尔盖茨被很多人奉为神圣,“少年电脑天才”,甚至有的人提到他的名字就做出“抱拳对天”的姿势。很多人谈到微软的“新技术”,“高科技” 都是眉飞色舞。各种“VC编程圣经”“深入了解 Visual C++”之类的书在开头几页都会出现非常肉麻的字眼“在那团团的混沌中一个开天辟地的精灵Windows 1.0,诞生了……”
微软的软件被这么多人盗用,那么人们是怎样使用这些盗版程序的呢?先看看电脑培训班,教的都是一些 DOS 命令打字Windows 基本操作Word 文档处理PowerPoint高级班可能有 ExcelAccess…… 参加各种微软认证考试MCSEMSDE 的人络绎不绝。考试辅导班都贴出了“280元考过为止”之类的字样。考试参考资料更是昂贵有些电脑书店整整两书架都是“Microsoft Press”的东西。我有个同学参加认证考试每门考试都要200多元。而且你一次考不过可以再考又要交钱。他后来还津津乐道跟我说看我花了XXXX(一个四位数)元考过了微软认证得到一张比尔盖茨亲笔签名的证书和价值6000元的 Windows XP 内部发行版。
“电脑要从娃娃抓起”,我们再来看看娃娃们学的是什么。大部分家长给孩子买了电脑之后,他们首先就会装一个盗版的 Windows然后买来盗版的游戏开始玩。如果哪个孩子会用 Delphi 编程序,那可不得了。报社记者,电视台争相报导,说,某某学校的初中生某某,在别人都还在玩电脑游戏这种“初级阶段”的时候就已经用 Delphi 写程序了。镜头还瞄准了他显示器上面的像框中的比尔盖茨头像!
我刚进入大学计算机系时还不懂得什么是操作系统,因为我以前只用过“中华学习机”。看到新入学的同学们各个谈论的都是 “Windows 95”“VC”…… 我简直觉得我落后了好几十年一样整个一土人根本跟他们答不上话。好不容易找到一个比较熟的同学问了一下“你们天天谈论的瘟95是什么啊”答“win95就是一个操作系统跟DOS是一类。”“朵死是什么” “你连DOS都不知道是什么别在计算机系混了。” 学校上课当然不讲VC编程之类的东西但是上 Pascal 的老师有一次就说:“嗨,我们学校真是落后。现在别人都用 C, C++,甚至 VC 了,我们还在讲 Pascal。不知道什么时候才能有VC课啊。你们出去也是要用VC的只好自学了。” 于是有些同学很多时候上课都捧着一本很重的“Windows 编程大全”之类的书,根本没有听课。吃饭时就念念有词的跟我说,“代码的优化是无止境的”,“匈牙利命名法真是伟大的发明 ” …… 这就是中国很多大学计算机系的情况。
感觉到无知了这不是偶然的而是微软长久以来埋下的伏笔。它要让无知的大家都把它奉为神圣它要让支持UNIXXwindow的人一旦说 UNIX 好Xwindow 好的时候,都被一群人围着说教:“这个 Windows 也能做到 ”,“你对 Windows 有偏见”,“微软才是主流啊”,“你敢瞧不起 win2k“.NET 就是世界潮流”,“微软的毕竟是新技术”,“有钱就是有技术”…… 甚至在一番论战比较后败下来还是要说“Windows 性能差点但是易用性强”“Windows 是老百姓用的,要求别那么高 ”,“微软那么有钱,以后想超过 UNIX 还不容易吗?”……
#### 发达国家的计算机教育
我前段时间在 USENET 发文问有关 Scheme 语言的问题时,认识了一位丹麦人。他解决了我所有的问题,并且建议我阅读一些很“深奥”的有关程序语言语法,文法的书,他告诉我很多网站可以学习 LISPScheme人工智能算法。他叫我看 Jonathan Rees 的论文 "Syntactic Closures"。他还打包给我寄过来一份 MIT 的 "How to Design Programs"。他说他在自己的 PC 机上装的是 Linux他用 Emacs 编辑,运行 Scheme 程序。他对 Emacs 的了解和爱好真是使人惊讶。他大学本科毕业时做的毕业设计是一个 Scheme 解释器。这对于我来说是望尘末及了。
他是那么的不厌其烦,我的每一个问题他都详细的回答。我有时都觉得过于详细了,怎么这么耐心啊?我觉得他似乎是我的高中老师。他是什么样的人呢?我好奇的打听了他的情况。原来,他是丹麦一所普通高中的计算机老师。
他说他在高中里讲授程序设计和算法,计算机语言文法。他说用 Scheme他的学生不用再为内存泄漏等程序语言本身的问题而烦恼而专注于问题和算法本身。有利于培养学生解决问题的能力特别是用计算机解决数学问题的能力。
天哪!为什么欧洲出现那么多数学家,几何学家?你看看别人重视的是什么!我们的计算机教育如果继续这样下去,只会沿着弯路越走越远!
#### 微软和它的朋友们的如意算盘
下面来看看微软的收入是怎么来的。首先Windows 98系列操作系统一个就是 100 多美元每次升级又是几乎同样的价钱。Windows NT 还要贵几倍而且有用户数目限制5个用户的10个用户的…… 以后如果要增加用户数目还要按比例付钱。
花了如此多钱买来的操作系统就能用了吗?它竟然连压缩程序都没有提供!你装上 Windows 之后一般第一件事就是去下载一个 WinZip 吧,“只要 29 美元”。Windows 会中病毒啊,马上花 70 美元买一个 Norton AntiVirus 吧。还有黑客呢?再买一个 Norton Internet Security 好了100 美元。系统需要优化,磁盘需要整理,买一个 Norton System Works 是你最佳的解决方案100美元。
可是你现在还是不能干正事啊!你想要一个 Word, PowerPoint那就买一套 Office XP 吧,一起买便宜些,$459.90。
那些程序不会用啊!那些菜单怎么设置,到底有什么功能啊?看“帮助”也学不会。买本书看看吧,我推荐 “Special Edition Using Microsoft Office XP”不贵$27.99。这本书里面大部分是屏幕抓图,还是买一本旧的比较划算,$17.85。
你如果只是当个秘书,上面的差不多还凑合了。可是你有更高的追求,你想成为 Windows 程序员。首先买一个 Visual Studio.NET 吧,要不然怎么编译程序。$494.95。
为了紧跟微软动向,世界潮流,不能不注册个 MSDN 什么的吧?这个贵一点,不过物有所值啊,$2,799。
嗯,你现在已经是上层阶级,白领人士了。你现在可以像这样“自由”的,“安全”的生活了
### 为什么要反对使用Windows
很多人都说不应该完全否定 WindowWindows 也有它的长处。不应该骂微软。
对。Windows 容易操作,适合普通用户。如果微软把它自己定位在 P&GPhilips 那样的地位,能够给我们的百姓提供周到的,完善的,价廉物美的服务。那我肯定是很喜欢它的。
但是从上面的种种情况说明微软是一个野心极大的国际垄断组织它的产品没有一个是不出问题的Windows 不稳定,容易中病毒,而微软不为大家免费提供杀毒软件。我就是要让你们花钱买我的朋友 Symantec 的杀毒软件,谁叫你们已经上了我的贼船?这叫什么售后服务啊!
你买来微软的程序,安装的时候一般都有一个协议,说:“由于微软的程序造成你的数据损坏或丢失,微软概不负责。” 我想很多人肯定觉得这个不合理,不想按那个 "I accept"。但是你的软件买都买来了,钱都花了,现在一按 "I decline",安装程序马上就会退出。你只好被迫点击了 "I accept"!这不是不平等条约吗?
我已经目睹了好几个朋友的文档被 Microsoft Word 损坏有的是编辑了十多天的30多页的论文有的是费了很大工夫做出来的个人简历那个朋友为此失去了到自己向往的 P&G 工作的机会。就在他要投简历的前一个晚上,就在那一瞬间…… 不知道他痛哭的时候有没有想起要投诉微软,可是谁叫我们用的都是盗版呢,况且你还点击了 "I accept"。
微软仗势已经占有大部分PC市场制定不符合国际标准的“微软的标准”以不合理的方式压制其它公司的软件这个问题已经在美国司法部闹了很久了。他甚至在 Windows 系列操作系统中放置能够通过网络泄漏用户信息的代码,以至于 Windows 刚进入澳大利亚时被澳大利亚政府禁止使用。
有些人说:“微软毕竟开创了一个历史,造就了今天的 IT 行业。” 但是,如果没有微软,我们今天早就用上非常稳定,非常可靠,非常方便,非常“傻瓜”的软件了!微软是阻挡信息技术发展的罪魁祸首。
微软的程序的工作方式(注意,我只是说操作方式,病毒的事情另外算)确实适合于一般家庭,上上网,发发邮件,打打游戏都不错。可是微软却要把自己包装成什么 “高科技”企业,要在世界各地设置“研究院”,在大学计算机系赠送不适合用于科研的 Windows 产品,甚至出钱请图灵奖得主来中国畅谈“二十一世纪的计算”,还在大会上宣传自己的 .NET 技术。非要把别人认为自己是科学的自己是领导世界高科技的。但是呢它什么高科技也没有。欧洲美国哪一个关键部门在用微软的东西NASA? DOE? CERN? 你仔细想一想,微软的程序对人类到底有什么重大作用?
#### 什么是Windows能干而Linux干不了的事情
“Windows 能干而 Linux 干不了的事情,那就是不需要干的事情。”
有个朋友看我半年没有用 Windows有时就会问我“你只用 Linux有没有发现有些 Windows 能处理的事情 Linux 干不了?”
我回答说“Windows 能干而 Linux 干不了的事情,那就是不需要干的事情。”
#### Windows能做的有益的事情Linux都能做
Windows 下的某些功能确实是我们需要的,那么 Linux 的开发者们和用户也需要这种功能,他们就会去实现这种功能,而且比 Windows 的方式好得多。由于大多数科学家,工程师用的都是 Linux 或者某种商业 UNIX, 所以几乎所有商业的科学工程程序,比如 Matlab, Mathematica, AutoCAD, Candence的Synopsys 的Avant! 的……全都是先有 UNIX 的版本(包括Linux),然后再考虑移植给 Windows甚至根本不移植给 Windows因为 Windows 的机器一般没有足够的能力运行这样的程序。你不要以为只有 Windows 才有 PSpice, UNIX 的 HSpice 要好得多,而且可以运行在大型主机上。当然它们不是免费的,但是它们值那个价钱。
但是 Windows 下有些东西在 Linux 下没有很相似的,或者你找到很多类似的,但是它们每一个比起 Windows 的那个程序都要差很多,那么原因有两种可能性:
有一个完全类似的程序,但是由于它乍一看不漂亮,被你忽略了。
而其它程序虽然看起来很漂亮,但是它们是一些初学编程的人写的。现在由于 Gtk, Qt 的诞生Linux 下开发图形界面程序极其简单,很多初中生甚至小学生都可以随手编出一些漂亮不中用的程序。如果你整天寻找这样的程序挑来挑去,永远也找不到你满意的。当然也有一流的程序用 Gtk 和 Qt比如 GVIM 就可以用 Gtk 作为图形界面,我还知道 Synopsys 一些程序用了 Qt。
我曾经也犯过这样的错误,从外表区分一切。结果优秀的 FVWM, lftp, Mutt, wget 都被我忽略过。当我找回它们的时候,我是那么的羞愧不已,它们现在都是我的朋友 :) 我第一次看到 FVWM 觉得它只不过是一个有很厚很难看边框的东西。可是现在,我的同学看到 FVWM 都说:“哇!真漂亮。”
#### 有另一种完全不同的方式可以达到相同的目的,甚至更好
很多人很关心 Open Office, Star Office, AbiWord, ... 他们多么盼望有一天某一个 Linux 程序能够完全兼容的打开一个复杂的 doc 文档。但是你永远也不可能有那一天。为什么呢?因为微软为了占有市场,必定不会让其它系统的程序能够完全兼容它的文档格式。它一定会不断变化 doc 文档的内部结构,隐藏一些秘密,让其它公司的程序打开 doc 文档时总是有某种问题,从而你必需购买 Microsoft Office 和 Windows。
你应该想一下,那么多的高智商的大学教授,科学家,学生,他们用的都是 Linux 或者其它类型的 UNIX他们没有 Word 可用,怎么处理文档呢?这么多年没有一个像 Open Office 的程序出现,难道大家没有办法写文档吗?
显然不是这样。你看看那些高水平的学术杂志,论文,那些大学教授的网页,那些漂亮的幻灯片,它们是什么做的?原来 UNIX 用户早就有非常方便的 troff, LaTeX, SGML 等东西可以处理文档,而且它们比起 Word 都要高明的多。Word 显然被这些大拿忽略了,以至于很久以来没有人想在 Linux 下开发一个类似 Word 的程序,除非某些公司想抢微软的饭碗。
很多人留着 Windows 在硬盘上的原因无非是为了用 Word 和 PowerPoint。我见过一个教授他的 Windows 笔记本电脑上除了 PowerPoint 什么都没有。有一天演示的时候,他指着堆乱字符说:“对不起,这是一个公式……怎么每次都是这样……” 其实有比 PowerPoint 好几百倍的东西可以制造幻灯片你可以用最简单的方法制造世界一流效果的论文和幻灯片。你待会儿可以看看我的TeX网页你就会知道为什么我可以完全离开 Windows。
### Windows能做的那些没用的事情Linux永远做不好
#### 电脑游戏
有些人说 Linux 下不能玩 Windows 下所能得到的所有游戏。的确Linux 下虽然也有少量的游戏,比如 Quake。但是它没有 Counter Strike, 没有 Star Craft, ……
并不是说电脑游戏不该玩,但是应该适可而止。电脑是用来处理事务,帮助你学习,解决问题的工具,而不是一个玩具!整天沉迷于电脑游戏中,而不出去感觉外面的世界,你会变得越来越冷酷,越来越缺乏人情味。你与真实的世界越来越远。
你可以在 CS 里杀人,你可以在 Tomb Raider 里探险,你甚至可以在 Tony Hawk's Pro Skaters 里滑板…… 但是 It's not real你虽然有很高的“反恐技巧”但是遇到歹徒的时候你是那么的怯懦你虽然控制 Laura 伸手敏捷,但是你打篮球的时候怎么总是被人断球?你虽然可以轻易的在 THPS 里作出一个 "360 kickflip to hangten grind to fakie",但是你踩在自己的滑板上的时候还不会 ollie
说回来,如果你偶尔玩一下电脑游戏未尝不可。但是世界上有远比 Windows + PC 更好的游戏方式。Sony 的 PlayStation2, SEGA 的 DreamCast, Nintendo 的 N64Namco 的街机……每一个都比 Windows 游戏精彩每一个都有如此高的3D性能以至于 Pentium4, Itanium + GForce4 都无法与它们比美!
Linux 的用户们都是关心解决世界的关键问题的份子他们哪里有时间用自己的机器来玩游戏啊他们每天用Linux高效的做完自己的工作就到阳光下享受自然去了。要玩游戏也是玩一些类似推箱子贪吃蛇之类的智力小游戏。所以你知道为什么 Linux 几乎没有游戏了吧?:)
#### 整理硬盘,优化系统
这是一个非常有意思的话题,仅次于有关“病毒”的话题。相信很多 Windows 用户都有整理硬盘的经历。在很多 Windows 用户眼里,“硬盘用久了,会出现碎片,速度会减慢,需要一个程序来整理,整理硬盘的时候不要做其它工作”,这好像是天经地义的事情。
我也曾经津津有味的看着 Norton Defrag 一点一点的把我的硬盘排序调整用图形的方式显示出来然后报告“100% 没有碎片。你的硬盘现在已经达到最佳状态。” 我现在才发觉我那时是多么的幼稚。
Linux 和 UNIX 用户似乎从来没有“整理硬盘”这种说法呢你觉得很奇怪吗如果你觉得很奇怪那说明你的思想在某种程度上被微软的垃圾程序禁锢了。你需要明白UNIX 的大型主机很多必须是一天24小时一年365又1/4天不停运转的要是每个星期都要整理一次硬盘在整理的时候几乎不能干任何事情那是绝对行不通的
Linux 机器根本不用整理硬盘,这就是为什么没有看到过 Linux 用户整理硬盘。Linux 的文件系统是比 Windows 的 FAT, FAT32, NTFS 高明得多的文件系统,它们不但可以对文件设置权限,实施完全的保护,而且可以“越用越整齐 ”,“越用碎片越少”[[10](https://i.linuxtoy.org/docs/guide/ch06s05.html#ftn.id3053196)]!你应该把文件大部分放在 Linux 的分区,而不是 Windows 分区,因为它比 Windows 分区可靠得多。
还有更滑稽的事情就是有很多“Norton System Doctor”“Windows 优化大师”,“超级兔仔注册表魔法” 之类的程序存在,而且价格昂贵。似乎一个操作系统本来应该有很多问题,需要别的厂商做程序来“优化”它,而且为了得到优化,你需要付钱!这些问题 Linux 根本就没有所以不需要什么优化。Linux 内核本身就是高度优化的。
#### IDE
有些人在抱怨为什么 Linux 没有一个良好的 IDE 开发环境。Linux 现在已经有一些 IDE 了,但是总是有很多问题。你是不是正在寻找,正在期望 Linux 某一天可以有一个VC那样的开发环境你有没有发现你正在进入微软给你设下的怪圈你为什么一定要用 IDE你说“IDE 开发迅速,调试方便,适合大型程序……” 那说明微软的程序在你脑子里已经比较根深蒂固,你需要好好清醒一下了,看看我来告诉你。
高明的 UNIX 程序员不用 IDEIDE 从来就是给初级 Windows 程序员用的。
你看看大型的 UNIX 程序,包括 Linux 内核各种网络服务程序Xwindow 程序在内,哪一个是 IDE 搞出来的?我们实验室的 EDA 程序也没有一个是 IDE 弄的,我还知道 Candence, SynopsysMentor 的高性能的图形界面 EDA 程序也都不是 IDE 写的。你信不信,微软的人在写 Windows 本身的时候也根本不用 IDE。微软内部程序员最喜欢的编辑器其实是 VIM用 VIM 的微软程序员上次向乌干达的可怜儿童捐助了1000多美元这是值得称赞的。
有一次某杂志采访一些出名的 Linux 内核程序员,包括 Linus 在内,没有一个人用 IDE有的人用 VIM有的用 Emacs只有 Linus 说 “GNU Emacs is evil”但是其实他用的是一种跟 Emacs 有同样键绑定功能的 MicroEmacs。大家都是用编辑器编辑了程序文件然后用 make 这样的自动工具调用 gcc 编译器完成编译工作的。甚至高级的 Windows 程序员也不用 IDE他们可以从命令行调用 clnmake 来编译自己的程序。虽然这样的 Windows 程序员很少,但是他们却是最了解 Windows最高明的 Windows 程序员。
为什么 UNIX 程序员不用 IDE明白了这个道理你就能体会到 UNIX 的设计思想了。首先,一个 IDE 集成了编辑器,编译器,汇编器,调试器,跟踪器…… 这个编辑器功能肯定比不上 VIM 或 Emacs编译器比不上 GCC汇编器比不上 as调试器比不上 gdb, ddd, 跟踪器比不上 strace, ltrace, truss。你得到的是一套整合的低能的程序。如果你对调试器的功能不满意你只好换用另外一套 IDE但是这套 IDE 的热键,菜单,编辑器功能,按钮…… 跟原来那个有很大不同。你不得不花很多时间来熟悉新的环境,而不能保持原来的某些东西。
而在 UNIX 下就不一样了。你可以用你最喜欢的 VIM 编辑程序,你在 VIM 里可以调用 GNU makemake 可以调用 gcc, ld, ... make 的出错信息可以被 VIM 捕获VIM 能帮你在源程序里定位。你如果喜欢 icc, 你可以让 make 用 icc 而不是 gcc。你如果觉得 gdb 跟踪变量时比较麻烦,你可以用 ddd 来显示各种数据结构之间的关系。你还可以在 Emacs 里调用 gdb那样就可以同步显示源代码了。而且 VIM 和 Emacs 还可以编辑很多其它东西比如信件LaTeX 文档HTML配置文件…… 你不用另外找一个什么编辑器来干这些杂活了。很多程序比如 Mutt, tin 都可以在内部使用 VIM这样就更方便了。实际上 make 在其它方面还能帮你很多忙,我的每一个比较大型的 LaTeX 文档都是用 make 维护的。
### Linux能干的高精尖的事情Windows都干不了
当然有很多事情是Linux/UNIX的专利了。因为 Windows 只能装在 PC 机上,好像以前也有 Alpha 可以使用 Windows NT但是就是没见到有人用。PC 机的能力是很低的,像我们编程序处理 NP-Hard 问题的人,用 Windows 的机器显然速度不够而且有时一个问题算上几天甚至几个星期Windows 机器是以“死机”著称的,我们怎么能放心?
所以几乎所有科学计算程序EDA 程序,高性能图像处理程序都不是 Windows 的。他们有时也会移植一些给 Windows但是常常降低那些程序的能力。你比较过 Windows 版本的 Mathematica 和 Linux 的有什么区别吗?
IBM 制造的最大的并行计算机有 8000 多个处理器Windows 不可能有能力管理这么多处理器,它用的是什么操作系统?答案是 Linux。
《泰坦尼克号》电影里的三维动画那么细腻逼真Windows机器能做出来吗不行。那也是 Linux 机器做的。
民航总局用来训练地情人员的虚拟现实训练设备Windows 当然无能为力。那都是商业的 IRIX 机器。
UNIX 是最早支持 TCP/IP 网络协议的系统。它上面有很多可以互相协作的网络服务程序它们经过多年的使用和修订已经达到比较完善的程度。而就在1997 年微软的比尔盖茨还在扬言“Internet 是没有前途的。” 微软的这个“远见卓识”大家应该都已见识它后来加上的网络服务程序IIS漏洞之多让公安部都频频发出警报大家也是见识了的。
其实你知道了Windows 没有一样有用的事情能比 UNIX 干的更好。
### Linux干不了的有用的事情Windows照样干不了
当然 Linux 不是万能的。它也有不能干的事情,电脑也有干不了的事情。但是 Linux 干不了的事情Windows 肯定也干不了。这些事情就是我们需要探索需要努力的事情了。在你探索的过程中Linux 必定是你的好伙伴。
不要把Linux和Xwindow掩盖起来
不要把我们的用户当成傻瓜。
什么?你早就知道 Windows 是垃圾?噢!你怎么不早说呢!害我废话这么多。嘿嘿。
“好了。你知道 Windows 是垃圾,你现在用什么?”
“Linux + Xwindow”
“那我问你Xwindow 是什么样的?”
“不就是跟 Windows 差不多吗?只不过 'Start' 按钮比较方,而且上面不是一个 Windows 标志,而是一个脚丫子[[11](https://i.linuxtoy.org/docs/guide/ch06s05.html#ftn.id3053473)]。点击一下居然还有很漂亮的中文菜单。我喜欢!”
“你知道什么是‘根窗口’吗?”
“不知道。从来没听说过呢?”
“根窗口就是遮盖整个屏幕的那个最大的窗口。”
“哪儿有什么窗口啊!我没有看到呢?”
你发现了问题吗?这些 Linux 用户说是在用 Linux 和 Xwindow但是他们对 Linux 和 Xwindow 几乎完全不了解。很多人用了那么久 Xwindow 都不知道根窗口是什么东西不知道其实按钮也是窗口不知道窗口管理器和其它程序有什么关系大家都以为窗口上面的按钮是程序自己放上去的不知道窗口的“class name”“resource name”是什么东西。他们也不知道 .Xdefaults 是用来干什么的。特别是他们很多人都不知道 Xwindow 的字体是如何命名的,什么是 fontset有了一个新的字体也不知道怎么安装。
他们被遮在 Linux 之上的一层一层的包装迷惑了,他们等待有图形界面的工具来帮助完成一切事情,他们认为 Linux 跟 Windows 一样,只是麻烦一点。他们知道 Linux 内核很好,但是他们感觉不到 Linux 和 Xwindow 在操作层面的天生的先进性,随后不久就把 Linux 完全删除掉了。你发现没有,要用户理解 UNIX 和 Xwindow 的操作层面的先进性,才是留住用户的最好办法。如果用户体会不到操作时的方便和高效,内核再好他们也不会理会。
但是用摹仿 Windows 的作法来吸引用户,永远会失败的。因为 Linux 如果摹仿 Windows 那一套低效率的方式,那么 Linux 的这套“低效率方式”永远比不上 Windows 的那一套“低效率方式”。那么用户就会说:“这个 Linux没有一样比的上 Windows。”
Linux 天生就是继承了 UNIX 的高效的工作方式,为什么我们要把它掩盖起来?我们为什么只告诉用户 KDE 的菜单怎么用?我们为什么不能像早期的 Xwindow 书籍那样第一节就告诉用户什么是 X server, 什么是 X client什么是 Window Manager, 什么是根窗口。第二章就告诉用户窗口有哪些属性,什么是 classname, resource name, hint怎样使用 .Xdefaults, xrdb ……
在这里我又不得不说一下那些 Linux 的发行公司和写书的人,他们把 Linux 和 Xwindow 包装起来,却没有从基本上告诉用户 Xwindow 的工作原理。很多书籍讲授的层次就是在 Gnome, KDE 的菜单操作的层次靠大量抓图来占篇幅“繁荣”Linux 书籍市场。
现在很多人已经把能够利用别人的库写出一个好看的程序作为自己编程水平的象征。在这个“图形化”,“可视化” 的年代,你如果还在用 troff, LaTeX 写文档,你还在用 VIM 自己编辑 HTML用 Mutt 处理邮件,你还在用文本模式的 gdb 调试程序,你还在用 Xlib 写程序, 你还在用 tin 上 USENET你还在自己写 Makefile写机器代码你还在玩 Clossal Cave 这样的字符模式冒险游戏,那你就是老古董。
其实这种思想是错误的。虽然你是一个坚决的 Linux 支持者,但是你的思想是 Windows 的思想。你认为图形界面,菜单,按钮就可以解决一切问题,就可以给你高效方便。你还是没能摆脱微软给你的潜移默化的东西。你其实离不开 Windows 那样的环境,你迟早会删掉自己的 Linux。
### GUI vs. CLI
做一个坚定不移的“两面派”
大家看到这个标题是不是热血沸腾?两派大虾都可以围攻我了:
GUI派用户“哇我一看你这小子就是 CLI 的。要不然自己写什么 Makefile用什么 Mutt
CLI派用户“切 你还用 X高手都不用 X。你是 GUI 那边的。”
可怜的我“555 你们都不要我~~ GUI 和 CLI 就那么水火不容吗?”
计算机界这样的门派之分还很多。很有特点的就是 CLI 和 GUI 了。CLI (Command LIne Interface) 的狂热份子声称永远不用 X。我上次在实验室看到一个同学用一个 SecureCRT(Putty更好用) 登录到 Sun 机器,然后用一个 vanilla vi 编辑程序,我建议他启动一个 GVIM 过来显示在 Exceed 上可以有语法加亮。但是他坚决反对“高手不用X。你想想要是我在一个很慢的网络连接怎么用 X而且好多服务器没有装 X 程序。”
但是我们实验室的网速可够快Windows 机器都有 Exceed 啊,而且 Sun 机器有全套 X 客户程序包括 GVIM。他说他是 CLI 的坚决拥护者,但是他却在用 Windows他后来打开了好几个 SecureCRT每次从文本框输入地址用户名和密码从下拉菜单选择 "SSH2"然后点击“Connnect”。他还不断的夸SecureCRT是“网络管理员投票选出的最受欢迎的登录方式”。老天SecureCRT 本身就是个 GUI 啊,他其实没有明白 Xwindow 的好处。
你说我是 GUI 的?我虽然很少在 console 下工作。但是我对 bash, VIM 很熟悉,我可以让 bash 按照我的键绑定方式来工作。我可以在 rxvt 里使用 Mutt 来收发 email。我的每个桌面上都常常堆放着一打不同大小的 rxvt。我用 VIM 编辑 LaTeX。我自己写 Makefile 来维护 LaTeX 文档。我有时用 mpg321 来放 mp3。我上BBS用的我自己写的 expect 脚本。 好了CLI 派的朋友可以收我做盟友了 :)
你说我是 CLI 的老古董?我的 FVWM 被我配置为可以“手写操作”,我只要画一个"r"就可以启动 rxvt我只要画一个 "U" 就可以启动 GVIM…… 我用 GVIM 语法加亮模式编辑程序,我用 Mozilla 浏览网页,…… GUI 派的现在好像认我做朋友了 :)
好了。CLI 派的朋友,虽然我很喜欢命令行,但是我有时在屏幕上左右画一下就可以执行:
```shell
Module FvwmConsole -terminal rxvt -geometry 45x5-0+0 \
-bg gold -fg midnightblue \
-fn "-adobe-courier-medium-r-*-*-14-*-*-*-*-*-*-*
```
你是不是现在又想把我逐出师门?
GUI 派的朋友,虽然我很喜欢窗口。但是我可以在 FvwmConsole 里输入:
```shell
All (rxvt) MoveToDesk
```
把我所有的 rxvt 移动到我现在工作的桌面。“这家伙,怎么这么快就叛变了!”
其实何必分什么 GUI 和 CLIUNIX 和 Xwindow 都是工业标准,它们从设计那天开始就有非常灵活的用法,各个程序,不管是 GUI 还是命令行的都可以互相协作。UNIX 和 X 是一家,何必搞的那么偏激,非此即彼?你从我上面的行为可以看出 GUI 和 CLI 的模糊界线吗?我就是坚定不移的“两面派”。
### UNIX是简单的
“我相信简单就是最好,如果太复杂,我是不能理解的。” —Seymour Cray
很多第一次用 Linux 的人会惊奇的发现Linux 的程序居然不用“安装”就可以运行,程序拷贝到随便那个目录都可以用,而不是一定要占用你第一个分区的空间。程序的设置只是一些简简单单的文本文件。你根本不需要什么“注册表修改器 ” 就可以改变系统的设置。这就叫做简单,但是简单就是美。虽然这只是 UNIX 简单性的一个肤浅的认识,你已经体会到了某些东西。
但是简单并不意味着功能弱,并不意味着落后。相反,简单意味着强大,意味着生命力。
我不会再继续阐述我理解到的“UNIX 的简单”,因为这个需要自己去体会。
### UNIX是永恒的
有人说“Plan9 会取代 UNIXMach 会取代 Linux 内核。”
但是你如果是一个深入体会了 UNIX 的人你就会知道UNIX 的思想是永恒的不管时过境迁Plan9 是否代替 UNIXUNIX 的灵魂都会在 Plan9 身上现形!
我为同一个设备写过 Linux 内核和 Windows VxD 驱动程序。写 Linux 驱动程序时,我对 UNIX 设计的完美的一致性远见性所折服。UNIX 用同样界面的 read(), write() 系统调用就可以对不同的对象普通文件设备文件管道管道文件socket…… 进行统一的读写操作。我跟本不需要写一个测试用的应用程序就可以对我的设备驱动进行测试,因为 cat, cp, dd, 它们也使用了同样的 read(), write(),设备和普通文件在应用程序眼里没有区别。在那个还没有 Smalltalk, 没有 C++ 的年代UNIX 的设计者已经使用了所谓的 “面向对象方法”。对C 语言也可以实现面向对象。
UNIX 的系统调用几十年都没有很大变化,这非但不是顽固,不进步的象征,反而是 UNIX 的远见卓识的体现!这就跟 TeX 程序几十年都不变的情况差不多。这些才是真正的永恒的 master piece! 你应该改变所有软件都必需从 0.1, 1.0, 1.1, 1.2, 2.0, ..., 3.0, 3.1, 95, 98, 2000, XP, ... 不断升级的想法。
Windows 就不同了,它在最开头只是一个 DOS 之上的图形包装而已。后来为了兼容以前的糟糕设计,不得不加上很多累赘。我写 VxD 驱动程序的时候就深有体会Windows 95 程序对设备的操作只有用 DeviceIoControl我不得不写了两个应用程序来对设备驱动进行测试。Windows 内核的不一致性和隐密性使我非常恼火。不过 Windows WDM 驱动程序现在也有了 ReadFile, WriteFile…… 那说明什么?那说明 Windows 在向 UNIX 学习,或者有可能是某个 UNIX 设计人员在微软打了几天临工,顺手加了几个 UNIX 的东西进去。这样做是没有用的Windows 从一开始就是非常糟糕的设计,它的历史的包袱太沉重了,缝缝补补有什么用?它只能永远的被 UNIX 甩在身后!
### UNIX是强大的
让聪明人干任何他们想干的事情。
UNIX 的一个特点就是非常高的灵活性Xwindow 也具有这种灵活性。这种灵活性体现在哪里呢?
UNIX 的程序一般都有很多参数,不管你现在用的着用不着,总有人需要某些参数。它们的行为很多都可以用配置文件来改变。比如 GNU bash, 通常缺省的命令行输入方式是 Emacs 方式,但是只要我编辑一个 .inputrc 文件,就可以把它变成 vi 的输入方式,而且我还可以自己绑定键序列到某些操作。我可以用 shopt 来设置它的很多特点比如是否进行通配符扩展是否可以把一个变量当作一个目录来cd是否可以自动纠正某些明显的目录名打字错误 ……
UNIX 程序设计的思想是提供给用户“机制”,而不限制用户制定“政策”。这是一个重要的尊重用户的作法。
我们再来看看 Xwindow。Xwindow 是一个出色的设计,它把显示服务器和客户程序分开。一个显示上既可以显示本机上的程序,也可以显示别的机器上的 X 程序,而它们都遵守你的窗口管理器的统一指挥,它们之间可以方便的传送剪贴版数据,各种事件 …… 比如有时我的 XFree86 上会出现四个不同机器上的 XTerm两个不同机器上的 GVIM…… 它们统一受本机上的 FVWM 指挥。
Xwindow 程序都具有很多很多命令行参数和 resource 参数。你可以随意的在命令行或者 .Xdefaults 文件设置所有的颜色,字体,尺寸…… 而且如果你用 xrdb 把 .Xdefaults 导入到根窗口,那么其它机器上没有经过配置的同样的程序,显示到你的机器上的时候也会遵守同样的外观规定。
Xwindow 的窗口具有 Property, 也就是一些可以自己定义的共享数据(原子)。正是因为这些 Property 的存在,使得 Xwindow 具有无比强大的生命力。X 的窗口管理器和其它客户程序之间并没有统一的协议,但是后来出现了 ICCCM(客户程序间通信规范),这个规范就是通过 property 定义的。现在又有人定义了一套“扩展的窗口协议(EWM Hints)”,使得 Xwindow 可以具有某些 Windows 的特征比如一个工具条程序可以告诉窗口管理器“这个屏幕下面被我占据了24个像素的空间你最大化程序的时候不要越过这个界线。 ”
一个强大的窗口管理程序比如 FVWM它收到这样的提示时可以答应工具条程序的这个要求也可以不答应。一切选择的权力在于谁当然是用户了一切窗口乖乖听话FVWM 给予用户最大的尊重。
你想想,是不是有些 Windows 程序常常弹出一个窗口要你选择 "Yes or No"?你不点击它它就不下去。你觉不觉得你的程序在侵犯你的尊严?你是一个人,一个智慧的生物,怎能受到一个程序如此的待遇?
还有就是很多 Windows 程序把人当成傻瓜,而它是“智能程序”。比如,有一个程序就是喜欢把你的每句话第一个字母都变成大写,我不说它是谁了,你遇到的时候就知道了。如果连“一句话开头一个字母要大写”这么明显的问题都需要程序帮你纠正的话,人脑还用来干什么?况且如果你故意想要不大写的话,那就更麻烦了,我楞是没有从它那一大堆菜单里找到怎么关闭这个愚蠢的选项。
#### 只有符号才能完全操纵计算机
我们来说说很多初学 Linux 的用户。虽然他们在用 Linux但是他们打心眼儿里是觉得 Windows 的工作方式好,他们希望 Linux 有一天能“像Windows那样”。你说“我鼠标一点我菜单一拉…… 就可以完成我的操作。” 但是我要告诉你“Linux 从来没有摹仿 Windows将来也不会。Linux 从诞生之日起,它的工作方式就比 Windows 的先进。Linux 属于能勇敢面对符号的人。只有符号才能完全操纵计算机。”
看看优秀的 UNIX 程序XFree86, FVWM, VIM, Emacs, proftpd, Mutt, wget, tin, ... 没有一个不是用配置文件来设置选项的。为什么这些程序没有方便的菜单可以用来配置?难道它们的设计者就那么低能,连个图形配置界面也写不出来?
当然不是。因为图形界面配置方式的能力是极其有限的而配置文件和程序语言的表达能力却是无限的。用图形界面配置这些程序的话如果你想达到配置文件的效果你需要成百上千的菜单checkbox, radio button, ... 到时候你根本没办法找到你需要修改的地方了!而各个程序的配置文件的语法都有很多相似之处,一般就是一些命令,设置一些变量,参数,…… 一旦用会了一个,其它的也就容易理解了。如果你用惯了 awk, sed, Perl你会觉得那才是真正的自动化啊。
鼠标虽然是很好的工具但是它的表达能力是有限的。你不可能光用鼠标就让电脑完全明白你的意思它毕竟只有3个按钮。看看我的MetaPost页你就能体会到鼠标的这一弱点。所以我们虽然很喜欢鼠标但是却不能完全依赖它。
#### 各个小程序的完美配合
这就是UNIX最重要的特点了它就是UNIX设计的思想。让每个程序只具有一项专门的能力然后让它们合作。Xwindow也继承了这种好传统。
这恐怕就是Windows和其它操作系统望尘末及的地方了。UNIX 程序设计之统一配合之完美真使我难以置信shell, grep, find, awk, sed, make, Perl, Emacs, vi, tin, Mutt, ... 它们是那么的具有一致性!你一旦学会了 sed 的正则表达式,其它程序基本上都能用了。你一旦学会了 vi 和 VIM, 你会发现它的操作是那么的有规律性似乎vi的设计者在几十年前就已经设计好了 VIM 在今天的完美而统一的操作方式而且vi的操作还体现在 Mutt, tin 等很多程序中。你甚至可以把 bash 设置为 vi 的输入方式来输入命令行,我就是这么做的。一个程序可以调用另外一个程序来得到数据,可以把数据交给它处理后返回来,可以在自己的窗口里“嵌入”另外一个程序。
在 Windows 和其它非 UNIX 操作系统中,这种合作是非常困难的。我曾经在 Windows 下使用 Perl来进行一些自动工作。但是 Windows 的文件操作,管道是如此的不稳定,程序之间基本不能合作。你别想在 Visual Studio 窗口里面嵌入 UltraEdit 编辑器,你别想用一个 expect 脚本来控制 telnet 到水木清华 BBS。
Windows 的程序都是大而全大而杂所有的电子邮件程序都需要自己提供编辑器自己发送和收取邮件自己显示邮件的附件。每一个BBS程序都提供自己的Virtual Terminal, 自己的通讯代码。每一个 IDE 都自己提供编辑器,编译器,汇编器,调试器。人们为了使用一种新的程序,需要适应所有这些它提供的界面,而不能使用自己喜欢的编辑器的键绑定,菜单组织…… 不能 DIY
你要知道最高级的电脑是定做的自己想要什么什么CPU什么主板多少内存什么硬盘键盘鼠标显示器都是自己选择的。最高级的滑板自己想要什么牌子的版面什么牌子的沙什么桥什么轮子什么轴承也都是自己选的。最高级的乒乓球拍木板胶皮海绵胶水都是可以自己选择…… 而用 Windows 程序你得到的是大杂烩就像你去买“品牌机”只有那么几种配置而且附带很多你不需要的软件和服务就像你去买组装好的滑板你想要大一点的轮子和窄一点的板子但是你没有这种选择余地Windows 程序就相当于最廉价,最次的滑板。但是它却会花你更多的钱,因为一旦一个部件坏了,或者你不喜欢了,你不能另外找一个好的换掉它,你必需重新买全套配件!
而 UNIX 和 Xwindow 就是高档的“组装货 ”。比如我用 Mutt 的时候,我可以用 VIM 也可以用 pico 来编辑邮件,我可以用 ImageMagick 也可以用 xv 来显示附件里的图片,我可以用 lynx 把 HTML 附件转成文本嵌入窗口中,我也可以把 HTML 附件交给 Mozilla 图形显示。我可以让 GnuPG 帮我把邮件进行数字签名和加密,我也可以用其它 PGP 程序。我想让 Postfix 而不是 sendmail 帮我发出邮件,我想让 fetchmail 帮我收邮件,转发给 postfix然后被我自己写的Perl过滤器处理…… 这一切我都可以办到!我可以选择我最喜欢的专门的程序来完成专门的工作,然后把它们结合在一起,我也可以分别得到它们的好处。
### 学UNIX绝对不是浪费时间
有人告诉我:“你看我用 Windows 什么都不用学。而用 Linux光是安装就花了我一个星期
首先,我要告诉你的是,你装 Linux 花了一个星期,不是因为 Linux 不好装,而是因为你已经习惯了 Windows对 Linux 最初难以理解而已。你想一想你最初安装 Windows 的时候呢?你花了多少时间搞明白什么是硬盘分区?什么是盘符?什么是目录?你认为 Windows 就是那么容易可以学会的吗?虽然你觉得没花时间学,但是你以前在用别人的机器的时候已经耳濡目染,自然就了解了。而且由于你想要 Linux 和 Windows 并存于硬盘上,又增加了安装难度。而且你肯定没有得到有经验的 Linux 用户的帮助,否则他们会在 20 分种之内帮你搞定。一个星期也太夸张了 :P
如果一开始用的就是Linux就没有这个问题。你想想如果你没有用过 windows你肯定会很习惯 /etc, /usr, /usr/local ,... 而不是 C:, D:, E:, ... 是不是?如果你只用过 Linux你第一次用 windows 时恐怕也会问:“/bin 目录哪里去了啊?”
最重要的是你用惯了的UNIX工具它们可以伴随你一生而不会那么容易变化或消失。你可以永远不用再换另外的工具了。除非那个工具比你这个好的太多而且可以完全模拟你现在的工具。
我们实验室一个60多岁的老师用vi, cc, make, ...都几十年了他以前的经验绝对没有白费而且教会了我们一批又一批的学生。vi 伴随着 UNIX 的最初发行而诞生,直到今天还是世界上头两号编辑器之一!有些人的 FVWM 配置文件已经用了 10 多年,现在完全不经修改还可以用。
看看 Windows 的工具,你从 Borland C++ 换到 VC, 就必需适应新的环境:菜单不同了,颜色不同了,按钮不同了,帮助信息不同了,热键不同了,编译器参数,调试器功能也不同了,…… 那个时候恐怕花要花你很多时间去适应。当你刚刚适应了 VC, 你又要换成 VJ, PowerBuilder, C++Builder, ...
很多windows程序员都是这样开头在dos下用 Turbo C, 然后是 Borland C, VC, C++ Builder, ......不断追赶微软的潮流。而且微软的 SDK, MFC, .NET …… 什么都在不断变化,不断出问题,又不断的在修改…… Windows 程序员不得不买又厚又重的 Microsoft Press 的书籍,看了才一个月,又过时了。今天你才学会了写 VxD明天你就必须用 WDM 了。你不得不注册 MSDN 才能赶上 Microsoft 的步伐。很多人说:“计算机是贵族的专业。” 这就是微软一手造成的。
这些东西才是没完没了的浪费大家的时间和金钱的。这是浪费生命!我们为什么不使用从诞生就那么一致和完美的 UNIX你需要理解先进工具的设计理念。UNIX 的工具就像我们用的汽车,它的离合器,油门,刹车,方向盘,后视镜,永远都在同样的位置。用惯了的话,你对你的汽车的每一个部件都会了如指掌,甚至你自己都可以修车了。这难道不好吗?
有人说:“你说我们需要了解 UNIX难道你要开车还必须了解汽车的结构吗” 你去问问开车的司机,哪一个不了解汽车的结构的,那他的驾照就是混来的。你难道想要傻瓜型的“微软牌汽车”吗?我们来看看:
你买的微软牌汽车最开头只有一个座位每加一个座位你得向汽车公司付钱。车上的防撞气囊不时会冒出来说是为了你的安全。每开100英里要大修一次每过一年要换一次引擎。附带的你还必须换用由微软汽车公司指定的石油公司提供的新型号的机油。你的车出了问题但是法律规定你不准私自拆开你的汽车来修理你必需到微软汽车公司指定的维修点去需要付相当多的钱才能修好一个小毛病。
最可气的是,你每换一个型号的微软牌汽车,它的刹车和离合器都在不同的位置,你需要重新去考驾驶执照。如果这辆汽车在途中刹车失灵,你受了重伤,你也不能状告微软汽车公司,因为你买来汽车之后必须签一个合同,说 “由于微软牌汽车对你和家人造成的一切死伤,微软概不负责。”
### 怎样完全用GNU/Linux工作
说了这么多 Windows 的不好。我还没有告诉你我怎么用 Linux 处理有些必要的事情。
半年以前我由于中文老是配置不好,一直是双系统,不时需要重起到 Win2k 来处理汉字。后来我找到了 miniChinput, XSIM 和 SCIM 输入法。这下可以处理汉字了。而且 VIM 和 Emacs 对汉字支持越来越好。我的大部分文本是用 VIM 编辑的包括程序信件网页LaTeX 论文MetaPost 绘图语言。
我不用 Word 这样的程序写论文,而是用 LaTeX因为这是世界上效果最好最方便的论文工具是大多数学术杂志要求的格式。幻灯都是用 ConTeXt 做的用起来很简单而且效果非常漂亮。你可以看看我的TeX介绍。
至于绘图,你可以用很多可视化的工具,比如 xfigdia。但是对于我来说任何可视化的工具都不能完成某些任务我需要一种可以精确描述图形的语言。我找到了MetaPost。它简单又好用而且效果是世界一流的。我的插图如果不是图像都是 MetaPost 画出来的。
我曾经抱怨 mozilla-mail 经常突然消失,损坏我好几封快要完成的信件。后来我发现 mozilla 的邮件处理程序确实是不稳定的,功能又弱,有经验的 UNIX 用户都不用这样的程序。Mutt 是一个非常稳定可靠的 UNIX 邮件处理程序,而且功能非常强大。
我曾经为 Gnome 和 KDE 的不稳定而烦恼。现在我找到了非常强大的 FVWM。KDEGnome 也能和 FVWM 一起工作。虽然 Gnome 和 KDE 总体不稳定,但是某些部件程序还不错,很多 gtk, Qt 的程序也很不错,它们很多都是可以独立于这些桌面环境运行的。
Linux 有很多强大方便的工作方式是 Windows 没有的,或者有类似的东西,但是很差劲或者用起来不方便。比如 ssh 服务rsynccvsexpect ……
### 结论
我写这么多的目的是什么?我希望喜欢 Linux 的朋友,完全清除微软和 Windows 灌输在你脑子里的谬论,别再相信它们所谓的“新技术”,别再追赶 Windows因为追赶 Windows 倒退。马克思有一个思想很重要,“新生事物并不一定是在最近出现的。” UNIXXwindow, TeX 虽然都比 Windows 先出现,但是它们才是先进生产力的代表。我们要清楚的认识到什么才是真正的现代化,什么才是真正的自动化。
消除学计算机很难的幻觉,勇敢的拿起像 bash, FVWM, TeX, VIM, Emacs, Mutt …… 这样强大的程序勇敢的面对符号。不要再埋怨“Linux 为什么不能像 Windows 那样”,不要再浪费时间试用这样那样的程序,不要再忙着升级。你需要理解 UNIX 的工作方式,因为那是大多数科学家的工作方式。 Linux 可以成为你的好朋友,你需要认识它,了解它,信任它,才能完全的靠它来高效的工作。当然,在游戏机,手机,掌上电脑里,或者在用电脑来娱乐的时候,用一些“傻瓜软件”还是不错的 :)
我希望小学,中学的计算机老师能够提高自己的素质,在孩子们的启蒙教育中充分利用 Linux 神秘的特点,引起孩子们对数学,对符号的好奇心。诱导他们用计算机来解决世界上的有趣问题,而不要把教学的范围局限于计算机的操作和它自身的问题。
### 附录我用来处理日常事务的Linux程序
好了好了。我知道你发现自己应该转向 Linux你很后悔当初为什么中了微软的邪。但是不要着急。因为这些东西本来只是工具它们是用来完成你的主要任务的辅助而已。你以前选错了工具这不要紧。你还是拥有你自己原来的专业技能那才是最重要的。工具的东西只有慢慢适应转换不能一蹴而就否则你会感到非常没意思甚至放弃。
如果你只想做一个像我这样的普通用户,主要目的是用 Linux 来完成自己的任务,那就可以不用系统管理员或者网络管理员的标准来要求自己,因为当一个系统和网络管理员确实很辛苦。这里我对实验室的网管同学鞠一躬,谢谢你的指点和帮助 :) 不用把你的机器当成网络服务器,不用开放没有必要的服务,设置好 ssh, ftp 已经足够了。这样会省去了解很多没必要了解的东西的时间。不用过度考虑“安全”,因为 Linux 缺省已经很安全了。不过你有兴趣了解更多那也无妨。
下面给出一些推荐使用的可以处理一般事情的程序。至于你的专业上要用到的科学和工程软件比如 Matlab, Mathematica, Maple, HSpice, Design Compiler, …… 还有其它物理上的,化学上的,生物上的 …… 都必然有 Linux 和 UNIX 的版本。当然他们很多不是免费的,不要总是觉得什么都应该免费,自由不等于免费。它们是经过很多人辛勤劳动的产物,是可靠的程序,它们物有所值。
下面列出我常用的一些 Linux 程序。一个列表里可能有很多,那是为了方便你来选择,我列出了比较信得过的。但其实很多只有第一个是我真正在用的,我不喜欢试用程序。我不是一个合格的网络管理员,我的服务器都只设置了我自己需要的功能,那样可以省去我很多麻烦 :P
Shell: bash。它结合了 csh 和 ksh 的优点,并且有 readline 功能,你可以随意绑定自己的键盘。
编辑器: VIM, Emacs。
程序开发: GCC, make, ld, Scheme48, j2sdk, Perl, Python, Tcl/Tk ...
论文幻灯工具LaTeX, ConTeXt
绘图工具MetaPost。这个语言太强了以至于我只用它了。你不熟悉的话可以用 xfig, dia 来画一些流程图之类的图片。
图像处理ImageMagick。其中的 import 程序可以屏幕抓图convert 程序可以转换图像格式display 可以显示图片和简单编辑(缩放,换质量,转格式,简单绘图,简单虑镜)。通常我就这么点需要。如果你要更强大的图像工具可以用 Gimp, 它几乎和 Photoshop 差不多。
自动管理工具make。我可以用make来自动编译程序自动编译文档自动更新插图…… 全自动,而且不会重复劳动。
数值计算程序SciLab。这个程序基本上可以代替 Matlab。
代数计算程序MAXIMA。这个程序基于世界上最老的计算机代数系统之一: 由美国能源部(DOE)发行的 MIT Macsyma 系统。它是用 Common Lisp 实现的。很多现在的符号计算程序比如 Maple 都从 MAXIMA 身上学到很多东西。它现在经过 DOE 批准以 GPL发行永远是一个自由软件。
加密程序GnuPG。我的 PGP 密钥就是它搞出来的。
打包,压缩程序。什么都有: tar, gzip, bzip2, zip, rar, ...
虚拟光驱程序。Linux 不需要虚拟光驱程序,直接 mount 就行了。
ftp 服务器proftpd, vsftpd。proftpd 功能很强,但是我只用了最简单的一种设置。
WWW 服务器apache。(我一般没有开)
ftp 客户程序lftpncftp。它们都是文本方式操作的但是比起图形界面的方便的多。比如 lftp 几乎具有 bash 的所有方便功能Tab 补全bookmark, queue, 后台下载,镜像…… Linux 也有图形界面的 ftp 客户程序,但是大多不稳定,有很多问题。这就是很多人抱怨 Linux 不如 Windows 的一个小原因。还有很多人用 Wine 模拟 Windows 的 leapftp其实 lftp 比 leapftp 好很多,你需要的只是适应一下。
自动下载工具wget。它非常稳定有一次我下载一个程序用 IE 和 Mozilla 下载回来的文件都是坏的,最后还是 wget 可靠的传输了数据。用它甚至可以镜像整个网站,比起 WebZip 这样的 Windows 程序强多了,而且不会因为你不付钱就在下载回来的网页里强制插入广告。
虚拟终端rxvt, xterm, gnome-terminal, mlterm, ...
X server: XFree86 (现在通常都是 X.Org
窗口管理器FVWM。编译加入了 libstroke。
中文输入XSIM。被我修改过以适应 FVWM 的需要。另外推荐你还可以用 SCIM。
email 处理Mutt + Postfix + fetchmail
看 PDF, PS, DJVU 文件Acrobat Reader, xpdf, GhostScript, gv, djvu工具包和 netscape 插件。
看CAJ文档。我从来不看CAJ之类的文档如果找不到PDF或PS直接去图书馆借最好。
看网页Mozilla, Phoenix, lynx。Mozilla-Xft 的显示效果比 IE 好很多。
英汉字典IBM智能词典星际译王。
编辑网页:我用 VIM 直接写 HTML。你如果想要图形方式的可以用其它的比如 screem, BlueFish。
登录其它 UNIX, Linux 机器openSSH, telnet。 我喜欢用 openSSH 把其它机器的 X 程序通过 ssh 加密的隧道传到我机器上显示。
登录 Windows2000 server 的 display service: rdesktop...我有一天试了一下,不错。后来就没有用过了。
同步程序rsync。我用 rsync 通过 ssh 来跟某些机器同步数据,或者做自己机器上不同目录间的同步。
上BBSrxvt(或任何一种终端) + telnet + chatbot(helloooo 机器人的程序)
QQ, ICQ: 我没有 QQ已经有了QQ for Linux或 ICQ。不过你可以用 Gaim, 它同时支持 QQ, ICQ 和很多其它的即时通信方式。ICQ 用户也可以用 Licq。
放录像MPlayer, RealPlayer。MPlayer 太好了,直接就可以放 VCD, DVD, divx, wma, wmv ... 用 Windows 的同学都很羡慕我,说 Windows 要放这个需要大堆插件。 rm 最好还是用 realplayer 放,它也是免费的。
放音乐: xmms(mp3,ogg都可以), mpg321(放mp3), ogg123(放ogg)。mpg321 不如 xmms 管理音乐文件那么方便,但是有时我还是用 mpg321 放 mp3 作为背景音乐因为懒得开一个xmms窗口 :)
游戏:我觉得 KDE 的那个 ksokoban(推箱子),很好玩 :)
看 Word 文档。请 Word 用户把文档全部转为 PDF 或 PS 再给我文档里没有特殊的格式干脆就用文本吧何必那么麻烦。以前很奇怪的是通知里本来没有什么特殊的格式居然还要发doc附件的email。现在好了我们系发通知都用文本PDF甚至图片了 :P
其它程序还有很多我需要用而你不一定用得着的。比如Doctor Scheme, Scheme48, Scsh, kawa...这些程序只有 Doctor Scheme 有 Windows版本。还有很多幕后工作但是你一般不察觉的xinetd, telnetd, sshd, crond, atd, lpd, ... 他们都比 Windows 的对应者强的多,或者根本没有对应者。
------
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,91 @@
---
layout: post
title: "开源世界旅行手册:病毒"
date: 2019-12-25 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 病毒
### 引言
这是一个非常流行的言论:
> Linux 下的病毒少,是因为 Linux 的使用者少,骇客显然不愿意浪费气力去攻击没有人使用的操作系统。
您可能已经知道了,互联网上用作重要用途的服务器,其中很大一部分是 Linux 系统,另外的一部分是 Unix 系统:) 如果骇客能够搞掉 Linux 系统的话,那么整个互联网就会陷于瘫痪!效果似乎更好一些。
当然了,您一定会想:骇客也是人,他们也喜欢上网,兔子还不吃窝边草呢……兔子那么笨,连乌龟都跑不过……骇客们可比兔子要聪明的多了!它们总没有理由让自己无家可归吧?
是的,我承认这一点……不过他们也不一定非得把互联网干掉。很多骇客作梦都想入侵美国军方的服务器,美军服务器中的绝密数据,我想拉登大叔一定愿意以重金买下的!
如果骇客可以入侵任何一台主机我看过一篇报道美国海军的一个港口的指挥塔以前用Windows每天不得不重启主机N次后来实在扛不住转用了Linux。指挥塔只不过是指挥几艘小船靠个岸负载不大也不是特别重要尚且不可以用 Windows军方的中央服务器就更不可能了他们为什么不去入侵美军的服务器而要入侵您的电脑呢
有些人又会认为这样的解释不能说明什么Linux 比 Windows 安全也许只是偶然的,或许下一个版本的 Windows 会超过 Linux。
个人倾向认为这种情况不可能出现后面的论述基本摘自《UNIX 编程艺术》( 这本书算是开源信仰的《福音书》,建议弄一本看看。由于这本书实在太长了,所以我就不全文摘抄了)
顺带提一下与上面的观点相呼应另一种论调也使人侧目Windows 服务器占到了服务器操作系统xx%的份额。
或许这个现象可以用8020法则来解释
***占服务器总数80%的 Windows 提供了服务总量的20%\***
请您务必注意这只是举一个例子Windows 服务器可能永远也不会占到服务器总数的80%它提供的服务以我个人的角度我不认为可以达到20%,而且永远不会有那一天。
### Windows的缺陷
尽管支持抢先式多任务处理,但进程生成却很昂贵——虽然比不上 VMS但是平均生成一个进程需要0.1秒左右)要比现在的 Unix 高出一个数量级。脚本功能薄弱,操作系统广泛使用二进制文件格式。除了此前我们总结过的,还有这些后果:
大多数程序都不能用脚本调用。程序间依赖复杂脆弱的远程过程调用RPC来通信这是滋生 bug 的温床。
………………
Unix 的系统配置和用户配置数据分散存放在众多的 dotfiles(名字以“.”开头的文件)和系统数据文件中,而 NT 则集中存放在注册表中。以下后果贯穿于设计中:
- 注册表使得整个系统完全不具备正交性。应用程序的单点故障就会损毁注册表,经常使得整个操作系统无法使用、必须重装。
- 注册表蠕变( registry creep ) 现象:随着注册表的膨胀,越来越大的存取开销拖慢了所有程序的运行。
互联网上的 NT 系统因易受各种蠕虫、病毒、损毁程序以及破解crack的攻击而臭名昭著。原因很多但有一些是根本性的最根本的就是NT 的内部边界漏洞太多。
NT 有访问控制列表,可用于实现用户权限组管理,但许多遗留代码对此视而不见,而操作系统为了不破坏向后兼容性又允许这种现象的存在。在各个 GUI 客户端之间的消息通讯机制也没有安全控制,如果加上的话,也会破坏向后兼容性。
虽然 NT 将要使用 MMU出于性能方面的考虑NT 3.5后的版本将系统 GUI 和优先内核一起塞进了同一个地址空间。为了获得和 Unix 相近的速度,最新版本的 NT 甚至将 Web 服务器也塞进了内核空间。
由于这些内部边界漏洞产生的协合效应,要在 NT 上达到真正的安全实际上是不可能的。如果入侵者随便作为什么用户把一段代码运行起来(例如,通过 Outlook email 宏功能),这段代码就可以通过窗口系统向其它任何运行的应用程序发送虚假信息。只要利用缓存溢出或 GUI 及 Web 服务器的缺口就可以控制整个系统。
### Unix的健壮
Unix 至少设立了三层内部边界来防范恶意用户或有缺陷的程序。一层是内存管理Unix 用硬件自身的内存管理单元MMU来保证各自的进程不会侵入到其它进程的内存地址空间。第二层是为多用户设置的真正权限组——普通用户非 root用户的进程未经允许就不能更改或者读取其他用户的文件。第三层是把涉及关键安全性的功能限制在尽可能小的可信代码块上。在 Unix 中,即使是 shell系统命令解释器也不是什么特权程序。
操作系统内部边界的稳定不仅是一个设计的抽象问题,它对系统安全性有着重要的实际影响。
彻头彻尾的反 Unix 系统,就是抛弃或回避内存管理,这样失控的进程就可以任意摧毁、搅乱或破坏掉其它正在运行的程序;弱化甚至不设置权限组,这样用户就可以轻而易举地修改他人的文件和系统的关键数据(例如,掌控了 Word 程序的宏病毒可以格式化硬盘);依赖大量的代码,如整个 shell 和 GUI这样任何代码的 bug 或对代码的成功攻击都可以威胁到整个系统。
### 历史
大部分1980年前的 Unix 竞争者都被拴到单个硬件平台上,随着这个硬件的消亡而消亡。为什么 VMS 可以坚持这么久值得我们作为案例研究一个原因是VMS 成功地从最初的 VAX 硬件移植到了 Alpha 处理器2003年正从 Alpha 移植到 Itanium 上。MacOS 也在1980年代后期成功完成了从摩托罗拉68000到 PowerPC 芯片的迁跃。微软的 Windows 处在计算机商品化将通用计算机市场扁平化到单一 PC 文化的时期,真是生逢其时。
自1980年起对于那些要么被 Unix 压倒要么已经先 Unix 而去的其它系统,不断重现的另一个特有弱点是:不具备良好的网络支持能力。
在一个网络无处不在的世界即使为单个用户设计的系统也需要多用户能力多种权限组——因为如果不具备这一点任何可能欺骗用户运行恶意代码的网络事务都将颠覆整个系统Windows 宏病毒只是冰山一角)。如果不具备强大的多任务处理能力,操作系统同时处理网络传输和运行用户程序的能力将被削弱。操作系统还需要高效的 IPC这样网络程序彼此能够通信并且能够与用户的前台应用程序通信。
Windows 在这些领域具有严重缺陷却逃脱了惩罚这仅仅因为它们在连网变得真正重要以前就形成了垄断地位并拥有一群已经对机器经常崩溃和无数安全漏洞习以为常的用户。微软的这种地位并不稳定Linux 阵营正是利用这一点成功地于2003年在服务器操作系统市场取得了重大突破。
在个人机刚刚进入全盛时期的1980年左右操作系统设计者认为 Unix 和其它传统的分时系统笨重、麻烦、不适合单用户个人机这个美丽新世界,而弃之不理 ——根本不顾 GUI 接口往往要求改造多任务处理能力,来适应不同窗口及其部件的绑定执行线程的事实。青睐客户端操作系统的趋势非常强烈,服务器操作系统就像已经逝去的蒸汽机时代的遗物一样遭到冷落。
但是,正如 BeOS 设计者们所注意到的那样,如果不实现某些近似通用分时系统的东西,就无法满足普遍联网的要求。单用户客户端操作系统在互联网世界里不可能繁荣。
这个问题促使客户端操作系统和服务器操作系统重新汇到了一起。首先互联网时代之前的1980年代晚期人们首次尝试通过局域网进行点对点联网这种尝试暴露了客户端操作系统设计模式的不足网络中的数据必须放到集合点上才能实现共享因此如果没有服务器就做不到这一点。同时人们对 Macintosh 和 Windows 客户端操作系统的体验也抬高了客户所能容忍的最低用户体验质量的门槛。
到了1990年随着非 Unix 分时系统模型的实际消亡,客户端操作系统设计者还是拿不出来多少可能解决这一挑战的方案。他们可以吸收 Unix如 MacOS X 所做的),或通过一次一个补丁重复发明一些大致等价的功能(如 Windows或试图重新发明整个世界如 BeOS但失败了。但与此同时各种开源 Unix 的类客户端能力不断增强,开始能够使用 GUI 并能在廉价的个人机上运行。
然而,这些压力在两类操作系统上并未达到上面描述所意味的那种对称。将服务器操作系统特性,如多用户优先权组和完全多任务处理,改装到客户端操作系统上非常困难,很可能打破对旧版本客户端的兼容性,而且通常做出的系统既脆弱又令人不满意,不稳定也不安全。另一方面,将 GUI 应用于服务器操作系统,所出现的问题却大部分可通过灵活处理和投入更廉价硬件资源得到解决。就像造房子一样,在坚实的地基上修理上层建筑当然要比更换地基而不破坏上层建筑来得容易。
------
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,91 @@
---
layout: post
title: "开源世界旅行手册:硬盘分区"
date: 2019-12-26 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 磁盘分区
### 分区概念
首先我们需要知道,硬盘分区的存在,是由硬盘的物理特性决定的,并不会因为操作系统的不同而有所改变。
请你把一块硬盘想象为一本书。。。即便您不喜欢读书,您也一定非常熟悉它,所有的书都是相同的,包括我们使用的课本。。。您肯定非常熟悉。
一本完整的书,通常包括书名、目录和正文。
如果您需要Linux您首先需要找到一本书名为《Linux》的书书名相当于硬盘的MBR也就是主引导记录。不同的是MBR可以是几个书名合在一起类似于《XX合订本》。这部分内容暂时还没有什么实用价值您只需要大概的了解。
而正文,就是硬盘中记录的数据,这也非常容易理解,且对于安装系统并没有什么影响,所以现在我们来了解目录:
目录相当于硬盘中的分区表,书中的每一个章节,相当于硬盘中的一个分区,它起始和结束的页次,都可以在目录中找到。试想,如果阅读一本撕掉目录的书,您将很难找到您想阅读的部分。同样,如果没有分区表,操作系统也不能够在硬盘上定位数据的位置。
由于历史的原因,硬盘中的分区表大小受到了限制,最多只可以容纳四个分区(主分区)。如果一本书,它的目录最多只能有四个章节,那不是太可怕了么?很多书的内容远远不止四个章节。
于是聪明的人们想到了一个变通的办法,就是利用其中的一个章节,来存储其他部分的目录。比如第一章是前言,第二章是其他部分的目录,我们翻到第二章,呵呵,这里是第二个目录,因为只有第一个目录受四个章节的限制,所以这个目录的内容可以非常的详尽。第二个目录就是分区表中的扩展分区了,其中定义的章节,就是硬盘中的逻辑分区,不是很难理解吧?
明白了这一点我们来看看Linux和Windows对于分区不同的表示方法
可能您已经很熟悉Windows了它使用盘符来表示分区比如CDE每一个分区使用一个盘符来标识并且顺序可以颠倒D不一定就是您系统中的第二个分区。如果您给第二个分区分配最后一个硬盘盘符把所有的盘符按照顺序排列好并且重装一次系统您就会理解什么叫做“头疼”了
而在Linux中分区是这样表示的
```shell
/dev/hda
/dev/hda1
/dev/hda2
/dev/hda5
/dev/sdb1
```
以/dev/hda5为例
因为在Linux中每一个设备都是用/dev/文件夹下的一个文件来表示的,所以/dev/hda5中/dev/表示的是跟目录下的dev目录我们来看剩下的部分hda5.
前两位的字母hd表示这是一块IDE硬盘如果是sd则代表SATA硬盘或者闪存等外设。
第三位的字母a表示这是该类型接口上的第一个设备。同理bcd。。。分别代表该类型接口上的第二三四。。。个设备。例如hdc表示第二个IDE接口上的主硬盘每个IDE接口上允许一个主设备和一个从设备
第四位的数字5并不表示这是该硬盘中的第5个分区而是第一个逻辑分区。因为在Linux中为了避免不必要的混乱分区的顺序是不能改变的分区标识则由它们在硬盘中的位置决定的。系统又要为所有可能的主分区预留标识所以1-4一定不会是逻辑分区5则是第一个逻辑分区以此类推。
### 挂载分区
在Linux系统的安装过程中您需要选择系统目录的挂载点。
我们知道安装Windows时我们可以选择把系统安装在哪一个分区把系统挂到分区上。而在Linux中则相反我们要把分区挂载到系统中。
当我们使用Windows的安装方式把系统挂载到分区上我们就不可能把Windows目录放在C盘而把MyDocuments目录放到其他分区。您或者出于习惯或者出于数据安全方面的考虑通常把文档放到其他分区中。但是Windows下很多软件保存文件到默认目录就是MyDocuments目录这就比较不方便。
当系统安装完成后我们还是可以将MyDocuments目录转移到其他分区不过有点麻烦可能许多朋友还不知道怎么去做。。。
而任何一种Linux系统我们可以在系统安装时就把分区挂载到目录下/home相当于Windows的MyDocuments我们可以把/dev/hda5挂载到此目录下这样我们往/home目录里存东西的时候其实保存在第一个扩展分区中。如果再一次安装系统只要把这个分区挂载到/home目录下那么进入新系统就像回家一样真是太棒了。
理论上来讲,您可以将分区挂载到任何目录下面,您可以自定义挂载的路径。但是我们并不推荐您这么做,因为那没有任何意义。
系统安装程序向您建议的挂载目录,通常也是我们向您建议的,现在我们来了解一下,这些目录通常是用来做什么的。
**/** 根目录唯一必须挂载的目录。不要有任何的犹豫选一个分区挂载它在绝大多数情况下有2G的容量应该是够用了。当然了很多东西都是多多益善的
**swap**:交换分区,可能不是必须的,不过按照传统,并且照顾到您的安全感,还是挂载它吧。它的容量只要大于您的物理内存就可以了,如果超过您物理内存两倍的容量,那绝对是一种浪费。
**/home**:前面已经介绍过了,这是您的家的目录,通常您自己创建的文件,都保存在这里,您最好给它分配一个分区
**/usr**:应用程序目录。大部分的软件都安装在这里。如果您计划安装许多软件,建议您也给它分配一个分区
**/var**:如果您要作一些服务器方面的应用,可以考虑给它分配一个较大的分区
**/boot**如果您的硬盘不支持LBA模式我想那不太可能您最好挂载它如果挂载硬盘的第一个分区应该比较稳妥。一般来说挂载的分区只要100M大小就足够了。
在文件文件这一环节中。我们建议您选择EXT4
> 也许您注意到了Windows中盘符既用于表示硬件硬盘上的分区又用于表示系统中的路径。而Linux中硬件就是硬件路径就是路径不会混淆在一起简单直接
------
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,246 @@
---
layout: post
title: "开源世界旅行手册无名师的Unix心传"
date: 2019-12-27 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 无名师的Unix心传
### 导言
《无根的根》这部心传的收录集在西山纯净的空气中得以保存数十年它的发现在学术圈中掀起轩然大波。这些出土文稿是否为早期Unix教义的新发现抑或仅是后世高明的赝品那些半神秘的人物像尊者Thompson、Ritchie和Mcllroy是否以此发展出我们所处时代的教义
答案无法确知。各方争论均被收入那本经典之作《编程之道》The Tao of Programming。但是《无根的根》在论调风格上都与那本James松散诗化的逸闻译作有显著差异所有一切都围绕着卓越而谜一般的无名师。
把AI KoansAI心传与之相提并论颇为恰当在AI Koans 中也可发现《无根的根》作者的着笔痕迹。它和Loginataka箴言剧也有密切关联实际上《无根的根》和Loginataka大有可能是出自一人之手笔此人正是无名师。
Tales of Zen Master Greg禅祖Greg传说也值得一说对“九寸钉”对引用已经使人对其古老程度产生怀疑它对《无根的根》影响微乎其微。
毫无疑问可以认为标题借鉴了Mumon的禅宗经典读物《无门之门》The Gateless Gate。数个心传都可以看作是对Mumon的回应。
无名师应该归于东派New Jersey还是西派尊者Thompson往Berkeley的划时代之旅。如果不能回答这个问题我们甚至也许无法宣称无名师确实存在它也许只是一群教师也许是一批达摩尊者。
即使把无名师的传说附会在单独的人身上那他最看重的学生Nubi怎么办Nubi是个有血有肉的形象是个完美的门徒。也许有人会想起佛祖最喜爱的追随者阿难达的传说但他的性格特质在神话传说中并没留下痕迹而佛祖也是永恒的谜。
最终,我们可做的便是讲述故事的本原,抽丝剥茧,挖掘其中真意。
《无根的根》还在编写之中,素材的整理和解释都是难题。难题解开后,将被收录到未来版本中。
### 无名师与万行码
无名师曾对来访的程序员说“Unix传统上认为一行shell脚本胜过万行C程序。”
这个程序员自以为对C极其精通“这不可能。UNIX的内核正是用C实现的。”
无名师回道“确是如此。不过UNIX传统上认为一行shell脚本胜过万行C程序。”
程序员颇为沮丧“但是在C中我们可领会到尊者Ritchie的智慧。我们与操作系统和机器合而为一可以获取无与伦比的性能。”
无名师回道“诚你所言。不过UNIX传统上认为一行shell脚本胜过万行C程序。”
程序员冷笑着想愤然离去无名师向学生Nubi颔首示意Nubi在黑板上写下一行shell脚本问道“尊敬的程序员看看这行管道用纯C实现是不是要一万行C代码
程序员沉吟念诵。最终他承认如此。
“你需要多长时间来实现和调试那个C程序”Nubi问道。
“很长”,来访的程序员承认。“但傻子才会干这个而不去完成更有价值的任务。”
“那么谁更了解Unix传统”无名师问道。“是写一万行代码的还是看到任务的无谓而不去编码的
听到此,程序员眼中一亮。
### 无名师与脚本狂
无名师和学生吃早饭时,从黑客大陆来了个陌生访客。
“Ihear y00 are very l33t,”他说。“Pl33z teach m3 all y00 know”。我听说你很牛请把你会的都教给我。
无名师的学生面面相觑都没听懂这类粗鄙言语。无名师微笑道“你想弄懂Unix
“I want to b3 a wizard hax0r”陌生人回答“and 0wn ever3one's b0xen。”我想当个顶尖黑客能掌握所有人的机器。
“我不教这个”,无名师答道。
陌生人很激动。“D00d y00 r nothing but a p0ser。”他说。“If y00 n00 anything, y00 wud t33ch m3。”哥们儿敢情你没真本事啊你要知道点儿东西就教给我了。
“有条路”无名师说“可以将你带入真知。”他在纸上写了个IP地址。“黑掉这台机器这对你来说应该不费什么力气它的管理员不称职。回来后告诉我你发现了什么。“
陌生人鞠了一躬就离开了。无名师把他的早饭吃完。
几天过去了,几个月过去了。没人再想起陌生人。
数年过去了,黑客大陆来的陌生人回来了。
”你混蛋“他说”我黑掉了那台机器你说的没错太容易了。但是我被FBI抓起来扔进监狱了。“
”好“无名师说”你可以继续下一课了。“他在另一张纸上写了个IP地址交给陌生人。
”你疯了?“陌生人喊道。”经过这事,我再也不黑别人的机器了。”
无名师脸现微笑。“这里就是”,他说,“真知的开始。”
听到此,陌生人眼中一亮。
### 无名师的双路论
无名师如是教导学生:
“达摩教义有条准线这在尊者McIlroy的符咒“做一件事并做好”中得到体现。它强调软件应当具有简单一致的行为这符合Unix惯例人和其它程序便都很容易想象其心理模型。
“但达摩教义还有另一条准线体现在尊者Thompson的符咒“有怀疑用穷举”中很多经文都教导我们现在得到的90%比等不来的100%更有价值。它强调实现的健壮性和简单性。
“现在告诉我什么程序符合Unix传统
想了一会儿后Nubi沉思道
“老师,这些教义有冲突。”
“简单的实现往往对边缘情况有欠考虑,比如资源耗竭、无法关闭竞争窗口以及在未完成事务中超时等等。”
“发生边缘情况时软件行为往往不规律、难以猜测。这当然不是Unix传统。“
无名师颔首同意。
“另一方面,大家都知道精巧的程序很脆弱。更进一步说,每个对边缘情况的修正往往牵扯到程序的核心算法,还牵扯处理其它边缘情况的代码。”
“于是对边缘情况防患于未然、确保描述的简单性反而会使得代码过分复杂、bug成堆、根本无法发售。这当然不是Unix传统。”
无名师颔首同意。
“那么什么是正确的达摩道”Nubi问道。
无名师说:
“当鹰飞翔时它忘记爪子与地面相触当虎捕食时它忘记腾空的一刻VAX只重三斤
听到此Nubi眼中一亮。
### 无名师与方法论
无名师和学生Nubi在圣地行走无名师习惯在晚间为城市和乡村的Unix新门徒布道。
一次,聆听者中混入了一名方法论者。
“优化程序时不对热点进行反复衡量,就像渔夫把网撒入空湖中。”无名师说。
“那么,”方法论者说,“管理资源时不持续地衡量你的产能,不也像渔夫将网撒入空湖中么?”
“我一次碰到一个渔夫时,他正将网撒入船下的湖中,”无名师说,“他摸了好一会儿船底,像在寻找他的船。”
“但是,”方法论者说,“如果他把网撒入湖中,为什么还要找船呢?”
“因为他不会游泳。”无名师答道。
听到此,方法论者眼中一亮。
### 无名师的GUI论
一晚无名师和Nubi参加一个程序员的探讨会。有个程序员问Nubi和他的老师来自哪所学校。当得知他们是Unix大道的追随者时程序员颇为不屑。
“Unix命令行工具太粗糙太落后”他讥讽道。“现代的、设计得当的操作系统可以在图形用户界面中做任何事情。”
无名师一言不发,只是指着月亮。旁边的一条狗对着他的手狂吠。
“我不明白。”程序员说。
无名师依然缄默,指着一幅佛祖像,然后又指着一扇窗。
“你想说什么?”程序员问。
无名师指着程序员的头,接着指着一块大石。
“请把话说清楚!”程序员要求道。
无名师深深蹙眉,轻拍程序员的鼻子两下,把他扔到旁边的垃圾箱中。
程序员试图从垃圾堆挣扎出来之时,那条狗跑过来在他身上便溺。
此时,程序员眼中一亮。
### 无名师与Unix狂
一个Unix狂热者听说无名师掌握Unix大道真知便跑来求教。无名师对他说
“当尊者Thompson发明Unix时他并不理解它。随后他理解了受益了不再发明了。“
“当尊者McIlroy发明管道时他只知道它将传递软件并不知道它能传递思想。”
“当尊者Ritchie发明C时他将程序员放到缓冲溢出、堆损坏和烂指针bug的地狱中惩罚。”
“说实话,这些尊者又瞎又蠢!”
狂热者对无名师的用词极为愤怒。
“这些智者”他抗议道“给了我们Unix的大道。我们嘲笑他们就是混淆是非比转世为牲畜和MCSE还不如。”
“你的代码全无污点和缺陷?”无名师问。
“不,”狂热者承认,“没人不犯猎。”
“这些尊者之智,”无名师说,“就是了解自身之愚。”
听到此,狂热者眼中一亮。
### 无名师的Unix传统论
一学生对无名师说“我们听说SCO公司把握着纯正的Unix。”
无名师颔首。
学生继续说“我们还听说OpenGroup公司也把握着纯正的Unix。”
无名师颔首。
“这怎么可能?”学生问。
无名师答道:
“SCO确实把握着Unix源码但是Unix的源码不是Unix。OpenGroup确实把握着Unix的名称但Unix的名称不是Unix。”
“那么什么是Unix传统”学生问。
无名师答道:
“非源码。非名称。非思想。非实物。恒变。不变。”
“Unix传统是简单和空。正是简单正是空才使得它更强胜飓风。”
“以自然法则前行,在程序员手中,吸纳各种优良设计。与之竞争的软件最终必与之想像;空,空,真空,虚无,万岁!”
听到此,学生眼中一亮。
### 无名师与最终用户
无名师又一次布道时,一个最终用户听说了他的智慧,跑来求教。
他对无名师三鞠躬。“我欲学习Unix大道”他说“但是弄不懂命令行。”
一个旁观的新门徒开始嘲讽最终用户说他脑子一锅粥说只有经训练者、有智慧者才配使用Unix。
无名师抚手不语,命这个嘲笑最终用户的新门徒前坐,坐到最终用户身边。
“告诉我,”他对新门徒说,“你写过什么代码,有过什么突出设计。”
新门徒嗫嚅了两句,然后沉默了。
无名师转向最终用户。“告诉我”,他问,“为何你要寻求大道?”
“我用的软件并不能令我满意”最终用户答“既不稳定也不美观。听说Unix之道尽管艰难但超越一切我愿抛去一切诱饵和虚像。”
“那么,”无名师问,“你为何想尽办法让软件帮你做事?”
“我是个建筑工”,最终用户答道,“这座城里的很多房屋都出自我手。”
无名师转向新门徒。“家猫也能欺负老虎”,无名师说,“但是猫叫永远比不过虎吼。”
听到此,新门徒眼中一亮。
------
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,363 @@
---
layout: post
title: "开源世界旅行手册:基础知识"
date: 2019-12-28 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 基础知识
### Shell
可能您早已能够熟练的使用 GUI图形用户界面例如您可以使用鼠标双击一个图标来打开或者执行它。
我们来看这个过程: 您使用鼠标定位桌面上的一个程序图标,按下左键两次。系统读取鼠标指针的位置,并且判断该位置下图标的涵义,根据预设的双击动作,运行程序或者打开文件。
这一套 GUI 系统,便是一种 Shell它的作用是实现人机交互。如果我们不能够控制电脑那么电脑还不如电视机好玩不是么电视机也可以选择频道电视机的遥控器也是一种人机交互的界面不过相对于电脑确实是相当简单了
易于上手、界面直观是 GUI 的优点,但是 GUI 并不意味着简单!或许您有类似经历: 桌面上有几十个程序的启动图标,也知道它们的名字,但是翻出一个来,并不是一件轻松的事情。
我的 Windows 系统中,桌面上摆满了各种图标,每当启动一个程序的时候,我都很是困扰。后来尝试了 音速启动 这类程序启动管理器,效果却不遂人意。(在我的不懈努力下,这个难道最终得到了解决: 将快捷方式名称简化,放到特定目录下,使用 Win+R 组合键呼出 运行 对话框,键入快捷方式的名称来运行该程序。比如 反恐精英 的快捷方式为 cs ,我把它放在 Windows 目录下; 运行 cs 命令,就可以去维护世界和平了。
这么多快捷方式,统统放到 Windows 目录下,非常混乱。因此,我在 D盘 建立了一个名为 path 的目录,并把它的路径加入到环境变量的 path 项中,快捷方式放在 D:\path 目录中。即便重装系统,只要在环境变量中重新加入此路径,原来的程序大多可以直接以命令来运行……我的许多朋友强烈要求我帮他们设定这种启动方式,因为这确实很方便:))
在 Linux 下,所有的程序都可以通过命令运行。虽然 Linux 也有 GUI但是它并不比 Windows 的 GUI 更优秀!上面只是简单的介绍了 CLI命令行界面相对 GUI 的优越之处,使用 CLI 还有更多的好处,您会慢慢体会到的。
当然了在您的印象中CLI 一定非常的不友善,缺少亲和力,冷漠而拒人于千里之外……您和 CLI 之间甚至有代沟的存在
### 命令
坦白的说,冷不丁见到那么老长的一串命令,谁都会毛骨悚然。
也许您使用过DOS留下这种印象命令先放一边光是正确的输入目录、文件名都够瞧的。而且DOS不区分大小写要是像Linux一样区分大小写那多恐怖啊
其实Linux命令行具有补全功能非常实用。假设有这样一个命令
```shell
command path/file
```
如果只有一个以`c`起始的命令,键入`c`,再按一次`tab`键,系统将自动补全该命令余下的部分。只要`c` `tab`两次按键,就可以完成`command`的输入。
现在我们来了解命令的语法结构,这一部分相当重要,您可得看仔细。
我们知道,任何语言都有特定的语法结构,以我们的中文为例:
```shell
我们①郑重地②推荐③您Linux④
```
 这个句子的语法尽管简单,却是大部分的命令行采用的句型。让我们看一下,这个句子里都有些什么。
① 主语Linux命令的执行者当然是用户所以主语一概省略。
② 状语用来修饰谓语。与之相对应Linux命令可以使用选项来精细调节程序的行为。为了与命令的操作对象相区别选项前通常要加-或者--符号。原则上,在命令名后,选项的位置可以随意,但是为了养成一个良好的习惯,我们建议您在命令名后直接跟选项。
③ 一个动词作为谓语而存在。Linux命令中这一步是必须的。这一部分也是不同命令之间最根本的区别方式所以它通常作为命令名写在最前面。键入date命令您可以查看当前的日期。
④ 这两个部分都是宾语,它们是命令的操作对象。大部分的命令只有一个操作对象,也有一些命令是双宾语结构的,具有一个直接宾语和间接宾语。比如拷贝这个命令`cp`copy`cp a b `表示把文件a拷贝为文件b`cp a /home` 表示把当前目录下的a文件拷贝到/home目录下。命令的不同部分使用空格分隔连续的空格视为一个空格
分隔符我们的汉语是象形文字没有分隔符。但是所有的拼音文字中都用空格作为分隔符来分隔单词。Linux命令中同样使用空格作分隔符。
上面的那句话翻译成Linux的命令应该是这个样子的
```shell
推荐 --①郑重地 您 Linux
```
① 按照传统,“-”后跟选项缩写,“--”后跟选项全称。不过也有例外
Linux的命令也蛮简单吧
### Linux程序、进程
或许您会这样想Linux命令的句型确实不难但是那么多命令我这么知道它们都是作什么的呢而且不同的系统中可以使用的命令似乎也不太一样这真让人困惑。。。
其实Linux的命令运行的是Linux系统中的程序。只要您已安装了程序您就可以通过命令来运行它并且可以使用选项来精细的调整它的运行状态。也可以通过点击启动图标来运行不过启动图标不能够方便的调整选项并不是很方便。
举一个例子:
```shell
mplayer -shuffle -loop 3 -playlist mymp3.list
```
可能您运行上面命令系统会提示您无法找到命令那是因为您没有安装mplayer这个程序。mplayer是我见过的支持格式最多的播放器几乎任何已知格式的多媒体文件都可以使用mplayer来播放。它包含一个图形界面的前端您可以在菜单中找到它鼠标点击运行也可以通过执行命令来运行它的命令行版本。
如果您的系统中没有mplayer播放器我们建议您安装一个。关于程序的安装参阅第16章 软件管理。
上面命令中mplayer调用了mplayer播放器程序。选项-shuffle表示随机播放-loop表示循环播放后面的3为循环的次数如果为0则为一直播放。-playlist 表示播放列表中的曲目。我们可以把mp3的路径放到mymp3.list文件中让mplayer来播放它们。
**进程**,为运行中的程序,是程序在内存中的镜像。
好了现在您已经了解了Shell、命令、程序、进程的概念您基本上也就了解了LinuxLinux系统真是非常简洁而且容易理解
但只知道这些,您并不能顺畅使用。接下来的章节中,我们来进一步介绍它的细节。
### 路径
路径分为绝对路径和相对路径。
绝对路径的起始点为根目录`/`,例如/usr/local/bin就是绝对路径它指向系统中一个绝对的位置不受其他因素影响。
相对路径的起始点为当前目录,如果您现在位于/usr目录那么相对路径local/bin所指示的位置为/usr/local/bin
也就是说相对路径所指示的位置除了相对路径本身还受到当前位置的影响。例如Linux系统中常见的目录/bin, /usr/bin, /usr/local/bin, 如果只有一个相对路径bin那么它指示的位置可能是上面目录中任意一个也可能是其他目录。
如果我告诉您到bin目录寻找一个文件您可能搞不清楚是哪一个bin目录。只有当前位置确定相对路径指示的位置才能够确定。
现在我说,/usr/local目录下它的相对路径bin中有某个文件这样就比较明确了。
在相对路径中,`.`表示当前目录,`..`表示当前目录的上一级目录。
假设您安装了一个程序它的主程序没有被放置到上面三个bin目录中的任何一个或者其他系统能够找到的地方您就得告诉系统它的可执行文件在哪里。
可以使用绝对路径,例如: **/home/user/bin/可执行文件**
或者定位到/home/user/bin目录使用相对目录来定位它 **./可执行文件**
路径相关命令:
```shell
cd (change directory) # 更改目录
pwd (print working directory) # 显示当前路径
ls (list) # 显示当前目录中的文件列表
```
请尝试一下操作:
```shell
cd /etc # 进入 “/etc/”目录,这里使用的是绝对路径
pwd # 显示当前路径,这个命令返回结果“/etc”
cd init.d # 进入 “/etc”目录的子目录“init.d”这里使用的是相对路径
cd .. # 进入上一级目录“/etc”
cd ../home # “/etc”目录的上一级目录为“/”它的子目录“home”为“/home”
cd - # 回到上一次的目录,我们在“/etc”目录跳转到“/home”目录所以这次是回道“/etc”目录
cd ~ # "~"代表当前用户的“$HOME”目录也就是“/home/{用户名}”目录。
ls # 在任何时候您都可以使用“ls”命令来了解当前目录下都有哪些文件。
```
远程路径:
远程路径的表示方法为 **协议://用户名:密码@位置/路径:端口**
大多数的远程路径可以使用默认端口匿名访问,由此用户名、密码、端口通常不需要填写,例如:
```shell
http://linuxtoy.org
```
要求身份验证的远程路径,您可以使用下面的方式访问:
```shell
ftp://user:passwd@ftp.linuxtoy.org:21
```
### 软件
Linux中没有注册表这个概念。安装软件理论上讲只要拷贝所有相关文件并运行它的主程序就可以了。
按照传统一个软件通常分别拷贝到同级目录下的bin、etc、lib、share等文件夹。
**bin**:可执行文件,程序的可执行文件通常在这个目录下。在环境变量中设定搜索路径,就可以直接执行,不需要定位其路径。
**etc**:配置文件,大部分系统程序的配置文件保存于/etc目录便于集中修改。
**lib**:库文件,集中在一起,方便共享给不同程序。相较不同的软件单独保存库文件,能够节约一些磁盘空间。
**share**:程序运行所需要的其他资源,例如图标、文本。这部分文件是专有的,不需要共享;而且目录结构相对复杂,混放在一起比较混乱,所以单独存放。
还有一些软件占用一个单独的目录所有的资源都在这个目录中。类似于Windows下的绿色软件不推荐在Linux系统下这样做。
- 执行时,系统找不到可执行文件(搜索所有路径,资源开销过大,是不现实的),需要定位其位置,像这样/home/user/bin/可执行文件,不够方便。
- 许多系统软件需要写作运行,配置文件分别保存,定位它们非常麻烦
- 如果程序使用的库文件,像图形库文件,都单独存放,那么磁盘空间的浪费会非常严重。
有一些大型软件,或者您部署的重要应用,您可以将它们单独安装在一个文件夹下。(通常源码安装支持这种方式)
### 配置方式
Linux下没有类似注册表的系统系统和软件都可以通过纯文本的配置文件进行设置。
事实上,图形界面的配置工具,通常就是以图形界面的方式修改配置文件,适合设置一些比较简单的程序。如果软件有几千个可以配置的选项,全部做成菜单,想象一下。。。开始发抖吧。。。
图形界面的配置工具可以看作特定配置文件专用编辑器。您一样可以使用通用文本编辑器来编辑配置文件比如Nano、Gedit、Knote、Vim或者Emacs等等。不考虑阅读、修改配置文本所占用的时间直接修改配置文件甚至更迅速在以后的章节中如果我们提示您修改某一文件例如/etc/fstab。您可以使用任何顺手的文本编辑器打开它。
如果只是要修改某一常用选项并且时常修改比如主机的IP地址。使用文本编辑器您要找到相应的配置文件还要在配置文件中找到相应的选项会浪费掉您的时间和耐性。
图形配置工具经常会受各种因素制约,比如网络服务器中不提供图形服务,图形界面不够稳定。。。这时,您可以使用命令行的配置工具来完成这些工作。
例如修改主机IP地址可以使用ifconfig这个程序执行下面的命令
```shell
ifconfig eth0 192.168.0.1
```
### 隐藏文件
Linux下名称中第一个字符为`.`的文件或者文件夹,系统会将它们隐藏起来。传统上,这种文件大多是用户的配置文件。
您可以尝试以下操作:
```shell
cd ~ # 进入您的用户目录
ls # 查看当前目录下的文件列表
ls -a # 查看所有文件列表(包括隐藏文件)
```
- 如果您只想查看隐藏文件而不包括这两个特殊目录您可以使用ls命令的选项 `-A`(ls -A)
- 每个目录下都包含两个特殊目录 `.``..`您也许猜到了, `.`代表当前目录, `..`代表上一级目录。
现在,您可以看多许多文件名以`.`起始的文件或者文件夹了吧?使用`ls`命令无法显示它们。
### 文件类型
Linux系统主要根据文件头信息来判断文件类型扩展名并非决定因素。
现在使用`ls -l` 命令,查看详细信息格式的文件列表,您将会看到如下内容:
```shell
total 5
drwxr-x--- 4 user group 4096 Mar 10 00:37 filename
drwxr-xr-x 21 user group 4096 Mar 10 20:16 文件名
-rw------- 1 user group 524 Mar 10 00:40 a
-rw-r--r-- 1 user group 24 Jun 11 2000 b
drwx------ 2 user group 4096 Mar 9 11:06 c
```
共显示了七列信息,从左至右依次为:权限、文件数、归属用户、归属群组、文件大小、创建日期、文件名称
```shell
d①rwxr-xr-x②
```
① 其中要特别留意的是第一列:
| 符号 | 含义 |
| ---- | ------------ |
| - | 普通文件 |
| d | 文件夹 |
| l | 符号链接 |
| b | 块设备文件 |
| c | 字符设备文件 |
| s | 套接文件 |
| p | 管道文件 |
② 剩下的9个字符分为三组分别是归属用户、归属群组、其他用户对于该文件的权限。我们看它的格式
| 符号 | 含义 |
| ---- | ------ |
| r | 可读 |
| w | 可写 |
| x | 可执行 |
- 它们的顺序不能颠倒,某一位置为空(-),则表示不具有相应的权限。
- Linux下的可执行文件并不是由扩展名例如 .exe决定的而是由其可执行权限位决定。
- 目录是一种特殊类型的文件。拥有目录的执行权限才可以进入这个目录!
### 权限
我们已经知道了文件的权限分为r可读、w可写、x可执行三种类型而一个文件可以针对归属用户归属群组其他用户或群组分别设定权限。
这种权限管理方式灵活、简单、严密、明晰。尽管如此在最初的阶段可能会有一点小小的不适。因为它无所不在而您习惯了的Windows的权限管理却不是这样非常混乱大多数时间形同虚设偶尔用到却让人伤透脑筋
使用`chmod`命令更改文件的权限,使用`chown`来更改文件的归属。例如:
```shell
chmod 755 xxx
chmod a+x xxx
chown user:group xxx #用来更改文件的归属用户。也可以同时更改其归属群组
chqrp group xxx # 用来更改文件的归属群组
```
上面命令中的755和a+x是两种类型的表达式
我们将在“权限管理”一节中详细介绍
#### 执行命令的权限
有一些命令普通用户也可以执行但是只有root用户才能执行成功这是为什么呢
例如在系统中增加一个新用户useradd我们看看这个命令的程序文件
```shell
ls -l /usr/sbin/useradd
-rwxr-xr-x 1 root root 56156 2006-04-03 21:37 /usr/sbin/useradd
```
所有的用户都可以执行?
这是因为useradd命令是修改/etc/passwd文件的一个工具来看看这个文件
```shell
ls -l /etc/passwd
-rw-r--r-- 1 root root 1835 2006-06-24 17:58 /etc/passwd
```
原来只有root用户才能写入修改结果非root用户执行useradd命令当然不会有结果。
### 用户
在Linux系统中有两种用户普通用户、root用户
root用户拥有对系统的完全控制权。实际上这没有看上去那样的美妙你必须对自己的行为负全部的责任。Linux继承了Unix的设计哲学系统毫无保留的执行你下达的命令哪怕这个命令是“向我开炮
普通用户只可以做系统允许的事情。尽管可以执行大多数命令但是root专有的命令却不能成功的执行因为这些命令往往关联着只有root才可以处理的文件。也就是说普通用户通常只可以处理自己$HOME目录下的文件。详见“执行命令的权限”一节。
而作为root用户可能会因为误操作给系统带来破坏作为普通用户可以做的事情又太少了点。
#### 执行命令的身份
默认情况下,您的命令提示符末尾$,这表示您将以普通用户的身份执行命令
您可以使用su switch user这命令来切换其他用户。
例如`su root`切换到root用户如果su命令后面没有参数那么这个命令默认切换到root用户
当您执行su这个命令系统会提示您输入密码请输入管理员的密码。这个时候您会发现命令提示符末尾变成了#您将以root用户的身份执行命令。
许多Linux系统随机设定系统root密码。这样更安全一些这个时候您可以执行sudo命令输入当前用户密码后暂时以root用户的身份执行命令。前提是sudoer列表中要包含您的ID
> 使用visudo需要root权限可以将普通用户加入到sudoer列表中。
但即便是用sudo来执行也不能保证系统不会在您的误操作下一命呜呼更安全的办法是直接赋予用户某种操作的权限。
```shell
gpasswd -a user audio
```
这个命令将user加入到audio群组您拥有了使用音频设备的权限。
事实上Linux中一切皆文件包括设备文件。
``` shell
ls -l /dev/sound
crw-rw---- 1 root audio 14, 4 10-04 09:41 audio
crw-rw---- 1 root audio 14, 3 10-04 09:41 dsp
crw-rw---- 1 root audio 14, 2 10-04 09:41 midi
crw-rw---- 1 root audio 14, 0 10-04 09:41 mixer
```
可以看到和音频有关的设备文件除了root用户可以使用audio群组中的用户也可以使用
#### 群组
群组分为两种,主组和辅组。在/etc/passwd文件中与用户相关联的是主组每个用户只可以加入一个主组/etc/group文件中则记录了每一个辅组所包含的用户同一个用户可以被多个辅组所包含。
这样看起来有点乱,但却可以提供更大的灵活性。例如:
```shell
crw-rw---- 1 root audio /dev/sound/audio
brw-rw---- 1 root optical /dev/cdrom
```
先假设一名用户只可以属于一个群组你是系统管理员你管理的用户要求听点音乐你把他加入了audio组于是该用户把CD放进了光驱结果一点反映也没有于是为了让他使用cdrom不得不让他拥有root权限你将不能防范他可能作出的破坏。
而一名用户可以属于多个群组能够很好的解决这个问题你可以让用户既属于audio群组又属于optical群组这样他就可以用cdrom来听音乐却没有root权限。
------
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,329 @@
---
layout: post
title: "开源世界旅行手册:命令系统"
date: 2019-12-29 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 命令系统
### Shell、Console、Terminal
在前面的章节中我们曾提到电视机的遥控器也是一种人机交互的界面算是一种Shell。
但是这个概念并不准确遥控器只是向Shell发送指令的工具Shell受到遥控器发出指令后将指令转换为系统命令由系统来执行。
例如我们按的遥控器上的数字键1遥控器将切换为1频道的指令发送到ShellShell将指令转换为系统可以识别的频道1系统执行它您就可以观看1频道的电视节目了。
通常每台电视机只有一种Shell比如有的电视机系统具有“画中画”的功能那么Shell中便有相应的功能定义您可以通过遥控器上的“画中画”功能键来开启它。假设您的电视机没有此功能Shell中也就没有相应的功能定义。拥有一个带“画中画”功能控制键的遥控器即便信号兼容您还是不能够使用这一功能。
不用遥控器也可以控制电视机假设您的遥控器丢了您还可以走到电视机前使用机身上的控制面板来控制它相当于使用Linux的控制台。但是您一定不喜欢这种方式除非您想锻炼身体。
在Linux系统中由于图形界面和控制台的分辨率通常不一致所以切换时要有一个延时。对于我们中文用户来讲控制台下中文的显示也比较麻烦。而且控制台显示内容通常不如终端显示的全面。
所以我们推荐您使用终端来执行命令,它使用起来感觉很像遥控器。
### rxvt-unicode
通常情况下,您买一台电视机,只能获得一个遥控器。虽然它为您的电视机量身定制,能够最大限度发挥电视机的能力,但您却不一定喜欢它。说不定这个遥控器体形太大,持握不方便;或者它体形太小,容易失踪;又或者它的按键要么太硬,要么太软;它的键盘要么太大,要么太小。。。
您一般也可以容忍,毕竟遥控器使用频率不算高
如果您的终端有些地方不讨您喜欢,比如响应太慢,或者不能正常显示中文。。。那就难以忍受了,您应该换一个其他的试试。
您所接触的第一个终端极有可能是Gnome-Termianl它是系统默认使用的终端显示中文不错不过响应比较慢。
我们推荐您使用urxvtmlterm也是不错的选择
您可以使用sudo apt-get install rxvt-unicode命令来安装它。
urxvt启动它urxvt不支持控制台您得在图形界面启动它。使用终端或者按下Alt+F2建议您在启动栏里新建一个启动图标
rxvt-unicode还支持“服务器/客户端”的运行模式:
- urxvtd启动一个守护进程daemon支持控制台
- urxvtc启动客户端client。多个客户端可以同时连接到一个urxvtd以达到节省系统资源的目的。
或许您对rxvt的默认设置不满意您可以修改用户配置文件~/.Xresources来设定它。修改全局配置文件/etc/X11/Xresources/Xresources则对所有用户生效只有root用户才可以修改此文件。
这里有一些简单的选项:(以!起始的行是注释,您可以直接拷贝此文件的内容)
> urxvt配置 ~/.Xresources
```shell
!!=============================================================================
!! RXVT-unicode setting
!!=============================================================================
!设置字体分辨率
Xft.dpi:96
!设置字体
URxvt.font:-misc-fixed-medium-r-normal--14-*-*-*-*-*-iso10646-1,xft:WenQuanYi Bitmap Song:size=10
!颜色
Rxvt.background:black
Rxvt.foreground:white
Rxvt.colorBD:yellow
Rxvt.colorUL:green
!滚动条
Rxvt.scrollBar:Trues
```
### 帮助系统
您可以使用命令man或者info来阅读Linux命令的在线文档。命令的格式非常简单
```shell
man xxx
```
### bash
好了现在我们换了一个遥控器感觉顺手多了。现在来操练一下。下载一首mp3
我们使用wget这个程序它非常可靠完全值得您信赖.
首先找到一个可以下载的地址,复制链接,在终端窗口内点击鼠标中键,把它粘贴进去。
现在终端中大概是这种情形:
```shell
http://linuxtoy.org/xxx.mp3
```
按下`ctrl+a`组合键,我们发现光标移动道了行首。输入`wget`和`空格`
```shell
wget http://linuxtoy.org/xxx.mp3
```
回车后终端中出现一些信息不一会工夫mp3便下载完成。
使用`ctrl+a`组合键,我们就不需要使用方向键来移动光标,方向键每次只能移动一个字符,没有效率。
您还可以使用`ctrl+f`向前移动光标,`ctrl+b`向后移动光标,`ctrl+e`将光标移动到行末。
#### 中止正在运行的程序
如果一个命令持续时间很长,以致于不能够进行其他操作。可以使用`ctrl+c`来强行中止它。
#### ctrl+s
有时您会不小心按下`ctrl+s`这个组合键shell便会冻结。尝试使用`ctrl+q`组合键,看能否恢复正常。
#### 键绑定
等等,有必要记这么多快捷键吗?都这么复杂!
我们强烈建议您记住以大幅度的提高操作效率。而且这是readline控件的键绑定在任何使用readline控件的程序中您都可以使用它们。例如bash、lftp、gdb等程序同时linux下倍受追捧的Emacs编辑器也是这种风格的键绑定其实是readline使用了Emacs风格的键绑定才对甚至firefox中也可以使用类似风格的快捷键linux下主要有两种风格的键绑定一种是Emacs风格另一种是vi风格。
现在列举一些ReadLine的键绑定您可以自行尝试。运行man readline命令来查看ReadLine手册
先来了解一些约定:
- \C-a表示`ctrl+a`
- \M-a表示`meta+a`,meta键在PC中通常为alt键。
![image-20210626210446827](/img/in-post/2019-12-29-linux-guide-command-system-01.png)
自动补全
| 命令 | 含义 | 按键 |
| ---- | ------------------------------------------------ | ----------- |
| Tab | 使用频率最好的功能! | |
| \C-o | 遍历补全 | (未定义) |
| \M-? | 列出所有可能选项相当于按两次Tab键 | Alt+Shift+/ |
| \M-# | 注释掉当前命令。用户将当前命令暂存于历史记录列表 | Alt+Shift+3 |
| \M-! | 补全命令,通常用来补全子命令,例如`sudo`的子命令 | Alt+Shift+1 |
| \M-~ | 补全用户名 | Alt+Shift+` |
| \M-@ | 补全主机名 | Alt+Shift+2 |
| \M-$ | 补全变量 | Alt+Shift+4 |
| \M-_ | 补全历史记录中的记录 | Alt+Shift+- |
| \M-* | 将所有可能选项放到命令行中 | Alt+Shift+8 |
#### 自定义键绑定
通过修改/etc/inputrc文件可以更改键绑定。建议您使用默认的键绑定以避免不必要的烦恼。当然了Emacs风格的键绑定是通用的随时都有可能用到。
在文件中添加该行可以将ReadLine的键绑定设为vi风格。bash、lftp等使用ReadLine的软件同时生效
```shell
set editing-mode vi
```
找到这一行:
```shell
$if mode=emacs
```
在它的下面添加如下内容
```shell
"\C-o": menu-complete
### 这两行不是必须的,视情况而定 ###
"\C-p": non-incremental-reverse-search-history
"\C-n": non-incremental-forward-search-history
```
重新登陆Shell您就可以使用`\c+o`(Ctrl+o)来遍历补全。假如您的文件名为中文,或者出现乱码时,您可以使用`\M-*`将所有文件名放入命令行,再删除多余的,这真是麻烦极了!简单一点的方法是使用`\C-o`遍历补全,将所有可能的选项轮流放入命令行。
#### 通配符
使用?代表任意单个字符。例如 ???lo表示lo前有三个字符它可以匹配hello
使用*代表随意几个任意字符。例如 `*.iso`代表所有iso格式的文件。
您可以将便利补全和通配符结合使用,以提高效率。例如
```shell
cd */ 则遍历补全只补全文件夹
chmview *.chm 则遍历补全只补全chm文件
```
#### 任务管理
- `&`:在命令的末尾加上一个&符号,表示背景任务,例如
```shell
wget http://www.download.net/xxx/mp3 &
```
- `;`: 使用 ; 将多个命令连结起来,则表示任务按顺序执行
- `&&`: 使用 && 将多个命令连结起来,则表示只有前面的命令执行成功,后面的命令才能得以执行
- \`\` : 如果一个 命令中包含以 \`\` (Esc下方的按键) 括起来的子命令,那么子命令将被优先执行,执行结果被代入上一级命令继续执行,例如创建一个以当前时间命名的文件:
```shell
touch `date + %m.%d_%H:%M:%S`
```
touch命令能够创建一个文件它的操作对象为date+%m%d%H%M%S命令的输出06.06_06:06:60
这样我们创建了一个名为06.06_06:06:60的文件六月六日六时六分刚过六十秒
- `Ctrl+z`; 将当前 Shell 中的任务挂起,这个时候任务的状态为
```shell
[1]+ Stopped xxx
```
- `bg`: 将挂起的任务背景运行。这时它的状态为
```shell
[1]+ xxx &
```
- `fg`: 将背景任务调到前台执行
- `jobs`: 查看背景任务,方括号中的数字为命令的任务编号如果后台运行多个任务,您可以在 bg 或者 fg 后跟任务编号,作为操作对象,例如:
```shell
bg 2
```
#### 管道、重定向
- `>`: 重定向符号,它的作用是将命令的输出重定向到一个文件中。比如我们想把命令`ls`的结果保存为FileList文件作为一个清单我们可以使用重定向符号来完成它
```shell
ls -l > FileList
```
- `>>`:作用与`>`基本相同,不同点在于,`>>`以追加的方式,将命令的输出写入文件的末尾。
- `<`: 是从文件到命令的重定向,将文件的内容作为命令的输入。
- `|`:为管道符号,它的作用是将前一个命令的输出,作为下一个命令的输入。假设一个目录下的文件太多,使用`ls`命令不能够在屏幕中完全显示,这个时候您可以将`ls`命令的输出通过管道符号作为浏览器less的输入。就可以使用浏览器的功能翻页、查找
```shell
ls -la | less
```
#### 脱字符
Shell中的一些功能是通过特殊符号作为控制符号来实现的。上面已经介绍了很多了。这产生一个问题如果一个文件名中刚好包含了这些字符比如`;`就很难对它进行操作。使用less浏览这个文件
```shell
less ;xxx
```
less会很快返回一个错误信息因为并没有一个文件名作为操作对象。接着shell会报告系统中没有`xxx`这个命令
这是因为Shell将文件名中的`;`解析为按顺序执行命令。
或者您的文件名以空白起始而在shell中无论多少个空格都会被解析为一个分隔符。您甚至不能使用命令重命名此文件。
这个时候就要用到脱字符`\`了,它能够将一个具有特殊含义的字符转换普通字符。上面的两个任务。可以在文件名中每个特殊字符前加一个`\`,像这样
```sh
less \;xxx
less \ \xxx
less \;\ \&\xxx
```
也可以使用""将文件名括起来,例如 `less "; &xxx"`,在很多情况下,这样甚至更方便。
脱字符在shell中也可以作为换行符在一个命令的末尾添加一个\,然后回车,在下一行继续输入命令甚于的部分,将一个命令拆分为多行且不影响它的执行(如果执行一个很长的命令,请将它拆分为多行以便于阅读)
事实上换行符也符合脱字符的定义。回车键有两个含义一个执行Enter另一个换行折线箭头。在shell中它作为控制字符执行使用脱字符后它便代表排版字符换行了。
### 设置您的默认Shell
如果能够拥有root权限可以直接修改/etc/passwd文件。找到您用户id起始的行。
```shell
user①:x②:1000③:112④:user⑤,,,:/home/user⑥:/bin/bash⑦
```
①用户登录名
②用户口令(通常转储在/etc/shadow文件中
③用户UID
④用户GID
⑤用户信息
⑥用户$HOME目录位置
⑦为用户登录后的默认shell/bin/bash是程序bash的主程序路径。zsh主程序的路径通常为/bin/zsh
/etc/shells中列出系统中所有可用shell/bin/false代表禁用shell
也可以通过以下命令设置您的默认shell
```she
chsh -s /bin/zsh
(需要输入您的密码)
```
### 设定命令的搜索路径
使用` echo $PATH`可以显示$PATH变量输出如下
```shell
/usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin /usr/bin/X11 /usr/games /usr/X11R6/bin
```
它是一个环境变量代表执行命令时shell的搜索路径。
执行一个命令时shell会到$PATH变量定义的路径去搜索并运行与命令同名的可执行文件。如果程序、脚本等可执行文件并不再上面的路径中就必须使用绝对路径或者相对路径定位可执行文件。
例如:
```shell
/usr/local/mplayer -menu xxx.rmvb
cd /usr/local/ && ./mplayer -menu xxx.rmvb
```
可以修改/etc/environment文件来设定您的命令搜索路径找到PATH起始的行
```shell
PATH:"$PATH:/user/"
```
在双引号中添加您的自定义路径,并以:分隔。
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,278 @@
---
layout: post
title: "开源世界旅行手册:基本系统"
date: 2019-12-30 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 基本系统
### 目录结构
各种Linux发行版的目录结构可能不太一样但它们都遵循FHSFilesystem Hierarchy Stand
实际上FHS只是规定了根目录下的各主要目录应该放些什么文件仅了解这些还不够下面是一般情况下Linux系统的目录结构。
```shell
/ 根目录
|
|-boot/ 启动文件。所有与系统启动相关的文件都保存在这里
| |-grub/ Grub引导器相关文件
|
|-dev/ 设备文件
|-proc/ 内核与进程镜像
|
|-mnt/ 临时挂载
|-media/ 挂载媒体设备
|
|-root/ root用户的$HOME目录
|-home/
| |-user/ 普通用户的$HOME目录
```
### 启动流程
1. 读取MBR的信息启动Boot Manager①
2. 加载系统内核启动init进程②
3. init进程 读取/etc/inittab文件中的信息并进行预设的运行级别按顺序运行该运行级别对应文件夹的脚本。脚本通常以start选项启动并指向一个系统中的程序。③
4. 根据/etc/rcS.d/文件夹中对应的脚本启动Xwindow服务器xorg④
5. 启动登录管理器,等待用户登录⑤
①Windows使用NTLDR作为Boot Manager如果您的系统中安装多个版本的Windows您就需要在NTLDR中选择您要进入的系统。Linux通常使用功能强大配置灵活的GRUB作为Boot Manager。
②init进程是Linux的根进程所有的系统进程都是它的子进程
③通常情况下,/etc/rcS.d/目录下的启动脚本首先被执行,然后是/etc/rcN.d目录。例如您设定的运行级别为3那么它对应的启动目录为/etc/rc3.d/。
④Xwindow为linux下图形用户界面系统
⑤大多Linux系统默认使用GDM作为登录管理器您在登录管理器界面中输入用户名和密码后便可以登录系统。您可以在/etc/rc3.d/文件夹中找到一个名为S13gdm的链接
#### 更改运行级别
在/etc/inittab文件中找到如下内容
```shell
# The default runlevel.
id:2①:initdefault:
```
①2为系统的运行级别默认的运行级别涵义如下
| 运行级别 | 含义 |
| -------- | -------------- |
| 0 | 关机 |
| 1 | 单用户维护模式 |
| 25 | 多用户模式 |
| 6 | 重启 |
### 服务管理
#### 更改启动服务
在运行级别对应的文件夹中您可以看到许多文件名以S##和K##起始的启动脚本链接。例如:
```shell
/etc/rcS.d/S35mountall.sh 挂载文件系统①
/etc/rcS.d/S40networking 启动网络支持
/etc/rc2.d/S13gdm 启动登录管理器
/etc/rc2.d/S20makedev 创建设备文件
/etc/rc2.d/S23xinetd 启动超级进程
/etc/rc2.d/K20powernowd 针对某种硬件的电源管理支持②
```
①init进程将以`start`为选项按文件名顺序执行所有以S##起始的脚本。脚本名称中的数字越小,它将被越早执行。例如在/etc/rc2.d/文件中S13gdm文件名中的数字小于S23xinetdS13gdm将比S23xinetd先执行。
②如果一个脚本链接以K##起始表示它将以stop选项被执行。如果相应服务没有启动则不执行该脚本。
如果您想禁用某一服务在启动时自动运行您可以将相应运行级别中的脚本由S##xxx重命名为K##xxx。
#### 手动控制服务
您也可以手动运行带有一下选项的启动脚本,来控制系统服务。`start`启动`stop`停止`restart`重启。
```shell
/etc/rc2.d/K20powernowd start
```
有时您并不清楚某一运行级别有什么启动脚本;而且此类脚本的前三位字符并不固定,不便于记忆。这时,可以直接使用/etc/init.d/文件夹中的启动脚本(/etc/rcX.d/中的启动脚本链接到/etc/init.d/文件夹下相应脚本)这也是推荐的方式。
```shell
/etc/init.d/powernowd start
```
> 注意:以上命令的位置并没有包含在环境变量的搜索路径中,所以要输入完整路径。
#### 常见系统服务
| 服务 | 解释 |
| ------------------------------- | ------------------------------------------------------------ |
| acpi-support | 高级电源管理支持 |
| acpid | acpi守护进程。这两个用于电源管理非常重要 |
| apmd | acpi的扩展 |
| alsa | 声音子系统 |
| alsa-utils | 声音子系统实用工具 |
| cron | 任务调度系统,建议开启 |
| anacron | cron的子系统将系统关闭期间的计划任务在下一次系统运行时执行 |
| atd | 类似于cron的任务调度系统。建议关闭 |
| binfmt-support | 核心支持其他二进制的文件格式 |
| bluez-utiles | 蓝牙设备支持 |
| bootlogd | 启动日志。开启它 |
| syslog-ng | 系统日志。建议开启 |
| klogd | 同上,使用一种就可以了 |
| sysklogd | |
| cupsys | 打印机子系统 |
| dbus | 消息总线系统message bus system。非常重要 |
| dns-clean | 使用拨号连接时清除dns信息 |
| evms | 企业卷管理系统 |
| fetchmail | 邮件用户代理守护进程,用于收取邮件 |
| gdm | gnome登录管理器 |
| gpm | 终端中的鼠标支持 |
| halt | 别动它 |
| hdparm | 调整硬盘的脚本,配置文件为/etc/hdparm.conf |
| hibernate | 系统休眠 |
| hotkey-setup | 笔记本功能键支持 |
| hotplug and hotplug-net | 即插即用支持,比较复杂,建议不要动它 |
| ifrename | 网络接口重命名脚本。如果您有十块网卡,您应该开启它 |
| inetd | 配置文件为/etc/inetd.conf |
| linux-restricted-modules-common | 受限模块支持① |
| lvm | 逻辑卷管理系统支持 |
| makedev | 创建设备文件,非常重要 |
| mdamd | 磁盘阵列 |
| module-init-tools | 从/etc/modules加载扩展模块建议开启 |
| networking | 网络支持,按/etc/network/interfaces文件预设激活网络非常重要 |
| ntpdate | 时间同步服务,建议关闭 |
| pcmcia | pcmcia设备支持 |
| powernowd | 移动CPU节能支持 |
| ppp | 拨号连接 |
| ppp-dns | |
| readahead | 预加载库文件 |
| reboot | 别动它 |
| resolvconf | 自动配置DNS |
| rmnologin | 清除nologin |
| rsync | rsync守护进程 |
| sendsigs | 在重启和关机期间发送信号 |
| single | 激活单用户模式 |
| sshd | ssh服务器 |
| sudo | 检查sudo状态 |
| udev | 用户控件dev文件系统(userspace dev filesystem)。重要 |
| umounttfs | 卸载文件系统 |
| urandom | 随机数生成器 |
| usplash | 开机画面支持 |
| vbesave | 显卡BIOS配置工具。保存显卡的状态 |
| xorg-common | 设置X服务ICE socket |
| adjtimex | 调整核心时钟的工具 |
| dirmngr | 证书列表管理工具 |
| hwtools | irqs优化工具 |
| libpam-devperm | 系统崩溃之后,用于修理设备文件许可的守护程序 |
| lm-sensors | 板载传感器支持 |
| mdadm-raid | 磁盘陈列管理器 |
| screen-cleanup | 清除开机屏幕的脚本 |
| xinetd | 管理其他守护进程的一个inetd超级守护程序 |
①/lib/linux-restricted-modules/文件夹中的模块为受限模块。例如某些驱动程序,如果您没有使用受限模块,就不需要开启它。
### 配置文件
> 无论何种情况,修改配置文件之前,先备份它
建议使用这个命令
```shell
sudo cp xxx xxx_`date + %y%m%d_%H:%M`
```
当然这很麻烦您可以新建一个名为bak的文件内容如下
```shell
#!/bin/bash
sudo cp $1 $1_`date + %y%m%d_%H:%M`
```
把它放在您能够记住的目录下,比如/home执行命令sh /home/bak xxx, 就可以将当前文件夹下的文件xxx另存为xxx_yymmdd_HH:MM的格式了。
#### 全局配置文件
![image-20210627205333114](/img/in-post/2019-12-30-linux-guide-basic-system-01.png)
#### 用户配置文件
/etc/目录下的文件只有root用户才有权修改。应用软件的全局配置文件普通用户也不应该修改因为所有用户都要用到。
如果要通过配置软件,来适应特殊需求,您可以修改用户配置文件。
用户配置文件通常为全局配置文件的同名隐藏文件,放在$HOME目录下例如
| 全局配置文件 | 用户配置文件 |
| ---------------- | --------------------- |
| /etc/inputrc | /home/user/.inputrc |
| /etc/vim/vimrc | /home/user/.vim/vimrc |
| /etc/bash.bashrc | /home/user/.bashrc① |
①也有少数例外。通常是系统程序。
### 环境变量
环境变量是作用在整个系统中的变量。
很多软件工作的时候都要读取环境变量必须由大量不同程序共享独立于用户程序的优先选项通常由环境变量指定如果通过配置文件给每个程序分别指定既麻烦又容易出差错。例如语言环境如果能够使用多语言的程序都使用自己的配置文件确定其工作界面的语言逐一去修改它们的配置文件将是一件苦恼的事情的值来确定其工作方式。例如cd这个程序如果不带任何选项执行它会读取$HOME这个变量的值然后进入到这个目录也就是用户的HOME目录。使用`echo`显示一个环境变量:
```shell
echo $HOME ①
/home/user
echo HOME②
HOME
```
①依据惯例,变量名称通常使用大写字母:$告诉echo这是一个变量不要将字符串“HOME”直接输出到屏幕
②输出字符串"HOME"到屏幕
```shell
WELCOME="Hello!"①
echo $WELCOME②
Hello!
env③ | grep WELCOME④
export WELCOME⑤
env | grep WELCOME
Hello!
```
①定义一个变量
②输出这个变量
③env输出所有环境变量
④将输出通过管道发送到grep检查其中是否有“WELCOME”这个值
⑤export将变量WELCOME导出为环境变量
定义一个环境变量,可以将定义变量和导出变量一起进行
```shell
export WELCOME="Hello!"
```
①实际上不会有什么程序要用到$WELCOME这个变量定义它只是为了演示。
#### 常用环境变量
| 变量名 | 说明 |
| -------------- | ---------- |
| SHELL | 默认shell |
| HISTSIZE | 历史记录 |
| PATH | 搜索路径 |
| EDITOR | 默认编辑器 |
| TERM | 默认终端 |
| HOME | 用户目录 |
| LANG | 系统语言 |
| XMODIFIERS=@im | 默认输入法 |
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,50 @@
---
layout: post
title: "开源世界旅行手册:软件管理"
date: 2020-01-01 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 软件管理
Linux下安装软件大致有两种方式二进制和源代码。
二进制预编译软件包,可以快速安装部署你所需要的软件,但是你不能决定软件的特性。
使用源代码安装软件,虽然比较耗时,但可以满足你的特殊要求。
### 预编译软件包
预编译软件包可以满足大多数用户的需求,如果没有特殊要求,很少有人会选择编译软件
最早的软件包管理系统不支持网络类似在Windows下安装软件你必须获取一个安装包比如通过网络下载然后在本地安装它。使用这种方式安装软件的包管理系统主要有dpkgdebian系、rpmredhat系
由于Linux系统中软件包拆分的比较细安装一个软件可能要许多软件包。
而用户自己动手获取这些软件包,是一个十分繁琐的过程,因此产生了通过网络安装软件的方式:包管理系统自动分析你将安装的软件需要哪些软件包,通过网络下载、安装、配置它们。
debian系发行版通过apt系统实现网络支持配合本地的dpkg实现这一过程redhat系发行版则通过yum和rpm实现。而一些新派发行版包管理系统兼具这两种能力例如archlinux的pacman而gentoo的emerge虽然基于源代码编译安装但也具备这种能力
常见软件包管理系统的基本操作:
| 任务 | apt | zypp | yum | pacman | emerge |
| -------------- | -------------------------- | -------------------------------- | ----------------------------------- | --------------------- | ---------------------------- |
| | DebianUbuntu | openSUSE | Fedora, CenOS(redhat系) | Archlinux | Gentoo |
| 安装包 | apt-get install pkg | zypper install pkg | yum install pkg | pacman -S pkg | emerge pkg |
| 移除包 | apt-get remove pkg | zypper remove pkg | yum erase pkg | pacman -R pkg | emerge -C pkg |
| 更新包列表 | apt-get update | zypper refresh | yum check-update | pacman -Sy | emerge --sync \| layman -S |
| 更新系统 | apt-get upgrade | zypper update | yum update | pacman -Su | emerge -avuDN world |
| 列出源 | cat /etc/apt/sources.list | zypper repos | yum repolist | cat /etc/pacman.conf | layman -L |
| 添加源 | edit /etc/apt/sources.list | zypper addrepo 仓库地址 仓库名称 | add 仓库 to /etc/yum.repos.d/ | edit /etc/pacman.conf | layman -a |
| 移除源 | edit /etc/apt/sources.list | zypper removerepo 仓库名称 | remove 仓库 from /etc/yum.repos.d/ | edit /etc/pacman.conf | layman -d |
| 搜索包 | apt-cache search pkg | zypper search pkg | yum search pkg | pacman -Qs pkg | emerge --search |
| 列出已安装的包 | dpkg -l | rpm -qa | rpm -qa | pacman -Qii | cat /var/lib/portage \| more |
把SUSE系和redhat系排在前面不是因为它们的软件包管理系统比较先进而是考虑到它们的用户多。
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,520 @@
---
layout: post
title: "开源世界旅行手册:核心工具集(一)"
date: 2020-01-02 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 核心工具集(一)
### 细节
Linux是大小写敏感的系统所有的命令、路径、选项、参数、变量......都区分大小写。
使用`TAB`键补全命令无论任何时候多按几次TAB总会有所帮助。
Shell的功能键能够协助您更高效的编辑命令请熟悉其键绑定、尽量使用它。
命令由`命令名`、`分隔符`、`选项`、`操作对象`组成
**命令名**
标识命令的功能,例如`cp`copy、`mv`(move)、`rm`(remove)......
有些命令包含一些子命令您有认为它的命令名由两个单词构成例如“apt”软件包管理系统
```shell
apt-get install 安装一个软件
apt-get remove 删除一个软件
```
**分隔符**
通常为空格,多个连续的空格视为一个空格,下面两个命令相同:
```shell
cp a b
cp a b
```
有一些特殊符号也属于分隔符,例如管道`|`,重定向`>`,`>>`,`<`、后台运行`&`,序列执行`&&`、`;`。使用这些符号时,你不需要再使用空格作为分隔符
```shell
ls -al|less #两个命令效果相同
ls -al | less #更容易阅读
```
**选项**
精细调节命令的行为,以`-`引导,通常为选项的首字母。许多软件都可以使用`-h`选项来阅读使用说明。例如:
```shell
apt-get -h
apt-get --help①
apt-get -help②
apt-get -h -e -l -p③
```
①也可以使用选项的全名,一般以`--`引导
②多数命令中,使用`-`引导多个字符,将会被视为多个选项
③与上面的命令效果一样
少数命令的选项,不需要以`-`引导,或者使用`-`引导选项全名,例如:
```shell
ps aux
/etc/ini.d/gdm start
mplayer -loop xxx
```
**递归**
表示在子层次中重复相同操作。例如递归复制某目录,不但复制当前目录及其下的所有文件;而且对当前目录的子目录,也进行递归复制的操作。
#### 格式约定
使用`[]`表示可选项,实际输入为方括号中的内容,例如
```shell
ls [-l]
```
实际输入为,`ls -l`
使用`|`表示“或”,以|分隔的项目不能同时使用,例如
```shell
tar [-z|j c|x vf] 归档文件 [源文件]
```
选项通常紧跟命令名,除非必要,在命令格式中,我们通常省略它们。
### 系统信息
**uptime**
联机信息-实际,显示如下:
```shell
11:27pm① up 9 days, 7:12②, 3 user③, load average: 0.07, 0.12, 0.14④
```
①当前系统时间
②系统运行时间
③当前在线用户数
④系统负荷1分钟前、5分钟前、15分钟前
**w**
联机信息-已登录用户,显示如下
```shell
01:04:10 up 1.34, 2 users, load average: 0.25, 0.16, 0.11①
USER② TTY③ FROM④ LOGIN@⑤ IDLE⑥ JCPU PCPU⑦ WHAT⑧
user tty1 192.168.0.1 23:30 1.33 0.14s 0.12s -bash
```
①uptime信息②用户名③登录方式④来源地址⑤登录时间⑥发呆时间⑦资源占用⑧当前任务
**who**
联机信息,`-r`选项为运行级别
**whoami**
显示当前用户名
**last**
最近用户登录信息,`-数字`使用数字作为选项,控制显示条目
**uname**
系统信息
| 选项 | 含义 |
| ---- | ---------------------- |
| -s | 内核名称(默认选项) |
| -a | 全部 |
| -p | CPU信息 |
| -n | 主机名 |
| -r | 内核发行信息(版本号) |
| -v | 内核版本信息 |
**date**
显示、设定系统时间
| 选项 | 格式 | 含义 |
| ----------------------- | ---------------------------- | ------------------------------------------ |
| -u | | 显示格林尼洛时间UTC |
| `MMDDhhmm[[CC]YY][.ss]` | 设定时间,需要管理员权限 | date 12292359 |
| | MM | 月份 |
| | DD | 天数 |
| | hh | 小时 |
| | mm | 分钟 |
| | CC | 年份前两位 |
| | YY | 年份后两位 |
| | ss | 秒钟 |
| | 秒钟、年份为可选 | date 122923592006.59 |
| +[%X] | 设定显示格式,默认输出格式: | date +%Y年%m月%d日%A%H:%M:%S%Z |
| 格式控制 | %n | 换行 |
| | %t | 制表符 |
| 小时 | %H | 0023 |
| | %I | 0112 |
| | %k | 023 |
| | %l | 112 |
| | %p | AMPM |
| 分、秒 | %M | 分钟00~59 |
| | %S | 秒00..61 |
| | %T | hh:mm:ss |
| | %r | hh:mm:ss AM\|PM |
| | %s | 从1970年1月1日00:00:00 UTC到目前为止的秒数 |
| | %X | %H:%M:%S |
| | %Z | 时区 |
| 星期 | %a | SunSat |
| | %A | SundaySaturday |
| | %w | 一周中的第几天(06) |
| 年份 | %Y | 00009999 |
| | %y | 0099 |
| 月份 | %m | 0112 |
| | %b | JanDec |
| | %B | January~December |
| 日期 | %c | 完整日期时间 |
| | %d | 0131 |
| | %j | 001366 |
| | %x | 本地格式日期 |
| | %D | mm/dd/yy |
| 一年中的第几周 | %U | 以Sunday为一周的第一天 |
| | %W | 以MonDay为一周的第一天 |
**cal**
显示日历
### 文件管理
#### 细节
`/` 目录为文件系统根目录,所有目录都是它的子目录
绝对路径以 `/` 起始,相对路径以当前所在目录起始
目录是一种特殊类型的文件,如果没有特别指明,文件包括文件和目录
`..` 表示上一级目录, `.` 表示当前目录,它们是两个特殊目录
**链接**为当前文件建立在其他路径中的访问方法。例如将系统中其他位置的可执行文件,链接到 `/usr/local/bin`目录下,使用命令调用。
**ls [路径]**
显示当前目录文件列表
| 选项 | 含义 |
| ------- | ------------------------------------------------------------ |
| --color | 不同属性以不同颜色显示(默认选项) |
| -a | 全部显示 |
| -i | 显示inode值 |
| -l | 详细信息 |
| -F | 显示文件类型后缀 目录 / 链接@ 可执行文件* 端口文件= 管道文件\| |
| -A | 显示隐藏文件 |
| -R | 递归显示子目录文件列表 |
| -S | 按文件大小排序 |
| -t | 按修改时间排序 |
| -u | 按访问时间排序 |
| -d | 只显示目录,不递归显示目录下的文件 |
**cd [目录路径] [特殊路径]**
切换目录,目录可以使用绝对路径或者相对路径
| 选项 | 含义 |
| ---- | ------------------- |
| ~ | $HOME目录默认值 |
| - | 上一次目录 |
| .. | 上一级目录 |
| . | 当前目录 |
您可以通过修改/etc/environment文件来定义$CDPATH变量设定`cd`命令的搜索路径。
**pwd**
显示当前路径
**file文件名**
显示文件类型,-i表示显示mine类型
**du [路径]**
计算文件或目录空间占用
| 选项 | 含义 |
| ---------------- | ----------------------------------------------- |
| -h | 人性化显示。自动以G、M、K为单位显示占用空间大小 |
| --max-depth=数字 | 显示目录深度 |
| -l | 重复计算硬链接文件大小 |
| -L | 计算符号链接文件大小 |
| -a | 显示当前目录子目录中的文件 |
| -c | 显示文件数 |
**less 文件名**
浏览文件使用VI和Emacs两种风格的键绑定。
**touch 目标文件**
触碰,在不修改文件的前提下,更改其时间属性。通常用来创建一个空文件
**mkdir 文件夹**
创建文件夹,-p按路径创建多级目录 -m数字权限值
**cp 源文件 目标目录|文件**
将源文件复制为目录文件,或将源文件复制到目标目录。多个源文件使用空格分隔。
**cp 源目录 目标目录**
将源目录复制到目标墓中,如果复制多个源目录,需要使用-R选项
| 选项 | 含义 |
| ----- | ---------------------- |
| -a | 相当于-dpr选项 |
| -d | 保留链接 |
| -f | 强制复制,覆盖目标文件 |
| -i | 覆盖时询问用户 |
| -p | 保留修改时间和访问权限 |
| -r -R | 递归复制(目录=>目录) |
| -l | 创建链接 |
| -v | 显示过程 |
**rm 目标目录|文件**
删除
| 选项 | 含义 |
| ----- | ------------------------------ |
| -r -R | 递归删除(用于删除目录) |
| -f | 强制删除(无需确认,直接删除) |
| -i | 交互式删除(询问用户) |
**rmdir 目标目录**
删除目录时,建议使用`rm -r`命令
**mv 源文件 目标目录|文件**
相当于复制后删除源文件,也可以作为“重命名”使用
| 选项 | 含义 |
| ---- | ------------------ |
| -f | 强制,覆盖目标文件 |
| -i | 覆盖时询问用户 |
| -v | 显示过程 |
**ln 源文件 链接**
| 选项 | 含义 |
| ---- | ---------------------- |
| -s | 符号链接 |
| -f | 强制链接,覆盖目标文件 |
| -i | 覆盖时询问用户 |
### 文件操作
**split 源文件 [目标文件名前缀]**
将源文件按一定规则分割成若干个目标文件。默认文件名前缀为x
| 选项 | 含义 |
| ------- | ------------------------------------------------------------ |
| -行数 | 按行数分割文件 |
| -l 行数 | 同上 |
| -b 字节 | 按大小分割文件。可以使用b、k、m做单位不指定单位的情况下默认单位为b |
| -C 字节 | 按大小分割文件,并尽量保持每行的完整 |
**cat 文件名**
输出文件内容。用空格分隔多个文件名
| 选项 | 含义 |
| ---- | ---------------------------------- |
| -n | 在输出中添加行号 |
| -b | 在输出中添加行号,空行不编号 |
| -s | 将两行或以上的空行,合并为一个空行 |
可以将多个文件内容连接到一起输出。使用重定向合并为一个文件 ` cat xaa xab xac > file.split`
**sort [-o 输出文件] [-t 分隔字符] [+起始字段 -结束字段] [文件]**
对文本内容排序
| 选项 | 含义 |
| ---- | ------------------------------------------------ |
| -m | 合并文件 |
| -c | 检查文件是否已按规则排序 |
| -b | 忽略行首空格字符 |
| -u | 忽略内容重复行 |
| -f | 忽略大小写 |
| -l | 忽略非打印字符 |
| -M | 作为月份比较 |
| -d | 按字典顺序排序,按照字母、数字、空格、制表符排序 |
| -r | 逆序输出 |
**more**
查看文件内容使用less
**diff 文件1 文件2**
比较文件
| 选项 | 含义 |
| ---------------- | -------------------------------- |
| 无选项 | 混合ed命令格式 |
| -u [数字] | 统一格式,数字为显示上下文行数 |
| --unified[=数字] | 同上 |
| -c [数字] | 上下文格式,数字为显示上下文行数 |
| --context[=数字] | 同上 |
| -e | ed命令格式 |
| --ed | 同上 |
| -f | RCS命令格式 |
**patch**
使用`diff -uN[r] 旧文件 新文件 > 补丁文件` 命令创建补丁文件。
在待补丁文件的目录下使用`patch -p[数字] < 补丁文件` 命令打补丁`-p [数字]`表示忽略补丁文件中记录的目录数字为忽略的层数 `patch -R 已打补丁文件 补丁文件`将已打补丁文件恢复到原来的状态
**cksum [文件名]**
计算文件的CRC值。不指定文件名则从标准输入设备读入数据
**md5sum [文件名]**
计算文件的md5值。不指定文件名则从标准输入设备读入数据
| 选项 | 含义 |
| ---------- | ------------------------------------------------------- |
| -t | 以文本模式读取 |
| -b | 以二进制模式读取 |
| -c md5记录 | 校验md5记录中的文件使用md5sum配合重定向生成记录文件 |
### 压缩解压
**tar [-]cxurt [zj] [v] -f 归档文件 [待打包文件]**
将多个文件打包为一个归档文件可以在打包的同时进行压缩。支持的格式为tar(归档)、gz压缩、bz2压缩率更高比较耗时
| 选项类型 | 选项 | 含义 |
| -------- | -------------- | ------------------------------------------ |
| 操作类型 | -c | 创建 |
| | -x | 解包 |
| | -u | 更新 |
| | -r | 添加 |
| | -t | 查看 |
| | -d | 比较压缩包内文件和文件 |
| | -A | 将tar文件添加到归档文件中 |
| 格式选项 | -z | 使用gz压缩格式 |
| | -j | 使用bz2压缩格式 |
| 其他 | -v | 显示过程 |
| | -f文件名 | 归档文件的文件名,使用 - 代表标准输入/输出 |
| | -C 解压路径 | 将压缩包中的文件解压到指定目录 |
| | --exclude=文件 | 排除文件 |
| | -P | 使用绝对路径压缩时,保留根目录“/” |
| | -W | 校验 |
| | -p | 还原文件权限 |
| | -w | 询问用户 |
| | --totals | 统计 |
| | -T 文件列表 | 处理文件列表中的文件 |
| | -X 文件列表 | 排除文件列表中的文件 |
**7z7za 子命令 [选项] 压缩包 [文件]**
| 选项类型 | 选项 | 含义 |
| -------- | -------------- | ---------------------------------- |
| 子命令 | a | 添加 |
| | d | 删除 |
| | e | 解压 |
| | x | 带路径解压 |
| | l | 列表查看 |
| | t | 测试 |
| | u | 更新 |
| 选项 | -m 压缩方式 | |
| | -m0=压缩算法 | 默认使用lzma |
| | -mx=数字 | 19压缩级别 |
| | -mfb=64 | number of fast bytes for LZMA = 64 |
| | -md=字典大小 | 设置字典大小,例如-md=32m |
| | -ms=on\|off | 是否固时压缩 |
| | -o输出目录 | 设置输出目录 |
| | -p密码 | 使用密码 |
| | -r数字 | 递归,使用数字定义递归子目录的深度 |
| | -sfx[模块名称] | 使用自解压模块 |
| | -si | 从标准输入设备读入数据 |
| | -so | 将数据写入标准输出设备 |
| | -y | 所有询问均回答Yes |
| | -w路径 | 设置工作目录 |
### 搜索
**whereis 程序名称**
查找软件的安装路径
| 选项 | 含义 |
| ------- | -------------------------- |
| -b | 只查找二进制文件 |
| -m | 只查找帮助文件 |
| -s | 只查找源代码 |
| -u | 排除指定类型文件 |
| -f | 只显示文件名 |
| -B 目录 | 在指定目录下查找二进制文件 |
| -M 目录 | 在指定目录下查找帮助文件 |
| -S 目录 | 在指定目录下查找源代码 |
**locate 文件名称**
在文件索引数据库搜索文件 `-d 数据库路径`搜索指定数据库,`updatedb`更新文件索引数据库
**find [路径] 表达式**
查找文件
| 选项 | 含义 |
| ---------------- | ------------------------------------------------------------ |
| -name 表达式 | 根据文件名查找文件 |
| -iname 表达式 | 根据文件名查找文件,忽略大小写 |
| -path 表达式 | 根据路径查找文件 |
| -ipath 表达式 | 根据路径查找文件,忽略大小写 |
| -amin 分钟 | 过去N分钟内访问过的文件 |
| -atime 天数 | 过去N天内访问过的文件 |
| -cmin 分钟 | 过去N分钟内修改过的文件 |
| -ctime 天数 | 过去N天内修改过的文件 |
| -anewer 参照文件 | 比参照文件更晚被读取过的文件 |
| -cnewer 参照文件 | 比参照文件更晚被修改过的文件 |
| -size 大小 | 根据文件大小查找文件,单位 b c w k M G |
| -type 文件类型 | 根据文件类型查找文件。b块设备 c 字符设备 d 目录 p 管道文件 f 普通文件 \| 链接 s 端口文件 |
| -user 用户名 | 按归属用户查找文件 |
| -uid 用户编号 | 按用户编号查找文件 |
| -group 群组号 | 按归属群组查找文件 |
| -gid 群组编号 | 按群组编号查找文件 |
| -empty | 查找空文件 |
**grep 字符串|正则表达式 文件名|标准输入**
在文件中搜索内容
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,512 @@
---
layout: post
title: "开源世界旅行手册:核心工具集(二)"
date: 2020-01-03 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 核心工具集(二)
### 权限管理
**细节**
一个文件主要包含下列属性,`ls -l`
```shell
- rwx①rwx②rwx③ user④ group⑤ date⑥ filename⑦
111 101 101
```
① 归属用户的权限
② 归属群组的权限
③ 其他用户的权限
④ 归属用户
⑤ 归属群组
⑥ 日期信息
⑦ 文件名称
- 对于文件夹必须拥有它的可执行权限才能够使用cd命令进入该文件夹拥有可读权限才能够使用ls命令查看该文件夹的文件列表
- root用户 拥有最高权限
可以使用3位的二进制数字来描述一组权限某一权限对应的数字为1则表示具有该种权限为0则不具有该种权限。
使用二进制数字来描述一组权限虽然非常直观但是3组权限需要用9位数来表示不够方便。因此我门将三组权限使用3位8进制数字来表示
每种权限对应的数字:
| 权限 | r | w | x |
| ------ | ---- | ---- | ---- |
| 二进制 | 100 | 010 | 001 |
| 八进制 | 4 | 2 | 1 |
将这3位8进制数字相加的结果就可以表示该组权限的具体内容例如
还可以使用a、u、g、o表示归属关系使用=、+、-表示权限变化使用r、w、x表示权限内容
a 所有用户 u 归属用户 g 归属群组 o 其他用户
= 具有权限 + 增加权限 - 去除权限
r 可读权限 w 可写权限 x 可执行权限
例如:
```shell
a+x 给所有用户增加可执行权限
go-wx 将归属群组和其他用户的可写、可执行权限去掉
u=rwx 归属用户 具有可读、可写、可执行权限
```
**chmod 权限表达式 文件|目录**
更改文件的权限。权限的表达式可以使用3位8进制数字表示或者使用augo +-=rxw-s 来表示
| 参数 | 含义 |
| -------------------------- | ------------------------ |
| -R | 递归 |
| -v | 显示过程 |
| -c | 类似“-v”仅显示更改部分 |
| --reference=参照文件或目录 | 以指定文件为参照更改权限 |
示例:
```shell
chmod -R a+x path
chmod -Rv 755 path
```
**chown 归属[:归属群组] 文件|目录**
更改文件的归属用户。可以使用用户名或者用户编号
| 参数 | 含义 |
| -------------------------- | ------------------------ |
| -R | 递归 |
| -v | 显示过程 |
| -c | 类似“-v”仅显示更改部分 |
| --reference=参照文件或目录 | 以指定文件为参照更改权限 |
示例:
```shell
chown user:admin path
chown -R user.admin path
chown user path
```
**chgrp 归属群组 文件|目录**
更改文件的归属群组。可以使用群组名或者群组编号,选项同上
**SUID、SGID、Sticky bit**
某些情况下需要以可执行文件归属用户的身份执行该文件可以为该文件设置SUID。同样设置SGID能够以该文件归属群组的身份执行它。
例如:用户自行设定密码。出于安全方面的考虑,/etc/shadow 只能由root用户直接修改
```shell
-rw------- root root /etc/shadow
```
这个时候,可以为程序 /usr/bin/passwd 设置SUID当普通用户执行`passwd`命令时便能够以该程序归属用户root的身份修改 /etc/shadow文件而 passwd程序自身带有身份验证机制不能通过验证时拒绝执行从而保证了安全。
```shell
ls -l /usr/bin/passwd
-r-s--x--x root root /usr/bin/passwd
```
我们发现,归属用户的可执行权限位使用 `s`表示SUID。同样归属群组的可执行权限位使用`s`表示SGID。任何用户或群组都拥有“其他用户”的权限所以不需要以其他用户身份执行文件其他用户的可执行权限位便不会出现`s`。该权限位可能出现的属性为`t`。也就是粘着位Sticky bit。
```shell
ls -ld /tmp
drwxrwxrwt root root /tmp
```
粘着位表示任何用户都可能具有写权限但只有该归属用户或root用户才能够删除。
SUID、SGID、Sticky bit也可以像权限一样使用一个八进制数表示如下
| 八进制数 | 含义 |
| -------- | ---------- |
| 4 | SUID |
| 2 | SGID |
| 1 | Sticky bit |
通过在`chmod`命令中使用4个八进制数的表达式如4755用第一位表示SUID、SGID或Sticky bit便能够为文件设置这些特殊权限。示例
```shell
chmod -R 4755 path
```
**lsattr [路径]**
查看文件的特殊属性
| 参数 | 含义 |
| ---- | ---------- |
| -a | 全部显示 |
| -d | 只显示目录 |
| -R | 递归 |
特殊属性包括:
| 参数 | 含义 |
| ---- | ---------------------- |
| a | 仅供附加用途 |
| b | 不更新最后存取世界 |
| c | 压缩后存放 |
| d | 排除在转储操作之外 |
| i | 不得任意更动文件或目录 |
| s | 保密性删除文件或目录 |
| S | 即时更新文件或目录 |
| u | 预防意外删除 |
**chattr +|-|=属性 路径**
更改文件特殊属性
| 参数 | 含义 |
| ---- | -------- |
| -R | 递归 |
| -V | 显示过程 |
### 用户管理
**细节**
root用户为根用户也就是系统管理员拥有全部权限。
一个用户只能拥有一个群组编号,但是还可以归属于其他附加群组
用户管理的重要配置文件:
| 文件 | 内容 |
| ------------ | ----------------------------------------------------------- |
| /etc/passwd | 用户名 密码位 用户编号 归属群组编号 姓名$HOME目录 登录shell |
| /etc/shadow | 用户名 已加密密码 密码改动信息 密码策略 |
| /etc/group | 群组名 密码位 群组编号 组内用户 |
| /etc/gshadow | 群组密码相关文件 |
| /etc/sudoers | 用户名 权限定义 权限 |
- 请使用`visudo`命令修改/etc/sudoers而不是直接编辑
- 可以使用`pwconv`命令创建影子密码,将/etc/passwd文件中的密码转换到/etc/shadow文件
**su [用户名]**
切换到其他用户默认切换到root用户。提示密码为目标用户密码
| 参数 | 含义 |
| -------- | -------------------------- |
| -f | 快速切换,忽略配置文件 |
| - -l | 重新登录 |
| -m \| -p | 不更改环境变量 |
| -c 命令 | 切换后执行命令,并退出切换 |
**sudo命令**
以其他用户的身份执行命令默认以root的身份执行。提示密码为当前用户密码
| 参数 | 含义 |
| ------------------- | -------------------------- |
| -s | 切换为root shell |
| -i | 切换为root shell并初始化 |
| -u 用户名\|用户编号 | 执行命令的身份 |
| -l | 显示自己的权限 |
**passwd [用户名]**
设定用户密码
| 参数 | 含义 |
| ------- | -------------------------------- |
| -d | 清除密码 |
| -l | 锁定账户 |
| -e | 使密码过期,在下次登录时更改密码 |
| -S | 显示密码认证信息 |
| -x 天数 | 密码过期,最大使用时间 |
| -n 天数 | 冻结密码,最小使用时间 |
| -s | 更改 登录shell |
| -f | 更改用户信息 |
示例:
```shell
passwd
Changing password for user
(current) UNIX password: #原密码
Enter new UNIX password: #新密码
Retype new UNIX password: #确认新密码
```
**gpasswd 群组名**
更改群组
| 参数 | 含义 |
| --------------------- | -------------------------------------------------------- |
| -a 用户名 | 将用户加入群组 |
| -d 用户名 | 将用户从群组中删除 |
| -r | 删除密码 |
| -A 用户名 | 将用户设置为群组管理员群组管理员或root用户才可以使用 |
| -M 用户1用户2...... | 批量设置群组成员 |
**chsh -s Shell [用户名]**
更改登录shell
**usermod 用户名**
修改用户账户
| 命令 | 含义 |
| ----------- | ------------------------------- |
| -d 目录 | 设定 $HOME 目录 |
| -m | 设定 $HOME 目录时自动建立该目录 |
| -s Shell | 修改用户 登录Shell |
| -l 新登录名 | 修改为新登录名 |
| -u 用户编号 | 修改用户编号 |
| -g 群组名 | 修改用户归属群组 |
| -G 群组名 | 修改用户归属辅组 |
| -L | 锁定账户 |
| -U | 解除锁定 |
| -e 过期时间 | 设定用户账号过期时间 |
| -f 缓冲天数 | 设定密码过期后多长时间关闭账号 |
| -c 字符串 | 修改用户备注 |
**useradd 用户名**
新建用户
| 参数 | 含义 |
| ----------- | ----------------------------------------------------- |
| -d 目录 | 设定 $HOME 目录 |
| -m | 自动建立 $HOME目录 |
| -M | 不自动建立 $HOME 目录 |
| -s Shell | 设定用户登录Shell |
| -u 用户编号 | 设定用户编号 |
| -g 群组名 | 设定用户归属群组 |
| -G 群组名 | 设定用户归属附加群组 |
| -n | 不建立以用户名为名称的群组 |
| -e 过期时间 | 设定用户账号过期时间 |
| -f 缓冲天数 | 设定密码过期后多长时间关闭账号 |
| -c 字符串 | 设定用户备注 |
| -D [表达式] | 更改预设值(预设值保存在/etc/default/useradd 文件中) |
- 新建用户规则保存于/etc/login.defs 文件中
- 新建用户默认文件保存于/etc/skel/目录中。新建用户时,系统自动拷贝此目录下的文件至新建用户的 $HOME 目录
**userdel 用户名**
删除用户, -r 删除用户相关文件和目录
**id [用户名]**
显示用户 用户编号 群组编号 归属附加群组
### 进程管理
**细节**
进程一般分为批处理进程、交互进程和守护进程三类。
守护进程总是活跃在系统启动时通过脚本自动启动或由root启动通常在后台运行。
一个进程可以拥有子进程。当父进程终止时,它的子进程也随之终止;而子进程终止时,父进程通常可以继续运行。
init进程为根进程所有进程都是它的子进程。
**ps**
显示进程信息,选项可省略“-”
| 参数 | 含义 |
| -------- | ------------------------ |
| aux | 以BSD风格显示进程常用 |
| -efH | 以SystemV风格 显示进程 |
| -e \| -A | 显示所有进程 |
| a | 显示终端上所有用户的进程 |
| x | 显示无终端进程 |
| u | 显示详细信息 |
| f | 树状显示 |
| w | 完整显示信息 |
| l | 显示长列表 |
输出字段
| 字段 | 含义 |
| ---- | ------------------------------------- |
| USER | 进程所有者 |
| PID | 进程编号 |
| PPID | 父进程编号 |
| %CPU | CPU占用率 |
| %MEM | 内存占用率 |
| NI | 进程优先级。数值越大占用CPU时间越少 |
| VSZ | 进程虚拟大小 |
| RSS | 页面文件占用 |
| TTY | 终端编号 |
| STAT | 进程状态 |
| | D 不可终端 |
| | R 正在运行,或在队列中的进程 |
| | S 处于休眠状态 |
| | T 停止或被追踪 |
| | Z 僵尸进程 |
| | X 死掉的进程 |
| | < 高优先级 |
| | N 低优先级 |
| | L 有些页被锁进内存 |
| | s 包含子进程 |
| | + 位于后台的进程组 |
| | l 多线程,克隆线程 |
**pstree [ 进程编号 |用户 ]**
树状显示进程信息。可选择显示某用户的进程或从某进程编号开始的进程。
| 参数 | 含义 |
| ----------- | ---------------------------------- |
| -a | 显示完整命令及选项 |
| -c | 完成显示重复进程 |
| -p | 显示进程编号,隐含-c |
| -n | 按进程编号排列进程 |
| -u | 显示进程所有者 |
| -h | |
| -H 进程编号 | 高亮显示进程表好指定的进程及其祖先 |
**pgrep 进程名**
显示进程编号
| 参数 | 含义 |
| ---- | -------------------- |
| -l | 显示进程名和进程编号 |
| -o | 进程起始编号 |
| -n | 进程终止编号 |
**kill [信号代码] 进程编号**
根据进程编号向进程发送信号,常用来结束进程,默认信号为-9
| 参数 | 含义 |
| ------------- | ------------------------ |
| -l [信号代码] | 显示、翻译信号代码 |
| -9 \| -KILL | 发送kill信号退出 |
| -6 \| -ABRT | 发送abort信号退出 |
| -15 \| -TERM | 发送Termination信号 |
| -1 \| -HUP | 挂起 |
| -2 \| -INT | 从键盘中断相当于Ctrl+c |
| -3 \| -QUIT | 从键盘退出相当于Ctrl+D |
| -4 \| -ILL | 非法指令 |
| -11 \| -SEGV | 内存错误 |
| -13 \| -PIPE | 破坏管道 |
| -14 \| -ALRM | |
| -STOP | 停止进程,但不结束 |
| -CONT | 继续运行已停止的进程 |
| -9 \| -1 | 结束当前用户的所有进程 |
**pkill [信号代码] 进程名称**
结束进程族。如果结束单个进程请用kill
**xkill**
在图形界面中点杀进程。执行此命令后,鼠标指针变为骷髅图案。在窗口中点击左键杀死进程,右键取消
**top**
动态、交互式进程管理器
| 启动选项 | 含义 |
| ----------- | ------------------------------------------------------------ |
| -b | |
| -c | 显示进程启动状态,包括选项、参数、操作对象等;而不只是进程名 |
| -d 秒 | 刷新频率。 -d 5 表示5秒刷新一次 |
| -n 次 | 刷新次数,然后退出。 -n 5 表示刷新5次后退出 |
| -i | 禁止显示空闲进程或僵尸进程 |
| -p 进程编号 | 仅监视指定进程的编号 |
| -s | 安全模式运行,禁用一些交互指令 |
| -S | 累积模式输出每个进程的总的CPU时间包括已死的子进程 |
| 交互命令 | 含义 |
| -------- | -------------------------------------------------------- |
| space | 立即刷新 |
| k | 交互式杀死进程提示输入进程编号默认发送信号15 |
| r | 设定renice提示输入进程编号和renice值 |
| s | 改变两次刷新时间间隔,以秒为单位 |
| n | 设定显示进程数0为不作限制 |
| i | 隐藏空闲进程和僵尸进程 |
| S | 切换到累积时间模式 |
| l | 开关在顶部显示uptime信息 |
| t | 开关在顶部显示进程和CPU状态 |
| m | 开关在顶部显示free信息 |
| c | 显示方式切换:进程名/进程启动状态 |
| A | 按进程启动顺序进行排序。由新到旧 |
| M | 按内存占用排序。由大到小 |
| N | 以进程编号排序。由大到小 |
| P | 按CPU占用排序。由大到小 |
| T | 按时间/累积时间排序 |
| f\|F | 设定显示字段。设定完成后空格退出 |
| o\|O | 设定显示字段的排序。大写向前移动,小写向后移动,空格退出 |
| h\|? | 显示有关安全模式和累积模式的帮助信息 |
| W | 把当前的配置写到~/.toprc中 |
**renice 优先级 进程**
重新设定进程优先级(通常无此必要)
| 优先级表达式 | 含义 |
| --------------- | ------- |
| +\|-\| = nice值 | |
| nice取值范围 | -2019 |
| 进程表达式 | 含义 |
| --------------- | ------------------------ |
| -p 进程编号 | 通过进程编号进行设定 |
| -g 进程群组编号 | 通过进程群组编号进行设定 |
| -u 用户编号 | |
**nohup 命令 [选项]**
将任务提交到后台,输出附加到~/nohup.out 文件。即便用户退出登录,提交到命令仍继续执行。
**命令 &**
背景执行此命令,如果用户退出登录,则命令停止执行
**命令1命令......**
命令队列,从左到右,依次执行以;分隔的命令
**命令1&&命令2&&......**
命令队列,从左向右,依次执行以&&分隔的命令。前一个命令执行成功,后一个命令才能执行。
**Ctrl + z**
挂起当前任务
**jobs**
显示背景任务,-l 显示完整信息
**bg [任务编号]**
将挂起的任务背景执行
**fg [任务编号]**
将背景任务调到前台执行
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,298 @@
---
layout: post
title: "开源世界旅行手册:核心工具集(三)"
date: 2020-01-04 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 核心工具集(三)
### 磁盘和内存管理
**细节**
Linux中设备用/dev/目录下的文件表示。例如
```shell
/dev/hda1 第一块硬盘的第一主分区
/dev/hdb5 第五块硬盘的第一逻辑分区
/dev/sda4 第一块SATA硬盘的第四主分区或者扩展分区
/dev/null 黑洞设备
```
**mount 设备文件 [挂载路径]**
挂载文件系统
| -t 指定文件系统的类型 | 含义 |
| --------------------- | --------------------------- |
| reiserfs | ReiserFS 3.6版 |
| xfs | SGI技术 |
| jfs | IBM技术 |
| ext3 | Linux传统文件系统 |
| vfat | fat fat32 |
| ext2 | Linux传统文件系统不带日志 |
| ntfs | WINNT |
| iso9660 | 光盘 |
| smbfs | Windows文件共享 |
| -o [选项] [选项2]...... | 含义 |
| ----------------------- | ------------------------------------------------------------ |
| loop | 环设备。光盘、ISO映像文件等通常用于挂载映像文件而不是设备文件 |
| bind | 绑定。将一个目录(而不是设备文件)挂载到另一个目录 |
| ro \| rw | 只读 readonly; 可读写 read-write |
| sync \| async | 同步模式|异步模式。决定修改是否立即写入文件系统 |
| atime \| noatime | 读取时是否修改访问时间。对于写入敏感设备例如闪存、软盘建议使用noatime |
| auto \| noauto | 自动挂载模式 |
| exec \| noexec | 是否允许执行 |
| defaults | 使用预设的选项 rw, suid, dev, exec, auto, nouser, async |
| iocharset=UTF-8 | 指定字符集可简写为utf8 |
| codepage=936 | 指定代码页可简写为cp936西文系统代码页为437 |
| umask=权限掩码① | 设定权限掩码 |
| uid=用户编号 | 设定归属用户 |
| gid=群组编号 | 设定归属群组 |
remount 以不同的选项重新挂载
-L 卷标 挂载带有特殊卷标的分区
① 权限掩码=777-目标权限三位7777-目标权限(四位)
假如权限掩码为022则目标权限为755 rwxr-xr-x
可以使用umask命令设置权限掩码
> 提示
>
> 挂载NTFS分区时请使用utf8选项挂载FAT分区时请使用iocharset=utf8,codepage=936选项以避免乱码
挂载/etc/fstab文件中定义的所有设备。示例
```shell
sudo mount -t iso9660 -o loop /dev/cdrom0 /media/cdrom
sudo mount -t vfat -o remount iocharset=utf8,codepage=936 /dev/hda5 /media/hda5
```
**umount 设备文件 |挂载路径**
卸载已挂载文件系统
**df**
查看已挂载文件系统的磁盘空间占用
| 参数 | 含义 |
| --------------- | ------------------------------------------------------------ |
| -a | 显示所有文件系统的磁盘使用情况包括0块block的文件系统如/proc 文件系统 |
| -T | 显示文件系统类型 |
| -k | 以k字节为单位显示 |
| -i | 显示i节点信息而不是磁盘块 |
| -t 文件系统类型 | 显示指定类型的文件系统的磁盘空间使用情况 |
| -x 文件系统类型 | 列出排除指定类型文件系统的磁盘空间使用情况与t选相反 |
| -l | 只显示本地文件系统 |
**free**
查看内存、缓冲区、交换空间的占用
| 选项 | 含义 |
| ----- | ---------------------- |
| -b | 以字节为单位显示数值 |
| -k | 以千字节为单位显示数值 |
| -m | 以兆字节为单位显示数值 |
| -g | 以吉字节为单位显示数值 |
| -l | 显示内存占用峰值 |
| -o | 不显示缓冲区占用 |
| -t | 统计结果 |
| -s 秒 | 刷新频率 |
**sync**
同步文件系统。将缓冲区中的数据写入文件系统
**fdisk 磁盘设备文件**
分区表修改工具(磁盘设备文件应为整块磁盘,而不是磁盘中的分区。例如/dev/sda而不是/dev/sda1
| 选项 | 含义 |
| ---- | ------------------------------- |
| m | 使用帮助 |
| l | 查看已知文件系统类型 |
| p | 显示分区信息 |
| n | 新建分区p:主分区 l:扩展分区) |
| d | 删除分区 |
| t | 改变分区类型 |
| w | 将改动写入分区表 |
| q | 放弃改动并退出 |
fdisk -l 查看所有磁盘分区信息
**cfdisk**
更加友善的分区表修改工具
**mkfs.文件系统类型 分区设备文件**
将分区格式化为文件系统。示例:
```shell
sudo mkfs.reiserfs /dev/hda1
sudo mkfs.xfs /dev/sda1
```
**hdparm 磁盘设备文件**
设置硬盘参数
| 参数 | 含义 |
| ---------- | -------------------- |
| -d 0\|1 | DMA模式开关 |
| -a 0\|1 | 预读模式开关 |
| -t | 性能测试 |
| -T | 缓存性能测试 |
| -c 0\|1\|3 | 32位传输模式开关 |
| -g | 显示柱面、扇区等信息 |
| -i\|-I | 显示磁盘信息 |
### 硬件管理
**lspci**
查看PCI总线连接的设备实际上也可以查看ISA、AGP、USB等总线信息
| 选项 | 含义 |
| ------- | ------------------------------------ |
| -v | |
| -vv | |
| -vvv | 显示详细信息 |
| -k | 同时显示使用的驱动和加载的模块 |
| -n | 显示设备ID号 |
| -b | 显示PCI、ISA扩展槽地址和中断 |
| -t | 显示总线树结构 |
| -F 文件 | 从指定文件读信息 |
| -m | |
| -mm | 以便于机器处理的格式输出所有硬件信息 |
**lsusb**
查看USB接口连接的设备
**lsmod**
查看已加载模块。/lib/modules/uname -r 目录下为所有可用模块
**modprobe 模块名称**
启用模块
| 参数 | 含义 |
| ---- | ------------------------------------------------------------ |
| -a | 加载所有匹配模块 |
| -c | 显示当前使用的配置 |
| -d | 显示调试信息 |
| -k | 将指定模块设置为“自动清除”模式 |
| -l | 显示所有匹配模块 |
| -n | 模拟执行 |
| -q | 不显示错误信息 |
| -r | 删除使用命令加载的模块;对非命令加载的模块设置“自动清除”模式 |
| -s | 将结果记录到系统记录中 |
| -t | 指定模块类型 |
| -v | 显示详细信息 |
| -C | 指定配置文件。默认使用/etc/modules.conf文件为配置文件 |
### 其他
**echo 字符串**
回显。较复杂的字符串,可以使用""括起来。
| 选项 | 含义 |
| ---- | -------------- |
| -n | 输出内容不换行 |
| -E | 不解析脱字符 |
| -e | 解析脱字符 |
| 控制字符 | 含义 |
| -------- | ---------- |
| \\\ | 反斜线 |
| \a | 警告 |
| \b | 退格 |
| \n | 换行 |
| \r | 回车 |
| \t | 水平制表符 |
**clear**
清空屏幕
**alias 输入内容=实际内容**
别名, 为命令指定一个别名,以简化输入。例如:
```shell
alias ls='ls -AF --color=auto'
```
可以将您的定义保存在~/.bashrc文件中
**export 变量名**
将变量导出为环境变量,常写变量赋值一同使用,例如:
```shell
export PATH="$PATH:xxx"
```
**shutdown**
关闭计算机向根进程init发送信号更改runlevel为0halt
| 选项 | 含义 |
| ------------- | ------------------------------ |
| -h | 关闭电源 |
| -r | 重启 |
| -n | 强行关机不向init进程发送信号 |
| -k | 模拟关机,向登录者发送关机警告 |
| -t 秒 | N秒后关机 |
| time 时间 | 定时关机 |
| -c [说明信息] | 取消关机 |
| -f | 重启时忽略检测文件系统 |
| -F | 重启时强制检测文件系统 |
**halt**
关闭计算机。调用shutdown -h结束系统进程同步文件系统停止内核。
| 选项 | 含义 |
| ---- | ----------------------------- |
| -n | 不同步文件系统 |
| -w | 模拟关机,写/var/log/wtmp记录 |
| -f | 不调用shutdown,强行关机 |
| -p | 默认选项关机时调用poweroff |
| -i | 关机前断开网络 |
**reboot **
重新启动计算机。选项与halt相似
**init 数字**
更改运行级别
| 级别 | 含义 |
| ---- | ---------- |
| 0 | 关机 |
| 1 | 单用户模式 |
| 6 | 重启 |
**chroot 路径**
更改根目录,重新定义会话的运行环境。(通常在多系统环境下使用)
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,503 @@
---
layout: post
title: "开源世界旅行手册:编译工具链"
date: 2020-01-05 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 编译工具链
### 标准编译安装
**为什么要编译安装**
包管理系统是绝大多数发行版的必备组件,也是一个发行版区别于其他发行版的主要特征。但是有些软件,并不能通过包管理系统安装,这就需要下载源码编译安装。
一个软件可能有许多功能,但是发行版中提供的安装包,通常只具有一些常见的功能。如果提供所有功能,那么无疑会占用更多的资源,而这些功能,大多数用户不会用到;而你会用到的功能,可能安装包中刚好没有。编译安装可以灵活的定制软件,选择自己需要的,取消自己不需要的。
编译安装还可以针对特定的软件进行优化,以获得更好的性能表现。
**编译环境**
编译环境包括多个工具,它们环环相扣,称作编译工具链。主要包括以下工具:
| 工具 | 简介 |
| -------- | -------------------------------------------- |
| binutils | 连接器、汇编器和其他用于目标文件和档案的工具 |
| gcc | 编译器,将源代码转换为机器代码 |
| glibc | C库提供标准例程C函数 |
| autoconf | |
| automake | 自动生成Makefile文件 |
| make | 安装Makefile文件中的规则编译程序 |
**标准编译安装**
首先下载源代码通常是压缩包比如xxx.tar.gz或者xxx.tar.bz2解包
| 压缩包格式 | 命令 |
| ---------- | -------------------- |
| .tar.gz | |
| .tgz | tar zxvf xxx.tgz |
| .tar.bz2 | tar jxvf xxx.tar.bz2 |
通常解包后会在当前位置得到一个xxx/目录,进入这个目录
```shell
cd xxx/
```
使用下列命令编译安装:
```shell
./configure ① --prefix=/opt/xxx②
make③
sudo make install④
make clean⑤
```
① 配置软件特性检查编译环境生成Makefile文件
② 最常用配置选项:指定软件的安装路径
③ 根据Makefile编译源代码
④ 将编译完成的程序安装到系统中。通常需要root权限
⑤ 清除源代码目录中的编译结果
### 编译过程
将下面代码保存为 Hello.c:
```c
#include <stdio.h>
int main(void)
{
printf("Hello World!\n")①;
return 0;
}
```
① printf()函数
执行命令`cc Hello.c`得到一个可执行文件a.out执行它 ./a.out
可以看到C的源代码Hello.C是纯文本不能够直接执行。可执行代码是计算机的本机语言或机器语言表示的代码这种语言是由数字代码表示的详细指令组成不同的计算机具有不同的机器语言。
编译器是一个程序,其工作是将源代码转换为可执行代码。
- 编译器用来将C语言转换成特定的机器语言。
- 编译器还从C的库中向最终程序加入代码。
- 编译器还检查源代码是否为有效的C语言程序。如果编译器发现错误将报告错误而且不生成可执行文件
编译器分三步完成这个工作:
1. 预处理调用与处理器cpp对源代码文件中的文件包含include、预编译语句如宏定义define等进行分析
2. 编译调用编译器cc将源代码转换为中间代码
3. 链接调用链接器ld将中间代码与其他代码结合起来生成可执行文件。
这种方法使用程序便于模块化。分别编译各个模块,然后使用链接器将编译过的模块结合起来。这样,如果需要改变一个模块,则不必重新编译所有其他模块。
可执行文件包含**目标文件**、**库例程**和**启动代码**
编译器将源代码转换为机器语言代码(中间代码),将结果放置在目标文件(*.o中。虽然目标文件包含机器代码但该文件还不能运行它还不是一个完整的程序。
启动代码start-up code) 相当于程序和操作系统之间的接口。
库例程为函数的实现。几乎所有C程序都利用标准库C库中所包含的例程目标代码文件不包含这一函数的代码它只包含调用函数的指令。实际代码存储在一个称为“库”的文件中。库文件中包含许多函数的目标代码
链接器的作用是将这3个元素目标代码、系统的标准启动代码和库代码结合在一起并将它们存放在可执行文件中。
### gcc编译器
gcc是GNU推出的功能强大、性能优越的多平台编译器是GNU的代表作品之一。它能将C、C++语言源程序、汇编语言源程序和目标程序编译、链接成可执行文件如果没有给出可执行文件的名字gcc将生成一个名为a.out的文件。
gcc通过后缀来区分输入文件的类型
| 后缀 | 类型 |
| ------------- | -------------------------- |
| .c | C语言源代码文件 |
| .a | 由目标文件构成档案库文件 |
| .C\|.cc\|.cxx | C++源代码文件 |
| .h | 程序所包含的头文件 |
| .i | 预处理过的C源代码文件 |
| .ii | 预处理过的C++源代码文件 |
| .m | Objective-C源代码文件 |
| .o | 编译后的目标文件 |
| .s | 汇编语言源代码文件 |
| .S | 预编译的汇编语言源代码文件 |
前面我们已经使用gcc编译了一个程序cc Hello.c
gcc还有许多选项
| 选项 | 含义 |
| --------- | ------------------------------------------------------------ |
| -c | 只编译,不链接成为可执行文件 |
| -o 文件名 | 设定输出文件名。默认为a.out |
| -g | 加入调试符号(默认) |
| -O | 编译、链接时进行优化,耗时比较多,但产生的可执行文件执行效率更高 |
| -O2 | 更高的优化级别,耗时更多 |
### 自动化编译
在前面的标准编译安装中,第一步是./configure它会根据Makefile.in生成Makefile文件然后make根据Makefile自动编译软件
通常在一个源码包中已经包含了configure脚本和Makefile文件作为课外知识我们大致了解一下怎么生成这两个文件
**autoconf**
autoconf用来生成configure脚本它可以检查系统特性、编译环境、环境变量、软件参数、依赖关系等
autoconf需要用到m4
1. 用autoscan扫描源代码目录生成configure.scan文件
2. 将configure.scan改名为configure.in
3. 用aclocal根据configure.in文件的内容自动生成aclocal.m4文件
4. 使用autoconf根据configure.in和aclocal.m4来产生configure文件
**automake**
automake可以从Makefile.am文件自动生成Makefile.in它主要用来配置源代码
automake需用到perl
- 手工写Makefile.am
- 使用automake根据configure.in和MakeFile.am来产生Makefile.in
**Makefile**
使用configure脚本配合Makefile.in可以生成Makefile文件然后用make自动化的编译软件
这里有一张生成Makefile的流程图
![image-20210722172633176](/img/in-post/2020-01-05-linux-guide-compile-toolkit-01.png)
Makefile的用途不只是编译软件还可以利用它完成一些琐碎的工作只要最后输出一个文件都可以用make来完成
这是一个最简单的Makefile
```shell
filelist①:②*③
④ ls -lF > filelist⑤
```
① 输出的目标文件不能省略。如果有多个文件可以使用all
② 分隔符,不能省略
③ 输入文件,可以省略
④ 这一行必须以TAB字符起始不能使用空格代替
⑤ make的命令
可以使用变量代替命令,便于维护
```shell
TARGET = filelist①
SOURCE = *
ARG = -lF
APPLICATION = ls
$(TARGET):$(SOURCE)②
$(APPLICATION) $(ARG) $(SOURCE) > $(TARGET)
```
① 定义变量,传统上用大写
② 使用变量写Makefile
Makefile可以有多个目标文件我们前面提到gcc编译时先生成目标文件再把目标文件链接成可执行文件Makefile应该是这样的
```shell
OBJECTS = main.o kbd.o command.o display.o \①
insert.o search.o files.o utils.o
exe : $(OBJECTS)
cc -o exe $(OBJECTS)
main.o : main.c defs.h ②
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
```
① 如果写在多行,要用脱字符换行
② 如何生成中间文件
③ 伪目标文件make clean 并不生成clean文件而是清理编译结果
### 使用make
利用configure所产生的Makefile文件有几个预先设定的目标可供使用
| 目标 | 用途 |
| -------------- | ----------------------------------------------------- |
| make all | 产生预设的目标只敲入make也可以 |
| make clean | 清除编译结果 |
| make distclean | 出了清除编译结果也把configure所产生的Makefile清除掉 |
| make install | 将程序安装到系统中 |
| make dist | 将程序和相关的文档打包为一个压缩文档以供发布 |
| make distcheck | 打包并检验 |
### emerge
虽然我们能够使用autoconf、automake、make等工具实现自动化编译但这种针对单个软件包的编译系统在编译多个软件时仍然十分繁琐
假设需要编译emacs和vim使用xft字体、图形界面支持并去掉调试符号需要分别作如下配置
```shell
emacs
./configure --prefix=/usr/local/ \
--no-debug \
--with-xft --with-x-toolkit=gtk \
--with-freetype
vim
./configure --prefix=/usr/local/ \
--no-debug \
--with-xft --with-x-toolkit=gtk
```
这就像点菜时,你必须告诉厨师:鱼香肉丝(多放辣椒、不放蒜)、宫保鸡丁(多放辣椒、不放蒜)、麻婆豆腐(多放辣椒、不放蒜)......
实际上,大多数人这样点菜:鱼香肉丝、宫保鸡丁、麻婆豆腐......多放辣椒、不放蒜
emerge就是这样一种点菜方式它是gentoo的包管理系统提供了更为现代化的编译方式
可以通过制定USE标记xft、gtk、-debug来确定所有软件的编译方式
**设置USE标记**
以下方法按优先级由低到高排列:
/etc/make.profile/目录是一个符号链接里面包含一些make.defaults文件放置开发者设置的USE标记
```shell
/usr/portage/profile/base/make.defaults
/usr/portage/profile/default-linux/make.defaults
/usr/portage/profile/default-linux/x86/make.defaults
/usr/portage/profile/default-linux/x86/2008.0/make.defaults
```
在/etc/make.conf文件中声明永久USE标记推荐
```shell
USE="nptl nptlonly nls cjk php mysql -kde -qt3 -qt4"
```
带-的USE标记表示排除
在/etc/portage/package.use文件中为单个包声明USE标记
```shell
app-editors/emacs-cvs xft
www-servers/lighttpd fastcgi
dev-lang/php mysqli cgi gd ctype pcre session unicode pic posix
dev-db/phpmyadmin vhosts
app-shells/zsh doc
net-ftp/pure-ftpd -ldap mysql pam ssl vchroot
```
使用环境变量声明临时USE标记
```shell
USE="-java"
emerge seamonkey
```
查看使用的USE标记
```shell
merge --pretend --verbose seamonkey
Calculating dependencies ...done!
[ebuild R ] www-client/seamonkey-1.0.7 USE="crypt gnome java -debug -ipv6
-ldap -mozcalendar -mozdevelop -moznocompose -moznoirc -moznomail -moznopango
-moznoroaming -postgres -xinerama -xprint" 0 kb
```
**编译选项**
/etc/make.conf
```shell
CFLAGS="-O2 -march=i686 -pipe"①
CXXFLAGS="-O2 -march=i686 -pipe"②
CHOST="i686-pc-linux-gnu"③
MAKEOPTS="-j2"④
FEATURES="parallel-fetch ccache" ⑤
CCACHE_DIR="/var/tmp/ccache"⑥
CCACHE_SIZE="2G"⑦
ACCEPT_KEYWORDS="x86"⑧
USE="nptl nptlonly nls cjk php mysql"⑨
FETCHCOMMAND="/usr/bin/axel -a -n4 \${URI} -o \${DISTDIR}" ⑩
RESUMECOMMAND="/usr/bin/axel -a -n4 \${URI} -o \${DISTDIR}"
```
① 针对C语言的优化选项-march=设置目标架构
② 针对C++语言的优化选项
③ 进行编译工作的机器架构
④ 编译选项
⑤ emerge特性。并行下载、使用ccache缓冲编译结果
⑥ ccache缓存目录
⑦ ccache缓存大小
⑧ 通过关键字选择分支。x86表示x86架构的稳定分支-x86表示x86架构的不稳定分支
⑨ USE标记
⑩ 使用axel加速下载
gentoo支持多种架构x86、sparc、amd64、ppc、ppc64、alpha、hppa、mips、ia64、arm我们使用的PC多为x86架构
假设你主要使用x86稳定分支但少数软件要使用最新版本在/etc/portage/package.keywords文件中为单个包设置关键字
```shell
app-editors/emacs-cvs -x86
x11-misc/emacs-desktop -x86
app-i18n/fcitx -x86
app-editors/vim x86
app-editors/vim-core x86
media-video/mplayer -x86
media-libs/win32codecs -x86
app-i18n/man-pages-zh_CN -x86
```
**微调**
在/etc/portage/目标下包含一些文件可以在软件包级别上进行调节。前面已经介绍了package.use和package.keywords
- **package.keywords**还未被确认适合你的系统或机构,但是你希望能安装的软件包
- **package.use**特定软件包而不是整个系统使用的USE标记
- **package.provided**屏蔽的软件包(需要指明版本号)
- **package.mask**永远不希望Portage安装的软件包。
- **package.unmask**被Gentoo开发者屏蔽的软件包但是你希望能安装的软件包
软件包可能由于以下原因被屏蔽
| 原因 | 详细 |
| -------------------------- | ------------------------------------------------------------ |
| ~架构 keyword | 意味着这个软件没有经过充分的测试,不能进入稳定分支,请等待一段时间后在尝试使用它 |
| -架构 keyword 或-* keyword | 意味着这个软件不能工作在您机器的体系结构中 |
| missing keyword | 意味着这个软件还没有在您机器的体系结构中进行过测试 |
| package.mask | 意味着这个软件被认为是损坏的,不稳定的活着有更严重的问题,它被故意标识为"不应使用" |
例如:
```shell
gnome-base/gnome-2.8.0_pre1 (masked by: -86 keyword)
lm-sensors/lm-sensors-2.8.7 (masked by: -sparc keyword)
sys-libs/glibc-2.3.4.20040808 (masked by: -* keyword)
dev-util/cvsd-1.0.2 (masked by: missing keyword)
games-fps/unreal-tournament-451 (masked by: package.mask)
sys-libs/glibc-2.3.2-r11 (masked by: profile)
```
**使用emerge**
查找名字包含pdf的软件包
```shell
emerge --search pdf
```
查找与pdf相关的软件包
```shell
emerge --searchdesc pdf
emerge -S pdf
```
查找软件拥有的USE标记
```shell
emerge -vp 软件包名称
```
安装软件包
```shell
emerge 软件包名称
```
模拟安装软件包
```shell
emerge --pretend 软件包名称
```
下载软件包的源代码包
```shell
emerge --fetchonly 软件包名称
```
从系统中删除软件包
```shell
emerge --unmerge 软件包名称
```
更新系统
```shell
emerge --update --ask world
emerge -ua world
```
更新整个系统
```shell
emerge --update --deep world
emerge -uD world
```
使用新的USE标记 重新构建系统
```shell
emerge --update --deep --newuse world
emerge -uDN world
```
移除孤立依赖的软件包
```shell
emerge --update --deep --newuse world①
emerge --depclean②
revdep-rebuild③
```
① 重新构建系统
② 清除孤立依赖包
③ 重新构建依赖关系
revdep-rebuild工具由gentoolkit包提供使用前别忘了首先emerge它`emerge gentoolkit`
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,251 @@
---
layout: post
title: "开源世界旅行手册:图形界面"
date: 2020-01-06 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 图形界面
### 简介
Xwindow是工作站图形系统的工业标准它有多种不同的实现多数Linux系统中使用Xorg。
当然Xwindow有悠久的历史和传统不过那不在我们讨论的范围。您要注意的有两点。
- Xwindow和Xbox中的X意义是不同的X只是W之后的一个字母差不多应该这样理解Xwindow是Window的接班人注意Window不是Windows
- 同样也不要吧Xwindow说成是Xwindows那是一种亵渎一切伟大的创造都应得到应有的理解和尊重。
### 架构及原理
Xwindow使用服务器-客户端架构。无论本地图形界面还是远程图形界面,都以同样的流程工作。这样便不需要分别进行设计和维护。
```shell
本地X客户端┐ ┌键盘
远程X客户端┼ X协议 - X服务器 - 驱动程序 ┼鼠标
远程X客户端┘ └显示器
```
#### Xserver
Xwindow系统服务器端通过驱动程序硬件规范来管理硬件资源。
例如当我们移动鼠标时通过驱动程序向Xserver发送信息
“向右移动200点向上移动100点”向右上移动“按下左键”......
Xserver作出如下响应
1. 上一次鼠标停止的坐标为600500
2. 向右200向上100。现在鼠标位于坐标800600
3. 坐标800600处为窗口Firefox的“关闭”按钮
4. 根据预设动作将“点击Firefox窗口的关闭按钮”翻译为“关闭窗口Firefox”
5. 向X客户端Firefox发送一个“退出”消息
6. Xserver通过显示子系统显卡、显示器全程显示鼠标的位置和移动
事实上,向程序发送“退出”信号,通常窗口管理器完成。为了描述方便,这里暂不区分。
#### Xclient
Xwindow系统客户端通过X协议实现与Xserver的交互。例如
1. Xclient假设Firefox接收Xserver的消息输入焦点在地址栏的范围内“linuxtoy.org”回车
2. Firefox根据预设动作将这些消息识别为“打开链接linuxtoy.org”
3. Firefox向域名服务器请求链接“linuxtoy.org”。域名服务器将这个请求转换为“http://linuxtoy/org/”和IP地址211.148.131.7发送回Firefox
4. Firefox将"http://linuxtoy.org/"显示在地址栏向Xserver发送请求在地址栏位置显示这个位置
5. Firefox向地址211.148.131.7请示显示页面
6. Firefox将服务器发送回的页面显示在主窗口中
#### Xprotocal
Xwindow系统协议Xserver和Xclient之间进行通信的规则。
### 窗口管理器
Window Manager是一种特殊的Xclient
使用窗口管理器时Xserver并不直接与其他Xclient通信而是通过WM中转当一些消息被定义为WM指令时它们会被拦截。例如Alt+F4关闭窗口、拖动标题栏......
消息“打开链接linuxtoy.org”具体内容如下
输入焦点在地址栏的范围内“linuxtoy.org”回车
Xserver并不能直接判断焦点而是这样
1. Xserver向WM发送位置和点击的信息WM根据当前的“焦点策略”确定激活最上层的窗口为Firefox
2. Xserver将Firefox显示在最上层高亮显示它的标题栏
3. 在窗口Firefox内点击地址栏或者Ctrl+LXserver将位置信息发送给WMWM再发送给Firefox
4. Firefox判断当前焦点后显示一个闪动的文字输入光标
5. Firefox将输入光标通过WM发送给XserverXserver在屏幕相应位置进行显示
那么,“窗口管理器”到底能做些什么呢?其实它所做的只有一件事——管理窗口。例如:
- 最上层的窗口会把其他窗口挡住
- 它通常是一个“已激活窗口”,根据不同的“焦点策略”,窗口管理器确定被激活的窗口。激活窗口标题栏高亮显示,接收大部分的 键盘消息和窗口内的鼠标点击信息
- 为了美观和容易分辨,大多数窗口都要有标题栏和边框。为了方便,标题栏上还要有一些按钮,比如:最小化,最大化,关闭(这些按钮是窗口管理器请求的小窗口)
- 一个窗口可以在另一个窗口旁边显示,而不一定完全被遮挡。为了实现这一点,就要控制窗口显示的位置
- 为了控制窗口的显示位置,需要将整个屏幕用坐标描述,最好的办法是绘制一个填充整个屏幕的窗口,也就是根窗口。
- 因为根窗口是最大的,所以它可以严严实实的遮挡任何窗口,为了避免这一点,根窗口永远在最底层。这很形象的说明了为什么它叫作“根窗口”
- 根窗口不一定只有一个,大多数的窗口管理器可以使用“工作区”,来切换显示多个根窗口
- 根窗口固定位置上通常放置一些其他Xclient的窗口例如底部面板顶部面板侧面板程序启动图标
- 面板上又可以放一些其他的Xclient窗口如任务条启动栏菜单......任务条可以以图表显示正在运行的任务还可以作其他的杂活像自动挂载USB设备......
### 启动流程
我们知道init是linux的根进程是所有进程的父进程。同样xinit是所有Xwindow进程的根进程。
#### Startx
startx可以在命令行下启动图形界面。执行startx命令时实际执行这一命令
```shell
xinit /etc/X11/xinit/xinitrc -- /etc/X11/xinit/xserverrc
```
根据脚本/etc/X11/xinit/xserverrc 启动Xserver同时根据脚本/etc/X11/xinit/xinitrc 启动指定Xclient进程例如窗口管理器
/etc/X11/xinit/xserverrc 以预设的参数运行程序 /usr/bin/X11/X
/etc/X11/xinit/xinitrc 脚本则指向/etc/X11/Xserssion依次启动/etc/X11/Xsession.d目录中的脚本
- 您可以在在用户配置文件~/.Xsession中定义使用WM它的优先级高于全局配置文件对于GDM会话不起作用
- startx启动时并不会再进行身份认证。因为它启动的是/etc/X11/Xsession.d/gnome-session而不是GDM会话
#### GDM会话
许多Linux系统启动时自动进入图形界面不需要运行startx命令
在某些启动级别中包含了gdm的启动脚本例如/etc/rc2.d/S13gdm
1. 指向/etc/gdm/gdm-cdd.conf文件加载预设视觉主题启动/usr/lib/gdm/gdmgreeter登录屏幕
2. 用户身份认证完成后,启动/etc/X11/default-display-manager这个文件中设定的默认窗口管理器/usr/sbin/gdm
gdm在启动时会要求用户名和密码也就是我们看到的登录屏幕gdmgreeter
/usr/share/xsessions目录下为所有可用登录会话的脚本
### 配置文件
#### X服务器
X服务器的主要配置文件为/etc/X11/xorg.conf
```shell
Section "ServerLayout" #布局
Identifier "Xorg Configured" #布局标识
Screen 0 "Default Screen" 0 0 #屏幕标识
InputDevice "Generic Keyboard" #键盘标识
InputDevice "Configured Mouse" #鼠标标识
EndSection
Section "Module"
Load "ddc"
Load "dbe"
Load "dri"
Load "extmod"
Load "glx"
```
默认分辨率为默认色深下的第一个分辨率
#### 配置文件内部结构
```shell
/
├/ "ServerLayout" 布局
|├ "InputDevice" keyboard 键盘
|├ "InputDevice" mouse 鼠标
||
|└/ "Screen" 显示子系统
│ ├ "Monitor" 显示器
│ ├ "Device" videocard 显卡
|
|
├ "Files" 字体
└ "Module" 模块
```
#### X客户端
在/etc/X11/Xsession文件中可以发现下列内容
```shell
OPTIONFILE=/etc/X11/Xsession.options①
SYSRESOURCES=/etc/X11/Xresources
USRRESOURCES=$HOME/.Xresources
SYSSESSIONDIR=/etc/X11/Xsession.d
USERXSESSION=$HOME/.xsession
ALTUSERXSESSION=$HOME/.Xsession
ERRFILE=$HOME/.xsession-errors
```
① 设定X进程的启动参数。例如运行用户进程allow-user-xsession
② X资源文件。许多程序保留了X接口允许X服务器管理一些视觉选项例如窗口内的字体配色等
③ X进程。可以设置一些启动时自动运行的程序也可以用来设定自己的窗口管理器窗口管理器和桌面环境或者登录管理器是无关的
### 字体
#### freetype 渲染引擎
作为Xorg服务器的一个模块freetype的功能包括读取Truetype字体信息如大小、分辨率、编码等并以之为依据渲染字体-freetype2.x相对于freetype1.x增加了抗锯齿等功能-(/etc/X11/xorg.conf 的Module字段中可以选择字体渲染模块)
freetype只负责渲染字体。而查找字体则可以由X服务器、X客户端或者字体服务器来完成。找到字体后使用freetype引擎进行渲染
**X核心字体**X服务器根据X客户端的请求字符编码查找字体并进行渲染然后显示
**Xft字体**X客户端自行查找字体并进行渲染X服务器只负责显示。由于Xft字体的渲染在客户端完成所以它可以动态的加载而不需要随同X服务器一同启动
**字体服务器**当客户端请求字体时X服务器将请求转发到字体服务器由字体服务器查找字体并使用freetype引擎渲染将结果传回X服务器X服务器进行显示
#### X核心字体
/etc/X11/xorg.conf中可以配置X核心字体的搜索路径
```shell
Section "Files"
FontPath "/usr/X11R6/lib/X11/fonts/misc/"
FontPath "/usr/X11R6/lib/X11/fonts/Type1/"
FontPath "/usr/X11R6/lib/X11/fonts/Speedo/"
FontPath "/usr/X11R6/lib/X11/fonts/100dpi/"
FontPath "/usr/X11R6/lib/X11/fonts/75dpi/"
EndSection
```
- 当X客户端向X服务器请求显示文字的时候X服务器会按上面列表的先后顺序查找字体。显示中文时如果第一个路径中的字体不包含中文则查找下面的路径直到发现中文字体
- 请将您便好的字体放在靠前的位置
要使安装的字体能够作为X核心字体使用将字体的安装路径添加到上面的列表中使用mkfontscale、mkfontdir扫描文件夹中的字体并生成索引就可以了建议使用ttmkfdir生成fonts.scale将其复制为fonts.dir
字体的选择及显示风格可以修改GTK1的配置文件或者在Xresources文件中对程序单独进行定义
事实上在我们的日常应用中X核心字体环境并不常见使用GTK1图形库的程序、某些类型的终端......
**XFT字体**
Xft字体相关选项在/etc/fonts/fonts.conf文件中配置
```shell
<dir>/usr/share/X11/fonts</dir>
<dir>/usr/share/fonts</dir>
<dir>/usr/local/share/fonts</dir>
<dir>~/.fonts</dir>
```
可以使用fc-cache命令递归扫描上面目录中的字体包括子文件夹中的字体建立字体缓存
多数支持GTK2或者Qt图形库的X客户端能够使用Xft字体渲染技术
安装字体只要将字体拷贝到以上任意目录fc-cache -fv刷新字体缓存即可选项-f强制刷新-v显示过程
使用命令fc-list列出所有可用字体。字体的选择及显示风格可以修改GTK2或者Qt的配置文件建议使用图形界面配置
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,157 @@
---
layout: post
title: "开源世界旅行手册:国际化"
date: 2020-01-07 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 国际化
### I18N简介
习惯上internationalization国际化简写为 I18N中间的数字为省略的字母个数。
在Linux系统中通过定义一组环境变量来设置程序的语言环境以实现对I18N的支持这种机制称为locale本地化
#### locale变量
| 变量名 | 含义 |
| ----------------- | ------------------------------------------------------------ |
| LANG | 低优先级全局locale变量如果下面的变量未赋值默认使用此变量的值 |
| LC_COLLATE | 比较和排序习惯,会影响目录列表的分类显示等 |
| LC_CTYPE | 定义系统等字符处理特性,哪些字符能被视为字母、数字,等等;与中文输入关系密切 |
| LC_MESSAGES | 提示信息,错误信息,状态信息,标题,标签,按钮和菜单等 |
| LC_MONETARY | 定义货币单位和货币型数值的格式 |
| LC_NUMERIC | 定义非货币型数值的格式,影响到千位分隔符和小数分隔符等 |
| LC_TIME | 定义日期和时间的格式 |
| LC_NAME | 姓名书写方式 |
| LC_ADDRESS | 地址书写方式 |
| LC_TELEPHONE | 电话号码书写方式 |
| LC_MEASUREMENT | 度量衡表达方式 |
| LC_PAPER | 定义默认的纸张尺寸 |
| LC_IDENTIFICATION | 对locale自身包含信息的概述 |
| LC_ALL | 高优先级全局locale变量为此变量赋值会强行覆盖上面变量的值不推荐 |
假如未设置以上变量系统将采用POSIX作为locale也就是C locale
#### locale 值
locale变量的值有三个要素语言代码Language Code)、地域代码Country Code和字符集Encoding
`语言代码[_地域代码[.字符集]]`
例如:
| 语言/国家代码 | 描述 |
| ---------------- | -------- |
| en_US.ISO-8859-1 | 美国英语 |
| en_US.UTF-8 | |
| zh_CN.UTF-8 | 简体中文 |
| zh_TW.UTF-8 | 繁体中文 |
#### 字符集
众所周知,计算机中的信息,是以数字形式表示的,字符也不例外。字符以数字编号的形式存储,使用时,根据这个编号,在字符集中找到相应的字符
字符集是字符在系统内的编码方式,也就是通常所说的内码(参见/usr/share/i18n/charmaps不同的字符集有不同的编码方式。
例如“码”字它的GB2312编码为426BUTF-8编码为E7A081。如果错误的以GB2312编码来检索E7A081将会产生类似“锘跨爜” 的乱码
只要系统中安装了中文字体通过字符集的支持便可以正常显示中文而无需设置localelocale可以使操作界面显示中文并可以使用中文输入等。
#### 其他
另外还有一个本地化变量叫做LINGUAS。它会影响到基于gettext的程序它还能决定某些特殊软件包的本地化比如kde-i18n和openoffice。这个变量的值为一组以空格分隔的语言代码
`LINGUAS="zh en"`
### 设置Locale
设置locale其实就是设置环境变量。在/etc/environment文件中设置全局环境变量
`LANG="zh_CN.UTF-8"`
在~/.profile或~/.bashrc中设置用户环境变量
```shell
export LANG="zh_CN.UTF-8"
```
完成后可以使用locale命令检验
#### 生成locale
如果你设置了一个不可用的locale请使用localedef生成该locale
```shell
localedef -c -i en_US -f ISO-8859-15 en_US.ISO-8859-15
```
可能你在系统中只要用到一个或者两个locale你可以在/etc/locale.gen中指定所需的locale添加locale到/etc/locale.gen:
```shell
en_GB ISO-8859-1
en_GB.UTF-8 UTF-8
de_DE ISON-8859-1
de_DE@euro ISO-8859-15
```
下一步是执行locale-gen。它会生成/etc/locale.gen文件中指定的所有locale
### locale策略
#### 中文图形界面
由于Linux的控制台不能方便的显示中文所以最使用的方案是“英文控制台+中文图形界面”
为了能够正常处理中文需要使用locale-gen生成locale在/etc/locale.gen文件中添加如下内容
```shell
zh_CN.UTF-8 UTF-8
zh_CN.GB18030 GB18030
zh_CN.GBK GBK
zh_CN GB2312
```
然后在gdm启动菜单中选择中文或者写入配置文件~/.dmrc
```shell
[Desktop]
Session=openbox
Language=zh_CN.UTF-8
```
#### 英文界面+中文输入
使用如下设定:
```shell
LANG="en_US.UTF-8"
LC_CTYPE="zh_CN.UTF-8"
```
#### 关于输入发的设置
在~/.profile或~/.bashrc中设置用户环境变量
输入法配置.profile
```shell
export SMODIFIERS="@im=fcitx①"
export GTK_IM_MODULE=xim②
export QT_IM_MODULE=xim③
fcitx&
```
① 使用fcitx输入法
② GTK2程序输入法引擎。fcitx使用xim引擎
③ QT输入法引擎
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,56 @@
---
layout: post
title: "开源世界旅行手册:内核"
date: 2020-01-08 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 内核
### 简介
内核是系统的引擎,它是一个系统运行起来的先决条件。
- 内核管理硬件,是程序和硬件之间的接口
- 内核对进程进行调度,将硬件资源分配给不同任务,使系统可以同时运行多个任务
- 内核对内存进行管理,将内存空间分配给任务,将使用不频繁的页面转移到交换分区
- 内核还管理文件系统,进程间通信和网络
内核包含几个重要的子系统:
**进程调度SCHED** 控制进程对CPU的访问。当需要选择下一个进程运行时由调度程序选择最值得运行的进程。可运行进程实际上是仅等待CPU资源的进程如果某个进程在等待其他资源则该进程是不可运行进程。Linux使用了比较简单的基于优先级的进程调度算法选择新的进程。
**内存管理MM** 允许多个进程安全的共享主内存区域。Linux的内存管理支持虚拟内存即在计算机中运行的程序其代码数据堆栈的总量可以超过实际内存的大小操作系统知识把当前使用的程序块保留在内存中其余的程序块则保留在磁盘中。必要时操作系统负责在磁盘和内存间交换程序块。内存管理从逻辑上分为硬件无关部分和硬件有关部分。硬件无关部分提供了进程的映射和逻辑内存的兑换硬件相关的部分为内存管理硬件提供了虚拟接口。
**虚拟文件系统VirtualFileSystemVFS** 隐藏了各种硬件的具体细节为所有的设备提供了统一的接口VFS提供了多达数十种不同的文件系统。虚拟文件系统可以分为逻辑文件系统和设备驱动程序。逻辑文件系统指Linux所支持的文件系统如ext2fat等设备驱动程序指为每一种硬件控制器所编写的设备驱动程序模块
**网络接口NET** 提供了对各种网络标准对存取和各种网络硬件对支持。网络接口可分为网络协议和网络驱动程序。网络协议部分负责实现每一种可能的网络传输协议。网络设备驱动程序负责与硬件设备通讯,每一种可能的硬件设备都有相应的设备驱动程序。
**进程间通信IPC** 支持进程间各种通信机制
处于中心位置的进程调度,所有其他的子系统都依赖它,因为每个子系统都需要挂起或恢复进程。一般情况下,当一个进程等待硬件操作完成时,它被挂起;当操作真正完成时,进程被恢复执行。例如,当一个进程通过网络发送一条消息时,网络接口需要挂起发送进程,直到硬件成功地完成消息的发送,当消息被成功的发送出去以后,网络接口给进程返回一个代码,表示操作的成功或失败。其他子系统以相似的理由依赖于进程调度。
### 启动流程
系统启动时,引导管理器首先加载内核,内核被载入后执行以下操作:
- 内核自解压
- 初始化阶段
- 检测,初始化硬件
- 设置各种入口地址
- 初始化IRQ中断处理机制
- 启动init进程
- 初始化控制台、显示器
- 初始化kernel_module
- init阶段。init进程启动后执行当前运行级别的脚本
- PCI总线初始化
- 网络初始化
- 文件系统初始化
- 加载文件系统
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,240 @@
---
layout: post
title: "开源世界旅行手册Grub"
date: 2020-01-09 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## Grub
### 硬件基础
一块硬盘,它起始的一部分扇区为主引导扇区,包括 MBR主引导记录和 DPT分区表
每个分区起始的一部分扇区为分区引导扇区。
在分区引导扇区之后的部分,为文件系统的索引,文件系统通过它定位文件在硬盘上的位置。不同的文件系统采用不同的索引,例如 FAT 文件系统使用文件分配表和目录区。
绝大多数操作系统,对硬盘的读写操作,通过文件系统来完成,因此引导扇区中的内容,我们不能够在文件系统中进行操作,而需要专用软件,比如引导管理器
我们对文件进行修改后,操作系统会将文件系统索引中的内容同步
#### 系统引导流程
1. 系统启动时,首先引导至 MBR将控制权移交安装在 MBR 中的引导管理器Windows 使用 NTLDRLinux 通常用 Grub
2. 引导管理器装载自身Grub 先装载 MBR 中的 stage1通过 stage1 来装载文件系统中的 stage2显示菜单和 shell 等待用户。有时 stage1 不能识别 stage2 所在分区的文件系统,这就需要装载 stage1.5 来连接 stage1 和 stage2
3. 引导管理器读取分区中的配置文件并按配置文件中预设的参数运行例如Grub 读取`/boot/grub/menu.list`文件中内容,将可引导系统通过菜单显示)
4. 引导管理器根据您的选择,可能会有如下活动
- 加载内核,启动 Linux 系统检查活动分区,并引导它
- Windows 系统:读取相应分区的引导扇区,将控制权移交该扇区中的引导管理器
Linux 系统在安装 Grub 时,会提示您安装在 MBR 或者分区引导扇区中,如果将 Grub 安装在分区引导记录中,您必须确保 MBR 中引导管理器能够正确的引导至分区引导扇区,如果您在 MBR 中使用的是 Windows 的引导管理器 NTLDR完成这件工作会非常困难因此我们推荐您使用 Grub。
### Grub 介绍
Grub 主要有以下功能:
- 菜单式选择
- 命令行模式
- 支持开机画面
- 支持大硬盘
您可以运行命令`grub`启动它,会显示一些版本信息和使用提示,当然还有命令提示符,如下
```shell
GNU GRUB version 0.97 (640K lower / 3072K upper memory)
[Minimal BASH-like line editing is supported. For
the first word, TAB lists possible command completions.
Anywhere else TAB lists the possible completions of
a device/filename.]
grub >
```
您可以使用`TAB`键补全命令和路径,这非常重要,因为 Grub 中路径表示方式与操作系统是不同的,您可能比较陌生,所以尽量用 TAB 补全它,既方便,也不容易出错
您可以在 grub>提示符后按`TAB`键,会将所有可用的命令显示出来。
呵呵,是不是有点晕,命令可真不少啊!!!其实我们会用到的命令只有两个:**root** **setup**
### Grub 术语
在分区概念章节里Linux 系统中表示分区的方法` /dev/sta5`
```shell
/dev/sdMN M为a起始的小写字母表示硬盘序号N为1起始的数字表示分区序号
```
Grub 中使用的表示方法为 `hd0,1`
```shell
hdX,Y X为0起始的数字表示硬盘序号Y为0起始的数字表示分区序号
```
留意它们的区别:
- N 从 1 开始计数X 和 Y 从 0 开始计数
- N 为 1 4它是一个主分区N 为 5 或者大于 5它是第N-4个逻辑分区Y 按分区在硬盘上排列的顺序排列,无论它表示的是主分区还是逻辑分区
举例来说:
| 分区分布 | 主分区 | 主分区 | 逻辑分区 | 逻辑分区 | 主分区 |
| --------- | ------ | ------ | -------- | -------- | ------ |
| /dev/sdMN | sda1 | sda2 | sda5 | sda6 | sda3 |
| hdX,Y | hd0,0 | hd0,1 | hd0,2 | hd0,3 | hd0,4 |
现在我们来看 root 和 setup 命令的使用sudo grub 进入 Grub 交互模式:
```shell
grub>root(hd0,1)①
grub>setup(hd0)②
```
① 这个命令将 Grub 的根分区定位为hd0,1
② 这个命令表示将 Grub 安装在hd0因为没有指定安装的分区所以安装位置为 MBR
#### Grub 的根分区
为 Grub 配置文件 grub/menu.lst 和 stage 文件所在分区,假如您单独为/boot 目录挂载了一个分区,那么 Grub 的根分区通常为您系统中/boot 目录所在的分区。
搞错了根分区Grub 就不能正确读取配置文件,自然不能正确引导。
#### 系统根目录所在分区
Linux 根目录/的挂载分区。Linux 系统的分区挂载信息保存在文件系统分配表/etc/fstab 文件中
Grub 首先读取根分区中/boot/grub/menu.lst 文件,并转到引导分区,如果是 Windows 等系统,则将控制权移动分区引导扇区中的启动管理器。如果是 Linux 系统,则加载内核和设备,并根据/etc/fstab 文件的内容挂载文件系统。
看这个例子:(假设 Grub 安装在 MBR 中Grub 的安装位置为hd0
```shell
title Linux
root (hd0,0)①
kernel (hd0,1)②/vmlinuz-2.6.15-25-686 root=/dev/sda3③ ro splash vga=0x31b
initrd (hd0,1)④/initrd.img-2.6.15-25-686
boot
```
① 这一行表示引导分区为第一块硬盘的第一个分区hd0,0如果下面也指定了这个参数那么下面的优先
② 表示当前系统的/boot 目录挂载到第一块硬盘的第二个分区hd0,1它是引导分区。一般情况下在“root”项中指定
③ 表示当前系统的/目录挂载到第一块硬盘的第三个分区hd0,2内核根据该分区中的/etc/fstab 文件来挂载文件系统。
④ 同 2
### Grub 配置文件
/boot/grub/menu.lst 文件,主要由一些下面这样的块构成的。
```shell
timeout 5①
default 0②
title Linux③
root (hd0,2)④
kernel (hd0,2)/boot/vmlinuz-2.6.15-25-686 root=/dev/sda3⑤ ro⑥ splash⑦ vga=0x31b⑧
initrd (hd0,2)/boot/initrd.img-2.6.15-25-686
boot
title Windows
root (hd0,0)
makeactive⑨
chainloader +1 ⑩
```
① 等待时间,如果用户在这段时间内未进行选择,则引导默认系统
② 默认系统,从 0 开始计数,每个 title 项为一个系统
③ 标题title 和分隔符后的内容为 Grub 菜单中显示的条目
④ 引导分区
⑤ 表示当前系统的/目录挂载到第一块硬盘的第三个分区(hd0,2),内核根据该分区中的/etc/fstab 文件来挂载文件系统
⑥ 以只读模式挂载
⑦ 显示启动画面
⑧ 启动屏幕分辨率
⑨ 设置活动分区
⑩ 链式引导
### Grub 安装
#### 将 Grub 安装到系统中
```shell
grub-install --no-floppy① --root-directory=/boot② /dev/sdM③
```
① 不使用软盘
② 文件安装目录,通常不需要指定
③ 目标磁盘
这一步只是拷贝 Grub 到文件。如果是修复系统,通常情况下它们已经存在,直接进行下一步就可以
#### 将 Grub 安装到 MBR
`sudo grub`进入 Grub 交互模式:
```shell
grub> find /boot/grub/stage2①
(hd0,0)
grub> root (hd0,0)③
Filesystem type is xfs, partition type 0x83④
grub> setup (hd0)⑤
Checking if "/boot/grub/stage1" exists... yes
Checking if "/boot/grub/stage2" exists... yes
Checking if "/boot/grub/xfs_stage1_5" exists... yes⑥
Running "embed /boot/grub/xfs_stage1_5 (hd0)"... 19 sectors are embeded.⑦succeeded
Running "install /boot/grub/stage1 (hd0) (hd0)1+19 p (hd0,0)/boot/grub/stage2/boot/grub/menu.lst"... succeeded⑧
```
① 搜索 stage2 文件所在的分区,通常就是 Grub 根分区
② 返回 Grub 根分区名称
③ 将这个分区设为 Grub 根分区
④ 返回 Grub 根分区的文件系统信息
⑤ 将 Grub 安装到 MBR。注意是(hd0)而不是(hd0,0)
⑥ 检查需要的文件是否存在,因为用了 XFS 文件系统,所以需要 xfs_stage1_5
⑦ 将 xfs_stage1_5 嵌入到 MBR不然 Grub 不能够读取 XFS 分区中的 stage2
⑧ 将 stage1 安装到 MBR并指向 stage2 和 menu.list
⑨ 退出
> 可以使用**`TAB`**补全,或者按两次**`TAB`**列出提示
### Grub 使用
在 Grub 启动菜单中,您可以选择您要的选项,按下 d 删除该项;或者按下 e 键,进入到编辑模式
修改您的启动参数,完成后回车
按 b 键Grub 将以您修改后的参数引导系统。
在 Grub 启动菜单中,按下 c 进入命令模式,操作与交互模式基本相同;只不过最后一个命令是 boot而不是 quit
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,432 @@
---
layout: post
title: "开源世界旅行手册:服务器"
date: 2020-01-10 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## 服务器
### LAMP
LAMP是一个缩写它包括Linux操作系统Apache网络服务器Mysql数据库Perl、PHP或者Python语言
毫不夸张的说LAMP说开源世界的“皇家海军”或许Emacs工具链可以算是“太平洋舰队”正是凭借LAMP的力量开源应用得以在服务器市场称雄。
web服务器Apache是LAMP的核心几乎所有远程访问都要通过web服务器进行。除了Apacheweb服务器还有一些其他的开源实现比如LighttpdTomcatZope等
Perl、PHP、Python是服务器端脚本语言web服务器以扩展的形式支持这些语言作为后端处理器以获得强大的可编程能力
数据库服务器MySQL提供数据存储、检索支持。除了MySQL还有PostgreSQL等开源实现
理论上可以直接通过网络访问数据库服务器不过出于安全需要多数LAMP应用中数据库服务器只能由web服务器本地访问
也就是说访问web服务器产生了数据处理请求web服务器把数据请求发送到数据库服务器然后将数据库服务器返回的处理结果发送给访问者
数据库服务器接收和返回数据处理请求要通过SQL查询语言。web服务器本身不支持SQL而服务器端脚本语言可以内嵌SQL语句web服务器通过脚本语言处理访问者对数据库服务器端请求
#### 虚拟主机
一个机器上可能有多个网络接口每一块网卡会分配一个IP地址类似127.0.0.1这样的回环地址指向本地机器
网络接口就像港口有很多泊位称为端口。可以使用不同的协议连接如http、ftp、ssh等
服务器工作时,会舰艇某一个地址的固定端口,称为“绑定”。如果该端口中传来请求,则进行响应
很多服务器可以针对不同的网络接口设置不同的响应规则例如从内网访问接口192.168.1.2,使用/home/user作为根目录从公网访问接口211.148.131.7使用/var/WWW作为根目录。
这就是虚拟主机。
### Lighttpd
**`Lighttpd`**是一个新兴的、轻量级的 web 服务器它开始越来越多的应用在一些重要场合YouTobe、Sourceforge、豆瓣……
Lighttpd 以安全、快速和内存消耗低著称,还专门为大型分布式连接环境做了优化,支持 FastCGI, CGI, Auth, 输出压缩(output compress), URL重写, Alias 等重要功能。
Lighttpd 已经进入大多数发行版的软件仓库
安装完成后,用启动脚本启动:**`/etc/init.d/lighttpd start`**
`/etc/lighttpd/lighttpd.conf`为 Lighttpd 服务器的配置文件(查看`/etc/init.d/lighttpd`文件,可以看到类似字句:
**`/usr/sbin/lighttpd -D -f /etc/lighttpd/lighttpd.conf`** **- f** 选项指定配置文件):
```shell
## 网站根目录 映射在机器上的物理路径
server.document-root = "/home/lighttpd/html/"
## 如果网站目录中出现以下文件名,不用指定文件名便可直接访问
index-file.names = ( "index.php", "index.html",
"index.htm", "default.htm" )
## Lighttpd 进程的归属用户
server.username = "nobody"
## Lighttpd 进程的归属群组
server.groupname = "nobody"
## 绑定到端口 默认为 80
#server.port = 81
## 绑定到地址 默认为 所有
#server.bind = "127.0.0.1"
## 访问日志 路径
accesslog.filename = "/var/log/lighttpd/access.log"
## 错误日志 路径
server.errorlog = "/var/log/lighttpd/error.log"
## 禁止访问以下文件
url.access-deny = ( "~", ".inc" )
## 与目录列表相关的设置
#dir-listing.activate = "enable"
#dir-listing.encoding = "utf8"
#dir-listing.show-readme = "enable"
```
配置文件中的**`server.modules`**字段决定Lighttpd使用哪些扩展模块
```shell
server.modules = ("mod_access","mod_fastcgi","mod_accesslog" )
```
- Lighttpd 通过 **`mod_fastcgi`** 模块支持 PHP
- **`mod_accesslog`** 模块为访问纪录
其实在 `/etc/lighttpd/lighttpd.conf` 文件中,这部分内容写在多行,方便用 **`#`** 作注释,禁用不需要的模块
```shell
server.modules = (
## 基础模块
"mod_access",
## 访问纪录
"mod_accesslog" )
## fastcgi 支持
"mod_fastcgi",
## cgi 支持
# "mod_cgi",
## 路径绑定
# "mod_alias",
## 代理 (转发页面)
# "mod_proxy",
## 虚拟主机
# "mod_evhost",
## 输出压缩
# "mod_compress",
## 网址重写
# "mod_rewrite",
## 用户认证
# "mod_auth",
# "mod_redirect",
# "mod_cml",
# "mod_trigger_b4_dl",
# "mod_status",
# "mod_setenv",
# "mod_simple_vhost",
# "mod_userdir",
# "mod_ssi",
# "mod_usertrack",
# "mod_expire",
# "mod_secdownload",
# "mod_rrdtool",
```
#### fastcgi配置
在配置文件的**`server.modules`**字段中启用**`mod_fastcgi`**模块,然后检查以下内容:
```shell
### fastcgi 脚本扩展名
static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )
### fastcgi 服务器设置
fastcgi.server = ( ".php" =>
( "localhost" =>
(
# TCP/IP 接口 (“套接字”)
"socket" => "/tmp/php-fastcgi.socket",
# PHP cgi 模式的可执行文件(PHP 有 cli 和 cgi 两种模式)
"bin-path" => "/usr/bin/php-cgi"
)
)
)
```
上面例子的第二部分,使用 Lighttpd 转发规则。大意为: **`.php`**文件按以下方式处理 => 从**`localhost`**(本地),发送到**`/tmp/php-fastcgi.socket`**接口,使用**`/usr/bin/php-cgi`**处理。写成一行比较直观:
```shell
fastcgi.server = ( ".php" => ( "localhost" => ( "socket" => "/tmp/php-fastcgi.socket", "bin-path" => "/usr/bin/php-cgi" )))
```
如果想要 fastcgi 和 PHP 协同工作,还需要对 PHP 作一些设置
#### proxy
该模块可以将文件转发到其它服务器进行处理,例如将`.jsp`文件转发到Tomcat服务器
```shell
### 首先启用 mod_proxy 模块
# += 表示在原来设置上增加
servers.modules +=( "mod_proxy")
### 设置 proxy 服务器转发规则
proxy.server = ( ".jsp" =>
( "localhost" =>
(
# 将 .jsp 文件发送到 地址“127.0.0.1”的“8080”端口(也就是本机的 Tomcat 服务器)
"host" => "127.0.0.1",
"port" => 8080
)
)
)
```
#### CGI
Lighttpd 可以支持 cgi
```shell
### 启用 mod_cgi 模块
server.modules += ("mod_cgi")
### 设置 cgi 解释器
cgi.assign = ( ".pl" => "/usr/bin/perl",
".cgi" => "/usr/bin/perl",
".py" => "/usr/bin/python" )
```
#### 路径绑定
将一个路径,映射到网站目录中
```shell
## 启用 mod_alias 模块
servers.modules +=( "mod_alias")
## 将 /home/lighttpd/html/man 映射到 http://host/docs
alias.url += ( "/docs" => "/home/lighttpd/html/man" )
```
#### 虚拟主机
Lighttpd 可以建立多个虚拟主机,绑定在不同的网络接口
```shell
### 启用 mod_evhost 模块
servers.modules +=( "mod_evhost")
### 虚拟主机绑定的网络接口
$HTTP["host"] == "192.168.1.2"
{
### 虚拟主机可以使用独立的选项
dir-listing.activate = "enable"
dir-listing.encoding = "utf8"
dir-listing.show-readme = "enable"
### 虚拟主机根目录
server.document-root = "/home/user/html"
### 虚拟主机路径绑定
alias.url = ( "/download/" => "/home/user/downloads/" )
alias.url += ( "/pictures/" => "/home/user/pictures/" )
}
```
### PHP&MySQL
PHP的配置文件为/etc/php/php.ini如果与Lighttpd配合使用请检查下面语句
```shell
cgi.fix_pathinfo=1
```
在web服务器根目录下新建一个index.php文件内容如下
```shell
<?php phpinfo(); ?>
```
与Lighttpd和PHP一样MySQL也进入了大多数发行版的软件仓库使用包管理系统安装。
MySQL安装后可能需要重设密码
```shell
## 停止MySQL服务器
/etc/init.d/mysqld stop
## 使用单用户维护模式运行
mysqld_safe --user=mysql --skip-grant-tables --skip-networking &
## 使用root身份进入名为mysql的数据库
mysql -u root mysql
## 更新表user中USER项 值为root的行
## 设定这一行Password项的值PASSWORD()函数用来给密码加密
mysql > UPDATE user SET Password=PASSWORD('这里设置密码') where USER='root';
mysql > FLUSH PRIVILEGES;
mysql > quit
## 重启MySQL服务器
/etc/init.d/mysqld restart
```
### PureFTPD
相对于ProFTPD、WuFTPD等老牌 ftp 服务器PureFTPD、vsftpd这些轻量级 ftp 服务器更加实用
PureFTPD 的配置文件通常为 `/etc/pure-ftpd.conf`(查看`/etc/init.d/pure-ftpd`文件,可以看到类似字句:
**`/usr/sbin/pure-config.pl /etc/pure-ftpd.conf`** 启用配置文件)
```shell
## 绑定的地址和端口(默认为所有 IP地址 的 21端口)
# Bind 127.0.0.1,21
## 将所有用户限制在主目录中 (不能跳出)
ChrootEveryone yes
## 如果前一个设置为 no下面组的成员(GID)可以跳出主目录;其他用户仍然受限
## 如果想取消所有限制,注释掉 ChrootEveryone 和 TrustedGID
# TrustedGID 100
## 如果用户主目录不存在,自动创建
# CreateHomeDir yes
## 新建目录及文件的权限掩码(禁止的权限)。文件掩码:目录掩码
Umask 133:022
## 被动模式响应的端口范围
# PassivePortRange 30000 50000
## 强制一个 IP地址 使用被动模式
# ForcePassiveIP 192.168.0.1
## 仅允许认证用户进行 FXP 传输 (服务器=>服务器)
AllowUserFXP yes
## 允许匿名 FXP 传输
AllowAnonymousFXP no
## 兼容 ie 等山寨 ftp 客户端
BrokenClientsCompatibility no
## 服务器允许的最大连接数
MaxClientsNumber 50
## 同一 IP 允许的最大连接数
MaxClientsPerIP 8
## 匿名用户最大带宽KB/s
# AnonymousBandwidth 8
## 所有用户最大带宽KB/s包括匿名用户
# UserBandwidth 8
# 匿名用户的上传/下载的比率
# AnonymousRatio 1 10
# 所有用户的上传/下载的比率
# UserRatio 1 10
## 客户端的最大空闲时间缺省15分钟无动作将会被踢出
MaxIdleTime 15
## 不允许认证用户 (仅作为一个公共的匿名 FTP)
AnonymousOnly no
## 不允许匿名连接,仅允许认证用户使用 (设置为 no 允许匿名连接)
NoAnonymous no
## 受信地址允许认证用户,其它地址只能匿名连接
#TrustedIP 10.1.1.1
## 禁止匿名上传( no = 允许上传)
AnonymousCantUpload no
## 是否允许匿名用户创建目录
AnonymousCanCreateDirs no
## 'ls' 命令的递归限制。(文件的最大数目 最大子目录深度)
LimitRecursion 2000 8
## 保留所有文件(禁止删除)
# KeepAllFiles yes
## 如果上传的文件已经存在,自动重命名
AutoRename no
## 禁止重命名
# NoRename yes
## 禁止更改文件权限
# NoChmod yes
## 禁止读取隐藏文件(如 .history, .ssh 等)
ProhibitDotFilesRead no
## 禁止下载所有者为 "ftp" 的文件 (匿名用户上传后未被本地管理员验证的文件)
AntiWarez yes
## 指定文件内容作为欢迎信息
# FortunesFile /usr/share/fortune/zippy
## 启用磁盘限额。第一个数字为最大文件数,第二个数字为存储空间大小(单位Mb)
## 1000:10 限制每一个用户只能使用 1000 个文件,共 10Mb
# Quota 1000:10
## 最大可用空间,保证日志文件不会被覆盖 (默认为 99%)
MaxDiskUsage 99
```
PureFTPD 允许同时使用多种用户认证方式。以下为关于用户认证的配置:
```shell
## LDAP 配置文件
# LDAPConfigFile /etc/pureftpd-ldap.conf
## MySQL 配置文件
# MySQLConfigFile /etc/pureftpd-mysql.conf
## Postgres 配置文件
# PGSQLConfigFile /etc/pureftpd-pgsql.conf
## PureDB 用户数据库
# PureDB /etc/pureftpd.pdb
## pure-authd 套接路径
# ExtAuth /var/run/ftpd.sock
## 启用 PAM 认证
# PAMAuthentication yes
# 启用 Unix 系统认证 (/etc/passwd)
# UnixAuthentication yes
```
PureFTPD可以使用syslog生成日志以下为关于日志的配置
```shell
## Syslog 日志 。默认为 "ftp" "none" 禁用日志
SyslogFacility ftp
## 在日志文件中不解析主机名
DontResolve yes
## 在日志中添加 PID
# LogPID yes
## 使用 Apache 格式创建额外日志
# AltLog clf:/var/log/pureftpd.log
## 使用优化格式创建额外日志
# AltLog stats:/var/log/pureftpd.log
## 使用 W3C 格式创建额外日志
# AltLog w3c:/var/log/pureftpd.log
```
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,304 @@
---
layout: post
title: "开源世界旅行手册Vim编辑器"
date: 2020-01-11 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## Vim编辑器
### Vim简介
我们使用的大多数编辑器都可以直接在编辑区输入字符并且能够通过一些快捷键来完成一些控制功能比如使用方向键移动光标使用BackSpace或者Delete键删除文字使用PgUp和PgDn翻页使用Home和End来定位首和行末......
而Vim是一个带模式的编辑器同样的按键在不同的模式下具有不同的功能定义。例如h j k l 在编辑模式下输入相应的字符,在普通模式下却相当于方向键的作用。
由于需要切换模式Vim的使用起来略显繁琐不过优点也显而易见您吱哟啊把手安安稳稳的放在打字区就可以了而不需要使用诸如方向键、排版键、小键盘等需要挪开双手的健伟从而提高了您的效率和专注程度。事实上Vim的前身Vi诞生的时候键盘上还没有方向键、排版键和小键盘。
### 命令
使用Vim编辑文件
```shell
vi [文件名]
vim [文件名]
```
教学模式:
```shell
vimtutor [语言]
```
vim教程相当于使用Vim编辑器以只读模式打开教程文件。您无论对这个文件做了什么都会在退出后恢复原来的样貌。与只读模式的区别在于它不会没有眼色的提醒您现在的状态为只读模式。您可以使用它做一些练习。
您可以指定教程文件的语言,如果使用本地语言(ZH_cn)出现乱码,您可以尝试使用英语
```shell
vimtutor en
```
使用Vim比较文件区别
```shell
vimdiff [文件1] [文件2] [其他文件] ......
```
### 配置文件
Vim的全局配置文件为/etc/vim/vimrc用户配置文件为/.vimrc"起始的行为注释行。我们提供的配置项,您直接加入配置文件就可以了
您可以先对Vim进行一些简单的配置
**Vim配置/etc/vim/vimrc**
```
"运行在非兼容(VI)模式下(命令模式下 TAB 补全)
set nocompatible
"设定文件编码
set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936
"开启语法加亮
syntax on
"配色风格
colorscheme pablo
"简洁启动模式
set shortmess=atI
"设定 GUI 选项
"set guioptions=gmrLtT m:菜单 T:工具栏
set guioptions=gmrLt
"命令行高度
set cmdheight=1
"设定行距 ( GUI 界面中生效 )
set linespace=4
"显示标尺
set ruler
"自动折行
"set nowrap
set wrap
"按完整单词折行
set nolinebreak
"set linebreak
"行宽(输入时自动插入换行符)
"set textwidth=80
set textwidth=0
"允许在 虚空间 内操作 (虚空间:不包含任何文本的空间。如换行符之后)
"set virtualedit=all
"禁止在 虚空间 内操作
set virtualedit=
"设定 Tab 键缩进的空格数
set tabstop=4
"设定编辑器将多少空格视为一个缩进
set shiftwidth=4
"将缩进转换为空格
"set expandtab
"设定自动缩进(新行与前一行缩进相同)
set autoindent
"set noautoindent
"插入模式下,“←”如何删除光标前的字符:行首空白、换行符、插入点之前的字符
set backspace=indent,eol,start
"命令行历史纪录
set history=500
"禁用增量搜索
set incsearch
"set noincsearch
"搜索时忽略大小写
set ignorecase
"set noignorecase
"高亮显示搜索结果
set hlsearch
"设定折叠方式
"set foldmethod=manual
"以下字符将被视为单词的一部分 (ASCII)
"set iskeyword+=33-47,58-64,91-96,123-128
```
#### 模式行
也可以通过"模式行"将配置选项嵌入在普通文件中。打开此文件时,优先使用模式行中的配置:
```shell
# vim: filetype=sh foldmethod=marker autoindent expandtab shiftwidth=4
模式行可以写在任意位置
但为了不影响文件功能,模式行通常写在注释中
模式行以 vim: 起始,前面至少要有一个空白字符(空格、换行符等)
模式行中的设置项以空格分隔
......
```
这种形式的缺点在于,不能在设置项后添加其他文本。如果有相关需要,可以使用另一种模式行:
```shell
<!-- vim:set filetype=html: 这里可以任意添加文本 -->
```
### 模式介绍
Vim常见的模式有**普通模式**、**插入模式**、**命令模式**,另外我们也会经常用到 **可视模式**
Vim启动时进入普通模式或者在其他模式下按下Esc键便可以回到普通模式。
使用vimtutor en命令进入教程现在是普通模式。随便按几下j、k、l、h您会发现光标的位置发生改变。
按下i键编辑器底部出现了--插入--或者--insert--您进入了插入模式。随便按几下j、k、l、h您会发现相应的字符出现在编辑去现在还可以通过方向键来移动光标可能您觉得使用方向键移动光标不是什么问题但是习惯了Vim后您反而会认为方向键太麻烦了简直不能容忍好了现在按下Esc回到普通模式我们又可以使用j、k、l、h来移动光标了。
在普通模式下按下也就是shift+在编辑器底部出现了一个您进入了命令模式后输入一个命令new回车后编辑器被分割为上下两栏。为了方便起见我们在命令前价一个来表示命令模式下输入的命令像这样
```shell
:vnew
```
命令能够以一些规则简化,上面的命令也可以写为这种形式
```shell
:vne
```
现在您的编辑区一定弄的四分五裂,您可以使用命令 **:quit** 来关闭当前栏,直接用简写就可以了
```shell
:q
```
这个命令是退出编辑器,如果编辑区被分成多栏,则是退出当前栏。
执行完一个个命令按下回车后编辑器会自动回到普通模式。如果想不执行当前命令直接回到普通模式您可以按下Esc键。
按下v键您进入了可视模式可以使用j、k、l、h移动光标高亮选取文本。
事实上,可视模式相当于高亮选取文本后的普通模式。
可视模式具有多种模式以行为单位进行选取的可视行模式使用v键进入也就是Shif+v和以块为单位进行选取的可视块模式使用Ctrl+v键进入。
### 模式切换
| 切换前 | 切换后 | 按键 |
| -------- | -------- | -------------------- |
| 其他模式 | 普通模式 | Esc |
| 普通模式 | 插入模式 | i 在光标前插入 |
| | | I 在行首插入 |
| | | a 在光标后插入 |
| | | A 在行末插入 |
| | | o 在当前行下新建行 |
| | | O 在当前行上新建行 |
| | | r 替换当前字符 |
| | | R 从当前字符开始替换 |
| 普通模式 | 命令模式 | : |
| 普通模式 | 可视模式 | v 可视模式 |
| | | V 可视行模式 |
| | | Ctrl+v 可视块模式 |
### 移动
在普通模式中您可以使用以下方式移动光标
| | | |
| ----- | ----- | ----- |
| | k(上) | |
| h(左) | | l(右) |
| | j(下) | |
您可以使用其他更有效率的方式移动光标
| | 向前 | 向后 |
| ---------------------- | ---- | ---- |
| 单词 | w | b |
| 单词,包括特殊符号 | W | B |
| 单词词尾 | e | |
| 单词词尾,包括特殊符号 | E | |
| 行 | 0 | $ |
| 行首文字(不包括空格) | ^ | |
| 页 | H | L |
| 页面中部 | M | |
在其他模式中,您可以使用方向键移动光标,不过那样比较麻烦,您可以在配置文件中绑定插入模式下的功能键
```shell
noremap! <M-j> <Down>
noremap! <M-k> <Up>
noremap! <M-h> <Left>
noremap! <M-l> <Right>
......
作用范围 键位 功能
```
> 其中map! 绑定的键盘映射,作用于所有模式; inoremap! 绑定的映射,仅作用于插入模式
### 数字参数
您也可以使用数字参数(普通模式),来重复执行。例如
```shell
100j
```
执行100次j键向下100行
### 浏览
| | 向前 | 向后 |
| -------------------------- | -------- | ------ |
| 整页 | Ctrl+f | Ctrl+b |
| 半页 | Ctrl+d | Ctrl+u |
| 文件 | G | gg |
| 按行号转到相应行 | 行号G | |
| 按百分比转到相应行 | 1100% | |
| 按行卷动 | Ctrl+e | Ctrl+y |
| 将光标所在行调整至页面中间 | zz | |
| 统计字数 | g Ctrl+g | |
| 显示位置信息 | Ctrl+g | |
| 刷新屏幕 | Ctrl+l | |
> gg定位到文件首行v进入可视行模式G定位到文件末行实现类似“全选”的功能。依次按下g g V(Shif+v) G(Shift+g)
### 标记
您可以在当前光标处做一个标记,以便快速返回
```
m标记名称 定义标记,标记名称为一个字符
`标记名称 返回标记
mx 将当前光标处定义为标记x
`x 返回标记x
:marks 查看标记列表
```
### 编辑
| | 复制 | 剪切 |
| ---- | ----------------------- | ---- |
| 字符 | y(需要在可视模式中选取) | x |
| 行 | yy | dd |
| 按键 | 含义 |
| ------ | ------------------ |
| p | 在光标后粘贴 |
| P | 在光标前粘贴 |
| u | 撤销 |
| Ctrl+r | 重做 |
| Ctrl+y | 逐字克隆上一行内容 |
| Ctrl+e | 逐字克隆下一行内容 |
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,336 @@
---
layout: post
title: "开源世界旅行手册Vim编辑器"
date: 2020-01-12 10:28:00
author: "Viletyy"
header-style: text
tags:
- Linux
---
## Vim编辑器
### 搜索和替换
按下 / 键,编辑器底部会出现 / 符号,接着输入字符串,便可以进行搜索
| 按键 | 含义 |
| ---- | ---------- |
| / | 向下搜索 |
| ? | 向上搜索 |
| n | 搜索下一个 |
| N | 搜索上一个 |
| 输入 | 含义 |
| ----------------------------------- | -------------------------------- |
| :s/源字符串/目标字符串 | 将源字符串替换为目标字符串 |
| :s/源字符串/目标字符串/g | 替换当前行中所有符合条件的字符串 |
| :行号1,行号2s/源字符串/目标字符串/g | 在指定行中进行替换 |
| :%s/源字符串/目标字符串/g | 全文替换 |
### 寄存器操作
Vim可以将不同字段剪切或复制到不同寄存器中您可以从不同寄存器中取出内容后粘贴
```
"寄存器名称
```
> 按下"和另一个字符键,便可以定义一个寄存器,例如 "a "1
定义寄存器后直接进行操作
```
"ayy 将当前行复制到寄存器a中
"ap 将寄存器a中的内容粘贴到光标之后
```
- :registers查看所有寄存器的内容
- 通常情况下,寄存器+(先按"再按Shift +=对于X下的剪贴板您在其他程序中复制的内容可以使用 "+p 粘贴到Vim中您在Vim中可以使用 "+y 将内容复制到剪贴板,再粘贴到其他程序中
- 没有指定寄存器时Vim使用“无名寄存器”存储内容
### 宏
您可以将一系列的操作录制为一个宏,然后执行它
```
q宏名称 开始录制宏,宏名称为一个字符
q 录制中按下“q”结束录制
@宏名称 执行宏
```
> 可以使用 :registers寄存器列表命令查看已记录的宏
### 插入模式下的快捷键
| 快捷键 | 含义 |
| ----------------- | -------------------- |
| Ctrl+r寄存器名称 | 插入指定寄存器内容 |
| Ctrl+k2个字符 | 输入二合字符 |
| Ctrl+v数字 | 通过数字编码输入字符 |
| Ctrl+v键位 | 输入键位的名称 |
### 键绑定、缩写
前面我们已经向您介绍来键绑定
```shell
map! <M-j> <Down>
```
尖括号及其中的内容为Vim配置文件的约定分别描述了按键和功能表示将功能编写到按键上。如果绑定的只是普通字符例如
```shell
map! xxx XXXXX
```
表示将XXXXX 绑定到xxx上。当您键入xxx时编辑器会自动替换为XXXXX
如果您只是想将字符串绑定为缩写方便输入我们建议您使用iabbrev来绑定。例如
```shell
iabbrev g google
```
在插入模式下键入g编辑器会自动替换为google。您可以将iabbrev命令缩写为iab例如
```shell
iab g google
```
以上命令,您可以直接在命令模式下输入,临时启用。也可以写入配置文件,永久启用。
### 单词补全
```shell
<Ctrl+n>下一个匹配项
<Ctrl+p>上一个匹配项
```
您可以在匹配文件中定义补全的方式
```
"自动补全方式:(使用逗号分隔)
set complete=k,.
" . 当前文件
" b 已被装缓冲区,但是没有在窗口内的文件
" d 在当前的文件中定义和由 #include 包含进来的文件
" i 由 #include 包含进来的文件
" k 由dectionary选项定义的文件
" kfile 名为{file}的文件
" t 标记(tags)的文件
" u 没有载入的缓冲区
" w 在其他窗口中的文件
"设定自动补全字典:
```
### 命令模式
前面介绍了普通模式和插入模式。我们发现,普通模式主要用来浏览和修改文本内容,而插入模式则用来向文本中添加内容。
而命令模式则多用于操作文本文件(而不是操作文本文件的内容),例如保存文件;或者用来更改编辑器本身的状态,例如设定多栏窗口、标签或者退出编辑器......
w(rote)将更改写入文件
```shell
:w
```
q(uit)退出编辑器
```shell
:q
```
某些情况下,编辑器会阻止命令的执行。例如您修改了文件,而没有保存,那么您使用:q命令退出时编辑器就不会执行这条命令而是提醒您保存文件。
这个时候,您可以在命令末尾追加!来强制执行命令
```shell
:命令!
```
> 例如:q!,即便您没有保存已修改的文件,使用此命令,编辑器也会放弃修改而强行退出
以:引导一个Shell命令则可以从Vim临时切换到Shell中执行一个Shell
```shell
:!命令
```
> 例如 :!ls
命令模式还可以用来调节一些选项,例如
```shell
:set linespace=6
```
- 使用Tab键补全命令
- 命令可以直接写入配置文件
- 在选项后加上一个&使用默认值,例如: :set linespace&
### 折叠
| 命令 | 含义 |
| ------------------ | ---------------------------- |
| zf数字 | 创建折叠,数字参数为折叠行数 |
| zo | 打开折叠 |
| zc | 关闭折叠 |
| zd | 删除折叠 |
| :set foldcolumn=4 | 显示折叠树 |
| :mkview | 保存折叠 |
| :loadview | 读取折叠 |
| zr | 打开同一层级所有折叠 |
| zm | 关闭同一层级所有折叠 |
| zO | 打开某一行的所有层级折叠 |
| zC | 关闭某一行的所有层级折叠 |
| zR | 打开所有折叠 |
| zM | 关闭所有折叠 |
| :set foldclose=all | 光标离开折叠自动关闭 |
| :set foldopen=all | 关闭遇到折叠自动打开 |
您可以在配置文件中定义折叠的方式
```
"设定折叠方式
set foldmethod=manual
" manual 手动折叠
" indent 按缩进折叠
" marker 按标记折叠
" syntax 按语法折叠
" expr 按表达式折叠
```
按标记折叠的例子:
```shell
#起始标记 ( 将标记放到注释里,不会影响文件的功能
起始标记和结束标记之间的内容折叠
显示为起始标记所在行
#结束标记 )
##### 将折叠方式写到模式行里 #####
# vim: foldmethod=marker
```
### 多栏窗口
您可以使用以下命令,将当前窗口水平分为两栏
```shell
:new
```
新建一栏空白窗口,将当前文件分两栏显示
```shell
:split
```
同理,您可以使用下列命令,将当前窗口垂直分为两栏
```shell
:vnew
:vsplit
```
先按下ctrl+w键再按下方向键 j、k、l、h您可以切换到其他栏在当前栏中使用 :q 命令,可以退出当前栏,也可以使用其他命令,对当前栏作处修改
如果您希望当前命令在所有栏中生效您可以在命令的末尾追加all
```she
:命令all
```
> 例如::qall
如果您希望这条命令强制执行,那么!位于命令的最末
> 例如::qall! 强行退出所有栏窗口
### 标签页
Vim在7以后的版本开始支持标签页的功能
```shell
:tabnew 新建一个标签
:tabnext 转到下一个标签
:tabprevious 转到上一个标签
```
您也可以使用鼠标点击标签进行切换
### 文件管理
使用Vim编辑目录时会进入文件管理模式
```shell
vim .
```
文件管理模式中可以实现一些简单的文件管理功能:
| 按键 | 含义 |
| ------ | --------------------------- |
| enter | 打开文件或文件夹 |
| Ctrl+o | 后退 |
| o | 新开一栏进行编辑 |
| O | 在前一次打开的栏中编辑 |
| p | 预览 |
| i | 显示/隐藏文件大小、日期 |
| s | 切换文件排序方式 |
| r | 切换顺序、逆序 |
| - | 回到上一级目录 |
| c | 将Vim执行目录设定为当前目录 |
| R | 重命名 |
| D | 删除 |
| x | 运行 |
> 其中,:cd 设定所有窗口执行目录; :lcd 设定当前栏窗口执行目录
### 加密
使用 :X 命令为文件设定一个密码
```shell
:X
Enter encryption key: ******
Enter same key again: ******
```
> 如果要取消加密,可以设置密码为空
可以禁用交换文件,以免泄密
```shell
vim -x -n file.txt
```
如果你已经编辑这个文件了那么交换文件swapfile可以用下面的命令禁止
```shell
:setlocal noswagfile
```
由于没了交换文件,文件复原就不可能了。为了避免失去编辑的成成果,要比平时更勤快地存盘你的文件
现在你可以像平时一样编辑这个文件并把你所有的秘密放进去。当你编完文件要退出Vim时这个文件就被加密存盘了
当你下次用Vim编辑这个文件时它就会询问你密码
如果你试图用另一个程序来阅读这个文件你将读到一堆垃圾。如果你用Vim来编辑这个文件但输入了错误的密码你也只能得到垃圾。Vim并不具备检验密码正确性的机制这一点使得破译密码更为困难
### 版本
在Windows平台下请使用gvim7ole版本
参考资料:
- [开源世界旅行手册](https://i.linuxtoy.org/docs/guide/)

View File

@ -0,0 +1,111 @@
---
layout: post
title: "Golang位运算符和移位"
date: 2020-05-07 10:28:00
author: "Viletyy"
header-style: text
tags:
- Golang
---
## 位运算符
- **取反**是一元运算符,对一个二进制数的每一位执行逻辑反操作。
```
NOT 0111(十进制7)
= 1000(十进制8)
```
许多程序设计语言,取反操作符用波浪线`”~“`表示。
- **按位或**处理两个长度相同的二进制数两个相应的二进位只要有一个为1该位的结果值为1
```
0101(十进制5)
OR 0011(十进制3)
= 0111(十进制7)
```
按位或操作符是`"|"`
- **按位异或**对等长二进制数的每一位执行逻辑异或操作。操作的结果是如果某位不同则该位为1否则该位为0.
```
0101
XOR 0011
= 0110
```
按位异或运算符是`"^"`
- **按位与**处理两个长度相同的二进制数两个相应的二进位都为1该位的结果值才为1 否则为0
```
0101
AND 0011
= 0001
```
按位与操作符是`"&"`
位运算符对整数在内存中的二进制位进行操作
| p | q | p&q | p\|q | p^q |
| ---- | ---- | ---- | ---- | ---- |
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 1 | 1 | 1 | 0 |
| 1 | 0 | 0 | 1 | 1 |
## 移位
**移位**是一个二元运算符,用来将一个二进制数中的每一位全部都向一个方向移动指定位,溢出的部分将被舍弃,而空缺的部分填入一定的值。
```
0001(十进制1)
<< 3(左移3位)
= 1000(十进制8)
```
左移用`"<<"`表示
```
1010(十进制10)
>> 2(右移2位)
= 0010(十进制2)
```
右移用`">>"`表示
## 位运算符、移位在golang中应用
```go
package main
import "fmt"
func main(){
var a uint = 60 /* 60 = 0011 1100*/
var b uint = 13 /* 13 = 0000 1101*/
var c uint = 0
c = a & b /* 12 = 0000 1100 */
fmt.Printf("第一行 - c 的值为 %d\n", c )
c = a | b /* 61 = 0011 1101 */
fmt.Printf("第二行 - c 的值为 %d\n", c )
c = a ^ b /* 49 = 0011 0001 */
fmt.Printf("第三行 - c 的值为 %d\n", c )
c = a << 2 /* 240 = 1111 0000 */
fmt.Printf("第四行 - c 的值为 %d\n", c )
c = a >> 2 /* 15 = 0000 1111 */
fmt.Printf("第五行 - c 的值为 %d\n", c )
}
```

View File

@ -0,0 +1,125 @@
---
layout: post
title: "Golang键盘输入和打印输入"
date: 2020-05-11 10:28:00
author: "Viletyy"
header-style: text
tags:
- Golang
---
### 打印输入
- fmt包常用打印函数
` func Print(a ...interface{})(n int, err error) `
`func Printf(format string, a ...interface{})(n int, err error) `
`func Println(a ...interface{})(n int, err error)`b
- 常用占位符
- %v, 原样输出
- %T, 打印类型
- %t, bool类型
- %s, 字符串
- %f, 浮点
- %d, 10进制的整数
- %b, 2进制的整数
- %o,, 8进制
- %x, %X, 16进制
- %x: 0-9, a-f
- %X: 0-9, A-F
- %c, 打印字符
- %p, 打印地址
- 示例代码
```go
package main
import (
"fmt"
)
func main() {
a := 100
b := 3.14
c := true
d := "Hello World"
e := Ruby
f := 'A'
fmt.Printf("%T, %b\n", a, a)
fmt.Printf("%T, %f\n", b, b)
fmt.Printf("%T, %t\n", c, c)
fmt.Printf("%T, %s\n", d, d)
fmt.Printf("%T, %s\n", e, e)
fmt.Printf("%T, %d, %c\n", f, f, f)
fmt.Println("-----------------------")
fmt.Printf("%v\n", a)
fmt.Printf("%v\n", b)
fmt.Printf("%v\n", c)
fmt.Printf("%v\n", d)
fmt.Printf("%v\n", e)
fmt.Printf("%v\n", f)
}
```
运行结果
![](/img/in-post/2020-05-11-golang-keyboard-input-output-01.png)
### 键盘输入
#### fmt包读取键盘输入
- fmt包读取键盘输入常用方法如下
`func Scan(a ...interface{})(n int, err error)`
`func Scanf(format string, a ...interface{})(n int, err error)`
`func Scanln(a ...interface{})(n int, err error)`
- 示例代码
```go
package main
import (
"fmt"
)
func main() {
var x int
var y float64
fmt.Println("请输入一个整数,一个浮点类型:")
fmt.Scanln(&x, &y)
fmt.Printf("x的数值%d, y的数值%f\n", x, y)
fmt.Scanf("%d, %f", &x, &y)
fmt.Printf("x:%d, y:%f\n", x,y)
}
```
#### bufio包读取
- 示例代码
```go
package main
import (
"fmt"
"os"
"bufio"
)
func main() {
fmt.Println("请输入一个字符串:")
reader := bufio.NewReader(os.Stdin)
s1, _ := reader.ReadString("\n")
fmt.Println("读到的数据:", s1)
}
```

View File

@ -0,0 +1,343 @@
---
layout: post
title: "Golang流程语句"
date: 2020-05-15 10:28:00
author: "Viletyy"
header-style: text
tags:
- Golang
---
## 程序的流程结构
一共有三种:顺序结构,选择结构,循环结构
顺序结构:从上向下,逐行执行
选择结构条件满足某些代码才会执行。0-1次
分支语句if, switch, select
循环结构条件满足某些代码会被反复的执行多次。0-N次
循环语句for
### if分支语句
**语法格式**
```go
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
} else {
/* 在布尔表达式为 false 时执行 */
}
if 布尔表达式1 {
/* 在布尔表达式1为 true 时执行 */
} else if 布尔表达式2{
/* 在布尔表达式1为 false ,布尔表达式2为true时执行 */
} else{
/* 在上面两个布尔表达式都为false时执行*/
}
```
**示例代码**
```go
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 10
/* 使用 if 语句判断布尔表达式 */
if a < 20 {
/* 如果条件为 true 则执行以下语句 */
fmt.Printf("a 小于 20\n" )
}
fmt.Printf("a 的值为 : %d\n", a)
}
```
### if变体
如果其中包含一个可选的语句组件(在评估条件之前执行)。它的语法是
```go
if statement; condition {
}
if condition{
}
```
**示例代码**
```go
package main
import (
"fmt"
)
func main() {
if num := 10; num % 2 == 0 {
fmt.Println(num, "is even")
} else {
fmt.Println(num, "is odd")
}
}
```
------
### switch分支语句
**语法格式**
```go
switch var1 {
case val1:
...
case val2:
...
default:
...
}
```
**示例代码**
```go
package main
import "fmt"
func main() {
/* 定义局部变量*/
var grade string = "B"
var marks int = 90
switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}
switch {
case grade == "A":
fmt.Printf("优秀!\n")
case grade == "B":
fmt.Printf("良好 \n")
case grade == "D":
fmt.Printf("及格 \n")
case grade == "F":
fmt.Printf("不及格 \n")
default:
fmt.Printf("差 \n")
}
fmt.Printf("你的等级是 %s \n", grade)
}
```
### fallthrough
如需贯通后续的case就添加fallthrough
```go
package main
import (
"fmt"
)
type data [2]int
func main() {
switch x :=5; x {
default:
fmt.Println(x)
case 5:
x += 10
fmt.Println(x)
fallthrough
case 6:
x += 20
fmt.Println(x)
}
}
```
输出结果
```go
15
35
```
------
### for循环语句
**语法结构**
```go
for init; condition; post { }
```
**示例代码**
```go
package main
import (
"fmt"
)
func main() {
for i := 1; i <= 10; i++ {
fmt.Printf(" %d", i)
}
}
```
### for循环变体
**语法结构**
```go
for condition { }
```
**示例代码**
```go
package main
import "fmt"
func main() {
var b int = 15
var a int
numbers := [6]int{1, 2, 3, 5}
/* for循环 */
for a := 0; a < 10; a++ {
fmt.Printf("a的值为 %d\n", a)
}
for a < b {
a++
fmt.Printf("a的值为 %d\n", a)
}
for i, x := range numbers {
fmt.Printf("第 %d 位x的值 = %d\n", i, x)
}
}
```
------
### 跳出循环的语句
#### break语句
break跳出循环体。break语句用于在结束其正常执行之前突然中止for循环
**示例代码**
```go
package main
import (
"fmt"
)
func main() {
for i := 1; i <= 10; i++ {
if i > 5 {
break
}
fmt.Printf("%d", i)
}
fmt.Printf("\nline after for loop")
}
```
#### continue语句
continue: 跳出一次循环。continue语句用于跳过for循环的当前迭代。在continue语句后面的for循环中的所有代码将不会在当前迭代中执行。循环将继续到下一个迭代
```go
package main
import (
"fmt"
)
func main() {
for i := 1; i <= 10; i++ {
if i%2 == 0 {
continue
}
fmt.Printf("%d ", i)
}
}
```
------
### goto语句
goto: 可以无条件的转移到过程中指定的行
**语法结构**
```go
goto label;
..
..
label: statement;
```
**代码示例**
```go
package main
import "fmt"
func main() {
/* 定义局部变量*/
var a int = 10
/* 循环 */
LOOP: for a < 20 {
if a == 15 {
/* 跳过迭代 */
a = a + 1
goto LOOP
}
fmt.Printf("a的值为%d\n", a)
a++
}
}
```

View File

@ -0,0 +1,97 @@
---
layout: post
title: "GolangFile文件操作"
date: 2020-05-18 10:04:53
author: "Viletyy"
header-style: text
tags:
- Golang
---
### File文件操作
#### FileInfo接口
> FileInfo接口中定义了File信息相关的方法。
```go
type FileInfo interface {
  Name() string  // base name of the file 文件名.扩展名 aa.txt
  Size() int64  // 文件大小, 字节数 12540
  Mode() FileMode // 文件权限 -rw-rw-rw-
  ModTime() time.TIme   // 修改时间 2018-04-13 16:30:53 +0800 CST
  IsDir() bool // 是否文件夹
  Sys() interface{} // 基础数据源接口(can return nil)
}
```
#### 权限
> 操作权限perm除非创建文件时才需要指定不需要创建新文件时可以将其设定为0.
> 虽然go语言给perm权限设定了很多的常量但是习惯上也可以直接使用数字如0666具体含义和Unix系统的一致
#### 3. 打开模式
```go
const (
  O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件
  O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件
  O_RDWR int = syscall.O_RDWR // 读写模式打开文件
  O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部
  O_CREATE int = syscall.O_CREAT // 如果不存在将创建一个新文件
  O_EXCL int = syscall.O_EXCL // 和O_CREATE配合使用文件必须不存在
  O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O
  O_TRUNC int = syscall.O_TRUNC // 如果可能,打开时清空文件
)
```
#### File操作
```go
type File
// File代表一个打开的文件对象
func Create(name string) (file *File, err error)
// Create采用模式0666 (任何人都可读写不可执行创建一个名为name的文件如果文件已存在会截断它为空文件。如果成功返回的文件对象可用于I/O对于的文件描述符具有O_RDWR模式。如果出错错误底层类型是*PathError.
func Open(name string) (file *File, err error)
// Open打开一个文件用于读取。如果操作成功返回的文件对象的方法可用于读取数据。对应的文件描述符具有O_RDONLY模式。如果出错错误底层类型是*PathError
func OpenFile(name string, flag int, perm FileMode) (file *File, err error)
// OpenFile是一个更一般的文件打开函数大多数调用者都应用Open或Create代替本函数。它会使用指定的选项如O_RDONLY等、指定的模式如0666等打开指定名称的文件。如果操作成功返回的文件对象可用于I/O。如果出错错误底层类型是*PathError
func NewFile(fd uintptr, name string) *File
//NewFile使用给出的Unix文件描述符和名称创建一个文件
func Pipe() (r *File, w *File, err error)
// Pipe返回一对关联的文件对象。从r的读取将返回写入w的数据。本函数会返回两个文件对象和可能的错误
func (f *File) Stat() (fi FileInfo, err error)
// Stat返回描述文件f的FileInfo类型值。如果出错错误底层类型是*PathError.
func (f *File) Fd() uintptr
// Fd返回与文件f对应的证书类型的Unix文件描述符
func (f *File) Chdir() error
// Chdir将当前工作目录改为f f必须是一个目录。如果出错错误底层类型是*PathError
func (f *File) Chmod(mode FileMode) error
// Chmod修改文件的模式如果出错,错误底层类型是*PathError
func (f *File) Chown(uid, gid int) error
// Chown修改文件的用户id和组id 如果出错,错误底层类型是*PathError
func (f *File) Close() error
// Close关闭文件f使文件不能用于读写。它返回可能出现的错误
func (f *File) Readdir(n int) (fi []FileInfo, err error)
// Readdir读取目录f的内容返回一个有n个成员[]FileInfo这些FileInfo是被Lstat返回的采用目录顺序。对本函数的下一次调用会返回上一次调用剩余未读取的内容的信息。如果n>0Readdir函数会返回一个最多n个成员的切片。这时如果Readdir返回一个空切片它会返回一个非nil的错误说明原因。如果到达了目录f的结尾返回err会是io.EOF。如果n<=0, Readdir函数返回目录中剩余所有文件对象的FileInfo构成的切片。此时如果Readdir调用成功(读取所有内容直到结尾它会返回该切片和nil的错误值。如果在到达结尾前遇到错误会返回之前成功读取的FileInfo构成的切片和该错误
func (f *File) Readdirnames(n int) (names []string, err error)
// Readdir读取目录f的内容, 返回一个有n个成员的[]string切片成员为目录中文件对象的名字采用目录顺序。对本函数的下一次调用会返回上一次调用剩余未读取的内容的信息。如果n>0Readdir函数会返回一个最多n个成员的切片。这时如果Readdir返回一个空切片它会返回一个非nil的错误说明原因。如果到达了目录f的结尾返回值err会是io.EOF。如果n<=0Readdir函数返回目录中剩余所有文件对象的名字构成的切片。此时如果Readdir调用成功读取所有内容直到结尾它会返回该切片和nil的错误值。如果在到达结尾前遇到错误会返回之前成功读取的名字构成的切片和该错误。
func (f *File) Truncate(size int64) error
// Truncate改变文件的大小它不会改表I/O的当前位置。如果截断文件多出的部分就会被丢弃。如果出错错误底层类型是*PathError
func Remove(name string) error
// 删除文件
func RemoveAll(name string) error
// 删除目录
```

View File

@ -0,0 +1,84 @@
---
layout: post
title: "Golangtime包"
date: 2020-05-17 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
- 包
---
在标准库中为我们提供了time包里面都是关于日期时间的操作都比较简单。
### 1. 获取时间对象
获取当前时间
```go
func Now() Time
// Now returns the current local time
t := time.Now()
```
获取指定时间
```go
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time
/*
Date returns the Time corresponding to
yyyy-mm-dd hh:mm:ss + nsec nanoseconds
in the appropriate zone for that time in the given location.
The month, day, hour, min, sec, and nsec values may be outside their usual ranges and will be normalized during the conversion. For example, October 32 converts to November 1.
A daylight savings time transition skips or repeats times. For example, in the United States, March 13, 2011 2:15am never occured, while November 6, 2011 1:15am occurred twice. In such cases, the choice of time zone, and therefore the time, is not well-defined. Date returns a time that is correct in one of the two zones involved in the transition, but it does not guarantee which.
Date panics if loc is nil.
*/
t := time.Date(2008, 7, 15, 16, 30, 28, 0, time.Local)
```
### 2. 日期和文本之间的转换
日期转为文本:
```go
func(t Time) Format(layout string) string
t := time.Now()
s := time.Format("2006年1月2日 15:04:05")
```
文本转为日期:
```go
func Parse(layout, value string) (Time, error)
s := "1999年10月10日"
t, err := time.Parse("2006年1月2日", s)
```
### 3. 时间戳
时间戳:指定的日期(当前日期,指定的某个日期)距离1970年1月1日0点0时0分0秒的时间差值。有秒纳秒两种函数。
获取单位为秒的时间戳:
```go
func (t Time) Unix() int64
t := time.Now()
i := t.Unix()
```
获取单位为纳秒的时间戳
```go
func(t Time) UnixNano() int64
t := time.Now()
i := t.UnixNano()
```
### 4. 获取年月日时间秒等
根据time对象可以获取对应等年、月、日、时、分、秒等等
```go
func (t Time) Year() int
func (t Time) Month() Month
func (t TIme) Day() int
func (t Time) Hour() int
func (t Time) Minute() int
func (t Time) Second() int
func (t Time) Date() (year int, month Month, day int)
func (t Time) Clock() (hour, min, sec int)
```
### 5. Sleep()
```go
func Sleep(d Duration)
time.Sleep(3*time.Second)
```

View File

@ -0,0 +1,135 @@
---
layout: post
title: "GolangI/O操作"
date: 2020-05-19 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
- 包
---
## I/O操作
I/O操作也叫输入输出操作。其中I是指InputO是指Output用于读或者写数据的有些语言中也叫流操作是指数据通信的通道。
Golang标准库对IO的抽象非常精巧各个组件可以随意组合可以做为接口设计的典范
### io包
io包中提供I/O原始操作的一系列接口。它主要包装了一些已有的实现如os包中的那些并将这些抽象成为实用性的功能和一些其他相关的接口
由于这些接口和原始操作以不同的实现包装了低级操作,客户不应假定它们对于并行执行是安全的
### 读操作
Reader接口的定义Read()方法用于读取数据
```go
type Reader interface {
Read(p []byte) (n int, err error)
}
// Read将len(p)个字节读取到p中它返回读取到字节数n(0 <= n <= len(p))以及任何遇到的错误。即使Read返回的n < len(p)它也会在调用过程中使用p的全部作为暂存空间若一些数据可用但不到len(p)个字节Read会照例返回可用的东西而不是等待更多
// 当Read在成功读取n>0个字节后遇到一个错误或EOF情况它就会返回读取的字节数它会从相同的调用中返回(非nil)的错误或从随后的调用中返回错误(和n==0)这中一般情况的一个例子就是Reader在输入流结束时会返回一个非零的字节数可能的返回不是err == EOF 就是 err == nil. 无论如何下一个Read都应当返回0, EOF
// 调用者应当总在考虑到错误err 前处理n 0 的字节这样做可以在读取一些字节以及允许的EOF行为后正确地处理I/O错误
// Read的实现会阻止返回零字节的计数和一个nil错误调用者应将这种请款视作空操作。
```
ReaderFrom接口的定义封装了基本的ReadFrom方法
```go
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}
// ReadFrom从r中读取数据到对象的数据流中
// 直到r返回EOF或r出现读取错误为止
// 返回n是读取的字节数
// 返回err就是r的返回值err
```
定义ReaderAt接口ReaderAt接口封装了基本的ReadAt方法
```go
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}
// ReadAt从对象数据流的off处读出数据到p中
// 忽略数据的读写指针从数据的起始位置偏移off处开始读取
// 如果对象的数据流只有部分可用不足以填满p
// 则ReadAt将等待所有数据可用之后,继续向p中写入
// 直到将p填满后再返回
// 在这点上ReadAt要比Read严格
// 返回读取的字节数 n 和读取时遇到的错误
// 如果 n < len(p),则需要返回一个err值来说明
// 为什么没有将p填满(比如EOF)
// 如果 n = len(p) 而且对象的数据没有全部读完则err将返回nil
// 如果 n = len(p),而且对象的数据刚好读完则err将返回EOF或者nil(不确定)
```
### 文件读取
file类是在os包中的封装了底层的文件描述符和相关信息同时封装了Read和Write的实现
```go
func(f *File) Read(b []byte) (n int, err error)
// Read方法从f中读取最多len(b)字节数据并写入b。它返回读取的字节数和可能遇到的任何错误。文件终止标志是读取0个字节且返回值err为io.EOF
func(f *File) ReadAt(b []byte, off int64) (n int, err error)
// ReadAt从指定的位置(相对于文件开始位置)读取len(b)字节数据并写入b。它返回读取的字节数和可能遇到的任何错误。当n小于len(b)时本方法总是返回错误如果是因为到达文件结尾返回值err会是io.EOF.
```
### 写操作
Writer接口的定义Write()方法用于写出数据
```go
type Writer interface {
Write(p []byte) (n int, err error)
}
// Write 将 len(p) 个字节从 p 中写入到基本数据流中。它返回从 p 中被写入的字节数n0 <= n <= len(p))以及任何遇到的引起写入提前停止的错误。若 Write 返回的n < len(p)它就必须返回一个非nil的错误Write 不能修改此切片的数据即便它是临时的
```
Seeker接口的定义封装了基本的Seek方法
```go
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
// Seeker用来移动数据的读写指针
// Seek设置下一次读写操作的指针位置每次的读写操作都是从指针位置开始的
// whence的含义
// 如果whence为0:表示从数据的开头开始移动指针
// 如果whence为1:表示从数据的当前指针位置开始移动指针
// 如果whence为2:表示从数据的尾部开始移动指针
// offset 是指针移动的偏移量
// 返回移动后的指针位置和移动过程中遇到的任何错误
```
WriterTo接口的定义封装了基本的WriteTo方法
```go
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}
// WriterTo将对象的数据流写入到w中
// 直到对象的数据流全部写入完毕或遇到写入错误为止
// 返回值n是写入的字节数
// 返回值err就是w的返回值err
```
定义WriterAt接口WriterAt接口封装了基本的WriteAt方法
```go
type WriterAt interface {
WriteAt(p []byte, off int64) (n int, err error)
}
// WriteAt将p中的数据写入到对象数据流的off出
// 忽略数据的读写指针从数据的起始位置偏移off处开始写入
// 返回写入的字节数和写入时遇到的错误
// 如果n < len(p),则必须返回一个err值来说明
// 为什么没有将p完全写入
```
### 文件写出
file类时在os包中的封装了底层的文件描述符和相关信息同时封装了Read和Write的实现
```go
func (f *File) Write(b []byte) (n int, err error)
// Write向文件中写入len(b)字节数据。它返回写入的字节数和可能遇到的任何错误。如果返回值n != len(b),本方法会返回一个非nil的错误
func (f *File) WriteString(s string) (ret int, err error)
// WriteString类似Write但接受一个字符串参数
func (f *File) WriteAt(b []byte, off int64) (n int, err error)
// WriteAt在指定的位置相对于文件开始位置写入len(b)字节数据。它返回写入的字节数和可能遇到的任何错误。如果返回值n!=len(b),本方法会返回一个非nil的错误
func (f *File) Seek(offset int64, whence int) (ret int64, err error)
// Seek设置下一次读/写的位置。offset为相对偏移量。而whence决定相对位置0为相对文件开头1为相对当前位置2位相对文件结尾。它返回新的偏移量相对开头和可能的错误
func (f *File) Sync()(err error)
// Sync递交文件的当前内容进行稳定的存储一般来说这表示将文件系统的最近写入的数据在内存中的拷贝刷新到硬盘中稳定保存
```

View File

@ -0,0 +1,86 @@
---
layout: post
title: "Golang实现文件复制"
date: 2020-05-20 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
---
### 文件复制
在io包中主要是操作流的一些方法今天主要学习一下。
它的原理都是通过程序,从源文件读取文件中的数据,在写出到目标文件里
![](/img/in-post/2020-05-20-golang-file-copy-01.png)
#### io包下的Read()和Write()方法实现
通过io包下的Read()和Write()方法,边读边写,就能实现复制。这个方法是按块读取文件,块的大小也会影响到程序的性能
```go
func File1(srcFile, destFile string) (int, error) {
file1, err := os.Open(srcFile)
if err != nil {
return 0, err
}
file2, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE, os.Modeperm)
if err != nil {
return 0, err
}
defer file1.Close()
defer file2.Close()
// 拷贝数据
bs := make([]byte, 1024, 1024)
n := -1
total := 0
for {
n, err = file1.Read(bs)
if err == io.EOF || n == 0 {
fmt.Println("拷贝完毕。。")
} else if err != nil {
fmt.Println("报错了。。")
}
total += n
file2.Write(bs[:n])
}
return total, nil
}
```
#### io包下的copy()方法实现
```go
func File2(srcFile, destFile string) (int64, error) {
file1, err := os.Open(srcFile)
if err != nil {
return 0, err
}
file2, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
return 0, err
}
defer file1.Close()
defer file2.Close()
return io.Copy(file2, file1)
}
```
#### ioutil包中的WriteFile()和ReadFile()实现
但由于使用一次性读取文件,再一次性写入文件的方式,所以该方法不适用于大文件,容易内存溢出。
```go
func File3(srcFile, destFile string) (int, error) {
input, err := ioutil.ReadFile(srcFile)
if err != nil {
fmt.Println(err)
return 0, err
}
err = ioutil.WriteFile(destFile, input, 0644)
if err != nil {
fmt.Println("操作失败:", destFile)
fmt.Println(err)
return 0, err
}
return len(input), nil
}
```

View File

@ -0,0 +1,75 @@
---
layout: post
title: "Golang实现断点续传"
date: 2020-05-21 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
---
### 断点续传
#### Seeker接口
```go
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
```
第一个参数:偏移量,
第二个参数:
0seekStart表示相对于文件开始
1seekCurrent表示相对于当前偏移量
2seekend表示相对于文件结尾
```go
const (
SeekStart = 0
SeekCurrent = 1
SeekEnd = 2
)
```
#### 示例
```go
func main() {
srcFile := "/Users/ruby/Documents/pro/a/guliang.jpeg"
destFile := "guliang4.jpeg"
tempFile := destFile + "temp.txt"
file1, _ := os.Open(srcFile)
file2, _ := os.OpenFile(destFile, os.O_CREATE|os.O_WRONLY, os.ModePerm)
file3, _ := os.OpenFile(tempFile, os.O_CREATE|os.O_RDWR, os.ModePerm)
defer file1.Close()
defer file2.Close()
file3.Seek(0, io.SeekStart)
bs := make([]byte, 100, 100)
n1, err := file3.Read(bs)
fmt.Println(n1)
countStr := string(bs[:n1])
fmt.Println(countStr)
count, _ := strconv.ParseInt(countStr, 10, 64)
fmt.Println(count)
// 设置读,写的偏移量
file1.Seek(count, 0)
file2.Seek(count, 0)
data := make([]byte, 1024, 1024)
n2 := -1
n3 := -1
total := int(count)
for{
n2, err = file1.Read(data)
if err == io.EOF {
fmt.Println("文件复制完毕...")
file3.Close()
os.Remove(tempFile)
break
}
// 将数据写入到目标文件
n3, _ = file2.Write(data[:n2])
total += n3
file3.Seek(0, io.SeekStart)
file3.WriteString(strconv.Itoa(total))
}
}
```

View File

@ -0,0 +1,165 @@
---
layout: post
title: "Golangbufio包"
date: 2020-05-22 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
- 包
---
### bufio包
这个包可以大幅提高文件读写的效率。
#### bufio包原理
bufio是通过缓冲来提高效率
io操作本身的效率并不低低的是频繁的访问本地磁盘的文件。所以bufio就提供了缓冲区(分配一块内存),读和写都先在缓冲区中,最后再读写文件,来降低访问本地磁盘的次数,从而提高效率。
简单的说就是把文件读取进缓冲内存之后再读取的时候就可以避免文件系统的io 从而提高速度。同理,在进行写操作时,先把文件写入缓冲(内存),然后由缓冲写入文件系统。看完以上解释有人可能会表示困惑了,直接把 内容-件 和 内容-冲-件相比, 缓冲区好像没有起到作用嘛。其实缓冲区的设计是为了存储多次的写入,最后一口气把缓冲区内容写入文件。
![](/img/in-post/2020-05-22-golang-bufio-01.png)
#### bufio包
bufio包实现了有缓冲的I/O。它包装一个io.Reader或io.Writer接口对象创建另一个也实现了该接口且同时还提供了缓冲和一些文本I/O的帮助函数的对象。
bufio.Reader
bufio.Reader 实现了如下接口: io.Reader io.WriterTo io.ByteScanner io.RuneScanner
```go
// NewReaderSize 将 rd 封装成一个带缓存的 bufio.Reader 对象,
// 缓存大小由 size 指定(如果小于 16 则会被设置为 16
// 如果 rd 的基类型就是有足够缓存的 bufio.Reader 类型,则直接将
// rd 转换为基类型返回。
func NewReaderSize(rd io.Reader, size int) *Reader
// NewReader 相当于 NewReaderSize(rd, 4096)
func NewReader(rd io.Reader) *Reader
// Peek 返回缓存的一个切片,该切片引用缓存中前 n 个字节的数据,
// 该操作不会将数据读出,只是引用,引用的数据在下一次读取操作之
// 前是有效的。如果切片长度小于 n则返回一个错误信息说明原因。
// 如果 n 大于缓存的总大小,则返回 ErrBufferFull。
func (b *Reader) Peek(n int) ([]byte, error)
// Read 从 b 中读出数据到 p 中,返回读出的字节数和遇到的错误。
// 如果缓存不为空,则只能读出缓存中的数据,不会从底层 io.Reader
// 中提取数据,如果缓存为空,则:
// 1、len(p) 缓存大小,则跳过缓存,直接从底层 io.Reader 中读
// 出到 p 中。
// 2、len(p) < 缓存大小则先将数据从底层 io.Reader 中读取到缓存
// 中,再从缓存读取到 p 中。
func (b *Reader) Read(p []byte) (n int, err error)
// Buffered 返回缓存中未读取的数据的长度。
func (b *Reader) Buffered() int
// ReadBytes 功能同 ReadSlice只不过返回的是缓存的拷贝。
func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
// ReadString 功能同 ReadBytes只不过返回的是字符串。
func (b *Reader) ReadString(delim byte) (line string, err error)
```
bufio.Writer
bufio.Writer 实现了如下接口: io.Writer io.ReaderFrom io.ByteWriter
```go
// NewWriterSize 将 wr 封装成一个带缓存的 bufio.Writer 对象,
// 缓存大小由 size 指定(如果小于 4096 则会被设置为 4096
// 如果 wr 的基类型就是有足够缓存的 bufio.Writer 类型,则直接将
// wr 转换为基类型返回。
func NewWriterSize(wr io.Writer, size int) *Writer
// NewWriter 相当于 NewWriterSize(wr, 4096)
func NewWriter(wr io.Writer) *Writer
// WriteString 功能同 Write只不过写入的是字符串
func (b *Writer) WriteString(s string) (int, error)
// WriteRune 向 b 写入 r 的 UTF-8 编码,返回 r 的编码长度。
func (b *Writer) WriteRune(r rune) (size int, err error)
// Flush 将缓存中的数据提交到底层的 io.Writer 中
func (b *Writer) Flush() error
// Available 返回缓存中未使用的空间的长度
func (b *Writer) Available() int
// Buffered 返回缓存中未提交的数据的长度
func (b *Writer) Buffered() int
// Reset 将 b 的底层 Writer 重新指定为 w同时丢弃缓存中的所有数据复位
// 所有标记和错误信息。相当于创建了一个新的 bufio.Writer。
func (b *Writer) Reset(w io.Writer)
```
#### 读取数据示例
```go
func main() {
fileName := "/Users/ruby/Documents/pro/a/english.txt"
file, err := os.Open(fileName)
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
// 创建Reader对象
b1 := bufio.NewReader(file)
// 1. Read()高效读取
p := make([]byte, 1024)
n1, err := b1.Read(p)
fmt.Println(n1)
fmt.Println(string(p[:n1]))
// 2. ReadLine()
data, flag, err := b1.ReadLine()
fmt.Println(flag)
fmt.Println(err)
fmt.Println(data)
fmt.Println(string(data))
// 3. ReadString()
for {
s1, err := b1.ReadString('\n')
if err == io.EOF {
fmt.Println("读取完毕..")
break
}
fmt.Println(s1)
}
// 4. ReadBytes()
data, err := b1.ReadBytes('\n')
fmt.Println(err)
fmt.Println(string(data))
// Scanner
s2 := ""
fmt.Scanln(&s2)
fmt.Println(s2)
b2 := bufio.NewReader(os.Stdin)
s2, _ := b2.ReadString('\n')
fmt.Println(s2)
}
```
#### 写数据示例
```go
func main(){
fileName := "/Users/ruby/Documents/pro/a/cc.txt"
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY, os.ModePerm)
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
w1 := bufio.NewWriter(file)
for i:=1; i<=1000; i++ {
w1.WriteString(fmt.Sprintf("%d:hello", i))
}
w1.Flush()
}
```

View File

@ -0,0 +1,88 @@
---
layout: post
title: "Golangioutil包"
date: 2020-05-23 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
- 包
---
### ioutil包
除了io包可以读写数据go还提供了一个辅助的工具包就是ioutil。
```go
import "io/ioutil"
```
该包的介绍只有一句话Package ioutil implements some I/O utility functions.
#### ioutil包的方法
```go
// Discard 是一个io.Writer 接口调用它的Write方法将不做任何事情
// 并且始终成功返回
var Discard io.Writer = devNull(0)
// ReadAll读取r中的所有数据返回读取的数据和遇到的错误
// 如果读取成功则err返回nil而不是EOF因为ReadAll定义为读取
// 所有数据所以不会把EOF当作错误处理
func ReadAll(r io.Reader) ([]byte, error)
// ReadFile读取文件中的所有数据返回读取的数据和遇到的错误
// 如果读取成功则err返回nil而不是EOF
func ReadFile(filename string) ([]byte, error)
// WriteFile向文件中写入数据写入前会清空文件。
// 如果文件不存在,则会以指定的权限创建文件
// 返回遇到的错误
func WriteFile(filename string, data []byte, perm os.FileMode) error
// ReadDir 读取指定目录中的所有目录和文件(不包括子目录)
// 返回读取到文件信息列表和遇到错误,列表是经过排序的。
func ReadDir(dirname string) ([]os.FileInfo, error)
// NopCloser 将r包装成一个ReadCloser类型但Close方法不做任何事情
func NopCloser(r io.Reader) io.ReadCloser
// TempFile在dir目录中创建一个以prefix为前缀的临时文件并将其以读写模式打开。返回创建的文件对象和遇到的错误
// 如果dir为空则在默认的临时目录中创建文件参加os.TempDir)多次调用会创建不通的临时文件调用者可以通过f.Name()获取文件的完整路径
// 调用本函数所创建的临时文件,应该由调用者字节删除
func TempFile(dir, prefix string) (f *os.File, err error)
// TempDir功能通TempFile只不过创建的是目录返回目录的完整路径
func TempDir(dir, prefix string) (name string, err error)
```
#### 示例
```go
package main
import (
"io/ioutil"
"fmt"
"os"
)
func main() {
//1.读取文件中的所有数据
fileName1 := "/Users/ruby/Documents/pro/a/aa.txt"
data, err := ioutil.ReadFile(fileName1)
fmt.Println(err)
fmt.Println(string(data))
//2.写出数据
fileName2 := "/Users/ruby/Documents/pro/a/bbb.txt"
s1 := "hello world"
err := ioutil.WriteFile(fileName2, []byte(s1), 0777)
fmt.Println(err)
// 3.读取所有数据
s2 := "qwertyuiop"
r1 := strings.NewReader(s2)
data, _ := ioutil.ReadAll(r1)
fmt.Println(data)
// 4.读取一个目录下子内容:子文件和子目录,但是仅有一层
dirName := "/Users/ruby/Documents/pro/a"
fileInfos, _ := ioutil.ReadDir(dirName)
fmt.Println(len(fileInfos))
for
}
```

View File

@ -0,0 +1,37 @@
---
layout: post
title: "Golang并发性Concurrency"
date: 2020-05-24 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
---
### 并发性Concurrency
#### 多任务
指操作系统可以同时执行多个任务
#### 什么是并发
**并发性Concurrency**是同时处理许多事情的能力。
举个例子,假设一个人在晨跑。在晨跑时,他的鞋带松了。现在这个人停止跑步,系鞋带,然后又开始跑步。这是一个典型的并发性示例。这个人能够同时处理跑步和系鞋带,这是一个人能够同时处理很多事情
什么是**并行性parallelism**,它与**并发concurrency**有什么不同
同样的例子。在这种情况下,我们假设这个人正在慢跑,并且使用它的手机听音乐。在这种情况下,一个人一边慢跑一遍听音乐,那就是他同时在做很多事情,这就是所谓的**并行性parallelism**
![](/img/in-post/2020-05-24-golang-concurrency-01.png)
#### 进程、线程、协程
**进程(Process)** 是一个程序在一个数据集中的一个动态执行过程可以简单理解为“正在执行的程序”它是CPU资源分配和调度的独立单位。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成那些功能以及如何完成数据集则是程序在执行过程中所需要使用的资源进程控制块用来记录进程的外部特征描述进程的执行变化过程系统可以利用它来控制和管理进程它是系统感知进程存在的唯一标志。
**线程(Thread)** 是在进程之后发展出来的概念。线程也叫轻量级进程它是一个基本的CPU执行单元也是程序执行过程中的最小单元由线程ID程序计数器、寄存器集合和堆栈共同组成一个进程可以包含多个线程。线程的优点是减小了程序并发执行时的开销提高了操作系统的并发性能缺点是线程没有自己的系统资源只拥有在运行时必不可少的资源但同一进程的各线程可以共享进程所拥有的系统资源如果把进程比做一个车间那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制处理不当可能会造成死锁。
**协程(Coroutine,也叫轻量级线程)** 是一种用户态的轻量级线程又称微线程英文名Coroutine协程的调度完全由用户控制。人们通常将协程和子程序函数比较着理解。子程序的调用总是一个入口一次返回一旦退出即完成了子程序的执行。
**与传统的系统级线程和进程相比,协程最大的优势在于其“轻量级”,可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过一万的。这也是协程也叫轻量级线程的原因**
> 协程与多线程相比,其优势体现在:协程的执行效率极高。因为子程序切换而不是线程切换,而是由程序自身控制。因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显
**Go语言对于并发的实现是靠协程——Goroutine**

View File

@ -0,0 +1,130 @@
---
layout: post
title: "GolangGoroutine"
date: 2020-05-25 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
---
### 什么是Goroutine
Go中使用Goroutine来实现并发concurrently。
Goroutine是Go语言特有的名词。区别于进程Process线程Thread协程Coroutine因为Go语言的创造者们觉得和他们是有所区别的所以专门创造了Goroutine
Goroutine是与其他函数或方法同时运行的函数或方法。Goroutines可以被认为是轻量级的线程。与线程相比创建Goroutine的成本很小它就是一段代码一个函数入口。以及在堆上为其分配的一个堆栈初始大小为4K会随着程序的执行自动增长删除。因此它非常廉价Go应用程序可以并发运行数千个Goroutines
#### Goroutines在线程上的优势
1. 与线程相比Goroutines非常偏移。它们只是堆栈大小的几个kb堆栈可以根据应用程序的需要增长和收缩而在线程的情况下堆栈大小必须指定并且是固定的。
2. Goroutines被多路复用到较少的OS线程。在一个程序中可能只有一个线程与数千个Goroutines。如果线程中的任何Goroutine都表示等待用户输入则会创建另一个OS线程剩下的Gooutines被转移到新的OS线程。所有这些都由运行时进行处理我们作为程序员从这些复杂的细节中抽象出来并得到了一个与并发工作相关的干净的API。
3. 当使用Goroutines访问共享内存时通过设计的通道可以防止竞态条件发生。通道可以被认为是Goroutines通信的管道。
#### 主Goroutine
**封装main函数的Goroutine称为主Goroutine**
主Goroutine所做的事情并不是执行main函数那么简单。它首先要做是设定每一个goroutine所能申请的栈空间的最大尺寸。在32位的计算机系统中此最大尺寸为250MB而在64位的计算机系统中此尺寸为1GB。如果有某个goroutine的栈空间尺寸大于这个限制那么运行时系统就会引发一个栈溢出stack overflow的运行时恐慌。随后这个go程序的运行也会终止。
此后主Goroutine会进行一系列的初始化工作涉及的工作内容大致如下
1. 启动专用于在后台清扫内存垃圾的Goroutine并设置GC可用的标识。
2. 执行main包中的init函数。
3. 执行main函数
执行完main函数后它还会检查主goroutine是否引发了运行时恐慌并进行必要的处理。最后主Goroutine会结束自己以及当前进程的运行。
#### 如何使用Goroutines
在函数或方法调用前面加上关键字go您将会同时运行一个新的Goroutine。
示例:
```go
package main
import (
"fmt"
)
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
fmt.Println("main function")
}
```
运行结果可能会值输出“main function”。
**Goroutine的规则**
1. 当新的Goroutine开始时Goroutine调用立即返回。与函数不同Go不等待Goroutine执行结束。当Goroutine调用并且Goroutine的任何返回值被忽略之后go立即执行到下一行代码
2. main的Goroutine应该为其他的Goroutine执行。如果main的Goroutine终止了程序将被终止而其他Goroutine将不会运行
修改以上代码:
```go
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
time.Sleep(1 * time.Second)
fmt.Println("main function")
}
```
运行结果:得到两行输出
在上面的程序中我们调用了时间包的Sleep方法它会在执行过程中睡觉。在这种情况下main的Goroutine被用来睡觉1秒。现在调用go hello()有足够的时间在main Goroutine终止之前执行。这个程序首先打印Hello world goroutine 等待1秒然后打印main函数。
#### 启动多个Goroutines
示例:
```go
package main
import (
"fmt"
"time"
)
func numbers() {
for i := 1; i <= 5; i++ {
time.Sleep(250 * time.Millisecond)
fmt.Printf("%d", i)
}
}
func alphabets() {
for i := 'a'; i <= 'e'; i++ {
time.Sleep(400 * time.Millisecond)
fmt.Printf("%c", i)
}
}
func main() {
go numbers()
go alphabets()
time.Sleep(3000 * time.Millisecond)
fmt.Println("main terminated")
}
```
运行结果:
`1 a 2 3 b 4 c 5 d e main terminated`
时间轴分析;
![](/img/in-post/2020-05-25-golang-goroutine-01.png)

View File

@ -0,0 +1,139 @@
---
layout: post
title: "Golang并发模型"
date: 2020-05-27 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
---
Go语言相比Java等一个很大的优势就是可以方便地编写并发程序。Go语言内置了goroutine机制使用goroutine可用快速的开发并发程序更好的利用多核处理器资源。
### 线程模型
在现代操作系统中,线程数处理器调度和分配的基本单位,进程则作为资源拥有的基本单位。每个进程是由私用的虚拟地址空间、代码、数据和其他各种系统资源组成。线程是进程内部的一个执行单元。每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其他线程,多个线程并发地运行于同一个进程中。
无论语言层面何种并发模型到了操作系统层面一定是以线程的形态存在的。而操作系统根据资源访问权限的不同体系架构可分为用户空间和内核空间内核空间主要操作访问CPU资源、I/O资源、内存资源等硬件资源为上层应用程序提供最基本的基础资源用户空间呢就是上层应用程序的固定活动空间用户空间不可以直接访问资源必须通过“系统调用”“库函数”或“shell脚本”来调用内核空间提供的资源。
我们现在的计算机语言可以狭义的认为是一种“软件”它们中所谓的“线程”往往是用户态的线程和操作系统本身内核态的线程简称KSE还是有区别的。
线程可以视为进程中的控制流。一个进程至少会包含一个线程因为其中至少会有一个控制流持续运行。因而一个进程的第一个线程会随着这个进程的启动而创建这个线程称为该进程的主线程。当然一个进程也可以包含多个线程。这些线程都是由当前进程中已存在的线程创建出来的创建的方法就是调用系统调用更确切的说是调用phtread create函数。拥有多个线程的进程可以并发执行多个任务并且即使某个或某些任务被阻塞也不会影响其他任务正常执行这可以大大改善程序的响应时间和吞吐量。另一方面线程不可能独立于进程存在。它的生命周期不可能逾越其所属进程的生命周期。
线程的实现模型主要有3个分别是用户级线程模型内核级线程模型和两级线程模型。它们之间最大的差异就在于线程与内核调度实体Kernel Scheduling Entity简称KSE之间的对应关系上。顾名思义内核调度实体就是可以被内核的调度器调度的对象。在很多文献和书中它也成为内核级线程是操作系统内核的最小调度单元。
#### 内核级线程模型
用户线程与KSE是1对1关系1:1。大部分编程语言的线程库比如linux的pthread Java的java.lang.Thread, C++11的std::thread等等都是对操作系统的线程内核级线程的一层封装创建出来的每个线程与一个不同的KSE静态关联因此其调度完全由OS调度器来做。这种实现简单直接借助OS提供的线程能力并且不同用户线程直接一般也不会相互影响。但其创建销毁以及多个线程之间的上下文切换等操作都是直接由OS层面亲自来做在需要使用大量线程的场景下对OS的性能影响会很大。
![](/img/in-post/2020-05-27-golang-g-p-m-01.jpeg)
每个线程由内核调度器独立的调度,所以如果一个线程阻塞则不影响其他的线程。
优点:在多核处理器的硬件的支持下,内核空间线程模型支持了真正的并行,当一个线程被阻塞后,允许另一个线程继续执行,所以并发能力较强
缺点:每创建一个用户级线程都需要创建一个内核级线程与其对应,这样创建线程的开销比较大,会影响到应用程序的性能。
#### 用户级线程模型
用户线程与KSE是多对1关系M:1),这种线程的创建销毁以及多个线程之间的协调等操作都是用户自己实现的线程库来负责对OS内核透明一个进程中所有创建的线程都与同一个KSE在运行时动态关联。现在有许多语言实现的协程基本都属于这种方式。这种实现方式相比内核级线程可以做的很轻量级对系统资源的消耗会小很多因此可以创建的数量与上下文切换所花费的代价也会小得多。但该模型有个致命的缺点如果我们在某个用户线程上调用阻塞式系统调用如用阻塞方式read网络IO那么一旦KSE因阻塞被内核调度出CPU的话剩下的所有对应的用户线程全部都会变成阻塞状态整个进程挂起。所以这些语言的**协程库**会把自己的一些阻塞的操作重新封装为完全的非阻塞形式然后在以前要阻塞的点上主动让出自己并通过某种方式通知或唤醒其他待执行的用户线程在该KSE上运行从而避免了内核调度器由于KSE阻塞而做上下文切换这样整个进程也不会阻塞了。
![](/img/in-post/2020-05-27-golang-g-p-m-02.jpeg)
优点这种模型的好处是线程上下文切换都发生在用户空间避免的模态切换mode switch从而对于性能有积极的影响。
缺点所有的线程基于一个内核调度实体即内核线程这意味着只有一个处理器可以被利用在多处理器环境下这是不能够被接受的本质上用户线程只解决了并发问题但是没有解决并行问题。如果线程因为I/O操作陷入了内核态。内核态线程阻塞等待I/O数据则所有的用户级线程都将会被阻塞用户空间也可以使用非阻塞的I/O但是不能避免性能及复杂度问题。
#### 两级线程模型
用户线程与KSE是多对多关系MN这种实现综合了前两种模型的优点为一个进程中创建多个KSE并且线程可以与不同的KSE在运行时进行动态关联当某个KSE由于其上工作的线程的阻塞操作被内核调度出CPU时当前与其关联的其余用户线程可以重新与其他KSE建立关联关系。当然这种动态关联机制的实现很复杂也需要用户自己去实现这算是它的一个缺点。Go语言中的并发就是使用的这种实现方式Go为了实现该模型自己实现了一个运行时调度器来负责Go中的“线程”与KSE的动态关联。此模型有时也被称为**混合型线程模型**,即**用户调度器实现用户线程到KSE的“调度”内核调度器实现KSE到CPU上的调度**
![](/img/in-post/2020-05-27-golang-g-p-m-03.jpeg)
### Go并发调度G-P-M模型
在操作系统提供的内核线程上Go搭建了一个特用的两级线程模型。Goroutine机制实现了M:N的线程模型Goroutine机制是协程Coroutine的一种实现golang内置的调度器可以让多核CPU中每个CPU执行一个协程。
#### 调度器是如何工作的
开始真正的介绍Go的并发机制了先用一段代码展示一下在Go语言中新建一个“线程”Go语言中称为Goroutine的样子
```go
// 用go关键字加上一个函数这里使用匿名函数
// 调用就做到了在一个新的“线程”并发执行任务
go func() {
// do something
}()
```
功能上等价于Java8的代码
```java
new java.lang.Thread(() -> {
// do something
}).start();
```
理解Goroutine机制的原理关键是理解Go语言scheduler的实现
Go语言中职称整个scheduler实现的主要有4个重要结构分别是M、G、P、Sched前三个定义在runtime.h中Sched定义在proc.c中
- Sched结构就是调度器它维护有存储M和G的队列以及调度器的一些状态信息等。
- M结构是Machine系统线程它由操作系统管理的goroutine就是跑在M之上的M是一个很大的结构里面维护小对象内存cachemcache、当前执行的goroutine、随机数发生器等等非常多的信息。
- P机构是Processor处理器它的主要用途就是用来执行goroutine的它维护了一个goroutine队列即runquene。Processor是让我们从N:1调度到M:N调度到重要部分。
- G是Goroutine实现的核心结构它包含了栈指令指针以及其他对调度Goroutine很重要的信息例如其阻塞的channel
> Processor的数量是在启动时被设置为环境变量GOMAXPROCS的值或者通过运行时调用GOMAXPROCS()进行设置。Processor数量固定以为着任意时刻只有GOMAXPROCS个线程在运行go代码
我们分别用三角形矩形和圆形表示Machine、Processor和Goroutine
![](/img/in-post/2020-05-27-golang-g-p-m-04.jpeg)
在单核处理器的场景下所有Goroutine运行在同一个M系统线程中一个M系统线程维护一个Processor任何时刻一个Processor中只有一个Goroutine其他goroutine在runqueue中等待。一个goroutine运行完自己的时间片后让出上下文回到runqueue中。多核处理器的场景下为了运行goroutine每个M系统线程会持有一个Processor。
![](/img/in-post/2020-05-27-golang-g-p-m-05.jpeg)
#### 线程阻塞
当正在运行的goroutine阻塞的时候例如进行系统调用会再创建一个系统线程M1当前的M线程放弃了它的ProcessorP转到新的线程中去运行。
![](/img/in-post/2020-05-27-golang-g-p-m-06.jpeg)
#### runqueue执行完成
当其中一个Processor的runqueue为空没有goroutine可以调度。它会从另一个上下文偷取一半的goroutine
![](/img/in-post/2020-05-27-golang-g-p-m-07.jpeg)
> 图中的G、P和M都是Go语言运行时系统其中包括内存分配器并发调度器垃圾收集器等组件可以想象为Java中的JVM抽象出来概念和数据结构对象GGoroutine的简称上面用go关键字加函数调用的代码就是创建了一个G对象是对一个要并发执行的任务的封装也可以称作用户态线程。属于用户级资源对OS透明具备轻量级可以大量创建上下文切换成本低等特点。MMachine的简称在linux平台上是用clone系统调用创建的其与用linux pthread库创建出来的线程本质上是一样的都是利用系统调用创建出来OS线程实体。M的作用就是执行G中包装的并发任务。**Go运行时系统中的调度器的主要职责就是将G公平合理的安排到多个M上去执行**。其属于OS资源可创建的数量上也受限于OS。通常情况下G的数量都多于活跃的M。P: Processor的简称逻辑处理器主要作用是管理G对象每个P都有一个G队列并为G在M上的运行提供本地化资源。
从两级线程模型来看似乎不需要P的参与有G和M就可以了。为什么要加入P
---
其实Go语言运行时系统早期(Go1.0)的实现中并没有P的概念Go中的调度器直接将G分配到合适的M上运行。
但这样带来了很多问题。例如不同的G在不同的M上并发运行时可能都需向系统申请资源如堆内存由于资源是全局的将会由于资源竞争造成很多系统性能损耗为了解决类似的问题后面的GoGo1.1运行时系统加入了P让P去管理G对象M要想运行G必须先与一个P绑定然后才能运行该P管理的G。
---
这样带来的好处是我们可以在P对象中预先申请一些系统资源本地资源G需要的时候先向自己的本地P申请无需锁保护如果不够用或没有再向全局申请而且从全局拿的时候会多拿一部分以供后面高效的使用。就像现在我们去政府办事情一样先去本地政府看能搞定不如果搞不定再去中央从而提供办事效率。 而且由于P解耦了G和M对象这样即使M由于被其上正在运行的G阻塞住其余与该M关联的G也可以随着P一起迁移到别的活跃的M上继续运行从而让G总能及时找到M并运行自己从而提高系统的并发能力。
Go运行时系统通过构造G-P-M对象模型实现了一套用户态的并发调度系统可以自己管理和调度自己的并发任务所以可以说Go语言原生支持并发。自己实现的调度器负责将并发任务分配到不同的内核线程上运行然后内核调度器接管内核线程在CPU上的执行与调度。
可以看到Go的并发用起来非常简单用了一个语法糖将内部复杂的实现结结实实的包装了起来。其内部可以用下面这张图来概述
![](/img/in-post/2020-05-27-golang-g-p-m-08.png)
#### 调度示例
```go
func task1() {
go task2()
go task3()
}
```
假如我们有一个G(Goroutine1)以及通过P被安排到了一个M上正在执行在Goroutine1执行的过程中我们又创建了两个G这两个G会被马上放入与Goroutine1相同的本地G任务队列中排队等待与该P绑定的M执行这是最基本的结构。
关键问题是:
1. 如何在一个多核心系统上尽量合理分配G到多个M上运行充分利用多核提高并发能力
> 如果我们在一个Goroutine中通过go关键字创建了大量G这些G虽然暂时会被放在同一个队列但如果这时还有空闲P系统内P的数量默认等于系统cpu核心数Go运行时系统始终能保证至少有一个通常也只有一个活跃的M与空闲P绑定去各种G队列去寻找可运行的G任务该种M称为**自旋的M**。一般寻找顺序为自己绑定的P的队列全局队列然后其他P的队列。如果自己P队列找到就拿出来开始运行否则去全局队列看看由于全局队列需要锁保护如果里面有很多任务会转移一批到本地P队列中避免每次都去竞争锁。如果全局队列还是没有就要开始玩狠的了直接从其他P队列偷任务了偷一半任务回来。这样就保证了在还有可运行的G任务的情况下总有与CPU核心数相等的M+P组合 在执行G任务或在执行G的路上(寻找G任务)
2. 如果某个M在执行G的过程中被G中的系统调用阻塞了怎么办
> 在这种情况下这个M将会被内核调度器调度出CPU并处于阻塞状态与该M关联的其他G就没有办法继续执行了但Go运行时系统的一个监控线程(sysmon线程)能探测到这样的M并把与该M绑定的P剥离寻找其他空闲或新建M接管该P然后继续运行其中的G大致过程如下图所示。然后等到该M从阻塞状态恢复需要重新找一个空闲P来继续执行原来的G如果这时系统正好没有空闲的P就把原来的G放到全局队列当中等待其他M+P组合发掘并执行。
3. 如果某一个G在M运行时间过长有没有办法做抢占式调度让该M上的其他G获得一定的运行时间以保证调度系统的公平性
> 我们知道linux的内核调度器主要是基于时间片和优先级做调度的。对于相同优先级的线程内核调度器会尽量保证每个线程都能获得一定的执行时间。为了防止有些线程"饿死"的情况内核调度器会发起抢占式调度将长期运行的线程中断并让出CPU资源让其他线程获得执行机会。当然在Go的运行时调度器中也有类似的抢占机制但并不能保证抢占能成功因为Go运行时系统并没有内核调度器的中断能力它只能通过向运行时间过长的G中设置抢占flag的方法温柔的让运行的G自己主动让出M的执行权。 说到这里就不得不提一下Goroutine在运行过程中可以动态扩展自己线程栈的能力可以从初始的2KB大小扩展到最大1G64bit系统上因此在每次调用函数之前需要先计算该函数调用需要的栈空间大小然后按需扩展超过最大值将导致运行时异常。Go抢占式调度的机制就是利用在判断要不要扩栈的时候顺便查看以下自己的抢占flag决定是否继续执行还是让出自己。 运行时系统的监控线程会计时并设置抢占flag到运行时间过长的G然后G在有函数调用的时候会检查该抢占flag如果已设置就将自己放入全局队列这样该M上关联的其他G就有机会执行了。但如果正在执行的G是个很耗时的操作且没有任何函数调用(如只是for循环中的计算操作)即使抢占flag已经被设置该G还是将一直霸占着当前M直到执行完自己的任务。

View File

@ -0,0 +1,97 @@
---
layout: post
title: "Golangruntime包"
date: 2020-05-29 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
- 包
---
官网文档对runtime包的介绍
> Package runtime contains operations that interact with Go's runtime system, such as functions to control goroutines.Is also includes the low-level type information used by the reflect package; see reflect's documentation for the programmable interface to the run-time type system
> 尽管Go编译器产生的是本地可执行代码这些代码仍旧运行在Go的runtime这部分的代码可以在runtime包中找到当中。这个runtime类似Java和.NET语言所用到的虚拟机它负责管理包括内存分配、垃圾回收、栈处理、Goroutine、channel、切片slice、map和反射reflection等等
### 常用函数
**runtime**调度器是个非常有用的东西,关于**runtime**包几个方法:
- NumCPU: 返回当前系统的**CPU**核数
- GOMAXPROCS: 设置最大的可同时使用**CPU**核数
> 通过runtime.GOMAXPROCS函数应用程序可以在运行期间设置运行时系统中的P最大数量。但这会引起“Stop the World”。所以应该在应用程序最早调用。并且最好是在运行Go程序之前设置好操作程序的环境变量GOMAXPROCS而不是在程序中调用runtime.GOMAXPROCS函数。
> 无论我们传递给函数的整数值是什么值运行时系统P的最大值总会在1256之间。
go1.8后默认让程序运行在多个核上可以不用设置但是在go1.8之前需要设置为了更高效的利益cpu
- Gosched让当前线程让出**CPU**以让其他线程运行,它不会挂起当前线程,因此当前线程未来会继续执行
- Goexit: 退出当前**Goroutine**(但是**defer**语句会照常执行)
- NumGoroutine: 返回正在执行核排队的任务总数
> runtime.NumGoroutine函数在被调用后会返回系统中的处于特定状态的Goroutine的数量。这里特指的是指Grunnable\Grunning\Gsyscall\Gwaition。处于这些状态的Goroutine即被看作是活跃的活着说正在被调度的。
> 注意垃圾回收所在Goroutine的状态也处于这个范围内的话也会被纳入该计数器
- GOOS: 目标操作系统,很多时候我们会根据平台的不同实现不同操作,就可以使用它
- runtime.GC: 会让运行时系统进行一次强制性的垃圾收集
1. 强制的垃圾回收:不管怎样,都要进行的垃圾回收
2. 非强制的垃圾回收:只会在一定条件下进行的垃圾回收(即运行时,系统自上次垃圾回收一周新申请的堆内存的担忧(也称为单元增量)达到指定的数值)。
- GOROOT: 获取goroot目录
### 示例
#### 获取GOROOT和OS
```go
// 获取GOROOT目录
fmt.Println("GOROOT-->", runtime.GOROOT())
// 获取操作系统
fmt.Println("os.platform-->", runtime.GOOS)
```
#### 获取CPU数量和设置CPU数量
```go
func init() {
// 1. 获取逻辑cpu的数量
fmt.Println("逻辑CPU的核数", runtime.NumCPU())
// 2. 设置go程序执行的最大cpu数
n := runtime.GOMAXPROCS(runtime.NumCPU())
fmt.Println(n)
}
```
#### 让出时间片,先让别的协议执行
```go
func main() {
go func() {
for i := 0; i < 5; i++ {
fmt.Println("goroutine...")
}
}()
for i := 0; i < 4; i++ {
//让出时间片,先让别的协议执行,它执行完,再回来执行此协程
runtime.Gosched()
fmt.Println("main..")
}
}
```
#### Goexit的使用
```go
func main() {
// 创建新建的协程
go func() {
fmt.Println("goroutine开始。。。")
// 调用了别的函数
fun()
fmt.Println("goroutine结束。。。")
}()
}
func fun() {
defer fmt.Println("defer..")
runtime.Goexit() //终止所在的协程
fmt.Println("fun函数")
}
```

View File

@ -0,0 +1,140 @@
---
layout: post
title: "Golang临界资源安全问题"
date: 2020-05-30 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
- 代码安全
---
### 临界资源
指并发环境中多个进程/线程/协程贡献的资源。
但是在并发编程中对临界资源的处理不当,往往会导致数据不一致的问题。
#### 示例代码
```go
package main
import (
"fmt"
"time"
)
func main() {
a := 1
go func() {
a = 2
fmt.Println("子goroutine。。", a)
}()
a = 3
time.Sleep(1)
fmt.Println("main goroutine..", a)
}
```
### 临界资源安全问题
并发本身并不复杂,但是因为有了资源竞争的问题,就使我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。
如果多个goroutine在访问同一个数据资源的时候其中一个线程修改了数据那么这个数值就被修改了对于其他的goroutine来讲这个数值可能是错误的。
举个例子。我们通过并发来实现火车站售票这个功能。一共有100张票4个售票口同时出售
#### 示例代码
```go
package main
import (
"fmt"
"math/rand"
"time"
)
// 全局变量
var ticket = 100
func main() {
// 4个goroutine模拟4个售票口4个子程序操作同一个共享数据。
go saleTickets("售票口1") // g1, 100
go saleTickets("售票口2") // g2, 100
go saleTickets("售票口3") // g3, 100
go saleTickets("售票口4") // g4, 100
time.Sleep(5*time.Second)
}
func saleTickets(name string) {
rand.Seed(time.Now().UnixNano())
for {
if ticket > 0 {
// 睡眠
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
// g1, g3, g2, g4
fmt.Println(name, "售出:", ticket)
ticket--
} else {
fmt.Println(name, "售罄,没有票了。。")
break
}
}
}
```
为了更好的观察临界资源问题每个goroutine先睡眠一个随机数然后再售票发现程序的运行结果还可以卖出编号为负数的票。
#### 分析
我们买票逻辑是先判断票数的编号是否为负数如果大于0然后我们就进行卖票只不过在卖票先睡眠然后再卖假如说此时已经卖票到只剩一张票了某一个goroutine持有了CPU的时间片那么它在判断是否有票的时候条件是成立的所以它可以卖票编号为1的最后一张票。但是因为它在卖之前先睡眠了那么其他的goroutine就会持有CPU的时间片而此时这张票还没有被卖出那么第二个gotoutine再判断是否有票的时候条件也是成立的那么它也可以卖出这张票然后它也进入了睡眠。其他的第三个第四个goroutine都是这样的逻辑当某个goroutine醒来的时候不会再判断是否有票而是直接售出了这样就卖出最后一张票了然而其他的goroutine醒来的时候就会陆续卖出第0张-1张-2张。
这就是临界资源的不安全问题。某一个goroutine在访问某个数据资源的时候按照数值已经判断好了条件然后又被其他的goroutine抢占了资源并修改了数值等这个goroutine再继续访问这个数据的时候数值已经不对了。
### 临界资源安全问题的解决
要想解决临界资源安全的问题很多编程语言的解决方案都是同步。通过上锁的方式某一个时间段只能允许一个goroutine来访问这个共享数据当前goroutine访问完毕解锁后其他的goroutine才能访问。
#### 借助sync包下的锁操作
```go
package main
import (
"fmt"
"math/rand"
"time"
"sync"
)
var ticket = 10
var wg sync.WaitGroup
var matex sync.Mutex // 创建锁头
func main() {
wg.Add(4)
go saleTickets("售票口1")
go saleTickets("售票口2")
go saleTickets("售票口3")
go saleTickets("售票口4")
wg.Wait()
}
func saleTickets(name string) {
rand.Seed(time.Now().UnixNano())
defer wg.Done()
for {
matex.Lock()
if ticket > 0 {
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
fmt.Println(name, "售出:", ticket)
ticket--
} else {
matex.Unlock()
fmt.Println(name, "售罄,没有票了。。")
break
}
}
matex.Unlock()
}
```
### 写在最后
> 不要以共享内存的方式去通信,而要以通信的方式去共享内存。
在Go语言中并不鼓励用锁保护共享状态的方式在不同的Goroutine中分享信息以共享内存的方式去通信。而是鼓励通过**channel**将共享状态或共享状态的变化在各个Goroutine直接传递以通信的方式去共享内存这样同样能像用锁一样保证在同一个时间只有一个Goroutine访问共享状态
当然在主流的编程语言中未来保证多线程直接共享数据安全性和一致性都会提供一套基本的同步工具集如锁条件变量原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制使用方式也和其他语言也差不多。

View File

@ -0,0 +1,264 @@
---
layout: post
title: "Golang: sync包"
date: 2020-05-31 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
- 包
---
官网文档对sync包的介绍
> Package sync provides basic synchronization primitives such as mutual exclusion locks. Other than the Once and WaitGroup types, most are intended for use by low-level library routines. Higher-level synchronization is better done via channels and communication.
> sync是synchronization同步这个词的缩写所以也会叫同步包。这里提供了基本同步草错比如互斥锁等等。这里除了Once和WaitGroup类型之外大多数类型都是供低级库例程使用的。更高级别的同步最好通过channer通道和communication通信来完成。
### WaitGroup
同步等待组
在类型上它是一个结构体。一个WaitGroup的用途是等待一个goroutine的集合执行完成。主goroutine调用了Add()方法来设置要等待的goroutine数量。然后每个goroutine都会执行并且执行完成后调用Done()这个方法。于此同时。可以使用Wait()方法来阻塞直到所有的goroutine都执行完成。
#### Add()方法
Add()这个方法用来设置到WaitGroup的计数器的值。我可以理解为每个waitgroup中都有一个计数器用来表示这个同步等待组中要执行的goroutine的数量。
如果计数器的数值变为0那么就表示等待时被阻塞的goroutine都被释放如果计数器的数值为负数那么就会引发恐慌程序报错了。
#### Done()方法
Done()方法就是当WaitGroup同步等待组中的某个goroutine执行完毕后设置这个WaitGroup的counter数值减1
其实Done()的底层代码就是调用了Add()方法
```go
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
```
#### Wait()方法
Wait()方法表示让当前的goroutine等待进入阻塞状态。一直到WaitGroup的计数器为0才解除阻塞这个goroutine才继续执行。
#### 示例
我们创建并启动两个goroutine来打印字母和数字并在main goroutine中将这两个子goroutine假如到一个WaitGroup中同时让main goroutine进入Wait(),让两个子goroutine先执行。当每个子goroutine执行完毕后调用Done()方法设置WaitGroup的counter减1。当两条子goroutine都执行完毕后WaitGroup中的counter的数值为零解除main goroutine的阻塞。
```go
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
wg.Add(2)
go fun1()
go fun2()
fmt.Println("main进入阻塞状态。。。等待wg中的子goroutine结束。")
wg.Wait()
fmt.Println("main解除阻塞。。")
}
func fun1() {
for i:=1;i<=10;i++{
fmt.Println("fun1.。。i:",i)
}
wg.Done() //给wg等待中的执行的goroutine数量减1.同Add(-1)
}
func fun2() {
defer wg.Done()
for j:=1;j<=10;j++{
fmt.Println("\tfun2..j,",j)
}
}
```
### Mutex(互斥锁)
锁就是某个协程(线程)在访问某个资源时先锁住,防止其他协程的访问,等访问完毕解锁后其他协程再来加锁进行访问。一般用于处理并发中的临界资源问题。
Go语言中的sync包提供了两种锁类型sync.Mutex 和 sync.RWMutex
Mutex锁最简单的一种锁类型互斥锁同时也比较暴力当一个goroutine获得了Mutex后其他goroutine就只能乖乖等到这个goroutine释放该Mutex
每个资源都对应一个可以称为“互斥锁”的标记,这个标记用来保证在任意时刻,只能有一个协程(线程)访问该资源。其他协程只能等待。
互斥锁是传统并发编程对共享资源进行访问控制的主要手段它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的执政方法Lock()和Unlock()。Lock锁定当前的共享资源Unlock进行解锁。
在使用互斥锁时一定要注意对资源操作完成后一定要解锁否则会出现流程执行异常死锁等问题。通常借助defer。锁定后立即使用defer语句保证互斥锁及时解开。
#### 部分源码
```go
// A Mutex is mutual exclusion lock
// The zero value for a Mutex is an unlocked mutex.
// A Mutex must not be copied after first use.
type Mutex struct {
state int32 // 互斥锁上锁状态枚举值如下所示
sema uint32 // 信号量向处于Gwaitting的G发送信号
}
// A Locker represents an object that can be locked and unlocked
type Locker interface {
Lock()
Unlock()
}
const (
mutexLocked = 1 << iota // mutex is locked, 1.互斥锁是锁定的
mutexWoken // 2 唤醒锁
mutexStarving
mutexWaiterShift = iota // 统计阻塞在这个互斥锁上的goroutine数目需要移位的数值
starvationThresholdNs = 1e6
)
```
#### Lock()方法
Lock()这个方法锁定mutex。如果该mutex已经使用中则调用goroutine将阻塞直到互斥体可以用。
#### Unlock()方法
Unlock()方法解锁锁mutex。如果mutex未在要解锁的条目上锁定则为运行时错误。
锁定的互斥体不于特定的goroutine关联。允许一个goroutine锁定互斥体然后安排另一个goroutine解锁互斥体
#### 示例
模拟4个售票口出售火车票的案例。4个售票口同时卖票会发生临界资源数据安全问题。我们使用互斥锁解决一下。(Go语言推崇的是使用Channel来实现数据共享但是也还是提供了传统的同步处理方式)
```go
package main
import (
"fmt"
"time"
"math/rand"
"sync"
)
//全局变量,表示票
var ticket = 10 //100张票
var mutex sync.Mutex //创建锁头
var wg sync.WaitGroup //同步等待组对象
func main() {
/*
4个goroutine模拟4个售票口
在使用互斥锁的时候,对资源操作完,一定要解锁。否则会出现程序异常,死锁等问题。
defer语句
*/
wg.Add(4)
go saleTickets("售票口1")
go saleTickets("售票口2")
go saleTickets("售票口3")
go saleTickets("售票口4")
wg.Wait() //main要等待
fmt.Println("程序结束了。。。")
//time.Sleep(5*time.Second)
}
func saleTickets(name string){
rand.Seed(time.Now().UnixNano())
defer wg.Done()
for{
//上锁
mutex.Lock() //g2
if ticket > 0{ //ticket 1 g1
time.Sleep(time.Duration(rand.Intn(1000))*time.Millisecond)
fmt.Println(name,"售出:",ticket) // 1
ticket-- // 0
}else{
mutex.Unlock() //条件不满足,也要解锁
fmt.Println(name,"售罄,没有票了。。")
break
}
mutex.Unlock() //解锁
}
}
```
### RWMutex(读写锁)
Go语言包中的 sync 包提供了两种锁类型sync.Mutex 和 sync.RWMutex。其中RWMutex是基于Mutex实现的只读锁的实现使用类似引用计数器的功能。
RWMutex是读/写互斥锁。锁可以由任意数量的读取器或单个编写器持有。RWMutex的零值锁未锁定的mutex
如果一个goroutine持有一个rRWMutex进行读取而另一个goroutine可能调用lock那么在释放初始读写锁止器任何goroutine都不应该期望能够获取读写锁。特别是禁止递归读取锁定。这是为了确保锁最终可用被阻止的锁调用会将新的读卡器排除在获取锁之外。
我们怎么理解读写锁呢?当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;当有一个 goroutine 获得读锁定其它读锁定仍然可以继续当有一个或任意多个读锁定写锁定将等待所有读锁定解锁之后才能够进行写锁定。所以说这里的读锁定RLock目的其实是告诉写锁定有很多人正在读取数据你给我站一边去等它们读读解锁完你再来写写锁定。我们可以将其总结为如下三条
1. 同时只能有一个goroutine能够获得写锁定
2. 同时可以有任意多个goroutine获得读锁定
3. 同时只能存在写锁定或读锁定(读和写互斥)
所以RWMutex这个读写锁该锁可以加多个读锁或者一个写锁其经常用于读次数远远多于写次数的场景。
读写锁的写锁只能锁定一次,解锁前不能多次锁定,读锁可以多次,但读解锁次数最多只能比读锁次数多一次,一般情况下我们不建议读解锁次数多于读锁次数。
基本遵循两大原则:
1. 可以随便读多个goroutine同时读
2. 写的时候,啥也不能干。不能读也不能写。
读写锁即是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。读写锁遵循的访问控制规则和互斥锁有所不同。在读写锁管辖的范围内,它允许任意个读操作的同时进行。但是在同一时刻,它只允许有一个写操作在进行。
并且在某一个写操作被进行的过程中,读操作的进行也是不被允许的。也就是说读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间却不存在互斥关系
#### RLock()方法
读锁,当有写锁时,无法加载读锁,只有读锁活着没有锁时,可以加载读锁,读锁可以加载多个,所以适用于“读多写少”的场景。
#### RUnlock()方法
读锁解锁RUnlock撤销单次RLock调用它对于其他同时存在的读取器则没有效果。若RWMutex并没有因为读取而锁定调用RUnlock就会引发一个运行时错误。
#### Lock()方法
写锁如果在添加写锁之前已经有其他的读锁和写锁则Lock就会阻塞直到该锁可用为确保该锁最终可用已阻塞的Lock调用会从获得的锁中排除新的读取锁即写锁权限高于读锁有写锁时优先进行写锁定。
#### Unlock()方法
写锁解锁,如果没有进行写锁定,则就会引起一个运行时错误。
#### 示例
```go
package main
import (
"fmt"
"sync"
"time"
)
var rwMutex *sync.RWMutex
var wg *sync.WaitGroup
func main() {
rwMutex = new(sync.RWMutex)
wg = new(sync.WaitGroup)
wg.Add(3)
go writeData(1)
go readData(2)
go writeData(3)
wg.Wait()
fmt.Println("main..over..")
}
func writeData(i int) {
defer wg.Done()
fmt.Println(i, "开始写write start..")
rwMutex.Lock() //写操作上锁
fmt.Println(i, "正在写writing..")
rwMutex.Unlock()
fmt.Println(i, "写结束write over..")
}
func readData(i int) {
defer wg.Done()
fmt.Println(i, "开始读read start..")
rwMutex.RLock() // 读操作上锁
fmt.Println(i, "正在读reading..")
rwMutex.RUnlock()
fmt.Println(i, "读结束read over..")
}
```
总结:
1. 读锁不能阻塞读锁
2. 读锁需要阻塞写锁,直到所有读锁都释放
3. 写锁需要阻塞读锁,直到所有写锁都释放
4. 写锁需要阻塞写锁

View File

@ -0,0 +1,632 @@
---
layout: post
title: "Golang: 通道"
date: 2020-06-01 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
---
通道可以被认为是Goroutine通信的管道。类似于管道中的水从一端到另一端的流动数据可以从一端发送到另一端通过通道接收。
在前面讲Go语言的并发的时候当多个Goroutine想实现共享数据的时候虽然也提供了传统的同步机制但是Go语言强烈建议的是使用Channel通道来实现Goroutines之间的通信。
> 不要通过共享内存来通信,而应该通过通信来共享内存
Go语言中要传递某个数据给另一goroutine(协程),可以把这个数据封装程一个对象然后把这个对象的指针传入某个channel中另外一个goroutine从这个channel中读出这个指针并处理其指向的内存对象。Go从语言层面保证同一时间只有一个goroutine能够访问channel里面的数据为开发者提供了一种优雅简单的工具所以Go的做法就是使用channel来通信通过通信来传递内存数据使得内存数据在不同的goroutine中传递而不是使用共享内存来通信。
### 什么是通道
通道是什么通道就是goroutine之间的通道。它可以让goroutine之间相互通信。
每个通道都有与其相关的类型。该类型是通道允许传输的数据类型。通道的零值为nil。nil通道没有任何用处因此通道必须使用类似于map和切片的方法来定义
#### 声明通道
声明一个通道和定义一个变量的语法一样:
```go
// 声明通道
var 通道名 chan 数据类型
// 创建通道如果通道为nil(就是不存在) 就需要先创建通道
通道名 = make(chan 数据类型)
```
示例:
```go
package main
import "fmt"
func main() {
var a chan int
if a == nil {
fmt.Println("channel是nil的不能使用需要先创建通道。。")
a = make(chan int)
fmt.Printf("数据类型是:%T", a)
}
}
```
也可以简短的声明:
```go
a := make(chan int)
```
#### 通道的数据类型
channel是引用类型的数据在作为参数传递的时候传递的是内存地址
示例
```go
package main
import (
"fmt"
)
func main() {
ch1 := make(chan int)
fmt.Printf("%T, %P\n", ch1, ch1)
test1(ch1)
}
func test1(ch chan int) {
fmt.Printf("%T, %P\n", ch, ch)
}
```
输出结果
```shell
chan int, 0xc00008e000
chan int, 0xc00008e000
```
#### 通道的注意点
1. 通道是用于goroutine传递消息的
2. 通道每个都有相关联的数据类型nil chan不能使用类似于nil map, 不能之间存储键值对
3. 使用通道传递数据: <- chan <- data,发送数据到通道data <- chan 从通道中获取数据
4. 阻塞:发送数据: chan <- data阻塞的直到另一条goroutine读取数据来解除阻塞读取数据data <- chan也是阻塞的直到另一条goroutine写出数据解除阻塞
5. 本身channel就是同步的意味着同一时间只能有一条goroutine来操作
最后通道是goroutine之间的连接所以通道的发送和接收必须处在不同的goroutine中。
### 通道使用语法
#### 发送和接收
```go
data := <- a // read from channel a
a <- data // write to channel a
```
#### 发送和接收默认是阻塞的
一个通道发送和接收数据默认是阻塞的。当一个数据被发送到通道时在发送语句中被阻塞直到另一个Goroutine从该通道读取数据。相对地当从通道读取数据时读取被阻塞直到一个Goroutine将数据写入该通道。
这些通道的特性时帮助Goroutines有效地进行通信而无需像使用其他编程语言中非常常见的显式锁或条件变量。
```go
package main
import "fmt"
func main() {
var ch1 chan bool
fmt.Println(ch1)
fmt.Printf("%T\n", ch1)
ch1 = make(chan bool)
fmt.Println(ch1)
go func() {
for i:= 0; i <10; i++ {
fmt.Println("子goroutine中, i:", i)
}
// 循环结束后,向通道中写数据,表示要结束了。。
ch1 <- true
fmt.Println("结束")
}()
data := <-ch1 // 从ch1通道中读取数据
fmt.Println("data-->", data)
fmt.Println("main..Over...")
}
```
在上面的程序中我们先创建了一个chan bool通道。然后启动了一个子Goroutine并循环打印10个数字。然后我们向通道ch1中写入输入true。然后在主goroutine中我们从ch1中读取数据。这一行代码是阻塞的这意味着在子Goroutine将数据写入到该通道之前主goroutine将不会执行到下一行代码。因此我们可以通过channel实现子goroutine和主goroutine之间的通信。当子goroutine执行完毕前主goroutine会因为读取ch1中的数据而阻塞。从而保证了子goroutine会执行完毕。这就消除了对时间的需求。在之前的程序中我们要么让主goroutine进入睡眠以防止主要的Goroutine退出。要么通过WaitGroup来保证子goroutine先执行完毕主goroutine才结束。
```go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
done := make(chan bool)
go func() {
fmt.Println("子goroutine执行")
time.Sleep(3 * time.Second)
data := <-ch1
fmt.Println("data:", data)
done <- true
}()
// 向通道中写数据
time.Sleep(5 * time.Second)
ch1 <- 100
<- done
fmt.Println("main..over")
}
```
打印一个数字的每位数的平方和
```go
package main
import (
"fmt"
)
func calcSquares(number int, squareop chan int) {
sum := 0
for num != 0 {
digit := number % 10
sum += digit * digit
number /= 10
}
squareop <- sum
}
func calcCubes(number int, cubeop chan int) {
sum := 0
for number != 0 {
digit := number % 19
sum += digit * digit * digit
number /= 10
}
cubeop <- sum
}
func main() {
number := 589
sqrch := make(chan int)
cubech := make(chan int)
go calcSquares(number, sqrch)
go calcCubes(number, cubech)
squares, cubes := <-sqrch, <-cubech
fmt.Println("Final output", squares + cubes)
}
```
运行结果
`Final output 1536`
#### 死锁
使用通道时要考虑的一个重要因素是死锁。如果Goroutine在一个通道上发送数据那么预计其他的Goroutine应该接收数据。如果没有发生那么程序将会在运行的时候出现死锁。
类似地如果Goroutine正在等待从通道接收数据那么其他的Goroutine应该写入数据否则程序也会出现死锁。
> 在主流的编程语言中味了保证多线程之间共享数据安全性和一致性都会提供一套基本的同步工具集如锁条件变量原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制使用方式也和其他语言差不多。除了这些基本的同步手段Go语言还提供了一种新的同步机制Channel它在Go语言中是一个像intfloat32等的基本类型一个channel可以认为是一个能够在多个Goroutine之间传递某一类型的数据的管道。Go中的channel无论是实现机制还是使用场景都和Java中的BlockingQueue很接近。
### 关闭通道
发送者可以通过关闭信道来通知接收方不会有更多的数据被发送到channel上。
```go
close(ch)
```
接收者可以在接收来自通道的数据时使用额外的变量来检查通道是否已经关闭。
```go
v, ok := <- ch
```
> 类似map操作存储keyvalue键值对
> v,ok := map[key] //根据key从map中获取value如果key存在 v就是对应的数据如果key不存在v是默认值
在上面的语句中如果ok的值是true表示成功的从通道中读取了一个数据value。如果ok是false这意味着我们正在从一个封闭的通道读取数据。从闭通道读取的值将是通道类型的零值。
例如如果通道是一个int通道那么从封闭通道接收的值将为0。
示例:
```go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
go sendData(ch1)
for {
time.Sleep(1 * time.Second)
v, ok := <- ch1
if !ok {
fmt.Println("已经读取了所有的数据,", ok)
break
}
fmt.Println("取出数据:", v, ok)
}
fmt.Println("main..over..")
}
func sendData(ch1 chan int) {
// 发送方10条数据
for i := 0; i < 10; i++ {
ch1 <- i
}
close(ch1)
}
```
在上面的程序中。send Goroutine将0到9写入ch1通道然后关闭通道。主函数里有一个无限循环。它检查通道是否在发送数据后使用变量ok关闭。如果ok是false则意味着通道关闭因此循环结束。还可以打印接收到的值和ok的值。
### 通道上的范围循环
我们可以循环从通道上获取数据直到通道关闭。for循环的for range形式可用于从通道接收值直到它关闭为止。
示例:
```go
package main
import (
"time"
"fmt"
)
func main() {
ch1 :=make(chan int)
go sendData(ch1)
// for循环的for range形式可用于从通道接收值直到它关闭为止。
for v := range ch1{
fmt.Println("读取数据:",v)
}
fmt.Println("main..over.....")
}
func sendData(ch1 chan int) {
for i:=0;i<10 ; i++ {
time.Sleep(1*time.Second)
ch1 <- i
}
close(ch1)//通知对方,通道关闭
}
```
### 缓冲通道
之前学习的所有通道基本上都没有缓冲。发送和接收到一个未缓冲的通道是阻塞的。
一次发送操作对应一次接收操作对于一个goroutine来讲它的一次发送在另一个goroutine接收之前都是阻塞的。同样的对于接收来讲在另一个goroutine发送之前它也是阻塞的。
缓冲通道就是指一个通道,带有一个缓冲区。发送到一个缓冲通道只有在缓冲区满时才被阻塞。类似地,从缓冲通道接收的信息只有在缓冲区为空时才会被阻塞。
可以通过将额外的容量参数传递给make函数来创建缓冲通道该函数指定缓冲区的大小。
语法:
```go
ch := make(chan type, capacity)
```
上述语法的容量应该大于0以便通道具有缓冲区。默认情况下无缓冲通道的容量为0因此在之前创建通道时省略了容量参数。
示例:
```go
package main
import (
"fmt"
"strconv"
"time"
)
func main() {
ch1 := make(chan int) // 非缓存的通道
fmt.Println(len(ch1), cap(ch1)) // 0 0
ch2 := make(chan int, 5)
fmt.Println(len(ch2), cap(ch2)) // 0 5
ch2 <- 100
fmt.Println(len(ch2), cap(ch2)) // 1 5
ch3 := make(chan string, 4)
go sendData3(ch3)
for {
time.Sleep(1 * time.Second)
v, ok := <-ch3
if !ok {
fmt.Println("读完了。。", ok)
break
}
fmt.Println("\t读取的数据是", v)
}
fmt.Println("main..over..")
}
func sendData3(ch3 chan string) {
for i := 0; i < 10; i++ {
ch3 <- "数据" + strconv.Itoa(i)
fmt.Println("子goroutine写出第", i, "个数据")
}
close(ch3)
}
```
### 定向通道
通道channel是用于实现goroutine之间的通信。一个goroutine可以向通道中发送数据另一条goroutine可以从该通道中获取数据。截止到现在所接触的都是既可以发送数据也可以读取数据的这种叫双向通道。
> 单向通道,也就是定向通道
之前我们学习的通道都是双向通道,我们可以通过这些通道接收或者发送数据。我们也可以创建单向通道,这些通道只能发送或者接收数据。
双向通道,示例:
```go
package main
import "fmt"
func main() {
/*
双向:
chan T -->
chan <- data, 写数据
data <- chan, 读数据
单向:
chan <- T, 只支持写
<- chan T, 只读
*/
ch1 := make(chan string)
done := make(chan bool)
go sendData(ch1, done)
data := <- ch1 // 阻塞
fmt.Println("子goroutine传来", data)
ch1 <- "我是main" // 阻塞
<- done
fmt.Println("main..over...")
}
//子goroutine-->写数据到ch1通道中
//main goroutine-->从ch1通道中取
func sendData(ch1 chan string, done chan bool) {
ch1 <- "我是小明"// 阻塞
data := <-ch1 // 阻塞
fmt.Println("main goroutine传来",data)
done <- true
}
```
创建仅能发送数据的通道,示例代码:
```go
package main
import "fmt"
func main() {
/*
单向:
chan <- T, 只支持写
<- chan T, 只读
*/
ch1 := make(chan int)
go fun1(ch1)
data := <- ch1
fmt.Println("fun1中写出的数据是", data)
go fun2(ch1)
ch1 <- 200
fmt.Println("main...over...")
}
// 该函数接收,只写的通道
func fun1(ch chan <- int) {
// 函数内部对于ch只能写数据不能读数据
ch <- 100
fmt.Println("fun1函数结束。。")
}
// 该函数接收,只读的通道
func fun2(ch <- chan int) {
data := <- ch
fmt.Println("fun2函数,从ch中读取的数据是:",data)
}
```
### time包中的通道相关函数
> 主要是定时器标准库中的Timer让用户可以定义自己的超时逻辑。尤其是在应对select处理多个channel的超时、单channel读写的超时等情形时尤为方便。
Timer是一次性的时间触发事件而Ticker是按照一定时间间隔持续触发事件。
常见的创建方式:
```go
t := time.NewTimer(d) // 定时时间就是d
t := time.AfterFunc(d, f) // 触发动作就是f
c := time.After(d) // 时间channel也就是t.C
```
#### time.NewTimer()
NewTimer()创建一个新的计时器该计时器将在其通道上至少持续d之后发送当前时间。它的返回值是一个Timer。
源代码:
```go
// NewTimer creates a new Timer that will send
// the current time on its channel after at least duration d.
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := &Timer {
C: c,
r: runtimeTimer{
when: when(d),
f: sendTime,
arg: c,
},
}
startTimer(&t.r)
return t
}
```
通过源代码我们可以看出首先创建一个channel关联的类型为Time然后创建了一个Timer并返回。
* 用于在指定的Duration类型时间后调用函数或计算表达式
* 如果只是想指定时间后执行。使用time.Sleep()
* 使用NewTimer(),可以返回Timer类型在计时器到期之前取消该计时器。
* 直到使用<-timer.C发送一个值该计时器才会过期
```go
package main
import (
"time"
"fmt"
)
func main() {
// 新建一个计时器timer
timer := time.NewTimer(3 * time.Second)
fmt.Printf("%T\n", timer) // *time.Timer
fmt.Println(time.Now()) // 2021-06-01 10:41:21.800768 +0800 CST m=+0.000461190
// 此处在等待channel中的信号执行此段代码时会阻塞3秒
ch2 := time.C
fmt.Println(<-ch2) //2021-06-01 10:41:24.803471 +0800 CST m=+3.003225965
}
```
#### timer.Stop
timer.Stop是计时器停止
示例:
```go
package main
import (
"time"
"fmt"
)
func main() {
timer2 := time.NewTimer(5 * time.Second)
// 新开goroutine处理触发后的事件
go func() {
//等触发时的信号
<-timer2.C
fmt.Println("Timer 2 结束。。")
}()
// 由于上面的等待信号是在新线程中,所以代码会继续往下执行
time.Sleep(3 *time.Second)
stop := timer.Stop()
if stop {
fmt.Println("Timer 2 停止。。")
}
}
```
#### time.After()
在等待持续时间之后然后在返回的通道上发送当前时间。它相当于NewTimer(d).C。在计时器触发之前垃圾收集器不会恢复底层计时器。如果效率有问题使用NewTimer代替并调用Timer。如果不再需要计时器请停止。
源码:
```go
func After(d Duration) <- chan Time {
return NewTimer(d).C
}
```
示例:
```go
package main
import (
"time"
"fmt"
)
func main() {
ch1 := time.After(3 * time.Second) //3s后
fmt.Printf("%T\n", ch1) // <-chan time.Time
fmt.Println(time.Now()) //2021-06-01 09:56:41.529883 +0800 CST m=+0.000465158
time2 := <-ch1
fmt.Println(time2) //2021-06-01 09:56:44.532047 +0800 CST m=+3.002662179
}
```
### select语句
#### 语法结构
select语句的语法结构和switch语句很相似也有case语句和default语句
```go
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
```
说明:
* 每个case都必须是一个通信
* 所有channel表达式都会被求值
* 所有被发送的表达式都会被求值
* 如果有多个case都可以运行select会随机公平的选出一个执行其他不会执行
* 否则:
* 如果default子句则执行该语句
* 如果没有default子句select将堵塞直到某个通信可以运行go不会重新对channel或值进行求值
#### 示例
```go
package main
import (
"fmt"
"time"
)
func main() {
/*
分支语句ifswitchselect
select 语句类似于 switch 语句,
但是select会随机执行一个可运行的case。
如果没有case可运行它将阻塞直到有case可运行。
*/
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
time.Sleep(2 * time.Second)
ch2 <- 200
}()
go func() {
time.Sleep(2 * time.Second)
ch1 <- 100
}()
select {
case num1 := <-ch1:
fmt.Println("ch1中取数据。。", num1)
case num2, ok := <-ch2:
if ok {
fmt.Println("ch2中取数据。。", num2)
}else{
fmt.Println("ch2通道已经关闭。。")
}
}
}
```
运行结果可能执行第一个case打印100也可能执行第二个case打印200。(多运行几次,结果就不同了)
select语句结合time包的和chan相关函数示例代码
```go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
//go func() {
// ch1 <- 100
//}()
select {
case <-ch1:
fmt.Println("case1可以执行。。")
case <-ch2:
fmt.Println("case2可以执行。。")
case <-time.After(3 * time.Second):
fmt.Println("case3执行。。timeout。。")
//default:
// fmt.Println("执行了default。。")
}
}
```

View File

@ -0,0 +1,82 @@
---
layout: post
title: "Golang: CSP模型"
date: 2020-06-02 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
---
Go语言的最大两个亮点一个是goroutine一个就是channel了。二者合体的典型应用CSP基本就是大家认可的并行开发神器简化了并行程序的开发难度。
### CSP是什么
CSP是Communicating Sequential Process简称中文可以叫做通信顺序进程是一种并发编程模型是一个很强大的并发数据模型是上个世纪七十年代提出的用于描述两个独立的并发实体通过共享的通讯channel管道进行通信的并发模型。相对于Actor模型CSP中channel是第一类对象它不关注发送消息的实体而关注与发送消息时使用的channel
严格来说CSP说一门形式语言类似于calculus用于描述并发系统中的互动模式因此称为一众面向并发的编程语言的理论源头并衍生出了Occam/Limbo/Golang...
而具体到编程语言如golang其实只用到了CSP的很小部分即理论中的Process/Channel对应到语言中的goroutine/channel这两个并发原语之间没有从属关系。Process可以订阅任意个channelchannel也并不关心是哪个Process在利用它通信Process围绕Channel进行读写形成一套有序阻塞和可预测的并发模型。
### Golang CSP
与主流语言通过共享内存来进行并发控制方式不太一样Go语言采用的是CSP模式。这是一种描述两个独立的并发实体通过共享的通讯Channel管道进行通信的并发模型。
Golang就是借用CSP模型的一些概念为之实现并发进行理论支持其实从实际出发go语言并没有完全实现CSP模型的所有理论仅仅是借用了process和channel这两个概念。process是在go语言上表现就是goroutine上实际并发执行的实体每个实体之间是通过channel通讯来实现数据共享。
Go语言的CSP模型是由协程Goroutine和通道Channel实现
1. Goroutine是一种轻量线程它不是操作系统的线程而是将一个操作系统线程分段使用通过调度器实现写作式调度。是一种绿色线程微线程。
2. Channel类似Unix的Pipe用于协程之间通讯和同步协程之间虽然解耦但是它们和Channel有着耦合。
### Channel
Goroutine和Channel是Go语言并发编程的两大基石。Goroutine用于执行并发任务channel用于goroutine之间的同步和通讯。
Channel在goroutine间架起了一条管道在管道里传输数据实现gouroutine之间的通信由于它是线程安全的所以用起来非常方便channel还提供“先进先出”的特性它还能影响goroutine的阻塞和唤醒。
> Do not communicate by sharing memory; instead, share memory by communicating.
> 不要通过共享内存来通信,要逃过通信来共享内存
这就是Go的并发哲学它依赖CSP模型基于channel实现。
Channel 是 Go 语言中一个非常重要的类型,是 Go 里的第一对象。通过 channelGo 实现了通过通信来实现内存共享。Channel 是在多个 goroutine 之间传递数据和同步的重要手段。
使用原子函数、读写锁可以保证资源的共享访问安全,但使用 channel 更优雅。
channel 字面意义是 “通道”,类似于 Linux 中的管道。声明 channel 的语法如下:
```go
chan T // 声明一个双向通道
chan <- T // 声明一个只能用于发送的通道
<-chan T // 声明一个只能用于接收的通道
```
单向通道的声明用`<-`来表示它指明通道的方向你只要明白代码的书写顺序是从左到右就马上能掌握通道的方向是怎样的
因为channel是一个引用类型所以它在被初始化之前它的值是nilchannel使用make函数进行初始化。可以向它传递一个int值代表channel缓冲区的大小容量构造出来的是一个缓冲型的channel不传或传0时构造的就是一个非缓冲型的channel
两者有一定的差别非缓冲型channel无法缓冲元素对它的操作一定顺序是“发送->接收->发送->接收->...“。如果想连续向一个非缓冲chan发送2个元素并且没有接收的话第一次一定会阻塞对于缓冲型channel的操作则要”宽松“一些毕竟是带了”缓冲“光环。
对 chan 的发送和接收操作都会在编译期间转换成为底层的发送接收函数。
Channel 分为两种:带缓冲、不带缓冲。对不带缓冲的 channel 进行的操作实际上可以看作 “同步模式”,带缓冲的则称为 “异步模式”。
同步模式下,发送方和接收方要同步就绪,只有在两者都 ready 的情况下,数据才能在两者间传输(后面会看到,实际上就是内存拷贝)。否则,任意一方先行进行发送或接收操作,都会被挂起,等待另一方的出现才能被唤醒。
异步模式下,在缓冲槽可用的情况下(有剩余容量),发送和接收操作都可以顺利进行。否则,操作的一方(如写入)同样会被挂起,直到出现相反操作(如接收)才会被唤醒。
小结一下:同步模式下,必须要使发送方和接收方配对,操作才会成功,否则会被阻塞;异步模式下,缓冲槽要有剩余容量,操作才会成功,否则也会被阻塞。
简单来说CSP 模型由并发执行的实体(线程或者进程或者协程)所组成,实体之间通过发送消息进行通信, 这里发送消息时使用的就是通道,或者叫 channel。
CSP 模型的关键是关注 channel而不关注发送消息的实体。Go 语言实现了 CSP 部分理论goroutine 对应 CSP 中并发执行的实体channel 也就对应着 CSP 中的 channel。
### Goroutine调度模型——G-P-M模型
在操作系统提供的内核线程之上Go搭建了一个特有的两级线程模型。goroutine机制实现了M : N的线程模型goroutine机制是协程coroutine的一种实现golang内置的调度器可以让多核CPU中每个CPU执行一个协程。
### 最后
Golang 的 channel 将 goroutine 隔离开,并发编程的时候可以将注意力放在 channel 上。在一定程度上,这个和消息队列的解耦功能还是挺像的。如果大家感兴趣,还是来看看 channel 的源码吧,对于更深入地理解 channel 还是挺有用的。
Go 通过 channel 实现 CSP 通信模型,主要用于 goroutine 之间的消息传递和事件通知。
有了 channel 和 goroutine 之后Go 的并发编程变得异常容易和安全,得以让程序员把注意力留到业务上去,实现开发效率的提升。
要知道,技术并不是最重要的,它只是实现业务的工具。一门高效的开发语言让你把节省下来的时间,留着去做更有意义的事情,比如写写文章。
* [CSP理论模型](http://www.usingcsp.com/cspbook.pdf)

View File

@ -0,0 +1,609 @@
---
layout: post
title: "Golang: reflect反射"
date: 2020-06-03 10:01:08
author: "Viletyy"
header-style: text
tags:
- Golang
---
### 反射的定义
官方文档中的定义:
> Reflection is computing is the ability of program to examine its own structure, particularly through types; it's a form of metaprogramming. It's also a gread source of confusion.
> 在计算机领域,反射是一种让程序——主要是通过类型——理解其自身结构的一种能力。它是元编程的组成之一,同时它也是一大引人困惑的难题。
> 在计算机科学中反射是指计算机程序在运行时Run time可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说反射就是程序在运行的时候能够“观察”并且修改自己的行为。
不同语言的反射模型不尽相同,有些语言还不支持反射。
> Go语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法但是在编译时并不知道这些变量的具体类型这称为反射机制。
需要反射的2个常见场景
1. 有时候你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就用得上了。
2. 有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。
但是对于反射,还是有几点不太建议使用反射地理由:
1. 与反射相关的代码,通常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标
2. Go语言作为一门静态语言编码过程中编译器能提前发现一些类型错误但是对于反射代码是无能为力的。所以包含反射相关的代码可能会运行很久才会出错这时候经常是panic可能会造成严重的后果
3. 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中运行效率关键位置的代码,尽量避免使用反射特性
### 相关基础
反射是如何实现的我们以前学习过interface它是Go语言实现抽象的一个非常强大的工具。当向接口变量赋予一个实体类型的时候接口会存储实体的类型信息反射就是通过接口的类型信息实现的反射简历在类型的基础上。
Go语言在reflect包里定义里各种类型实现了反射的各种函数通过它们可以在运行时检测类型的信息、改变类型的值。在进行更加详细的了解之前先温习一下Go语言相关的一些特性所谓温故知新从这些特性中了解其反射机制上如何使用的。
| 特点 | 说明 |
| --- | --- |
| go语言时静态类型语言 | 编译时类型已确定,比如对已定义基本数据类型的再定义之后的类型,反射时候需要确认返回的是何种类型。 |
| 空接口interface{} | go的反射机制是要通过接口来进行的而类似于Java的Object的空接口可以和任何类型进行交互因此对基本数据类型等的反射也直接利用了这一特点。 |
Go语言的类型
* 变量包括typevalue两部分
* 理解这一点就知道为什么nil != nil了
* type包括static type和concrete type.简单来说static type是你在编码看见的类型如int,string)concrete type是runtime系统看见的类型
* 类型断言是否成功取决于变量的concrete type而不是static type。因此一个reader变量如果它的concrete type也实现了write方法的话它也可以被类型断言为writer
Go是静态类型语言。每个变量都拥有一个静态类型这意味着每个变量的类型在编译时都是确定的int, float32, *AutoType, []byte, chan[]int 诸如此类。
在反射的概念中,编译时就知道变量类型是静态类型,运行时才知道一个变量类型的叫做动态类型。
静态类型 静态类型就是变量声明时的赋予的类型。比如:
```go
type MyInt int // int 就是静态类型
```
动态类型:运行时给这个变量赋值时,这个值的类型(如果值为nil的时候没有动态类型)。一个变量的动态类型在运行时可能改变,这主要依赖于它的赋值(前提是这个变量是接口类型)。比如:
```go
var A interface // 静态类型 interface{}
A = 10 // 静态类型为interface{} 动态为int
A = "string" // 静态类型为interface{} 动态为string
var M *int
A = M // A的值可以改变
```
Go语言的反射就是简历在类型上的Golang的指定类型的变量的类型是静态的也就是指定int、string这些的变量它的type是static type在创建变量的时候就已经确定反射主要与Golang的interface类型相关它的type是concrete type只有interface类型才有反射一说。
在Golang的实现中每个interface变量都有一个对应pairpair中记录了实际变量的值和类型:
```go
(value, type)
```
value是实际变量值type是实际变量的类型。一个interface{}类型的变量包含了2个指针一个指针指向值的类型【对应concrete type】另外一个指针指向实际的值【对应value】。
例如,创建类型为*os.File的变量然后将其赋给一个接口变量r
```go
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
var r io.Reader
r = tty
```
接口变量r的pair中将记录如下信息(tty, *os.File)这个pair在接口变量的连续赋值过程中是不变的将接口变量r赋给另一个接口变量w:
```go
var w io.Writer
w = r.(io.Writer)
```
接口变量w的pair与r的pair相同都是:(tty, *os.File)即使w是空接口类型pair也是不变的。
interface及其pair的存在是Golang中实现反射的前提理解了pair就更容易理解反射。反射就是用来检测存储在接口变量内部(值value类型concrete type) pair对的一种机制。
所以我们要理解两个基本概念 Type 和 Value它们也是 Go语言包中 reflect 空间里最重要的两个类型。
### Type和Value
既然反射就是用来检测存储在接口变量内部值value类型concrete type)pair对的一种机制。那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息它提供了两种类型或者说两个方法让我们很容易的访问接口变量内容分别是reflect.ValueOf()和reflect.TypeOf()
```go
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero
// ValueOf用来获取输入参数接口中数据的值如果接口为空则返回为0
func ValueOf(i interface{}) Value {...}
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil
// TypeOf用来动态获取输入参数接口中的值的类型如果接口为空则返回nil
func TypeOf(i interface{}) Type {...}
```
reflect.TypeOf()是获取pair中的typereflect.ValueOf()获取pair中的value
首先需要要把它转化成reflect对象(reflect.Type或者reflect.Value根据不同情况调用不同函数
```go
t := reflect.TypeOf(i) // 得到类型的元数据通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i) // 得到实际的值通过v我们获取存储在里面的值还可以去改变值
```
示例:
```go
package main
import (
"fmt"
"reflect"
)
func main() {
// 反射操作:通过反射,可以获取一个接口类型变量的 类型和数值
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x)) // type: float64
fmt.Println("type:", reflect.ValueOf(x)) // value: 3.4
fmt.Println("---------------------")
// 根据反射的值,来获取对应的类型和数值
v := reflect.ValueOf(x)
fmt.Println("kind of float64:", v.Kind() == relect.Float64)
fmt.Println("type: ", v.Type())
fmt.Println("value: ", v.Float())
}
```
运行结果:
```go
type: float64
value: 3.4
---------------------
kind is float64: true
type: float64
value: 3.4
```
说明:
1. reflect.TypeOf: 直接给到了我们想要的type类型如float64、int、各种pointer、struct等等真实的类型
2. reflect.ValueOf: 直接给到我们想要的具体的值如1.2345这个具体数值,或者类似&{1 "Allen.Wu" 25}这样的结构体struct的值
3. 也就是说明反射可以将“接口类型变量”转换为“反射类型对象”反射类型指的是reflect.Type和reflect.Value这两种
Type和Value都包含了大量的方法其中第一个有用的方法应该是Kind这个方法返回该类型的具体信息Uint、Float64等。Value类型还包含了一系列类型方法比如Int(),用于返回对于的值。以下是Kind的种类
```go
// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint
const (
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
)
```
### reflect对象获取接口变量信息
#### 反射的规则
其实反射的操作步骤非常的简单就是通过实体对象获取反射对象ValueType然后操作相应的方法即可。
下图描述了实例、Value、Type三者之间的转换关系
![](/img/in-post/2020-06-03-golang-reflect-01.png)
反射APi的分类总结如下
#### 从实例到 Value
通过实例获取Value对象直接使用reflect.ValueOf()函数
```go
func ValueOf(i interface{}) Value
```
#### 从实例到Type
通过实例获取反射对象的Type直接使用reflect.TypeOf()函数
```go
func TypeOf(i interface{}) Type
```
#### 从Type到Value
Type里面只有类型信息所以直接从一个Type接口变量是无法获得实例的Value的但可以通过该Type构建一个新实例的Value。reflect包提供了两种方法
```go
// New 返回的是一个Value该Value的type为PtrTo(typ),即Value的Type是指定typ的指针类型
func New(typ Type) Value
// Zero 返回的是一个typ类型的零值注意返回的Value不能寻址位不可改变
func Zero(typ Type) Value
```
如果知道一个类型值的底层存放地址则还有一个函数是可以一句type和该地址值恢复出Value的。
```go
func NewAt(typ Type, p unsafe.Pointer) Value
```
#### 从Value到Type
从反射对象Value到Type可以直接调用Value的方法因为Value内部存放着到Type类型的指针
```go
func(v Value) Type() Type
```
#### 从Value到实例
Value本身就包含类型和值信息reflect提供了丰富的方法来实现从Value到实例的转换
```go
// 该方法最通用用来将Value转换为空接口该空接口内部存放具体类型实例
// 可以使用接口类型查询去还原为具体的类型
func(v Value) Interface() (i interface{})
// Value自身也提供丰富的方法直接将Value转换为简单类型实例如果类型不匹配则直接引起panic
func(v Value) Bool() bool
func(v Value) Float() float64
func(v Value) Int() int64
func(v Value) Uint() uint64
```
#### 从Value到指针到值
从一个指针类型的Value获得值类型Value有两种方法
```go
// 如果v类型是接口则Elem()返回接口绑定的实例Value如果v类型是指针则返回指针的Value否则引起panic
func (v Value) Elem() Value
// 如果v是指针则返回指针指的Value否则返回v自身该函数不会引起panic
func Indirect(v Value) Value
```
#### Type指针和值的相互转换
指针类型Type到值类型Type
```go
// t 必须是Array、Chan、Map、Ptr、Slice否则会引起panic
// Elem返回的是其内部元素的Type
t.Elem() Type
```
值类型Type到指针类型Type。
```go
// PtrTo 返回的是指向t的指针型Type
func PtrTo(t Type) Type
```
#### Value值的可修改性
Value值的修改涉及如下两个方法
```go
// 通过CanSet判断是否能修改
func (v Value) CanSet() bool
// 通过Set进行修改
func (v Value) Set(x Value)
```
Value 值在什么情况下可以修改?我们知道实例对象传递给接口的是一个完全的值拷贝,如果调用反射的方法 reflect.ValueOf() 传进去的是一个值类型变量, 则获得的 Value 实际上是原对象的一个副本,这个 Value 是无论如何也不能被修改的。
根据Go官方关于反射的博客反射有三大定律
1. Reflection goes from interface value to reflection object. // 反射可以从接口值得到反射对象。
2. Reflection goes from reflection object to interface value. // 反射可以从反射对象中获得接口值。
3. To modify a reflection object, the value must be settable. // 如果需要操作一个反射变量,则其值必须可以修改
反射变量可设置的本质是它存储了原变量本身,这样对反射变量的操作,就会反应到原变量本身;反之,如果反射变量不能代表原变量,那么操作了反射变量,不会对原变量产生任何影响,这会给使用者带来疑惑。所以第二种情况在语言层面是不被允许的。
### 反射的使用
#### 从relfect.Value中获取接口interface的信息
当执行reflect.ValueOf(interface)之后,就得到了一个类型为"relfect.Value"的变量可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。不过,我们可能是已知原有类型,也有可能是未知原有类型
#### 已知原有类型
已知类型后转换为其对应类型的做法如下直接通过interface方法然后强制转换。
```go
realValue := value.Interface().(已知的类型)
```
示例:
```go
package main
import (
"fmt"
"reflect"
)
func main() {
var num float64 = 1.2345
poniter := reflect.ValueOf(&num)
value := reflect.ValueOf(num)
// 可以理解为“强制转换”但是需要注意的是转换的时候如果转换的类型不完全符合则直接panic
// Golang对类型要求非常严格类型一定要完全符合
// 如下两个,一个*float64, 一个是float64如果弄混则会panic
convertPointer := pointer.Interface().(*float64)
convertValue := pointer.Interface().(float64)
fmt.Println(convertPointer)
fmt.Println(convertValue)
}
```
输出结果:
```go
0xc000098000
1.2345
```
#### 未知原有类型
很多情况下我们可能并不知道其具体类型那么这个时候我们需要进行遍历探测其Field来得知
```go
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
Sex string
}
func (p Person) Say(msg string) {
fmt.Println("hello", msg)
}
func (p Person) PrintInfo() {
fmt.Printf("姓名:%s年龄%d性别%s\n", p.Name, p.Age, p.Sex)
}
func main() {
p1 := Person{"王二狗", 30, "男")
DofieldAndMethod(p1)
}
// 通过接口来获取任意参数
func DofieldAndMethod(input interface{}) {
getType := reflect.TypeOf(input) // 先获取input的类型
fmt.Println("get Type is :", getType.Name()) // Person
fmt.Println("get Kind is :", getType.Kind()) // struct
getValue := reflect.ValueOf(input)
fmt.Println("get all Fields is: ", getValue) // {王二狗 30 男}
// 获取方法字段
// 1.先获取interface的reflect.Type,然后通过NumField进行遍历
// 2.再通过reflect.Type的Field获取其Field
// 3.最后通过Field的Interface()得到对应的value
for i := 0; i < getType.NumField(); i++ {
field := getType.Field(i)
value := getValue.Field(i).Interface()
fmt.Printf("字段名称:%s字段类型%s字段数值%v \n", field.Name, field.Type, value)
}
// 通过反射操作方法
// 1.先获取interface的reflect.Type然后通过.NumMethod进行遍历
// 2.再通过reflect.Type的Method获取其Method
for i := 0; i < getType.NumMethod(); i++ {
method := getType.Method(i)
fmt.Printf("方法名称:%s,方法类型:%v \n", method.Name, method.Type)
}
}
```
运行结果
```go
get Type is : Person
get Kind is : struct
get all Fields is : {王二狗 30 男}
字段名称Name, 字段类型string字段数值王二狗
字段名称Age字段类型int字段数值30
字段名称Sex字段类型string字段数值
方法名称PrintInfo方法类型func(main.Person)
方法名称Say方法类型func(main.Person, string)
```
通过运行结果可以得知获取未知类型的interface的具体变量及其类型的步骤为
1. 先获取interface的reflect.Type然后通过NumField进行遍历
2. 再通过reflect.Type的Field获取其Field
3. 最后通过Field的Interface()得到对应的value
通过运行结果可以得知获取未知类型的interface的所属方法函数的步骤
1. 先获取interface的reflect.Type然后通过NumMethod进行遍历
2. 再分别通过reflect.Type的Method获取对应的真实的方法函数
3. 最后对结果取其Name和Type得知具体的方法名
4. 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
5. struct或者struct的嵌套都是一样的判断处理方式。
### 通过reflect.Value设置实际变量的值
reflect.Value是通过reflect.ValueOf(X)获得的只有当X是指针的时候才可以通过reflect.Value修改实际变量X的值要修改反射类型的对象就一定要保证其值是“addressable”的。
也就是说要想修改一个变量的值那么必须通过该变量的指针地址取消指针的引用。通过refPtrVal := reflect.ValueOf(&var)的方式获取指针类型你使用refPtrVal.elem().set(一个新的reflect.Value)来进行更改传递给set()的值也必须是一个reflect.value.
```go
package main
import (
"fmt"
"reflect"
)
func main() {
var num float64 = 1.2345
fmt.Println("num的数值", num)
// 需要操作指针
// 通过reflect.ValueOf获取num中的reflect.Value注意参数必须是是指针才能修改其值
pointer := reflect.ValueOf(&num)
newValue := pointer.Elem()
fmt.Println("类型:", newValue.Type())
fmt.Println("是否可以修改:", newValue.CanSet())
// 重新赋值
newValue.SetFloat(77)
fmt.Println("新的数值", num)
////////////////////
// 如果reflect.ValueOf的参数不是指针会如何
//尝试直接修改
//value := reflect.ValueOf(num)
//value.SetFloat(6.28) //panic: reflect: reflect.Value.SetFloat using unaddressable value
//fmt.Println(value.CanSet()) //false
//pointer = reflect.ValueOf(num)
//newValue = value.Elem() // 如果非指针这里直接panic“panic: reflect: call of reflect.Value.Elem on float64 Value”
}
```
运行结果:
```go
num的数值1.2345
类型float64
是否可以修改true
新的数值77
```
说明:
1. 需要传入的参数是*float64这个指针然后可以通过pointer.Elem()去获取所指向的Value,主要一定要是指针。
2. 如果传入的参数不是指针,而是变量,那么
1. 通过Elem获取原始值对应的对象则直接panic
2. 通过CanSet方法查询是否可以设置返回false
3. newValue.CanSet()表示是否可以重新设置其值如果输出的是true则可以修改否则不能修改修改完之后再进行打印发现真的已经修改了。
4. reflect.Value.Elem()表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的。
5. 也就是如果要修改反射类型对象其值必须是“address”【对应的要传入的指针同时要通过Elem方法获取原始值对应的反射对象】
6. struct或者struct的嵌套都是一样判断处理方式
尝试修改结构体中的字段数值:
```go
package main
import (
"reflect"
"fmt"
)
type Student struct {
Name string
Age int
School string
}
func main() {
// 通过反射,来更改对象的数值:前提是数据可以被修改
s1 := Student{"王二狗", 19, "hahha")
fmt.Printf("%T\n", s1) // main.Student
p1 := &s1
fmt.Printf("%T\n", p1) // *main.Student
fmt.Println(s1.Name)
fmt.Println((*p1).Name, p1.Name)
v1 := reflect.ValueOf(&s1)
if v1.Kind() == reflect.Ptr{
fmt.Println(v1.Elem().CanSet())
v1 = v1.Elem()
}
f1 := v1.FieldByName("Name")
f1.SetString("张三")
f3 := v1.FieldByName("school")
f3.SetString("幼儿园")
fmt.Println(s1)
}
```
运行结果:
```go
main.Student
*main.Student
王二狗
王二狗 王二狗
true
{"张三", 19, "幼儿园"}
```
### 通过reflect.Value来进行方法的调用
这算是一个高级用法前面只对类型、变量几种反射的用法做了介绍包括如何获取其值、其类型、以及如何重新设置新值。但是在项目应用中另外一个常用并且属于高级的用法就是通过reflect来进行方法【函数】的调用。比如我们要做框架工程的时候需要可以随意扩展方法或者说用户可以自定义方法那么我们通过什么手段来扩展让用户能够自定义关键点在于用户的自定义方法是未可知的因此我们可以通过reflect来搞定。
#### 通过反射,调用方法
先获取结构体对象,然后
示例:
```go
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
Sex string
}
func (p Person)Say(msg string) {
fmt.Println("hello",msg)
}
func (p Person)PrintInfo() {
fmt.Printf("姓名:%s,年龄:%d性别%s\n",p.Name,p.Age,p.Sex)
}
func (p Person) Test(i,j int,s string){
fmt.Println(i,j,s)
}
// 如何通过反射来进行方法的调用?
// 本来可以用结构体对象.方法名称()直接调用的,
// 但是如果要通过反射,
// 那么首先要将方法注册也就是MethodByName然后通过反射调动mv.Call
func main() {
p2 := Person{"Ruby",30,"男"}
// 1. 要通过反射来调用起对应的方法必须要先通过reflect.ValueOf(interface)来获取到reflect.Value
// 得到“反射类型对象”后才能做下一步处理
getValue := reflect.ValueOf(p2)
// 2.一定要指定参数为正确的方法名
// 先看看没有参数的调用方法
methodValue1 := getValue.MethodByName("PrintInfo")
fmt.Printf("Kind : %s, Type : %s\n",methodValue1.Kind(),methodValue1.Type())
methodValue1.Call(nil) //没有参数直接写nil
args1 := make([]reflect.Value, 0) //或者创建一个空的切片也可以
methodValue1.Call(args1)
// 有参数的方法调用
methodValue2 := getValue.MethodByName("Say")
fmt.Printf("Kind : %s, Type : %s\n",methodValue2.Kind(),methodValue2.Type())
args2 := []reflect.Value{reflect.ValueOf("反射机制")}
methodValue2.Call(args2)
methodValue3 := getValue.MethodByName("Test")
fmt.Printf("Kind : %s, Type : %s\n",methodValue3.Kind(),methodValue3.Type())
args3 := []reflect.Value{reflect.ValueOf(100), reflect.ValueOf(200),reflect.ValueOf("Hello")}
methodValue3.Call(args3)
}
```
#### 通过反射,调用函数。
首先我们要先确认一点函数像普通的变量一样之前的章节中我们在讲到函数的本质的时候是可以把函数作为一种变量类型的而且是引用类型。如果说Fun()是一个函数那么f1 := Fun也是可以的那么f1也是一个函数如果直接调用f1()那么运行的就是Fun()函数。
那么我们就先通过ValueOf()来获取函数的反射对象可以判断它的Kind是一个func那么就可以执行Call()进行函数的调用。
示例:
```go
package main
import (
"fmt"
"reflect"
)
func main() {
//函数的反射
f1 := fun1
value := reflect.ValueOf(f1)
fmt.Printf("Kind : %s , Type : %s\n",value.Kind(),value.Type()) //Kind : func , Type : func()
value2 := reflect.ValueOf(fun2)
fmt.Printf("Kind : %s , Type : %s\n",value2.Kind(),value2.Type()) //Kind : func , Type : func(int, string)
//通过反射调用函数
value.Call(nil)
value2.Call([]reflect.Value{reflect.ValueOf(100),reflect.ValueOf("hello")})
}
func fun1(){
fmt.Println("我是函数fun1(),无参的。。")
}
func fun2(i int, s string){
fmt.Println("我是函数fun2(),有参数。。",i,s)
}
```
#### 说明
1. 要通过反射来调用起对应的方法必须要先通过reflect.ValueOf(interface)来获取到reflect.Value得到“反射类型对象”后才能做下一步处理
2. reflect.Value.MethodByName这个MethodByName需要指定准确真实的方法名字如果错误将直接panicMethodByName返回一个函数值对应的reflect.Value方法的名字。
3. []reflect.Value这个是最终需要调用的方法的参数可以没有或者一个或者多个根据实际参数来定。
4. reflect.Value的 Call 这个方法这个方法将最终调用真实的方法参数务必保持一致如果reflect.Value.Kind不是一个方法那么将直接panic。
5. 本来可以用对象访问方法直接调用的但是如果要通过反射那么首先要将方法注册也就是MethodByName然后通过反射调用methodValue.Call

View File

@ -0,0 +1,53 @@
---
layout: post
title: "大话数据结构: 绪论"
date: 2020-06-04 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
### 什么是数据?
**数据**是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。
### 数据元素、数据项
**数据元素**是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理,也被称为记录。
**数据项**一个数据元素可以由若干个数据项组成。**数据项**是数据不可分割的最小单位。
### 数据结构、数据对象
**数据结构**是相互之间存在一个或多种**特定关系**的数据元素的集合。
**数据对象**是具有**相同性质**的数据元素的集合,是数据的一个子集。
### 逻辑结构与物理结构
#### 逻辑结构
**逻辑结构**是指数据对象中数据元素之间的相互关系
* 集合结构:**集合结构**中的数据元素除了同属于一个集合外,它们之间没有其它关系。
* 线性结构:**线性结构**中的数据元素之间是一对一的关系。
* 树形结构:**树形结构**中的数据元素之间存在一种一对多的层次关系
* 图形结构:**图形结构**中的数据元素是多对多的关系
#### 物理结构
**物理结构**是指数据的逻辑结构在计算机中的存储形式
* 顺序存储结构:**顺序存储结构**是把数据元素存放在地址连续的存储单元里,其数据之间的逻辑关系和物理关系是一致的。
* 链式存储结构:**链式存储结构**是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。
### 抽象数据类型
#### 数据类型
**数据类型**是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。
* 原子类型:是不可再分解的基本类型,包括整型、实型、字符型等。
* 结构类型:由若干个类型组合而成,是可以再分解的。例如,整型数组是由若干整型数据组成的。
#### 抽象数据类型
**抽象**是指抽取出事物具有的普遍性的本质。
Abstract Data Type(ADT)**抽象数据类型**是指一个数据模型及定义在该模型上的一组操作。
**抽象数据类型**体现了程序设计中问题分解、抽象和信息隐藏的特性。
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,83 @@
---
layout: post
title: "大话数据结构: 算法"
date: 2020-06-06 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
### 什么是算法
程序=数据结构+算法
**算法**是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
### 算法的特性
* 输入一个算法有0个或多个输入这些输入取自某个特定对象的集合。
* 输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。
* 有穷性:指**算法**在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。
* 确定性:算法每一条指令必须有确切的含义,不会出现二义性。
* 可行性:算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。
### 算法设计的要求
* 正确性:算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。
* 可读性:算法设计的另一目的是为了便于阅读、理解和交流
* 健壮性:当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。
* 高效率与低存储量需求:设计算法应该尽量满足时间效率高和存储量低的需求。
### 算法效率的度量方法
#### 事后统计方法
**事后统计方法**这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
#### 事前分析估算方法
**事前分析估算方法**在计算机程序编制前,根据统计方法对算法进行估算。
### 函数的渐进增长
给定两个函数f(n)和g(n)如果存在一个整数N使得对于所有的n>Nf(n)总是比g(n)大那么我们说f(n)的增长渐进快于g(n)
判断一个算法的效率时,函数中的常数和其它次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。
某个算法随着n的增大它会越来越优于另一算法或越来越差于另一算法。
#### 算法时间复杂度
在进行算法分析时语句总的执行次数T(n)时关于问题规模n的函数进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度也就时算法的时间量度记作T(n) = O(f(n))。它表示随问题规模n的增大算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称为**时间复杂度**。其中f(n)时问题规模n的某个函数。
#### 推导大O阶方法
1. 用常数1取代运行时间中的所有加法常数
2. 在修改后的运行次数函数中,只保留最高阶项
3. 如果最高阶项存在且不是1则去除与这个项相乘的常数
#### 常见的时间复杂度
| 执行次数函数 | 阶 | 非正式术语 |
| --- | --- | --- |
| 12 | O(1) | 常数阶 |
| 2n+3 | O(n) | 线性阶 |
| 3n^2+2n+1 | O(n^2) | 平方阶 |
| 5log2^2+20 | O(logn) | 对数阶 |
| 2n+3nlog2^n+19 | O(nlogn) | nlogn阶 |
| 6n^3+2n^2+3n+4 | O(n^3) | 立方阶 |
| 2^n | O(2^n) | 指数阶 |
```math
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
```
#### 最坏情况与平均情况
最坏情况运行时间是一种保证,那就是运行时间将不会再坏了。在应用中,这是一种最重要的需求,通常,除非特别指定,我们提到的运行时间都是最坏情况的运行时间。
平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。
一般在没有特殊说明的情况下,都是指最坏时间复杂度。
#### 算法空间复杂度
算法的空间复杂度通过计算算法所需的存储空间实现算法空间复杂度的计算公式记做S(n) = O(f(n)),其中n为问题的规模f(n)为语句关于n所占存储空间的函数。
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,87 @@
---
layout: post
title: "大话数据结构: 线性表"
date: 2020-06-07 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 线性表
### 线性表的定义
**线性表**零个或多个数据元素的有限序列。
若将线性表记为a1, ...,ai-1, ai, ai+1, ..., an)则表中ai-1领先于aiai领先于ai+1称ai-1时ai的直接先驱元素ai+i是ai的直接后继元素。当i=1,2,...,n-1时ai有且仅有一个直接后继当i=2,3,...,n时ai有且仅有一个直接前驱。
线性表元素的个数n(n >=0)定义为线性表的长度当n=0时称为空表。
### 线性表的抽象数据类型
```c
ADT 线性表(List)
Data
线性表的数据对象集合为{a1,a2,...,an}每个元素的类型均为DataType。其中除第一个元素a1每个元素有且仅有一个直接前驱元素除了最后一个元素an外每一个元素有且仅有一个直接后继元素。数据元素之间的关系是一对一的。
Operation
InitList(*L): 初始化操作建立一个空的线性表L。
ListEmpty(L): 若线性表为空返回true否则返回false
ClearList(*L): 将线性表清空
GetElem(L, i, *e): 将线性表L中的第i个位置元素值返回给e
LocateElem(L, e): 在线性表L中查找与给定值e相等的元素如果查找成功返回该元素在表中序号表示成功否则返回0表示失败
ListInsert(*L, i, e): 在线性表中的第i个位置插入新元素e。
ListDelete(*L, i, *e): 删除线性表L中第i个位置元素并用e返回其值。
ListLength(L): 返回线性表L的元素个数
endADT
```
> 实现两个线性表集合A和B的并集操作。即把存在集合B中但并不存在A中的数据元素插入到A中
```c
/*将所有的在线性表Lb中但不在La中的数据元素插入到La中*/
void union(List *La, List Lb)
{
int La_len, Lb_len, i;
ElemType e; /*声明与La和Lb相同的数据元素e*/
La_len = ListLength(La); /*求线性表的长度*/
Lb_len = ListLength(Lb);
for (i=1; i<=Lb_len; i++)
{
GetElem(Lb, i, e); /*取Lb中第i个数据元素赋给e*/
if(!LocateElem(La,e,equal)) /*La中不存在和e相同的数据元素*/
ListInsert(La, ++La_len, e);
}
}
```
### 线性表的顺序存储结构
线性表的顺序存储结构,指的是用一段地址连续的存储单元一次存储线性表的数据元素。
### 线性表的链式存储结构
为了表示每个数据元素a1与其直接后继数据元素ai+1之间的逻辑关系对于数据元素a1来说除了存储其本身的信息之外还需存储一个指示其直接后继的信息即直接后继的存储位置。我们把存储数据元素信息的域称为数据域把存储直接后继位置的域称为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素ai的存储映像称为结点Node)
n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,...,an)的链式存储结构,因为此链表的每个的结点中只包含一个指针域,所以叫做单链表。
链表中第一个结点的存储位置叫做头指针。
链表的最后一个结点为空通常用null或"^"表示)
单链表的第一个结点前附设一个结点,称为头结点。也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。
### 头指针与头结点的异同
头指针
* 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
* 头指针具有标识作用,所以常用头指针冠以链表的名字
* 无论链表是否为空,头指针均不为空。头指针是链表的必要元素
头结点
* 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)
* 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了
* 头结点不一定是链表必须要素
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,144 @@
---
layout: post
title: "大话数据结构: 线性表的顺序存储结构"
date: 2020-06-08 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 线性表的顺序存储结构
> 线性表的顺序存储的结构代码。
```c
#define MAXSIZE 20 /*存储空间初始分配量*/
typedef int ElemType; /*ElemType类型根据实际视情况而定这里假设为int*/
typedef struct
{
ElemType data[MAXSIE]; /*数组存储数据元素最大值为MAXSIZE*/
int length; /*线性表当前长度*/
}SqList
```
顺序存储结构需要三个属性:
* 存储空间的起始位置数组data它的存储位置就是存储空间的存储位置。
* 线性表的最大存储容量数组长度MaxSize
* 线性表的当前长度length
### 数组长度与线性表长度区别
数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的。
线性表的长度是线性表中元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。
### 地址计算方法
存储器中每个存储单元都有自己的编号,这个编号称为地址。
假设每个数据元素占用的是c个存储单元那么线性表中第i+1个数据元素的存储位置和第i个数据的存储位置满足下列关系LOC表示获得存储位置的函数
```math
LOC(a_{i+1}) = LOC(a_i) + c
```
所以对于第i个数据元素ai的存储位置可以由a1推算得出
```math
LOC(a_i) = LOC(a_1) + (i-1)*c
```
### 获得元素操作
只要i的数值在数组的下标范围内就是把数组第i-1下表的值返回即可
```c
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
/*Status是函数的类型其值是函数结果状态代码如OK等*/
/*初始条件顺序线性表L已存在1<=i<=ListLength(L)*/
/*操作结果用e返回L中第i个数据元素的值*/
Status GetElem(SqList L,int i,ElemType *e)
{
if(L.length==0 || i<1 || i>length)
return ERROR;
*e=L.data[i-1];
return OK;
}
```
### 插入操作
思路:
* 如果插入位置不合理,抛出异常;
* 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
* 从最后一个元素开始向前便利到第i个位置分别都将它们向后移动一个位置
* 将要插入元素填入位置i处
* 表长加1
```c
/*初始条件顺序线性表L已存在1<=i<=ListLength(L)*/
/*操作结果在L中第i个位置之前插入新的数据元素eL的长度加1*/
Status ListInsert(SqList *L, int i, ElemType e)
{
int k;
if (L->length==MAXSIZE) /*顺序线性表已满*/
return ERROR;
if (i<1 || i>L->length+1)/*当i不再范围内时*/
return ERROR;
if (i<=L->length) /*若插入数据位置不在表尾*/
{
for (k=L->length-1;k>=i-1;k--) /*将要插入位置后数据元素向后移动一位*/
L->data[k+1]=L->data[k];
}
L->data[i-1]=e; /*将新元素插入*/
L->length++;
return OK;
}
```
### 删除操作
思路:
* 如果删除位置不合理,抛出异常;
* 取出删除元素;
* 从删除元素位置开始便利到最后一个元素位置,分别将它们都向前移动一个位置;
* 表长减1
```c
/*初始条件顺序线性表L已存在1<=i<=ListLength(L)*/
/*操作结果删除L的第i个数据元素并用e返回其值L的长度减1*/
Status ListDelete(SqList *L,int i, ElemType *e)
{
int k;
if (L->length==0)
return ERROR;
if (i<1 || i>L->length)
return ERROR;
*e=L>data[i-1];
if (i<L->length)
{
for(k=i;k<=L->length;k++)
L->data[k-1]=L->data[k]
}
L->length--;
return OK;
}
```
线性表的顺序存储结构在存、读数据时不管是哪个位置时间复杂度都是O(1);而插入或删除时时间复杂度都是O(n)。这就说明,它比较适合元素个数不太变化,而更多是存取数据的应用。
### 线性表顺序存储结构的优缺点
* 优点
* 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
* 可以快速地存取表中任一位置的元素
* 缺点
* 插入和删除操作需要移动大量元素
* 当线性表长度变化较大时,难以确定存储空间的容量
* 造成存储空间的“碎片”
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,238 @@
---
layout: post
title: "大话数据结构: 线性表的单链表存储结构"
date: 2020-06-09 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
### 线性表的单链表存储结构
```c
/*线性表的单链表存储结构*/
typedef struct Node
{
ElemType data;
struct Node *next;
typedef struct Node *LinkList;
```
结点由存放数据元素的数据域存放后继结点地址的指针域组成。
### 单链表的读取
获得链表第i个数据的算法思路
1. 声明一个结点p指向链表第一个结点初始化j从1开始
2. 当j<i时就便利链表让p的指针向后移动不断指向下一结点j累加1
3. 若到链表末尾p为空则说明第i个元素不存在
4. 否则查找成功返回结点p的数据
```c
/*初始条件顺序线性表L已存在1<=i<=ListLength(L)*/
/*操作结果用e返回L中第i个数据元素的值*/
Status GetElem(LinkList L,int i, ElemType *e)
{
int j;
LinkList p; /*声明一结点p*/
p = L->next; /*让p指向链表L的第一个结点*/
j = 1; /*j为计数器*/
while (p && j<i) /*p不为空和计数器j还没有等于i时循环继续*/
{
p = p->next; /*让p指向下一个结点*/
++j;
}
if (!p || j > i)
return ERROR; /*第i个元素不存在*/
*e = p->data; /*取第i个元素的数据*/
return OK;
}
```
### 单链表的插入和删除
单链表第i个数据插入结点的算法思路
1. 声明一结点p指向链表第一个结点初始化j从1开始
2. 当j<i时就便利链表让p的指针向后移动不断指向下一结点j累加1
3. 若到链表末尾p为空则说明第i个元素不存在
4. 否则查找成功在系统中生存一个空结点s
5. 将数据元素e赋值给s->data;
6. 单链表的插入标准语句s->next=p->next; p->next=s;
7. 返回成功
```c
/*初始条件顺序线性表L已存在1<=i<=ListLength(L)*/
/*操作结果在L中第i个位置之前插入新的数据元素eL的长度加1*/
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p, s;
p = *L;
j = 1;
while (p && j<i) /*寻找第i个结点*/
{
p=p->next;
++j;
}
if (!p || j > i)
return ERROR; /*第i个元素不存在*/
s = (LinkList) malloc(sizeof(Node)); /*生成新结点c标准函数*/
s->data = e;
s->next = p->next; /*将p的后继结点赋值给s的后继结点*/
p->next = s; /*将s赋给p的后继结点*/
return OK;
}
```
单链表第i个数据删除结点的算法思路
1. 声明一结点p指向链表第一个结点初始化j从1开始
2. 当j<i时就便利链表让p的指针向后移动不断指向下一个结点j累加1
3. 若到链表末尾p为空则说明第i个元素不存在
4. 否则查找成功将与删除的结点p->next赋值给q
5. 单链表的删除标准语句p->next=q->next;
6. 将q结点中的数据复制给e作为返回
7. 释放q结点
8. 返回成功。
```c
/*初始条件顺序线性表L已存在1<=i<=ListLength(L)*/
/*操作结果删除L的第i个数据元素并用e返回其值L的长度减1*/
Status ListDelete(LinkList *L, int i, ElemType *e)
{
int j;
LinkList p, q;
p = *L;
j = 1;
while (p->next && j<i) /*遍历寻找第i个元素*/
{
p = p->next;
++j;
}
if (!(p->next) || j>i)
return ERROR; /*第i个元素不存在*/
q = p->next;
p->next = q->next; /*将q的后继复制给p的后继*/
*e = q->data; /*将q结点中的数据给e*/
free(q); /*让系统回收此结点,释放内存*/
return OK;
}
```
对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。
### 单链表的整表创建
顺序存储结构的创建,其实就是一个数组的初始化,即声明一个类型和大小的数组并复制的过程。而单链表和顺序存储结构就不一样,它不像顺序存储结构这么集中,它可以很散,是一种动态结构。对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。
所以创建单链表的过程就是一个动态生成链表的过程。即从“空表”的初始状态起,一次建立个元素结点,并逐个插入链表。
单链表整表创建的算法思路:
1. 声明一结点p和计数器变量i
2. 初始化一空链表L
3. 让L的头结点的指针指向NULL即建立一个带头结点的单链表
4. 循环
1. 生成一新结点赋值给p
2. 随机生成一数字复制给p的数据域p->data;
3. 将p插入到头结点与前一新结点之间
```c
/*随机产生n个元素的值建立带表头结点的单链线性表L头插法*/
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int 1;
srand(time(0)); /*初始化随机数种子*/
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL; /*先建立一个带头结点的单链表*/
for (i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Node)); /*生成新结点*/
p->data = rand()%100+1; /*随机生成100以内的数字*/
p->next = (*L)->next;
(*L)->next = p; /*插入到表头*/
}
}
```
这段算法代码里,我们其实用的是插队的办法,就是始终让新结点在第一的位置。这种算法也称为头插法。
可事实上,我们还是可以不这样干,为什么不把新结点都放到最后呢,这才是排队时到正常思维,所谓的先来后到。我们把每次新结点都插在终端结点的后面,这种算法称之为尾插法。
实现代码算法如下:
```c
/*随机产生n个元素的值建立带表头结点的单链线性表L尾插法*/
void CreateListTail(LinkList *L, int n)
{
LinkList p,r;
int i;
srand(time(0)); /*初始化随机数种子*/
*L = (LinkList)malloc(sizeof(Node)); /*为整个线性表*/
r = *L;
for (i=0; i<n; i++)
{
p = (Node *)malloc(sizeof(Node)); /*生成新结点*/
p->data = rand()%100+1; /*随机生成100以内的数字*/
r->next=p; /*将表尾终端结点指针指向新结点*/
r=p; /*将当前的新结点定义为表尾终端结点*/
}
r->next = NULL; /*表示当前链表结束*/
}
```
注意L与r的关系L是指整个单链表而r时指向尾结点的变量r会随着循环不断地变化结点而L则是随着循环增长为一个多结点的链表。
### 单链表的整表删除
算法思路如下:
1. 声明一结点p和q
2. 将第一个结点赋值给p
3. 循环:
1. 将下一个结点赋值给q
2. 释放p
3. 将q赋值给p
```c
/*初始条件顺序线性表L已存在操作结果将L重置为空表*/
Status ClearList(LinkList *L)
{
LinkList p, q;
p = (*L) ->next; /*p指向第一个结点*/
while (p) /*没到表尾*/
{
q=p->next;
free(p);
p = q;
}
(*L)->next=Null; /*头结点指针域为空*/
return OK;
}
```
### 单链表结构与顺序存储结构优缺点
* 存储分配方式
* 顺序存储结构用一段连续的存储单元一次存储线性表的数据元素
* 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
* 时间性能
* 查找
* 顺序存储结构O(1)
* 单链表O(n)
* 插入和删除
* 顺序存储结构需要平均移动表长一般的元素时间为O(n)
* 单链表在找出某位置的指针后插入和删除时间仅为O(1)
* 空间性能
* 顺序存储结构需要预分配存储空间,分大了,浪费,分小了易发生上溢
* 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。
结论:
* 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。
* 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,使用顺序存储结构效率会高很多。
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,32 @@
---
layout: post
title: "大话数据结构:线性表的循环链式存储结构"
date: 2020-06-11 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 循环链表
将单链表中终端结点的指针端由空指针改为指向头结点,就使得整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称**循环链表**circular linked list
其实循环链表和单链表的主要差异就在于循环的判断条件上原来是判断p->next是否空现在则是p->next不等于头结点则循环未结束。
在单链表中我们有了头结点时我们可以用O(1)的时间访问第一个结点但对于要访问到最后一个结点却需要O(n)时间,因为我们需要将单链表全部扫描一遍。
在循环链表中,不用头指针,二十用指向终端结点的尾指针来表示循环链表,此时查找开始结点和终端结点都很方便了。
终端结点用尾指针rear指示则查找终端结点是O(1)而开始结点其实就是rear->next->next其时间复杂也为O(1)
### 合并两个循环链表
```c
p = rearA->next; /*保存A链表的头结点*/
rearA->next=rearB->next->next; /*将本是指向B链表的第一个结点赋值给rearA->next*/
rearB->next=p /*将原A链表的头结点赋值给rearB->next*/
free(p); /*释放p*/
```
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,132 @@
---
layout: post
title: "大话数据结构: 线性表的静态链表存储结构"
date: 2020-06-11 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 静态链表
用数组代替指针首先让数组的元素都是有两个数据域组成data和cur。也就是说数组的每一个下表都对应一个data和一个cur。数据域data用来存放数据元素也就是通常需要处理的数据而游标cur相当于单链表中的next指针存放该元素的后继在数组中的下标。
> 这种用数组描述的链表叫做静态链表
### 线性表的静态链表存储结构
```c
/*线性表的静态链表存储结构*/
#define MAXSIZE 1000 /*假设链表的最大长度是1000*/
typedef struct
{
ElemType data;
int cur; /*游标cursor为0时表示无指向*/
} Component,StaticLinkList[MAXSIZE];
```
### 静态链表的初始化
```c
/*将一维数组space中各分量链成一备用链表*/
/*Space[0].cur为头指针“0”表示空指针*/
Status InitList(StaticLinkList space)
{
int i;
for (i=0; i<MAXSIZE-1; i++)
space[i].cur = i+1;
space[MAXSIZE-1].cur = 0; /*目前静态链表为空最后一个元素的cur为0*/
return OK;
}
```
### 静态链表结点的申请
```c
/*若备用空间链表非空则返回分配的结点下标否则返回0*/
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur /*当前数组第一个元素的cur存的值*/
/*就是要返回的第一个备用空闲的下标*/
if (space[0].cur)
space[0].cur = space[i].cur; /*由于要拿出一个分量来使用了,所以我们就得把它的下一个分量用来做备用*/
return i;
}
```
### 静态链表的插入操作
```c
/*在L中第i个元素之前插入新的数据元素e*/
Status ListInsert(StaticLinkList L,int i,ElemType e)
{
int j, k, l;
k = MAX_SIZE - 1; /*注意k首先是最后一个元素的下标*/
if (i < 1 || i > ListLength(L) + 1)
return ERROR;
j = Malloc_SSL(L); /*获得空闲分量的下标*/
if (j)
{
L[j].data = e; /*将数据赋值给此分量的data*/
for(l = 1; 1 <= i - 1; l++) /*找到第i个元素之前的位置*/
k = L[k].cur;
L[j].cur = L[k].cur; /*把第i个元素之前的cur赋值给新元素的cur*/
L[k].cur = j; /*把新元素的下标赋值给第i个元素之前元素的cur*/
return OK;
}
return ERROR;
}
```
### 静态链表结点的释放
```c
/*将下标为k的空闲结点回收到备用链条*/
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; /*把第一个元素cur值赋给要删除的分量cur*/
space[0].cur = k /*把要删除的分量下标赋值给第一个元素的cur*/
}
```
### 静态链表的删除操作
```c
/*删除在L中第i个数据元素e*/
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
if (i < 1 || i > ListLength(L))
return ERROR;
k = MAX_SIZE - 1;
for (j = 1; j <= i - 1; j++)
k = L[k].cur
j = L[k].cur
L[k].cur = L[j].cur;
Free_SSL(L, j);
return OK;
}
```
### 静态链表的长度
```c
/*初始条件静态链表L已存在。操作结果返回L中数据元素的个数*/
int ListLength(StaticLinkList L)
{
int j = 0;
int i = L(MAXSIZE-1).cur;
while (i)
{
i=L[i].cur;
j++;
}
return j;
}
```
### 静态链表优缺点
* 优点
* 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点。
* 缺点
* 没有解决连续存储分配带来的表长难以确定的问题
* 失去了顺序存储结构随机存取的特性。
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,45 @@
---
layout: post
title: "大话数据结构:线性表的双向链表存储结构"
date: 2020-06-12 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 双向链表
**双向链表**double linked list是在单链表的每个结点中再设置一个指向其前驱结点的指针域。
### 线性表的双向链表存储结构
```c
typedef struct DulNode
{
ElemType data;
struct DulNode *prior;
struct DulNode *next;
} DulNode, *DuLinkList
```
双向链表是单链表中扩展出来的结构所以它的很多操作是和单链表相同的比如求长度的ListLength查找元素GetElem获得元素位置的LocateElem等这些操作都只要涉及一个方向的指针即可另一指针多了也不能提供什么帮助。
### 双向链表的插入操作
存储元素e的结点为s实现结点s插入到结点p和p->next之间
```c
s->prior = p; /*把p赋值给s的前驱*/
s->next = p->next; /*把p->next赋值给s的后继*/
p->next -> prior = s; /*把s赋值给p->next的前驱*/
p->next = s; /*把s赋值给p的后继*/
```
### 双向链表的删除操作
删除p结点
```c
p->prior->next = p->next; /*把p->next赋值给p->prior的后继*/
p->next->prior = p->prior; /*把p->prior赋值给p->next的前驱*/
free(p); /*释放p*/
```
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,261 @@
---
layout: post
title: "大话数据结构:栈"
date: 2020-06-13 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 栈
**栈**Stack是限定仅在表尾进行插入和删除操作的线性表。
允许插入和删除的一段称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表简称LIFO结构。
栈的插入操作,也叫进栈,也称压栈、入栈。
栈的删除操作,也叫出栈,也有的叫做弹栈。
### 栈的抽象数据类型
```c
ADT 栈(stack)
Data
同线性表。元素具有相同类型,相邻元素具有前驱和后继关系。
Operation
InitStack(*S): 初始化操作,建立一个空栈。
DestroyStack(*S): 若栈存在,则销毁它。
ClearStack(*S): 将栈清空。
StackEmpty(S): 若栈为空返回true否则返回false。
GetTop(S, *e): 若栈存在且非空用e返回S的栈顶元素。
Push(*S, e): 若栈S存在插入新元素e到栈S中并成为栈顶元素。
Pop(*S, *e): 删除栈S中栈顶元素并用e返回其值。
StackLength(S): 返回栈S的元素个素。
endADT
```
### 栈的顺序存储结构及实现
结构定义
```c
typedef int SElemType; /*SElemType类型根据实际情况而定这里假设为int*/
typedef struct
{
SElemType data[MAXSIZE];
int top; /*用户栈顶指针*/
}SqStack
```
#### 进栈操作
```c
/*插入元素e为新的栈顶元素*/
Status Push(SqStack *S, SElemType e)
{
if (S->top == MAXSIZE - 1) /*栈满*/
return ERROR;
S->top++; /*栈顶指针增加1*/
S->data[S->top]=e; /*将新插入元素赋值给栈顶空间*/
return OK;
}
```
#### 出栈操作
```c
/*若栈不空则删除S的栈顶元素用e返回其值并返回OK否则返回ERROR*/
Status Pop(SqStack *S, SElemType *e)
{
if (S->top==-1)
return ERROR;
*e=S->data[S->top];
S->top--;
return OK;
}
```
### 两栈共享空间
结构定义
```c
/*两栈共享空间结构*/
typedef struct
{
SElemType data[MAXSIZE];
int top1; /*栈1栈顶指针*/
int top2; /*栈2栈顶指针*/
}SqDoubleStack;
```
#### 进栈操作
```c
/*插入元素e为新的栈顶元素*/
Status Push(SqDoubleStack *S, SElemType e, int stackNumber)
{
if (S->top1 + 1 == S->top) /*栈已满不能再push新元素了*/
return ERROR;
if (stackNumber == 1) /*栈1有元素进栈*/
S->data[++S->top1]=e; /*先增加栈1栈顶指针然后赋值为栈1栈顶元素*/
if (stackNumber == 2) /*栈2有元素进栈*/
S->data[++S->top2]=e; /*先增加栈2栈顶指针然后赋值为栈2栈顶元素*/
return OK;
}
```
#### 出栈操作
```c
/*若栈不空则删除S的栈顶元素用e返回其值并返回OK否则返回ERROR*/
Status Pop(SqDoubleStack *S, SElemType *e, int stackNumber)
{
if (stackNumber==1)
{
if (S->top1 == -1)
return ERROR; /*栈1已经是空栈溢出*/
*e=S->data[S->top1--]; /*将栈1栈顶元素出栈*/
}
else if (stackNumber==2)
{
if (S->top2 == MAXSIZE -1)
return ERROR; /*栈2已经是空栈溢出*/
*e=S->data[S->top2++]; /*将栈2栈顶元素出栈*/
}
return OK;
}
```
### 栈的链式存储结构及实现
结构如下
```c
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode, *LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack;
```
#### 进栈操作
```c
/*插入元素e为新的栈顶元素*/
Status Push(LinkStack *S, SElemType e)
{
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
s->data = e;
s->next=S->top; /*把当前的栈顶元素赋值给新结点的直接后继*/
S->top = s; /*将新的结点s赋值给栈顶指针*/
S->count++;
return OK;
}
```
#### 出栈操作
```c
/*若栈不空则删除S的栈顶元素用e返回其值并返回OK否则返回ERROR*/
Status Pop(LinkStack *S, SElemType *e)
{
LinkStackPtr p;
if (StackEmpty(*S))
return ERROR;
*e=S->top->data;
p=S->top; /*将栈顶结点赋值给p*/
S->top=S->top->next; /*使得栈顶指针下移一位,指向后一结点*/
free(p); /*释放结点p*/
S->count--;
return OK;
}
```
### 栈的应用——递归
#### 斐波那契数列
前面相邻两项之和,构成了后一项。
> 常规的迭代方法实现打印出前40位的斐波那契数列数
```c
int main()
{
int i;
int a[40];
a[0] = 0;
a[1] = 1;
printf("%d ", a[0])
printf("%d ", a[1])
for (i = 2; i < 40; i++)
{
a[i] = a[i-1] + a[i-2];
printf("%d ", a[i]);
}
return 0;
}
```
> 斐波那契数列的递归函数
```c
int Fbi(int i)
{
if ( i < 2 )
return i == 0 ? 0 : 1
return Fbi(i-1) + Fbi(i-2);
}
int main()
{
int i;
for (int i = 0; i < 40; i++)
printf("%d ", Fbi(i));
return 0;
}
```
#### 递归定义
把一个直接调用自己或通过一系列的调用语句简介地调用自己的函数,称作递归函数。
每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出。
### 栈的应用——四则运算表达式求值
#### 后缀(逆波兰)表示法定义
一种不需要括号的后缀表达法,我们也把它称为逆波兰(Reverse Polish Notation, RPN)表示。
叫后缀的原因在于所有的符号都是在要运算数字的后面出现。
#### 后缀表达式计算结果
后缀表达式
`9 3 1 - 3 * + 10 2 / +`
规则:从左到右遍历表达式的每个数字和符号,遇到数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
1. 初始化一个空栈。此栈用来对要运算的数字进出使用。
2. 后缀表达式中的前三个都是数字,所以`9`、`3`、`1`进栈
3. 接下来是`-`,所以将栈中的`1`出栈做为减数,`3`出栈做为被减数,并运算`3-1`得到`2`,再将`2`进栈
4. 接下来是数字`3`进栈
5. 后面是`*`,也就意味着栈中`3`和`2`出栈,`2`与`3`相乘,得到`6`,并将`6`进栈。
6. 下面是`+`,所以栈中`6`和`9`出栈,`9`和`6`相加,得到`15`,将`15`进栈
7. 接着是`10`与`2`两数字进栈
8. 接下来是符号`/`,因此,栈顶的`2`与`10`出栈,`10`与`2`相除,得到`5`,将`5`进栈
9. 最后一个符号`+`,所以`15`与`5`出栈并相加,得到`20`,将`20`进栈
10. 结果是`20`出栈,栈变为空。
#### 中缀表达式转后缀表达式
平时所用的标准四则运算表达式,即`9 + (3 - 1) * 3 + 10 / 2`叫做中缀表达式。
中缀表达式`9 + (3 - 1) * 3 + 10 / 2`
转化为后缀表达式`9 3 1 - 3 * + 10 2 / + `
规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即称为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直刀最终输出后缀表达式为止。
1. 初始化一空栈,用来对符号进出栈使用。
2. 第一个数字是`9`,输出`9`,后面是符号`+`,进栈
3. 第三个字符是``,依然是符号,因其是左括号,还未配对,故进栈。
4. 第四个字符是数字`3`,输出,总表达式为`9 3`,接着是`-`,进栈。
5. 接下来是数字`1`,输出,总表达式为`9 3 1`,后面是符号``,此时我们需要去匹配此前的``,所以栈顶一次出栈,并输出,直到``出栈为止。此时左括号上方只有`-`,因此输出`-`。总的输出表达式为`9 3 1 -`。
6. 接着是数字`3`,输出,总表达式为`9 3 1 - 3`。紧接这是符号`*`,因此此时的栈顶符号为`+`号,优先级低于`*`,因此不输出,`*`进栈
7. 之后是符号`+`,此时当前栈顶元素`*`比这个`+`优先级高,因此栈中元素出栈并输出(没有比`+`号更低的优先级,所以全部出栈),总输出表达式为`9 3 1 - 3 * +`。然后将当前这个符号`+`进栈也就是说前6步的栈底的`+`指的是中缀表示式开头的`9`后面的那个`+`,而现在栈底的`+`指的是最后一个`+`
8. 紧接着数字`10`,输出,总表达式变为`9 3 1 - 3 * + 10`,后面是符号 `/`,所以 `/`进栈
9. 最后一个数字`2`,输出,总的表达式为`9 3 1 - 3 * + 10 2`
10. 因已经到最后了,所以将栈中符号全部出栈并输出,最终输出到后缀表达式结果为`9 3 1 - 3 * + 10 2 / +`
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,155 @@
---
layout: post
title: "大话数据结构:队列"
date: 2020-06-14 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 队列
**队列**Queue是只允许在一端进入插入操作而在另一端进行删除操作的线性表
队列是一种先进先出First In First Out的线性表简称FIFO。允许插入的一端称为队尾允许删除的一端称为对头。
### 队列的抽象数据类型
```c
ADT 队列(Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue(*Q): 初始化操作建立一个空队列Q。
DestroyQueue(*Q): 若队列Q存在则销毁它。
ClearQuene(*Q): 将队列Q清空。
QueueEmpty(Q): 若队列Q为空返回true否则返回false。
GetHead(Q, *e): 若队列Q存在且非空用e返回队列Q的对头元素。
EnQueue(*Q, e): 若队列Q存在插入新元素e到队列Q中并称为队尾元素。
DeQueue(*Q, e): 删除队列Q中队头元素并用e返回其值。
QueueLength(Q): 返回队列Q的元素个数。
endADT
```
### 循环队列
队列这种头尾相接的顺序存储结构称为循环队列。
最大尺寸为QueueSIze
队列满的条件是`(rear+1)%QueueSize == front`
通用的计算队列长度公式:`(rear-front+QueueSize)%QueueSize`
#### 结构
```c
typedef int QElemType; /*QElemType类型根据实际情况而定这里假设为int*/
/*循环队列的顺序存储结构*/
typedef struct
{
QElemType data[MAXSIZE];
int front; /*头指针*/
int rear; /*尾指针,若队列不空,指向队列尾元素的下一个位置*/
}SqQueue;
```
#### 初始化
```c
Status InitQueue(SqQueue *Q)
{
Q->front=0;
Q->rear=0;
return OK;
}
```
#### 求队列长度
```c
int QueueLength(SqQueue Q)
{
return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}
```
#### 入队操作
```c
/*若队列未满则插入元素e为Q新的队尾元素*/
Status EnQueue(SqQueue *Q, QElemType e)
{
if ((Q->rear+1)%MAXSIZE == Q->front) /*队列满的判断*/
return ERROR;
Q->data[Q->rear]=e; /*将元素e赋值给队尾*/
Q->rear=(Q->rear+1)%MAXSIZE; /*rear指针向后移一位若到最后则转到数组头部*/
return OK;
}
```
#### 出队操作
```c
/*若队列不空则删除Q中队头元素用e返回其值*/
Status DeQueue(SqQueue *Q, QElemType *e)
{
if (Q->front == Q->rear) /*队列空的判断*/
return ERROR;
*e=Q->data[Q->front]; /*将队头元素赋值给e*/
Q->front=(Q->front+1)%MAXSIZE; /*front指针向后移一位若到最后则转到数组头部*/
return OK;
}
```
### 队列的链式存储结构及实现
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。
#### 结构
```c
typedef int QElemType; /*QElemType类型根据实际情况而定这里假设为int*/
typedef struct QNode /*结点结构*/
{
QElemType data;
struct QNode *next;
}Qnode, *QueuePtr;
typedef struct /*队列的链表结构*/
{
QueuePtr front, rear; /*队头,队尾指针*/
}LinkQueue;
```
#### 入队操作
```c
/*插入元素e为Q的新的队尾元素*/
Status EnQueue(LinkQueue *Q, QElemType e)
{
QueuePtr s =(QueuePtr)malloc(sizeof(QNode));
if(!s) /*存储分配失败*/
exit(OVERFLOW);
s->data=e;
s->next=NULL;
Q->rear->next=s; /*把拥有元素e新结点s赋值给原队尾结点的后继*/
Q->rear=s; /*把当前的s设置为队尾结点rear指向s*/
return OK;
}
```
#### 出队操作
```c
/*若队列不空删除Q的队头元素用e返回其值并返回OK否则返回ERROR*/
Status DeQueue(LinkQueue *Q, QElemType *e)
{
QueuePtr p;
if (Q->front==Q->rear)
return ERROR;
p=Q->front->next; /*将欲删除的队头结点暂存给p*/
*e = p->data; /*将欲删除的对头结点的值赋值给e*/
Q->front->next=p->next /*将原队头结点后继p->next赋值给头结点后继*/
if (Q->rear == p) /*若队头是队尾则删除后将rear指向头结点*/
Q->rear = Q->front;
free(p);
return OK;
}
```
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,289 @@
---
layout: post
title: "大话数据结构:串"
date: 2020-06-15 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 串
**串**string是由零个或多个字符组成的有限序列又名叫字符串。
一般记为`s="a1a2...an"(n>=0)`其中s是串的名称用双引号有些地方也用作单引号括起来的字符序列是串的值注意引号不属于串的内容。ai(1<=1<=n)可以是字母、数字或其它字符i就是该字符在串中的位置。串中字符数目n称为串的长度定义中谈到“有限”是指长度n是一个有限的数值。零个字符的串称为空串null string),它的长度为零,可以直接用两双引号““””表示,也可以用字母`𝛷`来表示。所谓的序列,说明串的相邻字符之间具有前驱和后继的关系。
空格串,是只包含空格的串。注意它与空串的区别,空格串是有内容有长度的,而且可以不止一个空格。
子串与主串,串中任意个数的连续字符组成的子序列称为该串的子串,相应地,包含子串的串称为主串。子串在主串中的位置就是子串的第一个字符在主串中的序号。
### 串的比较
给定两个串s="a1a2...an", t="b1b2...bn",当满足以下条件之一时s<t
1. n<m, 且ai=bi(i=1, 2, ..., n)
2. 存在某个k <= min(m, n), 使得ai=bi(i=1, 2, ..., k-1), ak<bk
### 串的抽象数据类型
```c
ADT 串(string)
Data
串中元素仅由一个字符组成,相邻元素具有前驱和后继关系。
Operation
StrAssign(T, *chars): 生成一个其值等于字符串常量chars的串T。
StrCopy(T, S): 串S存在由串S复制得串T.
ClearString(S): 串S存在将串清空。
StringEmpty(S): 若串S为空返回true否则返回false。
StrLength(S): 返回串S的元素个数即串的长度。
StrCompare(S, T): 若S>T,返回值>0,若S=T,返回0若S<T,返回值<0.
Concat(T,S1,S2): 用T返回由S1和S2联接而成的新串。
SubString(Sub,S,pos,len): 串S存在1<=pos<=StrLength(S)且0<=len<=StrLength(S)-pos+1,用Sub返回串S的第pos个字符起长度为len的子串。
Index(S,T,pos): 串S和T存在T是非空串1<=pos<=StrLength(S)。若主串S中存在和串T值相同的子串则返回它在主串S中第pos个字符之后第一次出现的位置否则返回0.
Replace(S,T,V): 串S、T和V存在T是非空串。用V替换主串S中出现的所有与T相等的不重叠的子串。
StrInsert(S,pos,T): 串S和T存在1<=pos<=StrLength(S)+1。在串S的第pos个字符之前插入串T。
StrDelete(S,pos,len): 串S存在1<=pos<=StrLength(S)-len+1。从串S中删除第pos个字符起长度为len的子串。
endADT
```
#### 操作Index
```c
/*T为非空串。若主串S中第pos个字符之后存在与T相等的子串 则返回第一个这样的子串在S中的位置否则返回0*/
int Index(String S, String T, int pos)
{
int n, m, i;
String sub;
if (pos > 0 )
{
n = StrLength(S); /*得到主串S的长度*/
m = StrLength(T); /*得到子串T的长度*/
i = pos;
while (i <= n-m+1)
{
SubString(sub, S, i, m); /*取主串第i个位置长度与T相等子串给sub*/
if (StrCompare(sub, T) != 0) /*如果两串不相等*/
++i;
else
return i; /*如果两串相等则返回i值*/
}
}
return 0; /*若无子串与T相等返回0*/
}
```
### 串的顺序存储结构
串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列的。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区。一般是用定长数组来定义。
既然是定长数组就存在一个与定义的最大串长度一般可以将实际的串长度值保存在数组0下标位置有的书中也会定义存储在数组的最后一个下标位置。但也有些编程语言不想这么干觉得存个数字占个空间麻烦。它规定在串值后面加一个不计入串长度的结束标记字符比如"\0"来表示串值的终结,这个时候,你要向知道此时的串长度,就需要遍历计算一下才知道了,其实这还是需要占用一个空间,何必呢。
刚才讲的串的顺序存储方式其实是有问题的因为字符串的操作比如两串的连接Concat新串的插入StrInsert以及字符串的替换Replace都有可能使得串序列的长度超过了数组的长度MaxSize
### 串的链式存储结构
对于串的链式存储结构,与线性表是相似的,但由于串结构的特殊性,结构中的每个元素数据是一个字符,如果用简单的应用链表存储串值,一个结点对应一个字符,就会存在很大的空间浪费。因此,一个结点可以存放一个字符,也可以考虑存放多个字符,最后一个结点若是未被占满时,可以用“#”或者其它非串值字符补全。
### 朴素的模式匹配算法
找一个单词在一篇文章中的定位问题。这种子串的定位操作通常称作串的模式匹配。
从主串S="goodgoogle"中找到T="google"这个子串的位置。步骤如下:
1. 主串S第一位开始S与T前三个字母都匹配成功但S第四个字母是d而T的是g。第一位匹配失败。
2. 主串S第二位开始主串S首字母是o要匹配的T首字母是g匹配失败
3. 主串S第三位开始主串S首字母是o要匹配的T首字母是g匹配失败
4. 主串S第四位开始主串S首字母是d要匹配的T首字母是g匹配失败
5. 主串S第五位开始S与T6个字母全匹配匹配成功。
简单的说就是对主串的每一个字符做为子串开头与要匹配的字符串进行匹配。对主串做大循环对每个字符开头做T的长度小循环知道匹配成功或全部遍历完成为止。
#### 用基本的数组来实现该算法
```c
/*返回子串T在主串S中第pos个字符之后的位置。若不存在则函数返回值为0*/
/*T非空1<=pos<=StrLength(S)*/
int Index(String S, String T, int pos)
{
int i = pos; /*i用于主串S中当前位置下标若pos不为1则从pos位置开始匹配*/
int j = 1; /*j用于子串T中当前位置下标值*/
while (i <= S[0] && j <= T[0])
{
if (S[i] == T[j]) /*两字母相等则继续*/
{
++i;
++j;
}
else /*指针后退重新开始匹配*/
{
i = i-j+2; /*i退回到上次匹配首位的下一位*/
j = 1; /*j退回到子串T的首位*/
}
}
if (j > T[0])
return i-T[0];
else
return 0;
}
```
### KMP模式匹配算法
在很多年前我们的科学家们觉得像这种有多个0和1重复字符的字符串却要挨个遍历的算法是非常糟糕的事情。于是有三位前辈D.E.Knuth、J.H.Morris和V.R.Pratt其中Knuth和Pratt共同研究Morris独立研究发表一个模式匹配算法可以大大避免重复遍历的情况我们把它称之为克努特-莫里斯-普拉特算法简称KMP算法。
#### next数组值推导
![image-20210628192152671](/img/in-post/2020-06-15-datastruct-string-01.png)
具体如何推导出一个串的next数组值呢。
> T="abcdex"
1. 当j=1时next[1]=0
2. 当j=2时j由1到j-1就只有字符“a",属于其它情况next[2]=1;
3. 当j=3时j由1到j-1串是“ab”显然“a"与“b"不相等属其它情况next[3]=1;
4. 以后同理所以最终此T串的next[j]为011111
> T="abcabx"
1. 当j=1时next[1]=0;
2. 当j=2时同上例说明next[2]=2;
3. 当j=3时同上next[3]=1;
4. 当j=4时同上next[4]=1;
5. 当j=5时此时j由1到j-1的串是"abca"前缀字符“a"与后缀字符“a“相等因此可推算出k的值为2p1..pk-1'='pj-k+1...pj-1',得到p1=p4)因此next[5]=2;
6. 当j=6时j由1到j-1的串是”abcab“由于前缀字符”ab“与后缀”ab“相等所以next[6]=3。
我们可以根据经验得到如果钱后缀一个字符相等k值是2两个字符k值是3
> T="ababaaaba"
1. 当j=1时next[1]=0;
2. 当j=2时同上next[2] = 1;
3. 当j=3时同上next[3]=1;
4. 当j=4时j由1到j-1到串是“aba"前缀字符“a”与后缀字符“a”相等next[4]=2;
5. 当j=5时j由1到j-1的串是“abab”由于前缀字符“ab”与后缀“ab”相等所以next[5]=3;
6. 当j=6时j由1到j-1的串是“ababa”由于前缀字符“aba”与后缀“aba”相等所以next[6]=4;
7. 当j=7时j由1到j-1的串是“ababaa”由于前缀字符“ab”与后缀“aa”并不相等只有“a”相等所以next[7]=2;
8. 当j=8时j由1到j-1的串是“ababaaa”只有“a”相等所以next[8]=2;
9. 当j=9时j由1到j-1到串是“ababaaab”由于前缀字符“ab”与后缀“ab”相等所以next[9]=3。
> T="aaaaaaaab"
1. 当j=1时next[1]=0;
2. 当j=2时同上next[2]=1;
3. 当j=3时j由1到j-1的串是“aa”前缀字符“a”与后缀字符“a”相等next[3]=2;
4. 当j=4时j由1到j-1的串是“aaa”由于前缀字符“aa”与后缀“aa”相等所以next[4] =3;
5. ......
6. 当j=9时j由1到j-1的串是“aaaaaaaa”由于前缀字符“aaaaaaa”与后缀“aaaaaaa”相等所以next[9]=8。
#### KMP模式匹配算法实现
```c
/*通过计算返回子串T的next数组*/
void get_next(String T, int *next)
{
int i,j;
i = 1;
j = 0;
next[1]=0;
while (i<T[0]) /*此处T[0]表示串T的长度*/
{
if (j == 0 || T[i] == T[j]) /*T[i]表示后缀的单个字符T[j]表示前缀的单个字符*/
{
++i;
++j;
next[i] = j;
}
else
j = next[j]; /*若字符不相同则j值回溯*/
}
}
```
```c
/*返回子串T在主串S中第pos个字符之后的位置。若不存在则函数返回值为0。*/
/*T非空1<=pos<=StrLength(S)*/
int Index_KMP(String S, String T, int pos)
{
int i = pos; /*i用于主串S当前位置下标值若pos不为1则从pos位置开始匹配*/
int j = 1; /*j用于子串T中当前位置下标值*/
int next[255]; /*定义一next数组*/
get_next(T, next); /*对串T作分析得到next数组*/
while (i <= S[0] && j <=T[0]) /*若i小于S的长度且j小于T的长度时循环继续*/
{
if (j==0 || S[i] == S[j]) /*两字母相等则继续与朴素算法增加了j=0判断*/
{
++i;
++j;
}
else /*指针后退重新开始匹配*/
{
j = next[j]; /*j退回合适的位置i值不变*/
}
}
if (j > T[0])
return i-T[0];
else
return 0;
}
```
#### KMP模式匹配算法改进
```c
/*求模式串T的next函数修正值并存入数组nextval*/
void get_nextval(String T, int *nextval)
{
int i, j;
i=1;
j=0;
nextval[1]=0;
while (i<T[0]) /*此处T[0]表示串T的长度*/
{
if (j == 0 || T[i] == T[j]) /*T[i]表示后缀的单个字符T[j]表示前缀的单个字符*/
{
++i;
++j;
if (T[i]!=T[j]) /*若当前字符与前缀字符不同则当前的j为nextval在i位置的值*/
nextval[i] = j;
else
nextval[i] = nextval[j]; /*如果与前缀字符相同则将前缀字符的nextval值赋值给nextval在i位置的值*/
}
else
j = nextval[j]; /*若字符不相同则j值回溯*/
}
}
```
#### nextval数组值推导
> T="ababaaaba"
先算出next数组的值分别为001234223然后再分别判断
1. 当j=1时nextval[1]=0;
2. 当j=2时因第二位字符“b”的next值是1而第一位就是“a”它们不相等所以nextval[2] = next[2] = 1,维持原值
3. 当j=3时因第三位字符"a"的next值为1所以与第一位的“a”比较得知它们相等所以nextval[3] = nextval[1] = 0;
4. 当j=4时第四位的字符"b"next值为2所以与第二位的“b”相比较得到的结果时相等的因此nextval[4] = nextval[2] = 1;
5. 当j=5时next值为3第五个字符“a”与第三个字符“a”相等因此nextval[5] = nextval[3] = 0;
6. 当j=6时next值为4第六个字符“a”与第四个字符“b”不相等因此nextval[6] = 4
7. 当j=7时next值为2第七个字符“a”与第二个字符“b”不相等因此nextval[7] = 2
8. 当j=8时next值为2第八个字符“b”与第二个字符“b”相等因此nextval[8] = nextval[2] = 1;
9. 当j=9时next值为3第九个字符“a”与第三个字符“a”相等因此nextval[9] = nextval[3] = 1。
> T="aaaaaaaab"
先算出next数组的值分别为012345678然后再分别判断
1. 当j=1时nextval[1] = 0;
2. 当j=2时next值为1第二个字符与第一个字符相等所以nextval[2] = nextval[1] = 0;
3. 同样的道理其后都为0.......
4. 当j=9时next值为8第九个字符"b"与第八个字符"a"不相等所以nextval[9] = 8。
总结改进过的KMP算法它是在计算出next值的同时如果a位字符与它next值指向的b位字符相等则该a位的nextbal就指向b位的nextval值如果不等则该a位的nextval值就是它自己a位的next的值。
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,163 @@
---
layout: post
title: "大话数据结构:树"
date: 2020-06-16 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 树
Tree是nn>=0个结点的有限集。n=0时称为空树。在任意一棵非空树中
1. 有且仅有一个特定的称为根Root的结点
2. 当n>1时其余结点可以分为mm>0个互不相交的有限集T1T2...Tm其中每一个集合本身又是一棵树并且称为根的子树SubTree
对于树的定义还需要强调两点:
1. n>0时根结点是唯一的不可能存在多个根结点别和现实中的大树混在一起现实中的树有很多根须那是真实的树数据结构中的树是只能有一个根结点。
2. m>0子树的个数没有限制但它们一定是互不相交的。
### 结点分类
树的结点包含一个数据元素及若干指向其子树的分支。结点拥有的子树数称为结点的度Degree)。度为0的结点称为叶结点Leaf或终端结点度不为0的结点称为非终端结点或分支结点。除根结点之外分支结点也称为内部结点。树的度是树内各结点的度的最大值。
### 结点间关系
结点的子树的根称为该结点的孩子Child相应地该结点称为孩子的双亲Parent。同一个双亲的孩子之间互称兄弟Sibling。结点的祖宗是从根到该结点所经分支上的所有结点。反之以某结点为根的子树中的任意结点都称为该结点的子孙。
### 树的其它相关概念
结点的层次Level从根开始定义起根为第一层根的孩子为第二层。若某结点在第一层则其子树的根就在第1+1层。其双亲在同一层的结点互为堂兄弟。树中结点的最大层次称为树的深度Depth或高度。
如果将树中结点的各子树看成从左到右是有次序的,不能互换的,则称为该树为有序树,否则称为无序树。
森林Forest是mm>=0)棵互不相交的树的集合。对树中每个结点而言,其子树的结点集合则为森林。
对比线性表与树的结构:
* 线性结构
* 第一个数据元素:无前驱
* 最后一个数据元素:无后继
* 中间元素:一个前驱一个后继
* 树结构
* 根结点:无双亲,唯一
* 叶结点:无孩子,可以多个
* 中间结点:一个双亲,多个孩子。
### 树的抽象数据类型
```c
ADT 树(tree)
Data
树是由一个根结点和若干棵子树构成的。树中结点具有相同数据类型及层次关系
Operation
InitTree(*T): 构造空树T。
DestroyTree(*T): 销毁树T。
CreateTree(*T, definition): 按definition中给出树的定义来构造树。
ClearTree(*T): 若树存在则将树T清为空树。
TreeEmpty(T): 若T为空树返回true否则返回false。
TreeDepth(T): 返回T的深度。
Root(T): 返回T的根结点。
Value(T, cur_e): cur_e时树T中一个结点返回此结点的值。
Assign(T, cur_e, value): 给树T的结点cur_e赋值为value。
Parent(T, cur_e): 若cur_e是树T的非根结点则返回它的双亲否则返回空。
LeftChild(T, cur_e): 若cur_e时树T的非叶结点则返回它的最左孩子否则返回空。
RightSibling(T, cur_e): 若cur_e有右兄弟则返回它的右兄弟否则返回空。
Insert(*T, *p, i, c): 其中p指向树T的某个结点i为所指结点p的度加上1非空树c与T不相交操作结果为插入c为树T中p指结点的第i棵子树。
DeleteChild(*T, *p, i): 其中p指向树T的某个结点i为所指结点p的度操作结果为删除T中p所指结点的第i棵子树。
endADT
```
### 树的存储结构
#### 双亲表示法
在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。也就是说,每个结点除了知道自己是谁以外,还知道它的双亲在哪里。
其中data是数据域存储结点的数据信息。而parent是指针域存储该结点双亲在数组中的下标。
```c
/*树的双亲表示法结点结构定义*/
#define MAX_TREE_SIZE 100
typedef int TElemType; /*树结点的数据类型,目前暂定为整型*/
typedeff struct PTNode /*结点结构*/
{
TElemType data; /*结点数据*/
int parent; /*双亲位置*/
} PTNode;
typedef struct
{
PTNode nodes[MAX_TREE_SIZE]; /*结点数组*/
int r, n; /*根的位置和结点数*/
} PTree;
```
有了这样的结构定义,我们就可以来实现双亲表示法了。由于根结点是没有双亲的。所以我们约定根结点的位置域设置为-1这也就意味着我们所有的结点都存有它双亲的位置。
这样的存储结构我们可以根据结点parent指针很容易找到它的双亲结点所用的时间复杂度是O(1)直到parent为-1时表示找到了树结点的根。可我们要知道结点的孩子是什么对不起请遍历整个结构才行。
我们增加一个结点最左边孩子的域,不妨叫它长子域,这又就可以很容易得到结点的孩子。如果没有孩子的结点,这个长子域就设置为-1。
对于有0个或1个孩子结点来说这样的结构是解决了要找结点孩子的问题了。甚至是有2个孩子知道了长子是谁另一个当然就是次子了。
另一个问题场景,我们很关注各兄弟之间的关系,双亲表示法无法体现这样的关系,可以增加一个右兄弟域来体现兄弟关系,也就是说,每一个结点如果它存在右兄弟,则记录下右兄弟的下标。同样的,如果右兄弟不存在,则赋值为-1。
但如果结点的孩子很多超过了2个。我们又关注结点的双亲又关注结点的孩子还关注结点的兄弟而且对时间遍历要求还比较高我们还可以把此结构扩展为有双亲域、长子域、再有右兄弟域。存储结构的设计是一个非常灵活的过程。一个存储结构设计得是否合理取决于给予该存储结构的运算是否适合、是否方便、时间复杂度好不好等。注意也不是越多越好有需要时再设计相应的结构。
#### 孩子表示法
由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法。
**方案一**
一种是指针域的个数就等于树的度。
其中data是数据域。child1到childd是指针域用来指向该结点的孩子结点。
这种方法对于树中个结点的度相差很大时,显然是很浪费空间的,因为有很多的结点,它的指针域都是空的。不过如果树的各结点度相差很小时,那就意味着开辟的空间被充分利用了,这时存储结构的缺点反而变成了优点。
**方案二**
每个结点指针域的个数等于该结点的度。我们专门取一个位置来存储结点指针域的个数。
其中data为数据域degree为度域也就是存储该结点的孩子结点的个数child1到childd为指针域指向该结点的各个孩子的结点。
这种方法客服来浪费空间的缺点,对空间利用率是很高了,但是由于各个结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的损耗。
**孩子表示法**是把每个结点的孩子结点排列起来以单链表作存储结构则n个结点有n个孩子链表如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表采用顺序存储结构存放进一个一维数组中。
为此,设计两种结点结构,一个是孩子链表的的孩子结点。
其中child是数据域用来存储某个结点在表头数组中的下标。next是指针域用来存储指向某结点的下一个孩子结点的指针。
另一个是表头数组的表头结点。
其中data是数据域存储某结点的数据信息。firstchild是头指针域存储该结点的孩子链表的头指针。
```c
/*树的孩子表示法结构定义*/
#define MAX_TREE_SIZE 100
typedef struct CTNode /*孩子结点*/
{
int child;
struct CTNode *next;
} *ChildPTr;
typedef struct /*表头结构*/
{
TElemType data;
ChildPtr firstchild;
} CTBox;
typedef struct /*树结构*/
{
CTBox nodes[MAX_TREE_SIZE]; /*结点数组*/
int r, n; /*根的位置和结点数*/
} CTree;
```
#### 孩子兄弟表示法
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟若果存在也是唯一的。因此,我们设置两个指针,分别只想该结点的第一个孩子和此结点的右兄弟。
其中data是数据域firstchild为指针域存储该结点的第一个孩子结点的存储地址rightsib是指针域存储该结点的右兄弟结点的存储地址。
```c
/*树的孩子兄弟表示法结构定义*/
typedef struct CSNode
{
TElemType data;
struct CSNode *firstchild, *rightsib;
} CSNode, *CSTree;
```
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,277 @@
---
layout: post
title: "大话数据结构:二叉树"
date: 2020-06-17 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 二叉树
比如开和关、0和1、真和假、上和下、对与错正面与反面等都适合用树状结构来建模而这种树是一种很特殊的树状结构叫做二叉树。
**二叉树Binary Tree**是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
### 二叉树特点
- 每个结点最多有两棵子树所以二叉树中不存在度大于2的结点。注意不是只有两棵子树而是最多有没有子树或者有一棵子树都是可以的。
- 左子树和右子树是有顺序的,次序不能任意颠倒。就像人是双手、双脚,但显然左手、左脚和右手、右脚是不一样的,右手戴左手套、右脚穿左鞋都会极其别扭和难受
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树具有五种基本形态:
1. 空二叉树。
2. 只有一个根结点
3. 根结点只有左子树
4. 根结点只有右子树
5. 根结点既有左子树又有右子树
### 特殊二叉树
#### 斜树
所有的结点都是只有左子树的二叉树叫左斜树。
所有的结点都是只有右子树的二叉树叫右斜树。
这两种统称为**斜树**。
斜树有明显的特点,就是每一层只有一个结点,结点的个数与二叉树的深度相同
#### 满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为**满二叉树**
满二叉树的特点:
1. 叶子只能出现在最下层。出现在其他层就不能达成平衡
2. 非叶子结点的度一定是2.否则就是“缺胳膊少腿”了。
3. 在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
#### 完全二叉树
对一棵具有n个结点的二叉树按层序编号如果编号为 i1<=i<=n的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同则这棵二叉树称为完全二叉树
完全二叉树的特点:
1. 叶子结点 只能出现在最下两层
2. 最下层的叶子一定集中在左部连续位置
3. 倒数二层,若有叶子结点,一定都在右部连续位置
4. 如果结点度为1则该结点只有左孩子即不存在只有右子树的情况
5. 同样结点树的二叉树,完全二叉树的深度最小。
### 二叉树的性质
- 二叉树性质1:**在二叉树的第i层上至多有2^(i-1)个结点i>=1)**
- 二叉树性质2:**深度为k的二叉树至多有2^k-1个结点k>=1**
- 二叉树性质3:**对任何一棵二叉树T如果其终端结点数为n0度为2的的结点数为n2则n0 = n2 + 1**
- 二叉树性质4:**具有n个结点的完全二叉树的深度为[log2^n]=1()([x]表示不大于x的最大整数)**
- 二叉树性质5:**如果对一棵有n个结点的完全二叉树其深度为[log2^n]+1的结点按层序编号从第1层到第[log2^n]+1层每层从左到右对任一结点i1<=i<=n**
1. 如果i=1则结点i是二叉树的根无双亲如果i>1则其双亲是结点[i/2]。
2. 如果2i>n则结点i无左孩子结点i为叶子结点否则其左孩子是结点2i
3. 如果2i+1>n则结点i无右孩子否则其右孩子是结点2i+1
### 二叉树的存储结构
#### 顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系,比如双亲和孩子的关系,左右兄弟的关系等。
#### 二叉链表
二叉树莓哥结点最多有两个孩子,座椅为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。
二叉链表的结点结构定义代码:
```c
/* 二叉树的二叉链表结点结构定义 */
typedef struct BiTNode /* 结点结构 */
{
TElemType data; /* 结点数据 */
struct BiTNode *lchild, *rchild; /* 左右孩子指针 */
} BiTNode, *BiTree;
```
### 遍历二叉树
**二叉树的遍历traversing binary tree**是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次
#### 前序遍历
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树
![image-20210810111018807](/img/in-post/2020-06-17-datastruct-binary-tree-01.png)
前序遍历算法:
```c
/* 二叉树的前序遍历递归算法 */
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
printf("%c", T->data); /* 显示结点数据,可以更改为其他对结点操作 */
PreOrderTraverse(T->lchild); /* 再先序遍历左子树 */
PreOrderTraverse(T->rchild); /* 最后先序遍历右子树 */
}
```
#### 中序遍历
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后访问根结点,最后中序遍历右子树。
![image-20210810111029162](/img/in-post/2020-06-17-datastruct-binary-tree-02.png)
```c
/*二叉树的中序遍历递归算法 */
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return;
InOrderTraverse(T->lchild); /*中序遍历左子树*/
printf("%c", T->data); /* 显示结点数据,可以更改为其他对结点操作 */
InOrderTraverse(T->rchild); /* 最后中序遍历右子树 */
}
```
#### 后序遍历
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。
![image-20210810111159592](/img/in-post/2020-06-17-datastruct-binary-tree-03.png)
```c
/* 二叉树的后序遍历递归算法 */
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lchild); /* 先后续遍历左子树 */
PostOrderTraverse(T->rchild); /* 再后续遍历右子树 */
printf("%c", T->data); /* 显示结点数据,可以更改为其他对结点操作 */
}
```
#### 层序遍历
规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问
![image-20210810111311617](/img/in-post/2020-06-17-datastruct-binary-tree-04.png)
#### 推导遍历结果
已知前序和后序遍历,是不能确定一棵二叉树的。
### 二叉树的建立
按照前序输入二叉树中结点的值,算法如下:
```c
/* 按前序输入二叉树中结点的值(一个字符)*/
/* #表示空树构造二叉链表表示二叉树T */
void CreateBiTree(BiTree *T)
{
TElemType ch;
scanf("%c", &ch);
if (ch == '#')
*T=NULL;
else
{
*T=(BiTree)malloc(size(BiTNode));
if(!*T)
exit(OVERFLOW);
(*T)->data=ch; /* 生成根结点 */
CreateBiTree(&(*T)->lchild); /* 构造左子树 */
CreateBiTree(&(*T)->rchild); /* 构造右子树 */
}
}
```
### 线索二叉树
指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为**线索二叉树Threaded Binary Tree**
对二叉树以某种次序遍历使其变为线索二叉树的过程称作是线索化
- ltag为0时指向该结点的左孩子为1时指向该结点的前驱。
- rtag为0时指向该结点的右孩子为1时指向该结点的后继。
#### 线索二叉树结构实现
二叉树的线索存储结构定义代码如下:
```c
/* 二叉树的二叉线索存储结构定义 */
typedef enum {Link, Thread} PointerTag; /* Link==0 表示指向左右孩子指针 */
/* Thread==1 表示指向前驱或后继的线索 */
typedef struct BiThrNode /* 二叉线索存储结点结构 */
{
TElemType data; /* 结点数据 */
struct BiThrNode *lchild, *rchild; /* 左右孩子指针 */
PointerTag LTag;
PointerTag RTag; /* 左右标志 */
} BiThrNode, *BiThrTree;
```
线索化的过程就是在遍历的过程中修改空指针的过程
中序遍历线索化的递归函数代码如下:
```c
BiThrTree pre; /* 全局变量,始终指向刚刚访问过的结点 */
/* 中序遍历进行中序线索化 */
void InThreading(BiThrTree p)
{
if(p)
{
InThreading(p->lchild); /* 递归左子树线索化 */
if(!p->lchild){
p->LTag=Thread; /* 前驱线索 */
p->lchild=pre; /* 左孩子指针指向前驱 */
}
if(!pre->rchild){
pre->RTag=Tread; /* 后继线索 */
pre->rchild=p; /* 前驱有孩子指针指向后继当前结点p*/
}
pre=p; /* 保持pre指向p的前驱 */
InThreading(p->rchild); /* 递归右子树线索化 */
}
}
```
中序遍历的代码:
```c
/* T指向头结点头结点左链lchild指向根结点头结点右链rchild指向中序遍历的最后一个结点。中序遍历二叉线索链表示的二叉树T */
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p = T->lchild; /* p指向根结点 */
while (p != T)
{
while (p->LTag==Link) /*当LTag==0时循环到中序序列第一个结点*/
p = p->lchild;
printf("%c", p->data); /*显示结点数据,可以更改为其他对结点操作*/
while(p->RTag==Thread && p->rchild!=T)
{
p = p->rchild;
printf("%c", p->data);
}
p = p->rchild; /* p进至其右子树根 */
}
return OK;
}
```
**如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择**
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,60 @@
---
layout: post
title: "大话数据结构:树、森林与二叉树的转换"
date: 2020-06-18 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 树、森林与二叉树的转换
### 树转换为二叉树
将树转换为二叉树的步骤如下:
1. 加线。在所有兄弟结点之间加一条连线
2. 去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
3. 层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子树二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
### 森林转换为二叉树
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作。步骤如下:
1. 把每个树转换为二叉树
2. 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了由森林转换来的二叉树
### 二叉树转换为树
二叉树转换为树树树转换为二叉树的逆过程,也就是反过来做而已。步骤如下:
1. 加线。若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点...反正就是左孩子的n个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。
2. 去线。删除原二叉树中所有结点与其右孩子结点的连线。
3. 层次调整。使之结构层次分明。
### 二叉树转换为森林
判断一棵二叉树能够转换成一棵树还是森林,标准很简单,那就是只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树。那么如果树转换成森林,步骤如下:
1. 从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若孩子存在,则连线删除......,直到所有右孩子连线都删除为止,得到分离的二叉树。
2. 再将每棵分离后的二叉树转换为树即可。
### 树与森林的遍历
树的遍历分为两种方式。
1. 一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树。
2. 另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根结点。
森林的遍历也分为两种方式:
1. **前序遍历**:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再一次用同样方式遍历除去第一棵树的剩余树构成的森林。
2. **后序遍历**:是先访问森林中的第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再一次同样方式遍历除去第一棵树的剩余树构成的森林。
森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,37 @@
---
layout: post
title: "大话数据结构:赫夫曼树及其应用"
date: 2020-06-19 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 赫夫曼树及其应用
### 赫夫曼树定义与原理
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称作路径长度。
树的路径长度就是从树根到每一结点的路径长度之和
带权路径长度WPL最小的二叉树称为赫夫曼树
构造赫夫曼树的赫夫曼算法描述。
1. 根据给定的n个权值{w1,w2, ... , wn} 构成n棵二叉树的集合F={T1, T2, ..., Tn}其中每棵二叉树Ti中只有一个带权为wi根结点其左右子树均为空
2. 在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树且置新的二叉树的根结点的权值为其左右子树上根结点权值之和。
3. 在F中删除这两棵树同时将新得到的二叉树加入F中。
4. 重复2和3步骤直到F只含一棵树为止。这棵树便是赫夫曼树
### 赫夫曼编码
若要设计长短不等的编码,则必须是任一字符的编码都不是另一字符的编码的前缀,这种编码称作前缀编码。
一般地,设需要编码的字符集为{d1,d2,...,dn},各个字符在电文中出现的次数或频率集合为{w1,w2,...,wn}以d1,d2,...,dn作为叶子结点以w1,w2,...,wn作为相应叶子结点的权值来构造一棵赫夫曼树。规定赫夫曼树的左分支代表0右分支代表1则从根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码这就是赫夫曼编码
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,84 @@
---
layout: post
title: "大话数据结构:图"
date: 2020-06-20 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 图
Graph是由顶点的有穷非空集合和顶点之间边的集合组成通常表示为G(V,E)其中G表示一个图V是图G中顶点的集合E是图G中边的集合
- 在图中数据元素我们称之为顶点Vertex
- 在图结构中不允许没有顶点。在定义中若V是顶点的集合则强调了顶点集合V有穷非空。
- 在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的
无向边若顶点vi到vj之间的边没有方向则称这条边为无向边Edge用无序偶对(vi,vj)来表示。如果图中任意两个顶点之间的边都是无向边则称该图为无向图Undirected graphs
有向边若从顶点vi到vj的边有方向则称这条边为有向边也称为弧Arc用有序偶对<vi,vj>来表示vi称为弧尾Tailvj称为弧头Head。如果图中任意两个顶点之间的边都是有向边则称为该图为有向图Directed graphs
在图中,若不存在顶点到其自身的边,且同一边不重复出现,则称这样的图为简单图。
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。
在有向图中,如果任意两个顶点都存在方向互为相反的两条弧,则称该图为有向完全图。
有很少条边或弧的图称为稀疏图,反之称为稠密图。
与图的边或弧相关的数叫做权Weight
这种带权的图通常称为网Network
假设有两个图G=(V,{E}) 和G' = (V',{E'})如果V'⊆V且E'⊆E则称G'为G的子图Subgraph
### 图的顶点与边之间关系
对于无向图G=(V,{E})如果边v,v')∊E,则顶点v和v'互为邻接点Adjacent即v和v'相邻接。边v,v'依附incident于顶点v和v'或者说v,v'与顶点v和v'相关联。顶点v的度Degree)是和v相关联的边的数目记为TDv
对于有向图G=(V,{E}),如果弧<v,v'>∊E则称顶点v邻接到顶点v'顶点v'邻接自顶点v。弧<v,v'>和顶点vv'相关联。以顶点v为头的弧的数目称为v的入度InDegree记为IDv以v为尾的弧的数目称为v的出度OutDegree记为ODv顶点v的度为TDv=IDv+ODv
无向图G=(V,{E})中从顶点v到顶点v'到路径Path是一个顶点序列v=vi,0,vi,1, ... , vi,m=v')其中(vi,j-1,vi,j)∊E1<=j<=m
路径的长度是路径上的边或弧的数目
第一个顶点到最后一个顶点相同的路径称为回路或环Cycle。序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外其余顶点不重复出现的回路称为简单回路或简单环
### 连通图相关术语
在无向图G中如果从顶点v到顶点v'有路径则称v和v'是连通的。如果对于图中任意两个顶点vi、vj∊Evi和vj都是连通的则称G是连通图Connected Gragh
无向图中的极大连通子图称为连通分量
- 要是子图
- 子图要是连通的
- 连通子图含有极大顶点数
- 具有极大顶点数的连通子图包含依附于这些顶点的所有边
在有向图G中如果对于每一对vi、vj∊V、vi⧧vj从vi到vj和vj到vi都存在路径则称G是强连通图。有向图中的极大强连通子图称作有向图的强连通分量。
一个连通图的生成树是一个极小的连通子图它含有图中全部的n个顶点但只有足以构成一棵树的n-1条
如果一个有向图恰好有一个顶点的入度为0其余顶点的入度均为1则是一棵有向树。
一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。
### 图的定义与术语总结
图按照有无方向分为无向图和有向图。无向图由顶点和边构成,有向图由顶点弧构成。弧有弧尾和弧头之分。
图按照边或弧的多少分稀疏图和稠密图。如果任意两个顶点之间都存在边叫完全图,有向的叫有向完全图。若无重复的边或顶点到自身的边叫简单图。
图中顶点之间邻接点、依附的概念。无向图顶点的边数叫做度,有向图顶点分为入度和出度。
图上的边或弧上带权则称为网。
图中顶点存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量。
无向图中连通且n个顶点n-1条边叫生成树。有向图中一顶点入度为0其余项点入度为1点叫有向树。一个有向图由若干棵有向树构成生成森林。
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,169 @@
---
layout: post
title: "大话数据结构:图的存储结构"
date: 2020-06-21 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 图的抽象数据类型
```c
ADT 图(Graph)
Data
顶点的有穷非空集合和边的集合。
Operation
CreateGraph(*G, V, VR): 按照顶点集V和边弧集VR的定义构造图G
DestroyGraph(*G): 图G存在则销毁
LocateVex(G, u): 若图G中存在顶点u则返回图中的位置
GetVex(G, v): 返回图G中顶点v的值
PutVex(G, v, value): 将图G中顶点v赋值value
FirstAdjVex(G, *v): 返回顶点v的一个邻接顶点若顶点在G中无邻接顶点返回空
NextAdjVex(G, v, *w): 返回顶点v相对于顶点w的下一个邻接顶点若w是v的最后一个邻接点则返回“空”
InsertVex(*G, v): 在图G中增添新顶点
DeleteVex(*G, v): 删除图G中顶点v及其相关的弧
InsertArc(*G, v, w): 在图G中增添弧<v,w>, 若G是无向图还需要增添对称弧<w,v>
DeleteArc(*G, v, w): 在图G中删除弧<v,w>, 若G是无向图则还删除对称弧<w,v>
DFSTraverse(G): 对图G中进行深度优先遍历在遍历过程对每个顶点调用
HFSTraverse(G): 对图G中进行广度优先遍历在遍历过程对每个顶点调用
endADT
```
## 图的存储结构
### 邻接矩阵
图的邻接矩阵Adjacency Matrix存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息一个二维数组称为邻接矩阵存储图中的边或弧的信息。
**邻接矩阵存储的结构**,代码如下:
```c
typedef char VertexType; /*顶点类型应由用户定义*/
typedef int EdgeType; /*边上权值类型应由用户定义*/
#define MAXVEX 100 /*最大顶点数,应由用户定义*/
#define INFINITY 65535 /*用65535来代表∞*/
typedef struct
{
VertexType vexs[MAXVEX]; /*顶点表*/
EdgeType arc[MAXVEX][MAXVEX]; /*邻接矩阵,可看作边表*/
int numVertexes, numEdges; /*图中当前的顶点数和边数*/
}MGraph;
```
**建立无向网图的邻接矩阵**,代码如下:
```c
void CreateMGraph(MGraph *G)
{
int i, j, k, w;
printf("输入顶点数和边数: \n");
scanf("%d,%d", &G->numVertexes, &G->numEdges); /*输入顶点数和边数*/
for(i = 0; i < G->numVertexes; i++) /*读入顶点信息,建立顶点表*/
scanf(&G->vexs[i]);
for(i = 0; i < G->numVertexes; i++)
for(j = 0; j < G->numVertexes; j++)
G->arc[i][j] = INFINITY; /*邻接矩阵初始化*/
for(k = 0; k < G->numEdges; k++)
{
printf("输入边vi,vj)上的下标i下表j和权w\n");
scanf("%d, %d, %d", &i, &j, &w); /*输入边(vi,vj)上的权w*/
G->arc[i][j]=w;
G->arc[j][i]= G->arc[i][j] /*因为上无向图,矩阵对称*/
}
}
```
### 邻接表
数组与链表相结合的存储方法称为邻接表Adjacency List
邻接表的处理办法是这样的
1. 图中顶点用一个一维数组存储,放入顶点也可以用单链表来存储,不过数组可以较容易地读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息。
2. 图中每个顶点vi的所有邻接点构成一个线性表由于邻接点的个数不定所以用单链表存储无向图称为顶点vi的边表有向图则称为顶点vi作为弧尾的出边表。
顶点表的各个结点由data和firstedge两个域表示data是数据域存储顶点的信息firstedge是指针域指向边表的第一个结点即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex是邻接点域存储某顶点的邻接点在顶点表中的下标next则存储指向边表中下一个结点的指针。比如v1顶点与v0v2互为邻接点则在v1的边表中adjvex分别为v0的0和v2的2
若是有向图邻接表结构是类似的。建立一个有向图的逆邻接表即对每个顶点vi都建立一个链接vi为弧头的表。
对于带权值的网图可以在边表结点定义中再增加一个weight的数据域存储权值信息即可。
**结点定义**的代码如下:
```c
typedef char VertexType; /*顶点类型应由用户定义*/
typedef int EdgeType; /*边上的权值类型应由用户定义*/
typedef struct EdgeNode /*边表结点*/
{
int adjvex; /*邻接点域,存储该顶点对应的下标*/
EdgeType weight; /*用于存储权值,对于非网图可以不需要*/
struct EdgeNode *next; /*链域,指向下一个邻接点*/
}EdgeNode;
typedef struct VertexNode /*顶点表结点*/
{
VertexType data; /*顶点域,存储顶点信息*/
EdgeNode *firstedge; /*边表头指针*/
}VertexNode, AdjList[MAXVEX];
typedef struct {
AdjList adjList;
int numVertexex, numEdges; /*图中当前顶点数和边数*/
}GraghAdjList;
```
**无向图邻接表的创建**,代码如下
```c
void CreateALGraph(GraphAdjList *G)
{
int i, j, k;
EdgeNode *e;
printf("输入顶点数和边数:\n")
scanf("%d,%d", &G->numVertexex, &G->numEdges); /*输入顶点数和边数*/
for (i = 0; i < G->numVertexes; i++)
{
scanf(&G->adjList[i].data); /*输入顶点信息*/
G->adjList[i].firstedge=NULL; /*将边表置为空表*/
}
for(k = 0;k < G->numEdges; k++)
{
printf("输入边vi,vj上的顶点序号\n");
scanf("%d,%d", &i, &j); /*输入边(vi,vj)上的顶点序号*/
e = (EdgeNode *) malloc(sizeof(EdgeNode)); /*向内存申请空间,生成边表结点*/
e -> adjvex=j; /*邻接序号为j*/
e -> next=G->adjList[i].firstedge; /*将e指针指向当前顶点指向的结点*/
G->adjList[i].firstedge=e; /*将当前顶点的指针指向e*/
e = (EdgeNode *)malloc(sizeof(EdgeNode)); /*向内存申请空间,生成边表结点*/
e->adjvex=i; /*邻接序号为i*/
e->next=G->adjList[j].firstedge; /*将e指针指向当前顶点指向的结点*/
G->adjList[j].firstedge=e; /*将当前顶点的指针指向e*/
}
}
```
### 十字链表
把邻接表和逆邻接表结合起来。
顶点表结点结构中firstin表示入边表头指针指向该顶点的入边表中第一个结点firstout表示出边表头指针指向该顶点的出边表中的第一个结点。
边表结点结构中tailvex是指弧起点在顶点表的下标headvex是指弧重点在顶点表中的下标headlink是指入边表指针域指向终点相同的下一条边taillink是指边表指针域指向起点相同的下一条边
如果是网还可以再增加一个weight域来存储权值。
### 邻接多重表
边表结点结构中ivex和jvex是与某条边依附的两个顶点在顶点表中下标。ilink指向依附顶点ivex的下一条边jlink指向依附顶点jvex的下一条边这就是邻接多重表结构
### 边集数组
边集数组是由两个一维数组构成。一个存储顶点的信息另一个是存储边的信息这个边数组每个数据元素由一条边的起点下标begin、终点下标end和权weight组成。
参考资料:
- [大话数据结构]()

View File

@ -0,0 +1,158 @@
---
layout: post
title: "大话数据结构:图的遍历"
date: 2020-06-22 10:01:08
author: "Viletyy"
header-style: text
tags:
- 数据结构
---
## 图的遍历
从图中某一顶点出发访遍图中其余顶点且使每一个顶点仅被访问一次这一过程就叫做图的遍历Traversing Graph
### 深度优先遍历
深度优先遍历Depth_First_Search也有称为深度优先搜索简称为DFS
它从图中某个顶点v出发访问此顶点然后从v的未被访问的邻接点出发深度优先遍历图直至图中所有和v有路径相通的顶点都被访问到。
若图中尚有顶点未被访问,则另选图中一个未曾被访问到顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
邻接矩阵的深度优先递归算法:
```c
typedef int Boolean; /*Boolean是布尔类型其值是TRUE或FALSE*/
Boolean visited[MAX]; /*访问标志的数组*/
void DFS(MGraph G, int i)
{
int j;
visited[i] = TRUE;
printf("%c ", G.vexs[i]); /*打印顶点,也可以进行其他操作*/
for(j = 0; j < G.numVertexes; j++)
if(G.arc[i][j] == 1 && !visited[j])
DFS(G, j); /*对为访问的邻接顶点递归调用*/
}
/*邻接矩阵的深度遍历操作*/
void DFSTraverse(MGraph G)
{
int i;
for(i = 0; i < G.numVertexes; i++)
visited[i] = FALSE; /*初始所有顶点状态都是未访问过状态*/
for(i = 0; i < G.numVertexes; i++)
if(!visited[i]) /*对未访问过的顶点调用DFS若是连通图只会执行一次*/
DFS(G, i);
}
```
邻接表的深度优先递归算法:
```c
void DFS(GraphAdjList GL, int i)
{
EdgeNode *p;
visited[i] = TRUE;
printf("%c ", GL->adjList[i].data); /*打印顶点,也可以其他操作*/
p = GL->adjList[i].firstedge;
while(p)
{
if(!visited[p->adjvex])
DFS(GL, p->adjvex); /*对为未访问的邻接顶点递归调用*/
p = p->next;
}
}
/*邻接表的深度遍历操作*/
void DFSTraverse(GraphAdjList GL)
{
int i;
for(i = 0; i < GL->numVertexes; i++)
visited[i] = FALSE; /*初始所有顶点状态都是未访问过状态*/
for(i = 0; i < GL->numVertexes; i++)
if(!visited[i]) /*对未访问过的顶点调用DFS若是连通图只会执行一次*/
DFS(GL, i);
}
```
### 广度优先遍历
广度优先遍历Breadth_First_Search,又称为广度优先搜索简称BFS
邻接矩阵的广度遍历算法:
```c
void BFSTraverse(MGraph G)
{
int i, j;
Queue Q;
for(i = 0; i < G.numVertexes; i++)
visited[i] = FALSE;
InitQueue(&Q); /*初始化一辅助用的队列*/
for(i = 0; i < G.numVertexex; i++)
{
if (!visited[i]) /*若是未访问过就处理*/
{
visited[i]=TURE; /*设置当前顶点访问过*/
printf("%c ", G.vexs[i]); /*打印顶点,也可以进行其他操作*/
EnQueue(&Q, i); /*将此顶点入队列*/
while(!QueueEmpty(Q)) /*若当前队列不为空*/
{
DeQueue(&Q, &i); /*将队中元素出队列赋值给i*/
for(j = 0; j < G.numVertexes; j++)
{
/*判断其他顶点若与当前顶点存在边且未访问过*/
if(G.art[i][j] == 1 &&. !visited[j])
{
visited[j] = TRUE; /*将找到的此顶点标记为已访问*/
printf("%c ", G.vexs[j]); /*打印顶点*/
EnQueue(&Q, j); /*将找到的此顶点入队列*/
}
}
}
}
}
}
```
邻接表的广度遍历算法
```c
void BFSTraverse(GraphAdjList GL)
{
int i;
EdgeNode *p;
Queue Q;
for(i = 0; i < GL->numVertexes; i++)
visited[i] = FALSE;
InitQueue(&Q);
for(i = 0; i < GL->numVertexes; i++)
{
if (!visited[i])
{
visited[i] = TRUE;
printf("%c ", GL->adjList[i].data); /*打印顶点,也可以进行其他操作*/
EnQueue(&Q, i);
while(!QueueEmpty(Q))
{
DeQueue(&Q, &i);
p = GL->adjList[i].firstedge; /*找到当前顶点边表链表头指针*/
while(p)
{
if(!visited[p->adjvex]) /*若此顶点未被访问*/
{
visited[p->adjvex]=TRUE;
printf("%c ",GL->adjList[p->adjvex].data);
EnQueue(&Q, p->adjvex); /*将此顶点入队列*/
}
p = p->next; /*指针指向下一个邻接点*/
}
}
}
}
}
```
参考资料:
- [大话数据结构]()

Some files were not shown because too many files have changed in this diff Show More