linux中的进程
在liunx中,每个进程每个时刻都是有状态的,可能的状态共有6个。
一个CPU中同一时刻只能运行一个进程,但是内核可以通过快速切换CPU上的进程造成多个进程同时运行的假象。
但是切换线程是有代价的,这涉及到进程状态的保存与恢复。另外,内核还需要考虑下次切换时运行哪个进程,什么时候切换等等。
解决上面问题的系统角度进程调度系统。
进程切换和进程调用是多个程序并发执行的基础。
原子操作必须由一个个单一的汇编指令表示,并且需要得到芯片级别的支持。
因为原子操作不可以被打断,所以原子操作只用于细粒度的简单操作。
临界区一般用语言层面的互斥锁来实现
不同进程中进行数据交换、信号通知等行为。 IPC是其中一个分类
消息队列是消息的链表,存放在内存中,由内核维护
特点:
信号量主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。
进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。
信号量有以下两种类型:
共享内存允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在进行的进程之间传递数据的一种非常有效的方式。
大多数的共享内存的实现,都把由不同进程之间共享的内存安排为同一段物体内存。
首先我们都知道我们执行的每一个程序,它看到的内存其实都是虚拟内存,虚拟内存需要进行页表的映射将进程地址映射到物理内存,具体处理大致如下面的图
当中共享内存的大致原理相信我们可以看明白了,就是让两个进程地址通过页表映射到同一片物理地址以便于通信,你可以给一个区域里面写入数据,理所当然你就可以从中拿取数据,这也就构成了进程间的双向通信。
共享内存是IPC通信当中传输速度最快的通信方式没有之一,理由很简单,客户进程和服务进程传递的数据直接从内存里存取、放入,数据不需要在两进程间复制,没有什么操作比这简单了。
用共享内存进行数据通信,它对数据也没啥限制,也不要求通信的进程有一定的父子关系。
最后就是共享内存的生命周期随内核。
即所有访问共享内存区域对象的进程都已经正常结束,共享内存区域对象仍然在内核中存在(除非显式删除共享内存区域对象),在内核重新引导之前,对该共享内存区域对象的任何改写操作都将一直保留;简单地说,共享内存区域对象的生命周期跟系统内核的生命周期是一致的,而且共享内存区域对象的作用域范围就是在整个系统内核的生命周期之内。
但是,共享内存也并不完美
/*
* 作用:创建共享内存
* 参数:key_t key 程序需要提供一个参数key,它为共享内存段提供一个外部名。(每个IPC对象都与一个键 即key相关联,然后此键再由内核变换为标识符)。还有一个特殊的键值IPC_PRIVATE(也就是0), 它用于创建一个只属于该创建进程的新共享内存,通常不会用到(就是说key基本上不会是IPC_PRIVATE或者0, 0值是private的,不能在进程间共享。)
* size_t size 以字节为单位指定需要共享的内存容量。
* int shmflg 包含9个比特的权限标志,它们的作用与创建文件时使用的mode标志是一样。权限标志对共享内存非常有用,因为它允许一个进程创建的共享内存可以被共享内存的创建者所拥有的进程写入,同时其它用户创建的进程只能读取共享内存。我们可以利用这个功能来提供一种有效的对数据进行只读访问的方法,通过将数据放共享内存并设置它的权限,就可以避免数据被其他用户修改。
* 返回值: 创建成功,则返回一个非负整数,即共享内存标识;
如果失败,则返回-1.
*/
int shmget(key_t key, size_t size, int shmflg)
IPC对象是活动在内核级别的一种进程间通信的工具。存在的IPC对象通过它的标识符来引用和访问,这个标识符是一个非负整数,它唯一的标识了一个IPC对象,这个IPC对象可以是消息队列或信号量或共享存储器中的任意一种类型。在Linux系统中标识符被声明成整数,所以可能存在的最大标识符为65535。这里标识符与文件描述符有所不同,使用open函数打开一个文件时,返回的文件描述符的值为当前进程最小可用的文件描述符数组的下标。IPC对象删除或创建时相应的标识符的值会不断增加到最大的值,归零循环分配使用。
/*
* 作用:将共享内存端挂载到自己地址空间shmat
* 参数: int shmid 是由shmget函数返回的共享内存标识。
* const void *shmaddr 指定共享内存连接到当前进程中的地址位置,通常为0,表示让系统来选择共享内存的地址。
* int shmflg 是一组标志位,通常为0。它还可取:
* SHM_RND,用以决定是否将当前共享内存段连接到指定的shmaddr上。该参数和shm_addr联合使用,用来控制共享内存连接的地址,除非只计划在一种硬件上运行应用程序,否则不要这样指定。填0让操作系统自己选择是更好的方式。
* SHM_RDONLY单独使用则是指让它使连接的内存段只读,否则以读写方式连接此内存段
* 返回值:调用成功返回挂载的虚拟地址空间起始地址,失败返回NULL
*/
void *shmat(int shmid, const void *shmaddr, int shmflg)
/*
* 作用: 与共享内存段分离 shmdt
* 注意:仅仅是共享内存分离但并未删除它,其标识符及其相关数据结构都在;直到某个进程的IPC_RMID命令的调用shmctl特地删除它为止,
* 只是使得该共享内存对当前进程不再可用。
* 参数: const void *shmaddr shmat返回的地址指针。
* 返回值: 成功时,返回0,失败时,返回-1.
*/
int shmdt(const void *shmaddr)
/*
* 作用: shmctl 共享内存控制函数
* 参数: shm_id : 是shmget返回的共享内存标识符。
* cmd: 它可以取3个值:
* IPC_STAT 把shmid_ds结构中的数据设置为共享内存的当前关联值
* IPC_SET 如果进程有足够的权限就把共享内存的当前关联值设置为shmid_ds结构中给出的值
* IPC_RMID 删除共享内存段
* buf:是一个指针,包含共享内存模式和访问权限的结构。
*/
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
用共享内存来再两进程间交换数据,比如交换一个结构体
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "memory.h"
typedef struct Stu{
int age;
char name[10];
}Stu;
int main(int argc,char *argv[]){
Stu s;
strcpy(s.name, "jack");
//创建共享内存段
int id = shmget(1234, 8, IPC_CREAT | 0644);
if (id == -1){
perror("shmget");
exit(1);
}
//挂载到进程的地址空间
Stu *p = (Stu *)shmat(id, nullptr, 0);
int i = 0;
while (1){
s.age = i++;
memcpy(p, &s, sizeof(Stu)); //写到共享段中
sleep(2);
}
return 0;
}
#include <iostream>
#include <string>
#include "unistd.h"
#include <sys/ipc.h>
#include <sys/shm.h>
typedef struct Stu{
int age;
char name[10];
}Stu;
int main() {
int id = shmget(1234, 8, 0);
if (id == -1){
perror("shmget");
exit(1);
}
//挂载到进程的地址空间
Stu *p = (Stu *)shmat(id, nullptr, 0);
while (1){
std::cout << "age = " << p->age << ", name = " << p->name << "\n";
sleep(2);
}
return 0;
}
golang不提供使用共享内存来通信的API,都是通过cgo来调用C语言来实现的。比较麻烦,暂时不写了。可以参考这个Golang直接操作共享内存
因为go提倡不要通过共享内存来通信,而应该通过通信来共享内存
总结:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信
本质上是调用系统API
#include
/*
* 作用:创建一个匿名管道
* 参数:fds[0]代表读; fds[1] 代表写
* 返回值:若成功返回0,失败返回-1
*/
int pipe(int fd[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())
}
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])
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)
}
总结:
读管道:
写管道:
本质上调用系统API:
#include <sys/stat.h>
/* 功能: 创建一个FIFO文件,该文件是真实存在于文件系统中的。
* 参数: path 为创建命名管道的全路径
* mod 为创建命名管道的模式,指的是其存取权限
* dev为设备值,改值取决于文件创建的种类,它只在创建设备文件是才会用到。
* 返回值:成功返回 0 ,失败返回 -1
*/
int mknod(const char* path, mode_t mod, dev_t dev);
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:
作用:创建命名管道
返回值:
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对每一个标准信号都有默认的操作方式,一定是下面的方式之一。
对于大部分标准信号,我们可以在程序中自定义应该怎么响应它。
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()
}
golang之socket编程详解
进程间共享数据