本篇博客参考了dzcgiegie的好文啦:系统调用与进程通信


3.1 Linux 之系统调用

任务描述:通过对作业二形成的系统新内核进行相应的修改,加入自己的系 统调用,并且在应用层编写一应用程序对其进行调用,产生相应的输出给予验证。

作为一个懒人,那肯定要省时间的方法啊,谁会再去等至少一个小时的内核编译时间啊

1.查询 sys_call_table 的地址

1
sudo cat /proc/kallsyms | grep sys_call_table

找到sys_call_table 的地址为0xffffffff91a013a0

2.查询可用的系统调用号

1
cat /usr/include/asm-generic/unistd.h

这里我是这个路径,其他人好像不是,

1
sudo find / -name unistd*.h

反正找到这个unistd文件就行,然后看一下哪个号没被占用

找到415号是可用的系统调用号

3.创建lnm.c文件

1
sudo vim lnm.c

在文件内定义系统调用,其中使用到的 sys_call_table 地址和可用系统调用号为上面找到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<linux/module.h>
MODULE_INFO(intree,"Y");
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("LNM");
#define SYS_CALL_TABLE_ADDRESS 0xffffffff91a013a0 //sys_call_table对应的地址
#define NUM 415 //系统调用号为415
static int __init hello_init(void)
{
printk(KERN_INFO "Welcome ! 20192131031+liangnuoming\n");
return 0;
}

static void __exit hello_exit(void)
{
printk(KERN_INFO "Bye ! 20192131031+liangnuoming\n");
}
module_init(hello_init);
module_exit(hello_exit);

4.创建Makefile调用文件

先运行下面的命令找到系统当前的内核,然后去/usr/src/目录找对应的文件夹,记住这个文件夹的路径/usr/src/linux-headers-5.4.0-88-generic

1
uname  -a

然后创建Makefile文件

1
sudo vim Makefile
1
2
3
4
5
6
7
obj-m:=lnm.o
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL_PATH:=/usr/src/linux-headers-5.4.0-88-generic
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

5.安装内核模块

执行sudo make

可以通过ls | grep ‘lnm.*’查看执行是否成功

使用如下命令插入模块:

1
sudo insmod lnm.ko

使用如下命令检查插入是否成功:

1
lsmod

可以看到lnm的模块,插入成功。

6.创建test.c调用文件

1
sudo vim test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
#include<stdlib.h>
#include<linux/kernel.h>
#include<sys/syscall.h>
#include<unistd.h>
int main()
{

unsigned long x = 0;
x = syscall(415);
printf("调用测试成功");
return 0;
}

使用 gcc test.c./a.out 命名查看运行结果:

通过 dmesg 命令查看系统调用的结果:

说明添加系统调用成功

3.2 IPC

代码实践 1:

本人在学者网的课程主页中上传了相关 Slides(教学资源里名 为“Linux-OS.zip“的压缩包),其第 12、13 章对上述四种 IPC 方式的使用过程和 示范代码进行了详细的解释。所以这里就请大家先根据上述压缩包里的教程,把 四种 IPC 方式的代码在自己的虚拟机里正确的运行起来并仔细理解。因为这些隶 属于应用层代码,所以难度较低。

若是执行gcc以后报错,可以用man + 函数名 命令解决该头文件缺失的问题

例如man fork

管道 - PIPE

1
sudo vim pipe.c

创建 pipe.c 文件,并在其中添加以下代码:

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
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/wait.h>
int main()
{
int p1,fd[2];
//定义读缓冲区
char outpipe[50];
//定义写缓冲区
char inpipe[50];
//创建无名管道fd
pipe(fd);
while((p1 = fork()) == -1);
//子进程返回
if(p1 == 0)
{
strcpy(inpipe, "This is a message!");
//写信息到管道
write(fd[1], inpipe, 50);
exit(0);
}
else//父进程返回
{
//等待子进程终止
wait(0);
//从管道读信息到缓冲区
read(fd[0], outpipe, 50);
//显示读到的信息
printf("%s\n",outpipe);
exit(0);
}
}

执行sudo gcc pipe.c -o pipe./pipe

信号 - Signal

1
sudo vim signal.c

创建 signal.c 文件,并在其中添加以下代码:

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<stdlib.h>
#include<signal.h>
int k;
void int_func(int sig)
{
k=0;
printf("int_func\n");
}
int main()
{
signal(SIGINT,int_func);
k=1;
while(k==1)
{
printf("Hello!20192131031 + liangnuoming\n");
}
printf("OK\n");
exit(0);

}

执行sudo gcc signal.c -o signal./signal

消息传递 - Message Passing Interface

1
sudo vim sndfile.c

创建 sndfile.c文件,并在其中添加以下代码:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/msg.h>
#define MAXMSG 512
struct my_msg //定义消息缓冲区数据结构
{
long int my_msg_type;
int i;
char some_text[MAXMSG];
} msg;
int main()
{
int msgid; //定义消息缓冲区内部标识
char buffer[BUFSIZ]; //定义用户缓冲区
msgid = msgget(12,0666 | IPC_CREAT); //创建消息队列, key为12
while (1)
{
puts("Enter some text:"); //提示键入消息内存
fgets(buffer, BUFSIZ, stdin); //标准输入送buffer
msg.i++ ;
printf("i=%d\n",msg.i);
msg.my_msg_type = 3;
strcpy(msg.some_text, buffer); //buffer中的内容送消息缓冲
msgsnd(msgid, &msg, MAXMSG, 0); //发送消息到消息队列
if (strncmp(msg.some_text, "end", 3) == 0) //消息为end结束
break;
}
exit(0);
}
1
sudo vim rcvfile.c

创建 rcvfile.c文件,并在其中添加以下代码:

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
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/msg.h>
#define MAXMSG 512
struct my_msg//定义消息缓冲区数据结构
{
long int my_msg_type;
int i;
char some_text[MAXMSG];
}msg;
int main()
{
int msgid;
msg.my_msg_type=3;
msgid=msgget(12,0666|IPC_CREAT);//获取消息队列。Key为1234
while(1)
{
msgrcv(msgid,&msg,BUFSIZ,msg.my_msg_type,0);//接收消息
printf("You wrote:%s and i = %d\n",msg.some_text,msg.i);//显示消息
if(strncmp(msg.some_text,"end",3)==0)//消息为end则结束
break;
}
msgctl(msgid,IPC_RMID,0);//删除消息队列
exit(0);

}

执行sudo gcc sndfile.c -o sndfilesudo gcc rcvfile.c -o rcvfile

执行./sndfile

执行./rcvfile

共享内存 - Shared Memory

1
sudo vim sndshm.c

创建 sndshm.c 文件,并在其中添加以下代码:

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<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/shm.h>
int main()
{
int shmid;
char *viraddr;
char buffer[BUFSIZ];
shmid = shmget(1234, BUFSIZ, 0666 | IPC_CREAT); //创建共享内存
viraddr = (char*)shmat(shmid, 0, 0);//附接到共享内存
while (1)
{
puts("Enter some text:"); //提示用户输入信息
fgets(buffer, BUFSIZ, stdin); //将标准输入送入到缓冲区中
strcat(viraddr, buffer); //采用追加方式写到共享内存
if (strncmp(buffer, "end", 3) == 0) //当输入的字符为"end"时,终止循环
break;
}
shmdt(viraddr); //切断与共享内存的链接
exit(0);
}

1
sudo vim rcvshm.c

创建 rcvshm.c 文件,并在其中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/shm.h>
int main()
{
int shmid;
char *viraddr;
shmid = shmget(1234, BUFSIZ, 0666 | IPC_CREAT); //创建共享内存
viraddr = (char *)shmat(shmid, 0, 0); //附接到共享内存
printf("Your message is :%s", viraddr); //输出共享内存的内容
shmdt(viraddr); //切断与共享内存的链接
shmctl(shmid, IPC_RMID, 0);//释放共享内存
exit(0);
}

执行sudo gcc sndshm.c -o sndshmsudo gcc rcvshm.c -o rcvshm

执行./sndshm./rcvshm

代码实践 2:

请对上述 IPC 方式的第四种——共享内存进行改写,实现以下 场景。

  • (1)创建两块共享内存 M1 和 M2;用数据结构“环形队列/数组”来抽象 M1 和 M2,也即,每个共享内存中的存储形式为一循环队列或数组,容量为 4096(可 以容纳 4096 个消息);
  • (2)发送者进程,名为 A,向 M1 中“生产”常规信息(可以是字符串),从 M2 中“消费”控制信息;接收者进程,名为 B,向 M2“生产”控制信息,从 M1 中 “消费”常规信息;这里可以理解为:M1 为 A 向 B 发送数据的缓冲(Data Buffer), M2 则为 B 向 A 发送应答(确认收到的应答)的缓冲(Response Buffer);
  • (3)初始状态下,M1 和 M2 皆为空;A 首先主动向 B 发送常规信息,数量 为[1-10]中的随机数目(但是每次发送的数据量肯定不能超过 M1 中的空闲槽数 量);发送之后,A 再向 B 传送信号(Signal),要其消费 M1;
  • (4)B 收到 A 发送来的信号后,从 M1 中消费信息,数量为[1-10]中的随机 数量(但是肯定不能超过 M1 中的当前存在消息数目);B 向 M2 中生产应答信 息,数目为其最近消费 M1 的消息个数,即:B 一次消费的常规信息数,要等于 生产的应答信息数;之后,B 向 A 发送信号,让其接收应答信息;
  • (5)A 收到信号后,消费应答信息,并继续向 M1 中生产常规信息。

本题就是一个pv操作,A读写的时候B不能读写,B读写的时候A不能,所以设置两个信号量就能控制这个问题,达到轮流操作的效果

代码参考了系统调用与进程通信生产者消费者问题 多进程共享内存 - LightningStar,并改成了随时间变化的随机数,使每次运行结果都为伪随机的

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <time.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>

#define MAX_NUM 4096

//int flag;
//共享内存缓冲区资源的循环队列
struct Resource
{
int message[MAX_NUM];
int front;
int rear;
};

//M1 M2
struct Resource M[2];

//信号量数据结构
union semun
{
int value;
};

//用于映射信号量编号与对应意义的映射
enum MUTEX
{
//是否允许A操作
A_ENABLE,
//是否允许B操作
B_ENABLE,
//信号量相关
NUM_MUX
};

//信号量集合描述符
int semid;

//共享空间描述符,M1,M2各占一个空间
int shmid[2];

//共享空间首地址
struct Resource *shm[2];

//绑定共享内存
void attach_shm()
{
//将共享内存连接到当前进程的地址空间
shm[0] = (struct Resource *)shmat(shmid[0], NULL, 0);
shm[1] = (struct Resource *)shmat(shmid[1], NULL, 0);
if (shm[0] == (struct Resource *)-1 || shm[1] == (struct Resource *)-1)
{
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
}

//解绑共享内存
void detach_shm()
{
//把共享内存从当前进程中分离
if (shmdt((void *)shm[0]) == -1 || shmdt((void *)shm[1]) == -1)
{
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
}

//获取资源,对应信号量实施P操作
void P(short unsigned int num)
{
struct sembuf sb =
{
num, -1, 0 //0表示信号量编号,-1表示P操作,SEM_UNDO表示进程退出后,该进程对sem进行的操作将被撤销
};
//修改集合中,一个或多个信号量值
semop(semid, &sb, 1);
}

//释放资源,对应信号量实施V操作
void V(short unsigned int num)
{
struct sembuf sb = {
num, 1, 0 //
};
semop(semid, &sb, 1);
}

//读取数据,id为0,表示A在操作,id为2,表示B在操作
int readMessage(int id)
{
// id = 0,读取M[1],id = 1,读取M[0]
int message = 0;
message = shm[-id + 1]->message[shm[-id + 1]->front];
shm[-id + 1]->front = (shm[-id + 1]->front + 1) % MAX_NUM;
// printf("M%d -> front = %d\n", -id + 2, shm[-id + 1]->front);
return message;
}

//写入数据,id为0,表示A在操作,id为2,表示B在操作
int writeMessage(int id)
{
sleep(1.0);
srand((unsigned)time(NULL));
//随机生成一个范围在[1,4096]的message
int message = rand() % 4096 + 1;
//id为0,向M[0]写入,id为1,向M[1]写入
shm[id]->message[shm[id]->rear] = message;
shm[id]->rear = (shm[id]->rear + 1) % MAX_NUM;
// printf("M%d -> rear = %d\n", id + 1, shm[id]->rear);
return message;
}

//进程A的工作
void processA()
{
attach_shm();
//sleep(1.0);
//srand((unsigned)time(NULL));
//extern int flag;
//flag = rand()%10+1;
//这里让A执行4次
for (int m = 0; m < 4; m++)
{
P(A_ENABLE);
//当前M1中元素个数
int ele1Number = (shm[0]->rear - shm[0]->front + MAX_NUM) % MAX_NUM;
//最多写入个数
int maxNum = MAX_NUM - ele1Number;
sleep(1.0);
srand((unsigned)time(NULL));
int randInt = rand() % 10 + 1;
//选一个小的数作为写入的信息数
int times = maxNum < randInt ? maxNum : randInt;
for (int i = 0; i < times; i++)
{
printf("A write message to M1 : %d \n", writeMessage(0));
}
//如果M2队列为空,也就是刚刚开始A向B,B还没向A发送过信息
//所以只在M2有消息的时候才读取
if (shm[1]->front != shm[1]->rear)
{
//当前M2中元素个数
int eleNumber = (shm[1]->rear - shm[1]->front + MAX_NUM) % MAX_NUM;
sleep(1.0);
srand((unsigned)time(NULL));
//随机读取[1, eleNumber]次
int times = rand() % eleNumber + 1;

for (int i = 0; i < times; i++)
{
printf("A read message from M2 : %d \n", readMessage(0));
}
}
V(B_ENABLE);
}
detach_shm();
}

//进程B的工作
void processB()
{
attach_shm();
//这里让B执行4次
//sleep(1.0);
//srand((unsigned)time(NULL));
//int flag2 = rand()%flag+1;
for (int m = 0; m < 4; m++)
{
P(B_ENABLE);
//当前M1中元素个数
sleep(1.0);
srand((unsigned)time(NULL));
int eleNumber = (shm[0]->rear - shm[0]->front + MAX_NUM) % MAX_NUM;
int randInt = rand() % 10 + 1;
//选一个小的数作为读取的信息数
int times = eleNumber < randInt ? eleNumber : randInt;
for (int i = 0; i < times; i++)
{
printf("B read message from M1 : %d \n", readMessage(1));
}
//向M2写times个信息
for (int i = 0; i < times; i++)
{
printf("B write message to M2 : %d \n", writeMessage(1));
}
V(A_ENABLE);
}
detach_shm();
}

int main(int argc, char const *argv[])
{
extern int flag;
//记录父进程pid
pid_t ppid = getpid();
//信号集名字,信号集中信号量的个数,信号量集合的权限
semid = semget((key_t)1234, NUM_MUX, IPC_CREAT | 0600); //创建信号量
if (semid == -1)
{
perror("semget");
}
// 初始化信号量
union semun s;
//初始时,允许A执行
s.value = 1;
semctl(semid, A_ENABLE, SETVAL, s);
//初始时,不允许B执行
s.value = 0;
semctl(semid, B_ENABLE, SETVAL, s);

//创建共享内存
shmid[0] = shmget((key_t)1234, sizeof(M[0]), 0666 | IPC_CREAT);
shmid[1] = shmget((key_t)5678, sizeof(M[1]), 0666 | IPC_CREAT);

if (shmid[0] == -1 || shmid[1] == -1)
{
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
attach_shm();
//初始化共享内存
for (int i = 0; i < 2; i++)
{
memset(shm[i]->message, 0, sizeof(shm[i]->message));
shm[i]->front = 0;
shm[i]->rear = 0;
}
//创建3个进程:1个父进程 + 1个A + 1个B
pid_t child_pid[2];
int i = 0;
for (i = 0; i < 2; i++)
{
child_pid[i] = fork();
//子进程
if (child_pid[i] == 0)
{
break;
}
}
//A
if (i == 0)
{
processA();
}
//B
else if (i == 1)
{
processB();
}
//父进程
if (getpid() == ppid)
{
//等待子进程结束
for (int i = 0; i < 2; i++)
{
waitpid(child_pid[i], NULL, 0);
}
detach_shm();
//删除共享内存
if (shmctl(shmid[0], IPC_RMID, 0) == -1 || shmctl(shmid[1], IPC_RMID, 0) == -1)
{
fprintf(stderr, "shmctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
}
return 0;
}

3.3 心得和体会

通过3.1的内核模块学会了一些关于内核模块,KO文件,Makefile的知识,懂了怎么插入和自定义内核模块,当然了,掌握的还是很浅薄,不过万丈高楼平地起,也算是有了一点基础。不得不说Linux的可玩性比Windows和macOS好太多了,不过也要有能力才能玩起来。

上学期学了操作系统,一直没什么实践的机会,这次写了一下pv操作的逻辑,学会了几个os相关的api,虽然也是一知半解的状态,不过感觉对底层的东西敬意又多了不少。