2021SC@SDUSC
项目环境:
- 树莓派4b
- Ubuntu Desktop 21.04
进程控制:
1. 进程共享内存通信:
首先讨论的是Linux中的System V共享内存IPC。与管道或消息队列要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC 技术的速度更快。下面是一个基于共享内存的读写程序。
// shmread.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include "shmdata.h"
int main()
{
int running = 1;
void *shm = NULL;
struct shared_use_st *shared;
int shmid;
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
shm = shmat(shmid, 0, 0);
if(shm == (void*)-1)
{
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
printf("\nMemory attached at %X\n", (int)shm);
shared = (struct shared_use_st*)shm;
shared->written = 0;
while(running)
{
if(shared->written != 0)
{
printf("You wrote: %s", shared->text);
sleep(rand() % 3);
shared->written = 0;
if(strncmp(shared->text, "end", 3) == 0)
running = 0;
}
else
sleep(1);
}
if(shmdt(shm) == -1)
{
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
if(shmctl(shmid, IPC_RMID, 0) == -1)
{
fprintf(stderr, "shmctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
// shmwrite.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>
#include "shmdata.h"
int main()
{
int running = 1;
void *shm = NULL;
struct shared_use_st *shared = NULL;
char buffer[BUFSIZ + 1];
int shmid;
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
shm = shmat(shmid, (void*)0, 0);
if(shm == (void*)-1)
{
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
printf("Memory attached at %X\n", (int)shm);
shared = (struct shared_use_st*)shm;
while(running)
{
while(shared->written == 1)
{
sleep(1);
printf("Waiting...\n");
}
printf("Enter some text: ");
fgets(buffer, BUFSIZ, stdin);
strncpy(shared->text, buffer, TEXT_SZ);
shared->written = 1;
if(strncmp(buffer, "end", 3) == 0)
running = 0;
}
if(shmdt(shm) == -1)
{
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
sleep(2);
exit(EXIT_SUCCESS);
}
?在 shmread.c 中,我们首先使用shmget()系统调用创建共享内存空间,其原型如下:
#include <sys/types.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
对于其中的各项参数,有如下说明:
size是一个正整数,它表示需分配的段的字节数。内核是以系统分页大小的整数倍来分配共享内存的,因此实际上size 会被提升到最近的系统分页大小的整数倍。 shmflg参数执行的任务与其在其他IPC get 调用中执行的任务一样,即指定施加于新共享内存段上的权限或需检查的既有内存段的权限。此外,在shmflg中还可以对下列标记中的零个或多个取OR 来控制shmget()的操作。
- IPC_CREAT
如果不存在与指定的key 对应的段,那么就创建一个新段。 - IPC_EXCL
如果同时指定了IPC_CREAT 并且与指定的key对应的段已经存在,那么返回EEXIST错误。
—— 《The Linux Programming Interface》
当使用共享内存时,我们有使用了函数shmat():
#include <sys/types.h>
#include <sys/shm.h>
int *shmat(int shmid, const void *shmaddr, int shmflg);
shmaddr 参数和shmflg 位掩码参数中SHM_RND 位的设置控制着段是如何被附加上去的。
- 如果 shmaddr 是NULL,那么段会被附加到内核所选择的一个合适的地址处。这是附
加一个段的优选方法。 - 如果 shmaddr 不为NULL 并且没有设置SHM_RND,那么段会被附加到由shmaddr 指
定的地址处,它必须是系统分页大小的一个倍数(否则会发生EINVAL 错误)。 - 如果 shmaddr 不为NULL 并且设置了SHM_RND,那么段会被映射到的地址为在
shmaddr 中提供的地址被舍入到最近的常量SHMLBA(shared memory low boundary address)的倍数。这个常量等于系统分页大小的某个倍数。将一个段附加到值为 SHMLBA 的倍数的地址处在一些架构上是有必要的,因为这样才能够提升CPU 的快 速缓冲性能和防止出现同一个段的不同附加操作在CPU 快速缓冲中存在不一致的视 图的情况。
—— 《The Linux Programming Interface》
然后用一个while 循环来一直读取内存中的数据,当 written 标志不为 0 的时候,我们进行读操作,并且如果读到的数据是 end ,则退出,读取完成之后,我们先将共享内存从当前进程分离,然后将其删除。?
当一个进程不再需要访问一个共享内存段时,就可以调用shmdt()来将该段分离出其虚拟地址空间了。需要注意,分离一个共享内存段与删除它是不同的。删除是通过shmctl()中IPC_ RMID操作来完成的。
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
对于shmctl()的各项参数,有如下说明:
- IPC_RMID
标记这个共享内存段及其关联shmid_ds 数据结构以便删除。如果当前没有进程附加该段,那么就会执行删除操作,否则就在所有进程都已经与该段分离(即当shmid_ds 数据结构中shm_nattch字段的值为0时)之后再执行删除操作。在一些应用程序中可以通过在所有进程将共享内存段附加到其虚拟地址空间之后立即使用shmat()将共享内存段标记为删除来确保在应用程序退出时干净地清除共享内存段。 - IPC_STAT
将与这个共享内存段关联的shmid_ds数据结构的一个副本防止到buf指向的缓冲区中 - IPC_SET
使用buf指向的缓冲区中的值来更新与这个共享内存段相关联的shmid_ds数据结构中被 选中的字段。
—— 《The Linux Programming Interface》
然后研究写者程序。shmwrite取得共享内存并连接到自己的地址空间中。检查共享内存中written是否为 0 ,若不是,表示共享内存中的数据还没有被完,则等待其他进程读取完成,并提示用户等待。若共享内存的 written 为 0 ,表示没有其他进程对共享内存进行读取,则提示用户输入文本,并再次设置共享内存中的 written 为 1 ,表示写完成,其他进程可对共享内存进行读操作。
2.?进程信号量通信
信号量是操作系统内部通信的一种原子操作,主要由两种操作signal()和wait()组成,本实验以经典的生产者消费者模型为例,使用信号量操作来模拟和实现进程间的通信。实验中并没有使用Linux原生的System V信号量,而是使用了Pthread库进行实现。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <dlfcn.h>
#define true 1
int product_id = 0;
int consumer_id = 0;
int N;
int producerNum;
int consumerNum;
typedef int semaphore;
typedef int item;
item* buffer;
int in = 0;
int out = 0;
int proCount = 0;
semaphore mutex = 1, empty , full = 0, proCmutex = 1;
void * producer(void * a){
int id = ++product_id;
while(true){
int flag = 0;
while(empty <= 0){
printf("生产者%d:缓冲区已满!阻塞中……\n",id);
flag =1;
sleep(1);
}
if(flag == 1)
printf("生产者%d因缓冲区有空位唤醒!\n",id);
flag = 0;
while(proCmutex <= 0){printf("生产者%d生产阻塞中……\n",id);flag = 1;sleep(1);};
proCmutex--;
if(flag == 1)
printf("生产者%d生产唤醒!\n",id);
proCount++;
printf("生产者%d:生产一个产品ID%d!\n",id,proCount);
flag = 0;
while(mutex <= 0){printf("生产者%d装入阻塞中……\n",id);sleep(1);flag=1;};
mutex--;
if(flag == 1)
printf("生产者%d装入唤醒,装入产品ID%d,缓冲区位置为%d!\n",id,proCount,in);
else
printf("生产者%d装入产品ID%d,缓冲区位置为%d!\n",id,proCount,in);
empty--;
buffer[in] = proCount;
in = (in + 1) % N;
full++;
mutex++;
proCmutex++;
sleep(1);
}
}
void * consumer(void *b){
int id = ++consumer_id;
while(true){
int flag = 0;
while(full <= 0){
printf("\t\t\t\t消费者%d:缓冲区为空!阻塞中……\n",id);
flag = 1;
sleep(1);
}
full--;
if(flag ==1)
printf("\t\t\t\t消费者%d因缓冲区有产品唤醒!\n",id);
flag = 0;
while(mutex <= 0){printf("\t\t\t\t消费者%d消费阻塞中……\n",id);sleep(1);};
mutex--;
if(flag == 1)
printf("\t\t\t\t消费者%d消费唤醒!\n",id);
int nextc = buffer[out];
buffer[out] = 0;
empty++;
printf("\t\t\t\t消费者%d:消费一个产品ID%d,缓冲区位置为%d\n",id, nextc,out);
out = (out + 1) % N;
mutex++;
sleep(1);
}
}
int main()
{
int tempnum;
printf("请输入生产者数目:\n");
scanf("%d",&tempnum);
producerNum = tempnum;
printf("请输入消费者数目:\n");
scanf("%d",&tempnum);
consumerNum = tempnum;
printf("请输入缓冲区大小:\n");
scanf("%d",&tempnum);
N = tempnum;
empty = N;
buffer = (item*)malloc(N*sizeof(item));
for(int i=0;i<N;i++)
{
buffer[i]=0;
}
pthread_t threadPool[producerNum+consumerNum];
int i;
for(i = 0; i < producerNum; i++){
pthread_t temp;
if(pthread_create(&temp, NULL, producer, NULL) == -1)
{
printf("ERROR, fail to create producer%d\n", i);
exit(1);
}
threadPool[i] = temp;
}
for(i = 0; i < consumerNum; i++){
pthread_t temp;
if(pthread_create(&temp, NULL, consumer, NULL) == -1){
printf("ERROR, fail to create consumer%d\n", i);
exit(1);
}
threadPool[i+producerNum] = temp;
}
void * result;
for(i = 0; i < producerNum+consumerNum; i++){
if(pthread_join(threadPool[i], &result) == -1){
printf("fail to recollect\n");
exit(1);
}
}
return 0;
}
?
首先我们在该文件中先定义好生产者,消费者的ID和数目,缓冲区的大小,商品的类型和大小,缓冲区首尾指针,然后我们定义4个信号量:分别为互斥信号量mutex,资源信号量empty和full,以及生产者互斥信号量promutex。
紧接着,我们在生产者函数中,先判断缓冲区是否已满,完成产品数量自增然后申请资源,阻塞进程,互斥信号量mutex自减,同时更新缓冲区的大小,即缓冲区非空白大小自加,然后 mutex 自加释放资源,实现互斥。
生产者的实现原理大致相同,不再赘述。
|