[2]开发一个Linux系统吧

进程通信IPC

我们知道系统本身就是一个程序,将封装好的方法提供给用户层,从而起到中间层的作用。PIPE通信是最早的线程通信方法,在内存中分配一块内存空间作为管道,创建管道的叫服务器,连接管道的叫客户机,在一个客户机向管道写入数据后,另一个管道就可以读出来,而为了防止读取和写入阻塞,每个客户机在通信时都需要建立两个连接,分别负责读出和写入。

PIPE

管道流只适用与父子进程之间,两个不同的程序是无法进行的(内存隔离)。

在环境里默认会有一个名字叫做unistd.h的头文件,这个头文件可以在内存空间上创建一个数组,这个数组可以用作管道:

1
2
#include<unistd.h>
pipe(int pp[2]);

它可以为pp数组分配2个文件描述符,一个用于读,一个用于写。管道的使用需要遵守以下的约定:

  • 先写入后读取,管道是一种环形队列,先进先出
  • 管道是无大小的,他不是结构体,而是一个流
  • 管道的数据一旦被读取过了就会从管道里面消失,就像自来水管里面的水,流走了就没有了

1

对于这样一个父子进程之间使用管道进行通信,父进程开启pp[1]关闭pp[0],子进程开启pp[0]关闭pp[1],这样就可以进行通信了。

2

同一个程序如果需要管道,只需要把管道当作数组使用即可。

FIFO

如果需要程序之间通信,管道就需要变成有名管道,有名管道属于系统头文件,并不在linux源码里面,下面示范以下如何创建一个这样的管道:

1
2
3
4
5
6
7
8
9
// main.c
#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
// main.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(){
// mkfifo("管道名",权限值);
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
// read.c
#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++);
}
// ----随便截取一点,很多很多,可以使用Ctrl+C截止
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);
}
}
// ----截取最后一部分,不可以使用Ctrl+C截止
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; /* mask last for extensibility */
};

让我们实现一下吧:

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;//信号处理程序,能够接受额外数据和sigqueue配合使用
act.sa_flags = SA_SIGINFO;//影响信号的行为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制定了实模式和保护模式两种模式寻址。

实模式需要使用寄存器提供段基址,然后输入一个偏移量,这个偏移量代表需要访问的内存距离段基址的距离,它使用通用寄存器提供。
$$
物理地址=段基址<<4+偏移量
$$
保护模式使用全局描述符表GDT保存内存段的信息,寄存器只作为索引,段描述符保存了段基址、段界限、内存信息(一个段描述符只能定义一个内存段)等属性。

段描述符

为了能将逻辑地址转成线性地址,我们就需要使用GDT,关于段寄存器主要有6个,分别为:

  • cs——包含指令
  • ss——包含栈
  • ds——全局存储区
  • es、fs和gs——任意内容

在段描述符内的Type可以定义四个属性:

  • 代码段描述符(CSD):代表一个代码段,可以放进GDT或LDT,属于非系统段
  • 数据段描述符(DSD):代表数据段,可以放进GDT或LDT,属于非系统段
  • 任务状态描述符(TSSD):用于保存处理器寄存器的内容,只存在于GDT
  • 局部描述符表描述符(LDTD):一个LDT值,只出现在GDT,系统段

$$
段描述符地址=\frac{sizeof(GDT)}{sizeof(LDT)}
$$

Linux的MM中最基础的就是分页管理,内存首先被氛围固定大小的组,连续的内存地址被连续的营销到物理内存上,这样内核可以对页面进行控制。

分页将RAM分成固定长度的页帧,页帧的大小和页面一样,页表负责把线性地址映射到物理地址。

读取时我们传递线性地址的内容,然后分为三个部分,分别对应:

  • 所在页
  • 所在行
  • 偏移量

三个元素,这样我们就可以精确定位内存了。关于CPU的L高速缓存(cache)可以在几十个时钟周期内读取到内容,这个效率大于SRAN,远远大于DRAM,速度很快,所以如何利用缓存提高运行效率?

首先我们需要理解程序的局部性,局部性主要有两个,时间的局部性和空间局部性:

  • 时间局部性:如果程序中的莫条指令一旦执行,不久后可能再次执行;某数据被访问,不久后可能会被再次访问
  • 空间局部性:访问某个存储单元之后,可能会访问邻近的存储单元,如果是数组则顺着连续方向速度更快。

最后是高速缓存的原理,接下来是它的详细步骤:

1、首先高速缓存进行分组,假设分N个组,每组L行,每行B个字节,从几何角度,缓存大小为N×L×B

2、每行有一个数据块,可以表明行的意义信息,还有一个标记位。

高速缓存就是负责标记内存位置的,所以这个过程也叫映射。高速缓存首先选择组进行判断合适的位置,然后进行 行匹配,最后选择具体的字节

总结

这一期是基础内容,所以只需要记住一些函数和参数就可以了,也没什么可以留作业或者演示的代码了。

阅读本期博文,读者可以学会Linux关于通信的基本形式,以及Linux最基本的信号机制,对最基本的内存形式有了解,在下一期的内存管理可以更加得心应手。

第一期传送门:>点我跳转


[2]开发一个Linux系统吧
https://blog.minloha.cn/posts/112552b355c7be2022122509.html
作者
Minloha
发布于
2022年12月25日
更新于
2023年12月21日
许可协议