进程通信IPC
我们知道系统本身就是一个程序,将封装好的方法提供给用户层,从而起到中间层的作用。PIPE通信是最早的线程通信方法,在内存中分配一块内存空间作为管道,创建管道的叫服务器,连接管道的叫客户机,在一个客户机向管道写入数据后,另一个管道就可以读出来,而为了防止读取和写入阻塞,每个客户机在通信时都需要建立两个连接,分别负责读出和写入。
PIPE
管道流只适用与父子进程之间,两个不同的程序是无法进行的(内存隔离)。
在环境里默认会有一个名字叫做unistd.h
的头文件,这个头文件可以在内存空间上创建一个数组,这个数组可以用作管道:
1 2
| #include<unistd.h> pipe(int pp[2]);
|
它可以为pp数组分配2个文件描述符,一个用于读,一个用于写。管道的使用需要遵守以下的约定:
- 先写入后读取,管道是一种环形队列,先进先出
- 管道是无大小的,他不是结构体,而是一个流
- 管道的数据一旦被读取过了就会从管道里面消失,就像自来水管里面的水,流走了就没有了
对于这样一个父子进程之间使用管道进行通信,父进程开启pp[1]关闭pp[0],子进程开启pp[0]关闭pp[1],这样就可以进行通信了。
同一个程序如果需要管道,只需要把管道当作数组使用即可。
FIFO
如果需要程序之间通信,管道就需要变成有名管道,有名管道属于系统头文件,并不在linux源码里面,下面示范以下如何创建一个这样的管道:
1 2 3 4 5 6 7 8 9
| #include <stdio.h> #include <sys/types.h> #include <sys/stat.h>
int main(){ int res = mkfifo("Mino",2333); if(res != 0) printf("Error"); }
|
我们运行一下就会发现,这个Mino管道可以被ls,也就是说,FIFO管道会变成Linux文件系统中的一部分,也就是说它可以像文件一样进行读写操作。
1 2 3 4 5 6 7 8 9
| minloha@minloha:~/c$ gcc main.c minloha@minloha:~/c$ ls a.out main.c minloha@minloha:~/c$ ./a.out minloha@minloha:~/c$ ls -l total 20 pr-S--xr-x 1 minloha minloha 0 Dec 23 13:55 Mino -rwxr-xr-x 1 minloha minloha 16008 Dec 23 13:55 a.out -rwxr-xr-x 1 root root 142 Dec 23 13:55 main.c
|
我们写两个程序,一个负责读取,一个负责写入,看看能否接收得到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h>
int main(){ int res = mkfifo("Mino",0777); if(res != 0) printf("Error"); int w = open("Mino",O_WRONLY|O_NONBLOCK); if(w < 0) printf("open Fifo"); char send[100] = "blog.minloha.cn"; write(w, send, 100); while (1); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>
int main(){ int r = mkfifo("Mino",0777); if(r != 0) printf("Error");
int file = open("Mino",O_RDONLY|O_NONBLOCK); if(file < 0) printf("Open");
while(1){ char str[100] = {0}; read(file, str, 100); printf("Have read somethings, its %s\n",str); } }
Have read somethings, its blog.minloha.cn
|
除了通过管道进行通信,也可以使用信号进行通信,信号的产生和中断有关,软中断是执行中断指令产生的,而硬中断是由外设引发的。>Linux硬中断和软中断
Signel
所有的信号在头文件的linux/signel.h下已经写好了,使用了POSIX标准
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| * +--------------------+------------------+ * | POSIX signal | default action | * +--------------------+------------------+ * | SIGHUP | terminate | * | SIGINT | terminate | * | SIGQUIT | coredump | * | SIGILL | coredump | * | SIGTRAP | coredump | * | SIGABRT/SIGIOT | coredump | * | SIGBUS | coredump | * | SIGFPE | coredump | * | SIGKILL | terminate(+) | * | SIGUSR1 | terminate | * | SIGSEGV | coredump | * | SIGUSR2 | terminate | * | SIGPIPE | terminate | * | SIGALRM | terminate | * | SIGTERM | terminate | * | SIGCHLD | ignore | * | SIGCONT | ignore(*) | * | SIGSTOP | stop(*)(+) | * | SIGTSTP | stop(*) | * | SIGTTIN | stop(*) | * | SIGTTOU | stop(*) | * | SIGURG | ignore | * | SIGXCPU | coredump | * | SIGXFSZ | coredump | * | SIGVTALRM | terminate | * | SIGPROF | terminate | * | SIGPOLL/SIGIO | terminate | * | SIGSYS/SIGUNUSED | coredump | * | SIGSTKFLT | terminate | * | SIGWINCH | ignore | * | SIGPWR | terminate | * | SIGRTMIN-SIGRTMAX | terminate |
|
使用信号也很简单,能发送信号的函数或者程序行为在后面都有列举,所以只需执行即可。我们生成一个子进程,然后干掉。发送这个信号使用的函数是kill,他的参数是kill(pid,信号值);信号值为0可以检查是否存在进程。
1 2 3 4 5 6 7 8
| #include <sys/types.h> #include <unistd.h> #include <signal.h>
int main(){ pid_t pid = fork(); kill(pid,1); }
|
如果程序触发了信号,可以选择下面三种中的一种处理信号,它可以做函数signel
的第二个参数,如果第二个参数传递一个函数指针,则就是使用此函数作为默认信号处理函数
1 2 3
| #define SIG_DFL #define SIG_IGN #define SIG_ERR
|
1 2 3 4 5 6 7 8 9 10
| #include<stdio.h> #include<signal.h>
int main(){ signal(SIGINT,SIG_DFL); int i = 1; while(1) printf("%d",i++); }
045074904508490450949045104904511490451249045134904514490451549045164904517490451849045194904^C
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include<stdio.h> #include<signal.h>
int main(){ signal(SIGINT,SIG_IGN); int i = 10; while(i){ printf("%d\n",i--); sleep(1); } }
3 2^C 1
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include<stdio.h> #include<signal.h>
int main(){ signal(SIGINT,func); int i = 10; while(i){ printf("%d",i--); sleep(1); } }
void func(){ printf("signel function run!"); }
54^Csignel function run!321
|
sigaction
为了让信号能够传递一些信息,我们可以使用sigaction结构体发送一个带有文本的信号,使用sigaction函数是信号接受函数,发出信号的函数为:sigqueue
1 2 3 4 5 6 7 8 9 10 11 12 13
| struct sigaction { #ifndef __ARCH_HAS_IRIX_SIGACTION __sighandler_t sa_handler; unsigned long sa_flags; #else unsigned int sa_flags; __sighandler_t sa_handler; #endif #ifdef __ARCH_HAS_SA_RESTORER __sigrestore_t sa_restorer; #endif sigset_t sa_mask; };
|
让我们实现一下吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include<signal.h> #include<stdio.h> #include <unistd.h>
void handler(int signum, siginfo_t* info, void* context){ if(signum == SIGIO) printf("SIGIO signal: %d\n", signum); else if(signum == SIGUSR1) printf("SIGUSR1 signal: %d\n", signum); else printf("error\n"); if(context){ printf("content: %d\n", info->si_int); printf("content: %d\n", info->si_value.sival_int); } }
int main(void){ struct sigaction act; act.sa_sigaction = handler; act.sa_flags = SA_SIGINFO; sigaction(SIGIO, &act, NULL); sigaction(SIGUSR1, &act, NULL); while(0); }
|
消息队列与信号量
为了让信息有序且有缓存余地,Linux使用了消息队列存储消息,每个消息都被链表串连起来进行分块管理,这里列举一下消息队列的方法:
- key_t ftok(const char *path ,int id); 把一个已存在的路径名和一个整数标识符转换成IPC键值
- int msgget(key_t key,int flag); 创建一个权限为flag的队列
- int msgsnd(int msgid,const void *ptr,size_t nbytes,int flag); 往消息队列内发送消息
- size_t msgrcv(int msgid,void *ptr,size_t nbytes,long type,int flag); 从消息队列内读取消息
- int msgctl(int msgid, int cmd, struct msqid_ds *buf); 删除消息队列
在通信的最后我说一下信号量,使用需要导入头semaphore.h
,这里不得不提一下锁机制,Linux内核有三把锁,分别是:
- mutex互斥锁,让某过程进入while(1)的循环中
- spinlock自旋锁,在开锁后依然会持续运行
- rwsem信号量锁,读和读互斥。读和写互斥,多个读可加锁,仅一个写可加锁,
信号量锁是这样定义结构体的,他的目的就是保证通信安全,防止出现数据安全问题。
1 2 3 4 5
| struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; };
|
内存
对于C指针指向的所谓的内存地址都是经过处理展示的逻辑内存地址,而对计算机使用的物理地址需要经过CPU内的MMU进行转换才能够被访问的到。
地址主要分为三类,分别为逻辑地址、线性地址和物理地址:
- 逻辑地址:地址值包含字段值和偏移量组成,偏移量表明了开始位置到目标位置的距离。
- 线性地址:是一种虚拟地址,16进制数字显示
- 物理地址:从地址总线上进行直接传播,直接与CPU的地址引脚对应。
为了让CPU能安全的寻址,CPU制定了实模式和保护模式两种模式寻址。
实模式需要使用寄存器提供段基址,然后输入一个偏移量,这个偏移量代表需要访问的内存距离段基址的距离,它使用通用寄存器提供。
保护模式使用全局描述符表GDT保存内存段的信息,寄存器只作为索引,段描述符保存了段基址、段界限、内存信息(一个段描述符只能定义一个内存段)等属性。
为了能将逻辑地址转成线性地址,我们就需要使用GDT,关于段寄存器主要有6个,分别为:
- cs——包含指令
- ss——包含栈
- ds——全局存储区
- es、fs和gs——任意内容
在段描述符内的Type可以定义四个属性:
- 代码段描述符(CSD):代表一个代码段,可以放进GDT或LDT,属于非系统段
- 数据段描述符(DSD):代表数据段,可以放进GDT或LDT,属于非系统段
- 任务状态描述符(TSSD):用于保存处理器寄存器的内容,只存在于GDT
- 局部描述符表描述符(LDTD):一个LDT值,只出现在GDT,系统段
Linux的MM中最基础的就是分页管理,内存首先被氛围固定大小的组,连续的内存地址被连续的营销到物理内存上,这样内核可以对页面进行控制。
分页将RAM分成固定长度的页帧,页帧的大小和页面一样,页表负责把线性地址映射到物理地址。
读取时我们传递线性地址的内容,然后分为三个部分,分别对应:
三个元素,这样我们就可以精确定位内存了。关于CPU的L高速缓存(cache)可以在几十个时钟周期内读取到内容,这个效率大于SRAN,远远大于DRAM,速度很快,所以如何利用缓存提高运行效率?
首先我们需要理解程序的局部性,局部性主要有两个,时间的局部性和空间局部性:
- 时间局部性:如果程序中的莫条指令一旦执行,不久后可能再次执行;某数据被访问,不久后可能会被再次访问
- 空间局部性:访问某个存储单元之后,可能会访问邻近的存储单元,如果是数组则顺着连续方向速度更快。
最后是高速缓存的原理,接下来是它的详细步骤:
1、首先高速缓存进行分组,假设分N个组,每组L行,每行B个字节,从几何角度,缓存大小为N×L×B
2、每行有一个数据块,可以表明行的意义信息,还有一个标记位。
高速缓存就是负责标记内存位置的,所以这个过程也叫映射。高速缓存首先选择组进行判断合适的位置,然后进行 行匹配,最后选择具体的字节
总结
这一期是基础内容,所以只需要记住一些函数和参数就可以了,也没什么可以留作业或者演示的代码了。
阅读本期博文,读者可以学会Linux关于通信的基本形式,以及Linux最基本的信号机制,对最基本的内存形式有了解,在下一期的内存管理可以更加得心应手。
第一期传送门:>点我跳转/)