关于操作系统的同步问题
一. 通过fork的方式,产生4个进程P1,P2,P3,P4,每个进程打印输出自己的名字,例如P1输出“I am the process P1”。要求P1最先执行,P2、P3互斥执行,P4最后执行。通过多次测试验证实现是否正确。
首先我们设计执行状态前驱图
由图可直观得到,四个程序的执行顺序是:P1最先执行,P2、P3互斥执行,P4最后执行。
于是设计代码如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void){
pthread_mutex_t mutex;//创建互斥锁
pid_t pid1 = fork(); //创建p1
if(pid1 == 0){
printf("I am the process P1\n");
return 0;
}
pthread_mutex_init(&mutex,NULL); //初始化p2p3互斥锁
waitpid(pid1, NULL, 0); //等P1进程结束
pid_t pid2 = fork();//创建p2
if(pid2 == 0){
pthread_mutex_lock(&mutex); //加互斥锁
printf("I am the process P2\n");
pthread_mutex_unlock(&mutex);
return 0;
}
waitpid(pid1, NULL, 0);//等p1进程结束
pid_t pid3 = fork();//创建p3
if(pid3 == 0){
pthread_mutex_lock(&mutex);
printf("I am the process P3\n");//加互迟锁
pthread_mutex_unlock(&mutex);
return 0;
}
waitpid(pid2, NULL, 0);
waitpid(pid3, NULL, 0);//等p2p3结束
pid_t pid4 = fork();//创建p4
if(pid4 == 0){
printf("I am the process P4\n");
return 0;
}
pthread_mutex_destroy(&mutex);//销毁互斥锁
return 0;
}
运行如上代码可以得到下面的运行结果
遇到问题:反复运行程序依旧是如上结果,无法看出p2p3的互斥关系,可能是由于fork的原因,导致p3总是要比p2先进入程序。
解决方法,在p3之前加入sleep(1),修改代码如下
waitpid(pid1, NULL, 0);//等p1进程结束
pid_t pid3 = fork();//创建p3
if(pid3 == 0){
pthread_mutex_lock(&mutex);
sleep(1);
printf("I am the process P3\n");//加互迟锁
pthread_mutex_unlock(&mutex);
return 0;
}
再次运行程序,得到结果如下:
可知p2p3满足实验要求的互斥关系。
二.火车票余票数ticketCount 初始值为1000,有一个售票线程,一个退票线程,各循环执行多次。添加同步机制,使得结果始终正确。要求多次测试添加同步机制前后的实验效果。
设计代码如下:
退票和售票的进程p1p2使用同步机制,保证了售票和退票之后总票数始终为1000,由此可以避免由于并发而导致的票数不对的问题。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <unistd.h>
#include <assert.h>
int ticket = 1000; //票的总数为1000张
int temp = 0;
int i=100;//执行次数
sem_t blanks; //退票余量
sem_t datas; //售票余量
void *sold(void *arg){
while(i--){
sem_wait(&blanks);
temp = ticket;
temp = temp - 1;
ticket = temp;
printf("we have %d tickets now\n",ticket);
sem_post(&datas); //卖完一张票后 售票数增加1
}
return NULL;
}
void *back(void *arg){
while(i--){
sem_wait(&datas);
temp = ticket;
temp = temp + 1;
ticket = temp;
printf("we have %d tickets now\n",ticket);
sem_post(&blanks); //退完一张票后,票余量增加1
}
return NULL;
}
int main(int argc, char *argv[]){
sem_init(&blanks, 0, 1000);
sem_init(&datas, 0, 0);
pthread_t p1, p2;//创建进程
pthread_create(&p1, NULL, sold, NULL);
pthread_create(&p2, NULL, back, NULL);
pthread_join(p1, NULL);
pthread_join(p2, NULL); //运行
sem_destroy(&blanks);
sem_destroy(&datas);
printf("we have %d tickets now\n",ticket);
return 0;
}
首先,设置运行100次,得到运行结果
运行结果正确,首先售票一百张,再退票一百张,最后得到票数为1000张,正确。
将运行次数改为1500,超出了票的总张数。得到如下的运行结果:
运行结果依旧正确。
将运行次数改为15000,运行得到如下结果
最后票数为0,不正确。
分析问题,可能是出现了读脏数据的问题,由于循环的次数较多导致了进程的切换使得最后结果出问题。
三.一个生产者一个消费者线程同步。设置一个线程共享的缓冲区, char buf[10]。一个线程不断从键盘输入字符到buf,一个线程不断的把buf的内容输出到显示器。要求输出的和输入的字符和顺序完全一致。(在输出线程中,每次输出睡眠一秒钟,然后以不同的速度输入测试输出是否正确)。要求多次测试添加同步机制前后的实验效果。
使用两个信号量实现两个进程同步,与此同时再多定义两个int变量方便输入数组的指针移动。
设计代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <unistd.h>
#include <assert.h>
char buf[10] = {0};//初始化
sem_t blanks;
sem_t datas;
int readnum =0;//输入字符个数
int writenum =0;//写出字符个数
void *doread(void *arg){
while(readnum<10) {
sem_wait(&blanks); //空闲内存判断
scanf("%c",&buf[readnum]);
readnum++;
readnum %= 10; //超过10 自动消掉十位数
sem_post(&datas); //输入字符个数加1
}
return NULL;
}
void *dowrite(void *arg){
while(writenum<10) {
sem_wait(&datas); //判断是否有输入字符
printf("%c ",buf[writenum]);
sleep(1); //sleep(1)
writenum++;
writenum %= 10;//超过10 自动消掉十位数
sem_post(&blanks); //空余内存加1
return NULL;
}
int main(int argc, char *argv[]){
sem_init(&blanks, 0, 10);
sem_init(&datas, 0, 0);
pthread_t p1, p2;//创建线程p1p2
pthread_create(&p1, NULL, doread, NULL);
pthread_create(&p2, NULL, dowrite, NULL);
pthread_join(p1, NULL);
pthread_join(p2, NULL);
sem_destroy(&blanks);
sem_destroy(&datas);
return 0;
}
运行结果如下图
运行结果正确,无论输入快慢字符个数多少都可以将输入字符输出,实现了输入输出的进程同步。
注意:由于除了定义的两个信号量以外,两个int变量是用来存字符的指针,字符串的大小为10,所以当变量超出10后要自动去掉十位,用除十取余来实现
四.进程通信问题。阅读并运行共享内存、管道、消息队列三种机制的代码。
a)通过实验测试,验证共享内存的代码中,receiver能否正确读出sender发送的字符串?如果把其中互斥的代码删除,观察实验结果有何不同?如果在发送和接收进程中打印输出共享内存地址,他们是否相同,为什么?
b)有名管道和无名管道通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?
c)消息通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?
a)根据参考资料,两个代码4-1.c 实现sender功能,4-2.c实现receiver功能
4-1.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>
int main(int argc, char *argv[])
{
key_t key;
int shm_id;
int sem_id;
int value = 0;
//1.Product the key key = ftok(".", 0xFF);
//2. Creat semaphore for visit the shared memory sem_id = semget(key, 1, IPC_CREAT|0644);
if(-1 == sem_id)
{
perror("semget");
exit(EXIT_FAILURE);
}
//3. init the semaphore, sem=0 if(-1 == (semctl(sem_id, 0, SETVAL, value)))
{
perror("semctl");
exit(EXIT_FAILURE);
}
//4. Creat the shared memory(1K bytes) shm_id = shmget(key, 1024, IPC_CREAT|0644);
if(-1 == shm_id)
{
perror("shmget");
exit(EXIT_FAILURE);
}
//5. attach the shm_id to this process char *shm_ptr;
shm_ptr = shmat(shm_id, NULL, 0);
if(NULL == shm_ptr)
{
perror("shmat");
exit(EXIT_FAILURE);
}
//6. Operation procedure struct sembuf sem_b;
sem_b.sem_num = 0; //first sem(index=0) sem_b.sem_flg = SEM_UNDO;
sem_b.sem_op = 1; //Increase 1,make sem=1 while(1)
{
if(0 == (value = semctl(sem_id, 0, GETVAL)))
{
printf("\nNow, snd message process running:\n");
printf("\tInput the snd message: ");
scanf("%s", shm_ptr);
if(-1 == semop(sem_id, &sem_b, 1))
{
perror("semop");
exit(EXIT_FAILURE);
}
}
//if enter "end", then end the process if(0 == (strcmp(shm_ptr ,"end")))
{
printf("\nExit sender process now!\n");
break;
}
}
shmdt(shm_ptr);
return 0;
}
4-2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>
int main(int argc, char *argv[])
{
key_t key;
int shm_id;
int sem_id;
int value = 0;
//1.Product the key key = ftok(".", 0xFF);
//2. Creat semaphore for visit the shared memory sem_id = semget(key, 1, IPC_CREAT|0644);
if(-1 == sem_id)
{
perror("semget");
exit(EXIT_FAILURE);
}
//3. init the semaphore, sem=0 if(-1 == (semctl(sem_id, 0, SETVAL, value)))
{
perror("semctl");
exit(EXIT_FAILURE);
}
//4. Creat the shared memory(1K bytes) shm_id = shmget(key, 1024, IPC_CREAT|0644);
if(-1 == shm_id)
{
perror("shmget");
exit(EXIT_FAILURE);
}
//5. attach the shm_id to this process char *shm_ptr;
shm_ptr = shmat(shm_id, NULL, 0);
if(NULL == shm_ptr)
{
perror("shmat");
exit(EXIT_FAILURE);
}
//6. Operation procedure struct sembuf sem_b;
sem_b.sem_num = 0; //first sem(index=0) sem_b.sem_flg = SEM_UNDO;
sem_b.sem_op = -1; //Increase 1,make sem=1
while(1)
{
if(1 == (value = semctl(sem_id, 0, GETVAL)))
{
printf("\nNow, receive message process running:\n");
printf("\tThe message is : %s\n", shm_ptr);
if(-1 == semop(sem_id, &sem_b, 1))
{
perror("semop");
exit(EXIT_FAILURE);
}
}
//if enter "end", then end the process if(0 == (strcmp(shm_ptr ,"end")))
{
printf("\nExit the receiver process now!\n");
break;
}
}
shmdt(shm_ptr);
//7. delete the shared memory if(-1 == shmctl(shm_id, IPC_RMID, NULL))
{
perror("shmctl");
exit(EXIT_FAILURE);
}
//8. delete the semaphore if(-1 == semctl(sem_id, 0, IPC_RMID))
{
perror("semctl");
exit(EXIT_FAILURE);
}
return 0;
}
得到如下运行结果
如上可得到,receiver成功输出sender的输入的内容
再次运行
可见没有输入dc但是输出了dc,这是上一次执行的时候,存在内存中的数据。
输出内存地址
可见两个的内存地址是相同的
b)
无名管道代码
#include <stdio.h> #include <unistd.h>
int main()
{
int filedes[2];
char buf[80];
pid_t pid_1;
if(-1 == pipe(filedes))
{
perror("pipe");
exit(EXIT_FAILURE);
}
pid_1 = fork();
if(pid_1 > 0){
// father thread sleep(2);
printf("This is the father thread.\n");
char s[] = "Hello, this is written by father.\n";
write(filedes[1],s,sizeof(s));
close(filedes[0]);
close(filedes[1]);
} else if(pid_1 == 0)
{
read(filedes[0],buf,sizeof(buf));
printf("%s\n",buf);
close(filedes[0]);
close(filedes[1]);
}
return 0;
}
代码运行结果:
只有当父进程向管道输入数据后,子进程才能够得把管道内容输出。
修改代码
#include <stdio.h> #include <unistd.h>
int main()
{
int filedes[2];
char buf[80];
pid_t pid_1;
if(-1 == pipe(filedes))
{
perror("pipe");
exit(EXIT_FAILURE);
}
pid_1 = fork();
if(pid_1 > 0){
// father thread printf("This is the father thread.\n");
char s1[] = "Hello, this is written by father.\n";
char s2[] = "Babalabeba~\n";
write(filedes[1],s1,sizeof(s1));
write(filedes[1],s2,sizeof(s2));
close(filedes[0]);
close(filedes[1]);
}else if(pid_1 == 0)
{
// child thread sleep(2);
printf("This is the child thread.\n");
read(filedes[0],buf,sizeof(buf));
printf("%s\n",buf);
close(filedes[0]);
close(filedes[1]);
}
return 0;
}
运行结果为:
我们将父进程进行了两次延时,子进程进行了两次延时,得到如上结果,所以可以看出无名管道实现了同步机制,但只有父进程写入的时候,子进程才不会被阻塞。
接下来我们实验有名管道
有名管道代码4-3.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <fcntl.h>
#define FIFO "/home/lh/my_fifo" int main()
{
char buf[] = "hello,world";
int ret;
ret = access(FIFO, F_OK);//检查文件是否存在
if(ret != 0)
{
{
perror("mkfifo");
exit(EXIT_FAILURE);
}
}
fifo_fd = open(FIFO, O_WRONLY);//以只写的方法打开FLFO文件
if(-1 == fifo_fd)
{
perror("open");
exit(EXIT_FAILURE);
}
num = write(fifo_fd, buf, sizeof(buf));//向文件写入
if(num < sizeof(buf))
{
perror("write");
exit(EXIT_FAILURE);
}
printf("write the message ok!\n");
close(fifo_fd);
return 0;
}
4-4.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <fcntl.h>
#define FIFO "/home/lh/my_fifo" int main()
{
char buf[20] ;
memset(buf, '\0', sizeof(buf));
int ret;
ret = access(FIFO, F_OK);//查看文件是否存在
if(ret != 0)
{
fprintf(stderr, "FIFO %s does not existed", FIFO);
exit(EXIT_FAILURE);
}
int fifo_fd;
fifo_fd = open(FIFO, O_RDONLY);//只读方式打开文件FIFO
if(-1 == fifo_fd)
{
perror("open");
exit(EXIT_FAILURE);
}
int num = 0;
num = read(fifo_fd, buf, sizeof(buf)); //读取文件
printf("Read %d words: %s\n", num, buf);
close(fifo_fd);
return 0;
}
由于两个程序一个为只读另一个为只写,所以会交叉阻塞。
操作 | 运行结果 |
---|---|
只运行4-3 | 4-3被阻塞 |
运行4-3,再运行4-4 | 4-3,4-4分别完成 |
只运行4-4 | 4-4阻塞 |
运行4-4,再运行4-3 | 4-4,4-3分别完成 |
所以,不论是无名管道还是有名管道,都实现了同步机制,有名管道运行时在没有一方的时候另一方会被阻塞,这与无名管道中,父进程没有输入而子进程会被阻塞一样,实现的同步机制。
c)
4-5.c代码,即client代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <signal.h>
#define BUF_SIZE 128
struct msgbuf
{
long mtype;
char mtext[BUF_SIZE];
};
int main(int argc, char *argv[])
{
key_t key;
int msgId;
printf("THe process(%s),pid=%d started~\n", argv[0], getpid());
key = ftok(".", 0xFF);
msgId = msgget(key, IPC_CREAT|0644);//创建一个消息队列
if(-1 == msgId)
{
perror("msgget");
exit(EXIT_FAILURE);
}
pid_t pid;
if(-1 == (pid = fork()))
{
perror("vfork");
exit(EXIT_FAILURE);
}
if(0 == pid)
{
while(1)
{
alarm(0);
alarm(100);
struct msgbuf rcvBuf;
memset(&rcvBuf, '\0', sizeof(struct msgbuf));
msgrcv(msgId, &rcvBuf, BUF_SIZE, 2, 0);//子进程读取消息
printf("Server said: %s\n", rcvBuf.mtext);
}
exit(EXIT_SUCCESS);
}
else {
while(1)
{
usleep(100);
struct msgbuf sndBuf;
memset(&sndBuf, '\0', sizeof(sndBuf));
char buf[BUF_SIZE] ;
memset(buf, '\0', sizeof(buf));
printf("\nInput snd mesg: ");
scanf("%s", buf);
strncpy(sndBuf.mtext, buf, strlen(buf)+1);
sndBuf.mtype = 1;
if(-1 == msgsnd(msgId, &sndBuf, strlen(buf)+1, 0))//父进程发送消息
{
perror("msgsnd");
exit(EXIT_FAILURE);
}
if(!strcmp("end~", buf))
break;
}
printf("THe process(%s),pid=%d exit~\n", argv[0], getpid());
}
return 0;
}
4-6.c代码即server代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <signal.h>
#define BUF_SIZE 128
struct msgbuf
{
long mtype;
char mtext[BUF_SIZE];
};
int main(int argc, char *argv[])
{
key_t key;
int msgId;
key = ftok(".", 0xFF);
msgId = msgget(key, IPC_CREAT|0644);
if(-1 == msgId)
{
perror("msgget");
exit(EXIT_FAILURE);
}
printf("Process (%s) is started, pid=%d\n", argv[0], getpid());
while(1)
{
alarm(0);
alarm(600);
struct msgbuf rcvBuf;
memset(&rcvBuf, '\0', sizeof(struct msgbuf));
msgrcv(msgId, &rcvBuf, BUF_SIZE, 1, 0); //读取消息
printf("Receive msg: %s\n", rcvBuf.mtext);
struct msgbuf sndBuf;
memset(&sndBuf, '\0', sizeof(sndBuf));
strncpy((sndBuf.mtext), (rcvBuf.mtext),strlen(rcvBuf.mtext)+1);
sndBuf.mtype = 2;
if(-1 == msgsnd(msgId, &sndBuf, strlen(rcvBuf.mtext)+1, 0))//发送消息
{
perror("msgsnd");
exit(EXIT_FAILURE);
}
if(!strcmp("end~", rcvBuf.mtext))
break;
}
printf("THe process(%s),pid=%d exit~\n", argv[0], getpid());
return 0;
}
运行结果如下图
队列是链表,每一个消息的最大内存长度是有限的,因此队列中可以存放的数据也是有限的,所以队列知识和少量数据的传递。
五.阅读Pintos操作系统,找到并阅读进程上下文切换的代码,说明实现的保存和恢复的上下文内容以及进程切换的工作流程。
进程切换是运行多个进程中必不可少的步骤,它可以将处理器从正在占用的进程中回收回来,并将处理器给带运行的进程来使用。进程切换通过将正在运行的进程存放在处理器的寄存器中的数据暂时储存,将处理器转交给其他进程来使用。这些被保存的数据将被存放在该进程的私有堆栈中储存起来。最后再将存放在堆栈中的数据恢复到处理器的寄存器中,继续运行该进程。在这其中这些储存在寄存器中的数据叫做进程的上下文。
流程图如下:
github代码链接:https://github.com/mitsdisy/OS
多谢查看指导!