下面程序都是在单核单线程的CPU的基础上运行的
进程
什么是进程
- 一个程序的执行称为一个进程,所有的代码都是在进程中执行的。
- 进程也是操作系统进程资源分片的基本单位
进程是怎么产生的
- Linux中,除了内核启动进程之外,其他的进程都是由它的父进程产生的。【通过调用fork函数】
fork()
- 头文件:
#include<unistd.h>/*#包含<unistd.h>*/
#include<sys/types.h>/*#包含<sys/types.h>*/
- 函数原型:
pid_t fork( void);
- pid_t 是一个宏定义,其实质是int 被定义在#include<sys/types.h>中。
- 返回值: fork函数被调用一次但返回两次
- 负值:创建子进程失败。
- 在父进程中,fork返回新创建子进程的进程ID;
- 在子进程中,fork返回0;
- 由fork创建的新进程被称为子进程, 子进程和父进程会同时运行[但是谁先运行是不一定的,这取决与内核调度]。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
- fork函数返回的值为什么在父子进程中不同? “其实就相当于链表,进程形成了链表,父进程的fork函数返回的值指向子进程的进程id, 因为子进程没有子进程,所以其fork函数返回的值为0.
- 子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本, 但是会共享代码段,都从fork函数中返回,箭头表示各自的执行处。当父子进程有一个想要修改数据或者堆栈时,两个进程真正分裂。
- 每一个副本都是独立的,子进程对于数据他的副本的修改对于其他进程比如父进程、兄弟进程都是不可见的。如果我们相同进程之间相互感知,必须使用进程间的通信手段来通知。
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char *argv[]){
pid_t pid=fork();
if ( pid < 0 ) {
fprintf(stderr,"错误!");
} else if( pid == 0 ) {
printf("子进程空间");
exit(0);
} else {
printf("父进程空间,子进程pid为%d",pid);
}
// 可以使用wait或waitpid函数等待子进程的结束并获取结束状态
exit(0);
}
- fork返回的pid是一个叫做“文件描述符”的数据结果中的一个属性:
- pid是进程在操作系统中的唯一标志。
- pid是可以重复使用的,比如说前一个为pid=11的进程死掉了,那么pid=11的这个进程就可以分配给其他进程使用了:当pid达到最大限制时,内核会从头开始查找闲置的pid并使用最先找到的那一个作为新进程的id。
- 进程描述符除了记录pid之外,还记录了进程的优先级、状态、虚拟地址范围以及各种访问权限等,还用一个很有用的属性叫做ppid,它是当前进程的父进程的id。
- golang中如何获取pid和ppid
pid := os.Getpid()
ppid := os.Getppid()
进程状态
在liunx中,每个进程每个时刻都是有状态的,可能的状态共有6个。
- 可运行状态(TASK_RUNNING(task_running), 简称R): 系统立刻要或者正在CPU上运行,不过运行的时机是不确定的,这由进程调度器决定。
- 可中断的睡眠状态(TAST_INTERRUNPTIBLE, 简称S):当进程正在等待某个事件(比如网络连接/信号量)到来,此时进程进入对应事件的等待状态中。当事件发生时,对应的等待队列中的一个或者多个进程会被唤醒。
- 不可中断的睡眠状态(TAST_UNINTERRUNPTIBLE, 简称D):与上面的唯一区别在于这种状态的进程不会对任何信号响应。 这样的进程一般是在等待某个特殊事件,比如同步IOC操作 【???这个怎么醒的???】
- 暂停状态或者跟踪状态(TASK_STOPPED或者TASK_TRACED,简称T):
- 向不处于D状态的进程发送SIGSTOP信号,该进程会进入暂停状态,直到另一个进程向它发送SIGCONT信号,这个进程会转为R状态。
- 处于跟踪状态的进程也会暂停,但是向它发送SIGCONT信号,这个进程不会恢复。比如我们使用GDB(调试进程)调试程序时,对应的进程运行到断点处就会停下来,这个时候,该进程就处于跟踪状态,向这个进程发送SIGCONT信号,这个进程不会恢复,只有当调试进程进行响应的系统调用或者退出之后,被跟踪的进程才能恢复。
- 僵尸状态(TASK_DEAD-EXIT_ZOMBIE,简称Z):处于该状态的进程即将结束,该进程占用的绝大多数资源也被回收,不够还有一些信息比如退出码没有被删除。之所以保留这些信息,是因为该进程的父进程可能需要它们。由于此时进程主题已经被删除而只留下一个空壳,所以叫做僵尸进程。
- 退出状态(TASK_DEAD-EXIT_DEAD,简称X):处于退出状态的进程会被结束掉,它占用的系统资源也会被操作系统自动回收。可能进入X状态的原因如下
- 显式的让该进程的父进程忽略掉SIGCHLD信号(当一个进程结束的时候,内核会给父进程发送SIGCHLD)
- 父子进程已经分离:分离后的子进程将不再共享父进程的代码,而是加载一个权限的程序。
简单来说,进程的状态只会在运行和非运行状态之间转换
系统调用
- 普通进程工作在用户空间,内核进程工作在内核空间
- 普通进程式没有办法访问内核空间的
- 用户空间不可与硬件交互,内核可以与硬件交互
- 内核提供了一些API,用户进程通过调用这些API(这个行为叫做系统调用)来访问内核空间,从而操控系统或者内核数据
- 系统调用和普通函数调用的区别:系统调用是向内核康健发出一个明确请求,而普通函数只是定义了如何获取一个给定的服务。
- 为了保证操作系统的稳定和安全,内核提供了两个状态,内核态和用户态。大部分事件CPU处于用户态,这时CPU只能访问用户空间。当CPU调用系统API时,内核会先转为内核态,然后让CPU执行对应的内核函数。当内核函数执行完成知乎,内核会切换回用户态,并将执行结果返回给用户进程。
进程切换和调度
一个CPU中同一时刻只能运行一个进程,但是内核可以通过快速切换CPU上的进程造成多个进程同时运行的假象。
但是切换线程是有代价的,这涉及到进程状态的保存与恢复。另外,内核还需要考虑下次切换时运行哪个进程,什么时候切换等等。
- 什么时候切换: 内核认为当前在CPU上运行的线程已经运行的足够久了,就会把这个进程换掉,让另外一个线程来执行。
解决上面问题的系统角度进程调度系统。
进程切换和进程调用是多个程序并发执行的基础。
关于同步
假设有两个线程正在操作一个共享内存
解决方法:
临界区
加锁形成临界区
原子操作
原子操作和临界区的区别在于原子操作不可以被中断。
原子操作必须由一个个单一的汇编指令表示,并且需要得到芯片级别的支持。
因为原子操作不可以被打断,所以原子操作只用于细粒度的简单操作。
临界区一般用语言层面的互斥锁来实现
进程间通信什么是IPC
不同进程之间的通信就叫做IPC
IPC的目的
- 数据传输:一个进程需要将它的数据发送给另外一个进程
- 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到
- 通知事件:一个进程需要向另一个或者一组线程发送消息,通知它们发生了某种事件
- 资源共享:多个线程之间共享资源 。 【需要通信、同步、锁】
- 进程控制:有些进程希望完全控制另一个进程的执行(比如debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时直到它的状态改变。
管道
匿名管道
- 单向通信
- 父子进程或者兄弟进程中使用
本质上是调用系统API
#include <unistd.h>
int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1
- 调用pipe函数时,首先在内核中开辟一块缓冲区用于通信,它有一个读端和一个写端,然后通过fd参数传出给用户进程两个文件描述符,fd[0]指向管道的读端,fd[1]指向管道的写段。在用户层面看来,打开管道就是打开了一个文件,通过read()或者write()向文件内读写数据,读写数据的实质也就是往内核缓冲区读写数据。
- 匿名管道是半双工的,若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。要关闭管道只需将这两个文件描述符关闭即可。
第1个例子
shell会为每个命令创建一个进程,在shell操作中我们常常用到管道
ps aux | grep go
grep go|ps aux
第2个例子
package main
import "fmt"
import "os/exec"
import "bufio"
import "bytes"
func main() {
//create cmd
cmd_go_env := exec.Command("go", "env")
cmd_grep := exec.Command("grep", "GOROOT")
stdout_env, env_error := cmd_go_env.StdoutPipe()
if env_error != nil {
fmt.Println("Error happened about standard output pipe ", env_error)
return
}
//env_error := cmd_go_env.Start()
if env_error := cmd_go_env.Start(); env_error != nil {
fmt.Println("Error happened in execution ", env_error)
return
}
/*
a1 := make([]byte, 1024)
n, err := stdout_env.Read(a1)
if err != nil {
fmt.Println("Error happened in reading from stdout", err)
return
}
fmt.Printf("Standard output of go env command: %s", a1[:n])
*/
//get the output of go env
stdout_buf_grep := bufio.NewReader(stdout_env)
//create input pipe for grep command
stdin_grep, grep_error := cmd_grep.StdinPipe()
if grep_error != nil {
fmt.Println("Error happened about standard input pipe ", grep_error)
return
}
//connect the two pipes together
stdout_buf_grep.WriteTo(stdin_grep)
//set buffer for reading
var buf_result bytes.Buffer
cmd_grep.Stdout = &buf_result
//grep_error := cmd_grep.Start()
if grep_error := cmd_grep.Start(); grep_error != nil {
fmt.Println("Error happened in execution ", grep_error)
return
}
err := stdin_grep.Close()
if err != nil {
fmt.Println("Error happened in closing pipe", err)
return
}
//make sure all the infor in the buffer could be read
if err := cmd_grep.Wait(); err != nil {
fmt.Println("Error happened in Wait process")
return
}
fmt.Println(buf_result.String())
}
第3个例子
package main
import (
"fmt"
"os/exec"
)
func main(){
cmd := exec.Command("echo", "-n", "pipe communicate") // ===>等效于shell命令 echo -n pipe communicate
stdout1, err := cmd.StdoutPipe() // 将结果输出到一个管道中
if err != nil{
fmt.Println("can't obtain the stdout pipe for command cmd: %s", err)
return
}
if err := cmd.Start(); err != nil{ //启动命令
fmt.Println("cmd can not be startup: %s", err)
}
out := make([]byte, 30)
n, err := stdout1.Read(out)
if err != nil {
fmt.Println("can't read data from the pipe: %s", err)
return
}
fmt.Println(out[:n])
}
- 如果 管道已经被写满了,那么写进程就会被阻塞。
如何读取管道中的数据:
- 方法一:
out := make([]byte, 30)
n, err := stdout1.Read(out)
if err != nil {
fmt.Println("can't read data from the pipe: %s", err)
return
}
fmt.Println(out[:n])
- 方法二:将读取到的数据存放到缓冲区outbuf中
var outbuf bytes.Buffer
for{
out := make([]byte, 30)
n, err := stdout1.Read(out)
if err != nil {
if err == io.EOF{
break
}else{
fmt.Println("can't read data from the pipe: %s", err)
return
}
}
outbuf.Write(out[:n])
}
fmt.Println(outbuf.String())
- 方法三:使用缓冲读取器从管道中读取数据
outbuf1 := bufio.NewReader(stdout1) // 默认情况下,该读取器会携带一个长度为4096的缓冲区
outbuf0, _, err := outbuf1.ReadLine()
if err != nil{
fmt.Println("can't read data from the pipe: %s", err)
return
}
fmt.Println(string(outbuf0))
API:
(c *Cmd) StdoutPipe() (io.ReadCloser, error)
io.ReadCloser是一个扩展了io.Reader接口的接口类型,定义了可关闭的数据读取行为
作用: 将数据读取到p中
参数: []byte用来存储数据
返回值:
* n:
*当管道中数据小于p的长度,n为实际读取到的字节数
*当管道中数据大于等于p的长度,n为p的长度
* 可能没有读完
* 当管道中没有数据可以读取,n返回0,err返回io.EOF(用于判断数据释放已经被读完)
type Reader interface {
Read(p []byte) (n int, err error)
}
命名管道
上述管道虽然实现了进程间通信,但是它具有一定的局限性:
- 匿名管道只能是具有血缘关系的进程之间通信;
- 它只能实现一个进程写另一个进程读,而如果需要两者同时进行时,就得重新打开一个管道。
为了使任意两个进程之间能够通信,就提出了命名管道(named pipe 或 FIFO)。
- 与管道的区别:提供了一个路径名与之关联,以FIFO文件的形式存储于文件系统中,能够实现任何两个进程之间通信。而匿名管道对于文件系统是不可见的,它仅限于在父子进程之间的通信。
- FIFO是一个设备文件,在文件系统中以文件名的形式存在,因此即使进程与创建FIFO的进程不存在血缘关系也依然可以通信,前提是可以访问该路径。
- FIFO(first input first output)总是遵循先进先出的原则,即第一个进来的数据会第一个被读走。
- FIFO默认是阻塞的,只有对这个管道的读操作和写操作都已经准备就绪之后,数据才开始流转
本质上调用系统API:
#include <sys/stat.h>
/*
* 参数: path 为创建命名管道的全路径
* mod 为创建命名管道的模式,指的是其存取权限
* dev为设备值,改值取决于文件创建的种类,它只在创建设备文件是才会用到。
* 返回值:这两个函数都是成功返回 0 ,失败返回 -1
*/
int mknod(const char* path, mode_t mod, dev_t dev);
int mkfifo(const char* path, mode_t mod);
- 这两个函数都能创建一个FIFO文件,该文件是真实存在于文件系统中的。
第1个例子
[[email protected] path]$ ls
src.log
[[email protected] path]$ cat src.log
111111111111111
[[email protected] path]$ mkfifo -m 664 myfifo
[[email protected] path]$ ls
myfifo src.log
[[email protected] path]$ tee dst.log < myfifo &
[2] 39437
[[email protected] path]$ ls
myfifo src.log
[[email protected] path]$ cat src.log > myfifo
[[email protected] path]$ 111111111111111
[2]+ 完成 tee dst.log < myfifo
[[email protected] path]$ ls
dst.log myfifo src.log
[[email protected] path]$ cat dst.log
111111111111111
第2个例子
package main
import (
"fmt"
"os"
)
func main(){
reader, writer, err := os.Pipe()
if err != nil{
fmt.Println("can create named pipe")
return
}
go func() {
n, err := writer.Write([]byte("write"))
if err != nil{
fmt.Println("can write named pipe , %s", err)
return
}
fmt.Println("write ", n)
}()
output := make([]byte, 100)
n, err := reader.Read(output)
if err != nil{
fmt.Println("can'r read data from named piped")
}
fmt.Println("read ", n)
}
API:
- 在os包中
作用:创建命名管道
返回值:
r *File 管道输出端
w *File 管道输入端
err error 可能发生的错误
注意:命名管道的读/写必须同时进行,否则会阻塞到读/写
func Pipe() (r *File, w *File, err error)
上面创建的管道是并发不安全的,在io包中也提供了一个类似的包,创建了一个基于内存的有原子性操作保证的管道【这个管道不是基于文件系统的,所以没有作为中介的缓冲区,所以通过它传递的数据只会被复制一次。】
作用:创建命名管道
返回值:
r *File 管道输出端
w *File 管道输入端
注意:命名管道的读/写必须同时进行,否则会阻塞到读/写
func Pipe() (r *File, w *File)
信号
signal是IPC中唯一一种异步的通信方法,它的本质是用软件来模拟硬件的中断机制
信号的种类
使用kill命令查询当前系统所支持的信号
]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
- linux支持的信号一共有62种(没有32和33号信号)
- 1-31是不可靠信号(标准信号)
- 对同一个进程来说,每种标准信号只会被记录并处理一次
- 如果发送给某一个进程的标准信号的种类有多个,它们的处理顺序是不确定的
- 34-64是可靠信号(实时信号)
- 多个同种类的实时信号都可以记录在案,可以按照信号的发送顺序被处理
- 1-31是不可靠信号(标准信号)
信号的来源
- 键盘输入(比如Ctrl+c)
- 硬件故障
- 系统函数调用
- 软件种的非法运算
如何处理信号
进程响应信号的方式:
- 忽略
- 捕捉
- 执行默认操作
linux对每一个标准信号都有默认的操作方式,一定是下面的方式之一。
- 终止进程
- 忽略信号
- 终止进程并保存内存信息
- 停止进程
- 恢复进程(如果进程已经停止)
对于大部分标准信号,我们可以在程序中自定义应该怎么响应它。
- SIGKILL和SIGSTOP不能自行处理,也不能忽略,对它们的响应只能是系统的默认操作
golang与信号
os/signal
type Signal interface {
String() string
Signal() // to distinguish from other Stringers
}
API:
/*
* 作用:当操作系统当当前进程发送指定信号是发出通知。 当用户接收到相应信号之后,就不会执行默认操作,而是由用户自定义行为
*/
func Notify(c chan<- os.Signal, sig ...os.Signal)
/*
* 作用:恢复系统的默认操作
*/
func Stop(c chan<- os.Signal)
第一个例子:
package main
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
)
func main() {
sigRecv := make(chan os.Signal, 1)
var wg sync.WaitGroup
wg.Add(1)
go func() {
fmt.Println("11111")
// 运行程序之后,会阻塞到这里。如果你ctrl+c,会发现程序没有终止,而是打印 receiver:Internpt
for s := range sigRecv {
fmt.Println("receiver:",s)
}
wg.Done()
}()
fmt.Println("--------")
sigs := []os.Signal{syscall.SIGINT, syscall.SIGQUIT}
signal.Notify(sigRecv, sigs...)
// signal.Stop(sigRecv)
// close(sigRecv) // 必须关闭,否则sigRecv的接收通道会一致阻塞
fmt.Println("****")
wg.Wait()
}