再次感谢
最后更新于:2022-04-01 02:30:46
## Thanks for reading!
![document/2015-08-18/55d2f42169750](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/document_2015-08-18_55d2f42169750.png)
项目学习
最后更新于:2022-04-01 02:30:44
### 项目学习
- [Linux](https://github.com/torvalds/linux)
- [Go](https://github.com/golang/go)
- [Docker](https://github.com/docker/docker)
- [Run](https://github.com/runscripts/run)
- [GoByExample](https://github.com/mmcgrana/gobyexample)
参考书籍
最后更新于:2022-04-01 02:30:41
### 参考书籍
- [理解Unix进程](http://book.douban.com/subject/24298701/)
- [Unix编程艺术](http://book.douban.com/subject/1467587/)
- [Unix环境高级编程](http://book.douban.com/subject/1788421/)
- [Go Web编程](http://book.douban.com/subject/24316255/)
- [Go并发编程实战](http://book.douban.com/subject/26244729/)
后记
最后更新于:2022-04-01 02:30:39
### 后记
最后一章列举本文参考的过的书籍和项目,欢迎大家补充和讨论更多有关进程的知识。
Sendfile系统调用
最后更新于:2022-04-01 02:30:37
### 系统调用sendfile
[Sendfile](http://man7.org/linux/man-pages/man2/sendfile.2.html)是Linux实现的系统调用,可以通过避免文件在内核态和用户态的拷贝来优化文件传输的效率。
其中大名鼎鼎的分布式消息队列服务Kafka就使用sendfile来优化效率,具体用法可参见其[官方文档](http://kafka.apache.org/documentation.html)。
### 优化策略
在普通进程中,要从磁盘拷贝数据到网络,其实是需要通过系统调用,进程也会反复在用户态和内核态切换,频繁的数据传输在此有效率问题。因此我们必须意识到Linux给我们提供了sendfile这样的系统调用,可以提高进程的数据传输效率。
捕获SIGKILL
最后更新于:2022-04-01 02:30:34
### 捕获SIGKILL
SIGKILL是常见的Linux信号,我们使用`kill`命令杀掉进程也就是像进程发送SIGKILL信号。
和其他信号不同,[SIGKILL](https://en.wikipedia.org/wiki/Unix_signal#SIGKILL)和SIGSTOP是不可被Catch的,因此下面的代码是能编译通过但也是无效的,更多细节可以参考[golang/go#9463](https://github.com/golang/go/issues/9463).
~~~
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGKILL, syscall.SIGSTOP)
~~~
### 注意事项
这是Linux内核的限制,这种限制也是为了让操作系统有可能控制进程的生命周期,理解后我们也不应该去尝试捕获SIGKILL。
不过还是有人这样去做,最后结果也不符合预期,这需要我们对底层有足够的理解。
创建目录权限
最后更新于:2022-04-01 02:30:32
### 创建目录权限
如果你想创建一个目录并授予777权限,你需要怎么做?查看Go的API文档我们可以这样写。
源文件为mkdir.go。
~~~
package main
import (
"fmt"
"os"
)
func main() {
err := os.MkdirAll("/tmp/gotest/", 0777)
if err != nil {
panic(err)
}
fmt.Println("Mkdir /tmp/gotest/")
}
~~~
### 运行结果
~~~
➜ understand_linux_process_examples git:(master) ✗ ll /tmp/
drwxr-xr-x 2 tobe wheel 68B Dec 30 10:06 gotest
➜ understand_linux_process_examples git:(master) ✗ umask
022
~~~
### 正确做法
代码在mkdir_umask.go中。
~~~
package main
import (
"fmt"
"os"
"syscall"
)
func main() {
mask := syscall.Umask(0)
defer syscall.Umask(mask)
err := os.MkdirAll("/tmp/gotest/", 0777)
if err != nil {
panic(err)
}
fmt.Println("Mkdir /tmp/gotest/")
}
~~~
### 注意事项
这并不是Go的Bug,包括Linux系统调用都是这样的,创建目录除了给定的权限还要加上系统的Umask,Go也是如实遵循这种约定。
如果你想达到你的预期权限,知道Umask及其用法是必须的。
注意事项
最后更新于:2022-04-01 02:30:30
### 第五章 注意事项
对进程有了深入理解后,我们编写实际应用可能遇到这些坑,这里总结一下。
代码实现
最后更新于:2022-04-01 02:30:28
### 实现Run
### 实现Flock
前面提到进程的文件锁,实际上Run也用到了,可以试想下以下的场景。
用户A执行`run pt-summary`,由于本地已经缓存了所以会直接运行本地的脚本。同时用户B执行`run -u pt-summary`,加上`-u`或者`--update`参数后Run会从远端下载并运行最新的脚本。如果不加文件锁的话,用户A的行为就不可预测了,而文件锁很好得解决了这个问题。
具体使用方法如下,我们封装了以下的接口。
~~~
var lockFile *os.File
// Lock the file.
func Flock(path string) error {
return fcntlFlock(syscall.F_WRLCK, path)
}
// Unlock the file.
func Funlock(path string) error {
err := fcntlFlock(syscall.F_UNLCK)
if err != nil {
return err
} else {
return lockFile.Close()
}
}
// Control the lock of file.
func fcntlFlock(lockType int16, path ...string) error {
var err error
if lockType != syscall.F_UNLCK {
mode := syscall.O_CREAT | syscall.O_WRONLY
mask := syscall.Umask(0)
lockFile, err = os.OpenFile(path[0], mode, 0666)
syscall.Umask(mask)
if err != nil {
return err
}
}
lock := syscall.Flock_t{
Start: 0,
Len: 1,
Type: lockType,
Whence: int16(os.SEEK_SET),
}
return syscall.FcntlFlock(lockFile.Fd(), syscall.F_SETLK, &lock)
}
~~~
在运行脚本前就调用锁进程的方法。
~~~
// Lock the script.
lockPath := cacheDir + ".lock"
err = flock.Flock(lockPath)
if err != nil {
utils.LogError("%s: %v\n", lockPath, err)
os.Exit(1)
}
~~~
### 实现HTTP请求
使用Run时它会自动从网上下载脚本,走的HTTP协议,具体实现方法如下。
~~~
// Retrieve a file via HTTP GET.
func Fetch(url string, path string) error {
response, err := http.Get(url)
if err != nil {
return err
}
if response.StatusCode != 200 {
return Errorf("%s: %s", response.Status, url)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
}
if strings.HasPrefix(url, MASTER_URL) {
// When fetching run.conf, etc.
return ioutil.WriteFile(path, body, 0644)
} else {
// When fetching scripts.
return ioutil.WriteFile(path, body, 0777)
}
}
`
~~~
Run的总体代码是很简单的,主要是通过解析run.conf下载相应的脚本并执行。
项目架构
最后更新于:2022-04-01 02:30:25
### Run项目架构
Run是一个命令行工具,没有复杂的CS或BS架构,只是通过解析命令行或者配置文件来下载运行相应的脚本。
### Flock
Run使用了前面提到的进程文件锁,避免同时运行同一个脚本。同时运行同一个脚本会有什么问题呢?例如我们`run pt-summary`,同时另一个终端执行`run -u pt-summary`,这样前一个命令有可以使用旧脚本也可能使用新脚本,这需我们规避这样的问题。
项目实例Run
最后更新于:2022-04-01 02:30:23
### 第四章 项目实例Run
Run是开源的脚本管理工具,官方网站[http://runscripts.org](http://runscripts.org),项目地址[https://github.com/runscripts/run](https://github.com/runscripts/run)。
Run可以执行任意的脚本,当然使用到Go库提供的系统调用程序。
Namespaces
最后更新于:2022-04-01 02:30:21
### Namespaces简介
Linux Namespaces是资源隔离技术,在2.6.23合并到内核,而在3.12内核加入对用户空间的支持。
Namespaces是容器技术的基础,因为有了命名空间的隔离,才能限制容器之间的进程通信,像虚拟内存对于物理内存那样,开发者无需针对容器修改已有的代码。
### 使用Namespaces
阅读以下教程前建议看看,[https://blog.jtlebi.fr/2013/12/22/introduction-to-linux-namespaces-part-1-uts/](https://blog.jtlebi.fr/2013/12/22/introduction-to-linux-namespaces-part-1-uts/)。
Linux内核提供了`clone`系统调用,创建进程时使用`clone`取代`fork`即刻创建同一命名空间下的进程。
更多参数建议`man clone`来学习。
Cgroups
最后更新于:2022-04-01 02:30:18
### Cgroups
Cgroups全称Control Groups,是Linux内核用于资源隔离的技术。目前Cgroups可以控制CPU、内存、磁盘访问。
### 使用
Cgroups是在Linux 2.6.24合并到内核的,不过项目在不断完善,3.8内核加入了对内存的控制(kmemcg)。
要使用Cgroups非常简单,阅读前建议看sysadmincasts的视频,[https://sysadmincasts.com/episodes/14-introduction-to-linux-control-groups-cgroups。](https://sysadmincasts.com/episodes/14-introduction-to-linux-control-groups-cgroups。)
我们首先在文件系统创建Cgroups组,然后修改这个组的属性,启动进程时指定加入的Cgroups组,这样进程相当于在一个受限的资源内运行了。
### 实现
Cgroups的实现也不是特别复杂。有一个特殊的数据结构记录进程组的信息。
有人可能已经知道Cgroups是Docker容器技术的基础,另一项技术也是大名鼎鼎的Namespaces。
Copy On Write
最后更新于:2022-04-01 02:30:16
### 写时复制(Copy On Write)
一般我们运行程序都是Fork一个进程后马上执行Exec加载程序,而Fork的是否实际上用的是父进程的堆栈空间,Linux通过Copy On Write技术极大地减少了Fork的开销。
Copy On Write的含义是只有真正写的时候才把数据写到子进程的数据,Fork时只会把页表复制到子进程,这样父子进程都指向同一个物理内存页,只有再写子进程的时候才会把内存页的内容重新复制一份。
共享内存
最后更新于:2022-04-01 02:30:14
### 共享内存
对于共享内存是好是坏,我们不能妄下定论,不过学习一下总是好的。
不同进程之间内存空间是独立的,也就是说进程不能访问也不会干扰其他进程的内存。如果两个进程希望通过共享内存的方式通信呢?可以通过`mmap()`系统调用实现。
### Go实例
Go也实现了`mmap()`函数支持共享内存,不过也是通过cgo来调用C实现的系统调用函数。Cgo是什么?它是Go调用C语言模块的功能,当然这种调用很可能是平台相关的,也就是无法保证在Windows也能正确运行。
具体代码参见[Golang对共享内存的操作](http://studygolang.com/articles/743),有时间我们也愿意写一个更简单易懂的例子。
Epoll
最后更新于:2022-04-01 02:30:12
### 简介
Epoll是poll的改进版,更加高效,能同时处理大量文件描述符,跟高并发有关,Nginx就是充分利用了epoll的特性。讲这些没用,我们先了解poll是什么。
### Poll
Poll本质上是Linux系统调用,其接口为`int poll(struct pollfd *fds,nfds_t nfds, int timeout)`,作用是监控资源是否可用。
举个例子,一个Web服务器建了多个socket连接,它需要知道里面哪些连接传输发了请求需要处理,功能与`select`系统调用类似,不过`poll`不会清空文件描述符集合,因此检测大量socket时更加高效。
### Epoll
我们重点看看epoll,它大幅提升了高并发服务器的资源使用率,相比poll而言哦。前面提到poll会轮询整个文件描述符集合,而epoll可以做到只查询被内核IO事件唤醒的集合,当然它还提供边沿触发(Edge Triggered)等特性。
不知大家是否了解C10K问题,指的是服务器如何支持同时一万个连接的问题。如果是一万个连接就有至少一万个文件描述符,poll的效率也随文件描述符的更加而下降,epoll不存在这个问题是因为它仅关注活跃的socket。
### 实现
这是怎么做到的呢?简单来说epoll是基于文件描述符的callback函数来实现的,只有发生IO时间的socket会调用callback函数,然后加入epoll的Ready队列。更多实现细节可以参考Linux源码,
### Mmap
无论是select、poll还是epoll,他们都要把文件描述符的消息送到用户空间,这就存在内核空间和用户空间的内存拷贝。其中epoll使用mmap来共享内存,提高效率。
Mmap不是进程的概念,这里提一下是因为epoll使用了它,这是一种共享内存的方法,而Go语言的设计宗旨是"不要通过共享来通信,通过通信来共享",所以我们也可以思考下进程的设计,是使用mmap还是Go提供的channel机制呢。
文件描述符
最后更新于:2022-04-01 02:30:09
### 文件描述符
Linux很重要的设计思想就是一切皆文件,网络是文件,键盘等外设也是文件,很神奇吧?于是所有资源都有了统一的接口,开发者可以像写文件那样通过网络传输数据,我们也可以通过`/proc/`的文件看到进程的资源使用情况。
内核给每个访问的文件分配了文件描述符(File Descriptor),它本质是一个非负整数,在打开或新建文件时返回,以后读写文件都要通过这个文件描述符了。
### 应用
我们想想操作系统打开的文件这么多,不可能他们共用一套文件描述符整数吧?这样想就对了,Linux实现时这个fd其实是一个索引值,指向每个进程打开文件的记录表。
POSIX已经定义了STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO三个常量,也就是0、1、2。这三个文件描述符是每个进程都有的,这也解释了为什么每个进程都有编号为0、1、2的文件而不会与其他进程冲突。
文件描述符帮助应用找到这个文件,而文件的打开模式等上下文信息存储在文件对象中,这个对象直接与文件描述符关联。
### 限制
注意了,每个系统对文件描述符个数都有限制。我们网上看到配置`ulimit`也是为了调大系统的打开文件个数,因为一般服务器都要同时处理成千上万个起请求,记住socket连接也是文件哦,使用系统默认值会出现莫名奇怪的问题。
讲文件描述符其实是为高深莫测的epoll做铺垫,掌握epoll对进程已经有很深的理解了。
Linux系统调用
最后更新于:2022-04-01 02:30:07
### 系统调用
我们要想启动一个进程,需要操作系统的调用(system call)。实际上操作系统和普通进程是运行在不同空间上的,操作系统进程运行在内核态(todo: kernel space),开发者运行得进程运行在用户态(todo: user space),这样有效规避了用户程序破坏系统的可能。
如果用户态进程想执行内核态的操作,只能通过系统调用了。Linux提供了超多系统调用函数,我们关注与进程相关的系统调用后面也会详细讲解。
信号
最后更新于:2022-04-01 02:30:05
### 信号
我们知道信号是进程间通信的其中一种方法,当然也可以是内核给进程发送的消息,注意信息只是告诉进程发生了什么事件,而不会传递任何数据。
这是进程这个概念设计时就考虑到的了,因为我们希望控制进程,就像一个小孩我们想他按我们的想法做,前提就是他能够接受信号并且理解信号的含义。
### 信号种类
Linux中定义了很多信号,不同的Unix-like系统也不一样,我们可以通过下面的命令来查当前系统支持的种类。
~~~
➜ kill -l
HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2
~~~
其中1至31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),32到63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。
简单介绍几个我们最常用的,在命令行中止一个程序我们一般摁Ctrl+c,这就是发送SIGINT信号,而使用kill命令呢?默认是SIGTERM,加上`-9`参数才是SIGKILL。
### 编程实例
~~~
import os/signal
siganl.Notify()
signal.Stop()
~~~
这是Go封装的信号接口,我们可以以此实现一个简单的信号发送和处理程序。
进程间通信
最后更新于:2022-04-01 02:30:02
### 进程间通信
IPC全称Interprocess Communication,指进程间协作的各种方法,当然包括共享内存,信号量或Socket等。
### 管道(Pipe)
管道是进程间通信最简单的方式,任何进程的标准输出都可以作为其他进程的输入。
### 信号(Signal)
下面马上会介绍。
### 消息队列(Message)
和传统消息队列类似,但是在内核实现的。
### 共享内存(Shared Memory)
后面也会有更详细的介绍。
### 信号量(Semaphore)
信号量本质上是一个整型计数器,调用`wait`时计数减一,减到零开始阻塞进程,从而达到进程、线程间协作的作用。
### 套接口(Socket)
也就是通过网络来通信,这也是最通用的IPC,不要求进程在同一台服务器上。