目录
5)阅读Pintos操作系统,找到并阅读进程上下文切换的代码,说明实现的保存和恢复的上下文内容以及进程切换的工作流程。
一.实验目的
1、系统调用的进一步理解;2、进程上下文切换;3、同步与通信方法。
二.实验题目
1)通过fork的方式,产生4个进程P1,P2,P3,P4,每个进程打印输出自己的名字,例如P1输出“I am the process P1”。要求P1最先执行,P2、P3互斥执行,P4最后执行。通过多次测试验证实现是否正确。
1、进程前驱图
2、程序代码
设置三个信号量,初始值均为0,分别用来记录进程1、2、3是否执行。
起初使用while(P2!= 0)语句判断是否进入子进程,发现程序会顺序执行而非互斥。于是改为if判断。
3、运行结果
运行的整个流程,因为没关闭进程所以P1会一直执行(题目未做要求),但能够按照P1-P3-P2-P4顺序进行。多次试验结果相同。
4、为体现互斥,在进程3中加入sleep()函数,再次运行。
此时P2先于P3。证明互斥。
2)火车票
余票数ticketCount 初始值为1000,有一个售票线程,一个退票线程,各循环执行多次。添加同步机制,使得结果始终正确。要求多次测试添加同步机制前后的实验效果。(说明:为了更容易产生并发错误,可以在适当的位置增加一些pthread_yield(),放弃CPU,并强制线程频繁切换)
1、程序代码
起初将循环写在了sold、cancel内,发现线程顺序执行,并不是预期的同步,于是修改代码将while写入主函数,创建多个线程。
问题:在使用pthread_yield()发生编译错误,显示未声明
查询得知要在编译时加上-D_GNU_SOURCE选项
2、运行结果
结果正确。初始值与最终值相同。
3、为了测试信号量作用,将信号量代码删去再次运行
结果错误。在运行过程中出现读脏数据、不可重复读的现象。例如测试中,卖出时读取的数据并没有更新退还后数据。可见互斥信号量能够保证同步机制。
3)一个生产者一个消费者线程同步。
设置一个线程共享的缓冲区, char buf[10]。一个线程不断从键盘输入字符到buf,一个线程不断的把buf的内容输出到显示器。要求输出的和输入的字符和顺序完全一致。(在输出线程中,每次输出睡眠一秒钟,然后以不同的速度输入测试输出是否正确)。要求多次测试添加同步机制前后的实验效果。
1、程序代码
经典的生产者消费者问题,非互斥,设置两个信号量。
empty:代表缓冲区空闲数,即空间
full:代表缓冲区填充数,即数据
2、运行结果
慢速输入时如上。
快速输入时,仍旧可以按序输出。
4)进程通信问题
阅读并运行共享内存、管道、消息队列三种机制的代码
(https://www.cnblogs.com/Jimmy1988/p/7706980.html
https://www.cnblogs.com/Jimmy1988/p/7699351.html
https://www.cnblogs.com/Jimmy1988/p/7553069.html )
a)通过实验测试,验证共享内存的代码中,receiver能否正确读出sender发送的字符串?如果把其中互斥的代码删除,观察实验结果有何不同?如果在发送和接收进程中打印输出共享内存地址,他们是否相同,为什么?
1、代码如下:
/*
* Filename: Sender.c
* Description:
*/
#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;
}
/*
* Filename: Receiver.c
* Description:
*/
#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;
}
2、运行结果
能够实时发送与接收消息。
3、删去互斥代码
sender:
receiver:
4、再次运行
sender无变化,但receiver不再等待发送端消息,直接不停输出共享内存中的数据,
5、打印输出共享内存地址
printf("\tsender addr : %p\n", shm_ptr);
printf("\treceiver addr : %p\n", shm_ptr);
地址不同。
解释:此题与实验一中共享类似。操作系统虚拟化了内存。每个进程访问自己的私有虚拟地址空间,操作系统以某种方式映射到机器的物理内存。一个正在运行的程序中的内存引用不会影响其他进程(或OS本身)的地址空间。实际上他们物理内存地址是相同的,是由操作系统管理的共享资源。
若关闭空间地址随机化,可以看到地址相同。
b)有名管道和无名管道通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?
1、无名管道示例程序代码及运行结果:
#include <stdio.h>
#include <unistd.h> //for pipe()
#include <string.h> //for memset()
#include <stdlib.h> //for exit()
int main()
{
int fd[2];
char buf[20];
if(-1 == pipe(fd))
{
perror("pipe");
exit(EXIT_FAILURE);
}
write(fd[1], "hello,world", 12);
memset(buf, '\0', sizeof(buf));
read(fd[0], buf, 12);
printf("The message is: %s\n", buf);
return 0;
}
2、无名管道同步机制验证
修改代码,不写入数据时,不会进行读取:
write(fd[1], "", 0);
修改代码,写入过多数据时,无法再写入:
write(fd[1], "1234567890123456789012", 22);
同步机制:当写端存在时,管道中没有数据时,读取管道时将阻塞;当管道数据满时,此时再向管道写数据,写端将阻塞。
3、有名管道示例程序代码
fifo_snd.c:
/*
*File: fifo_send.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <fcntl.h>
#define FIFO "/tmp/my_fifo"
int main()
{
char buf[] = "hello,world";
//`. check the fifo file existed or not
int ret;
ret = access(FIFO, F_OK);
if(ret == 0) //file /tmp/my_fifo existed
{
system("rm -rf /tmp/my_fifo");
}
//2. creat a fifo file
if(-1 == mkfifo(FIFO, 0766))
{
perror("mkfifo");
exit(EXIT_FAILURE);
}
//3.Open the fifo file
int fifo_fd;
fifo_fd = open(FIFO, O_WRONLY);
if(-1 == fifo_fd)
{
perror("open");
exit(EXIT_FAILURE);
}
//4. write the fifo file
int num = 0;
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;
}
fifo_rcv.c:
/*
*File: fifo_rcv.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 "/tmp/my_fifo"
int main()
{
char buf[20] ;
memset(buf, '\0', sizeof(buf));
//`. check the fifo file existed or not
int ret;
ret = access(FIFO, F_OK);
if(ret != 0) //file /tmp/my_fifo existed
{
fprintf(stderr, "FIFO %s does not existed", FIFO);
exit(EXIT_FAILURE);
}
//2.Open the fifo file
int fifo_fd;
fifo_fd = open(FIFO, O_RDONLY);
if(-1 == fifo_fd)
{
perror("open");
exit(EXIT_FAILURE);
}
//4. read the fifo file
int num = 0;
num = read(fifo_fd, buf, sizeof(buf));
printf("Read %d words: %s\n", num, buf);
close(fifo_fd);
return 0;
}
4、有名管道同步机制验证
先打开写端,还没打开读端时,写端会阻塞:
打开读之后成功通信:
先打开读端,后打开写端,同时阻塞。
同步机制:(参考https://blog.csdn.net/ly52352148/article/details/52302867)
A.从FIFO中读取数据
约定:如果一个进程为了从FIFO中读取数据而以阻塞的方式打开FIFO, 则称内核为该进程的读操作设置了阻塞标志
<1>如果有进程为写而打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说返回-1,当前errno值为EAGAIN,提醒以后再试。
<2>对于设置阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其他进程正在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论写入数据量的大小,也不论读操作请求多少数据量。
<3>如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞
<4>如果写端关闭,管道中有数据读取管道中的数据,如果管道中没有数据读端将不会继续阻塞,此时返回0。
注意:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。
B.向FIFO中写入数据
约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作设置了阻塞标志。
对于设置了阻塞标志的写操作:
<1>当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳写入的字节数时,才开始进行一次性写操作。
<2>当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。
对于没有设置阻塞标志的写操作:
<1>当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。
<2>当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写。
注意:只有读端存在,写端才有意义。如果读端不在,写端向FIFO写数据,内核将向对应的进程发送SIGPIPE信号(默认终止进程);
c)消息通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?
1、程序源代码
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
//Rebuild the strcut (must be)
struct msgbuf
{
long mtype;
char mtext[BUF_SIZE];
};
int main(int argc, char *argv[])
{
//1. creat a mseg queue
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);
}
//2. creat a sub process, wait the server message
pid_t pid;
if(-1 == (pid = fork()))
{
perror("vfork");
exit(EXIT_FAILURE);
}
//In child process
if(0 == pid)
{
while(1)
{
alarm(0);
alarm(100); //if doesn't receive messge in 100s, timeout & exit
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 //parent process
{
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 scanf "end~", exit
if(!strcmp("end~", buf))
break;
}
printf("THe process(%s),pid=%d exit~\n", argv[0], getpid());
}
return 0;
}
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
//Rebuild the strcut (must be)
struct msgbuf
{
long mtype;
char mtext[BUF_SIZE];
};
int main(int argc, char *argv[])
{
//1. creat a mseg queue
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); //if doesn't receive messge in 600s, timeout & exit
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 scanf "end~", exit
if(!strcmp("end~", rcvBuf.mtext))
break;
}
printf("THe process(%s),pid=%d exit~\n", argv[0], getpid());
return 0;
}
2、验证同步机制
正常运行
当输入过长数据时:
同步机制:
当没有消息时,接收端阻塞;当消息队列满时,发送端阻塞。
如果msgflg和常数IPC_NOWAIT合用,则在msgsnd()执行时若是消息队列已满,则msgsnd()将不会阻塞,而会立即返回-1,如果执行的是msgrcv(),则在消息队列呈空时,不做等待马上返回-1,并设定错误码为ENOMSG。当msgflg为0时, msgsnd()及msgrcv()在队列呈满或呈空的情形时,采取阻塞等待的处理模式。
5)阅读Pintos操作系统,找到并阅读进程上下文切换的代码,说明实现的保存和恢复的上下文内容以及进程切换的工作流程。
参考:http://www.cnblogs.com/laiy/p/pintos_project1_thread.html
从timer_sleep函数分析。
/* Sleeps for approximately TICKS timer ticks. Interrupts must
be turned on. */
void
timer_sleep (int64_t ticks)
{
int64_t start = timer_ticks ();
ASSERT (intr_get_level () == INTR_ON);
while (timer_elapsed (start) < ticks)
thread_yield();
}
第6行调用了timer_ticks函数,
/* Returns the number of timer ticks since the OS booted. */
int64_t
timer_ticks (void)
{
enum intr_level old_level = intr_disable ();
int64_t t = ticks;
intr_set_level (old_level);
return t;
}
timer_ticks函数中intr_disable函数获取了当前的中断状态, 然后将当前中断状态改为不能被中断,然后返回执行之前的中断状态。即禁止当前行为被中断,保存禁止被中断前的中断状态。
然后获取ticks的当前值返回。从pintos被启动开始,ticks就一直在计时,代表着操作系统执行单位时间的前进计量。
第8行循环中,timer_elapsed返回了当前时间距离then的时间间隔, 那么这个循环实质就是在ticks的时间内不断执行thread_yield。
/* Yields the CPU. The current thread is not put to sleep and
may be scheduled again immediately at the scheduler's whim. */
void
thread_yield (void)
{
struct thread *cur = thread_current ();
enum intr_level old_level;
ASSERT (!intr_context ());
old_level = intr_disable ();
if (cur != idle_thread)
list_push_back (&ready_list, &cur->elem);
cur->status = THREAD_READY;
schedule ();
intr_set_level (old_level);
}
其中第6行thread_current调用的running_thread, 把CPU栈的指针复制到esp中,然后调用pg_round_down,返回当前线程起始指针。
第9行断言这是个软中断, 第11和16行包裹起来的就是我们线程机制保证的一个原子性操作。第12-15行:当前线程不是空闲的线程就调用list_push_back把当前线程的元素扔到就绪队列里面,并把线程改成THREAD_READY状态。
schedule:
/* Schedules a new process. At entry, interrupts must be off and
the running process's state must have been changed from
running to some other state. This function finds another
thread to run and switches to it.
It's not safe to call printf() until thread_schedule_tail()
has completed. */
static void
schedule (void)
{
struct thread *cur = running_thread ();
struct thread *next = next_thread_to_run ();
struct thread *prev = NULL;
ASSERT (intr_get_level () == INTR_OFF);
ASSERT (cur->status != THREAD_RUNNING);
ASSERT (is_thread (next));
if (cur != next)
prev = switch_threads (cur, next);
thread_schedule_tail (prev);
}
首先获取当前线程cur和调用next_thread_to_run获取下一个要run的线程。
如果就绪队列空闲直接返回一个空闲线程指针,否则拿就绪队列第一个线程出来返回。
然后3个断言确保不能被中断,当前线程是RUNNING_THREAD等。
如果当前线程和下一个要跑的线程不是同一个的话调用switch_threads返回给prev。
这个函数实现是用汇编语言实现的在threads/switch.S里:
#### struct thread *switch_threads (struct thread *cur, struct thread *next);
####
#### Switches from CUR, which must be the running thread, to NEXT,
#### which must also be running switch_threads(), returning CUR in
#### NEXT's context.
####
#### This function works by assuming that the thread we're switching
#### into is also running switch_threads(). Thus, all it has to do is
#### preserve a few registers on the stack, then switch stacks and
#### restore the registers. As part of switching stacks we record the
#### current stack pointer in CUR's thread structure.
.globl switch_threads
.func switch_threads
switch_threads:
# Save caller's register state.
#
# Note that the SVR4 ABI allows us to destroy %eax, %ecx, %edx,
# but requires us to preserve %ebx, %ebp, %esi, %edi. See
# [SysV-ABI-386] pages 3-11 and 3-12 for details.
#
# This stack frame must match the one set up by thread_create()
# in size.
pushl %ebx
pushl %ebp
pushl %esi
pushl %edi
# Get offsetof (struct thread, stack).
.globl thread_stack_ofs
mov thread_stack_ofs, %edx
# Save current stack pointer to old thread's stack, if any.
movl SWITCH_CUR(%esp), %eax
movl %esp, (%eax,%edx,1)
# Restore stack pointer from new thread's stack.
movl SWITCH_NEXT(%esp), %ecx
movl (%ecx,%edx,1), %esp
# Restore caller's register state.
popl %edi
popl %esi
popl %ebp
popl %ebx
ret
.endfunc
先4个寄存器压栈保存寄存器状态(保护作用),然后全局变量thread_stack_ofs记录线程和棧之间的间隙。之后先把当前的线程指针放到eax中, 并把线程指针保存在相对基地址偏移量为edx的地址中。切换到下一个线程的线程棧指针,保存在ecx中,再把这个线程相对基地址偏移量edx地址(上一次保存现场的时候存放的)放到esp当中继续执行。
这里ecx, eax起容器的作用, edx指向当前现场保存的地址偏移量。
简单来说就是保存当前线程状态, 恢复新线程之前保存的线程状态。
然后再把4个寄存器拿出来,到现在eax(函数返回值是eax)就是被切换的线程棧指针。
我们由此得到一个结论,如果下一个线程和当前线程不一样的话,schedule先把当前线程丢到就绪队列,然后把线程切换。
thread_schedule_tail
先是获得当前线程cur, 注意此时是已经切换过的线程了(或者还是之前run的线程, 因为ready队列为空)。然后把线程状态改成THREAD_RUNNING, 然后thread_ticks清零开始新的线程切换时间片。然后调用process_activate触发新的地址空间:1.更新页目录表 2.更新任务现场信息。
总结:
thread_schedule_tail:获取当前线程,分配恢复之前执行的状态和现场, 如果当前线程死了就清空资源。
schedule:拿下一个线程切换过来继续run。
thread_yield:把当前线程扔到就绪队列里,然后重新schedule,注意这里如果ready队列为空的话当前线程会继续在cpu执行。
timer_sleep:在ticks时间内,如果线程处于running状态就不断把他扔到就绪队列不让他执行。
github代码地址:https://github.com/Woochy-Young/OS/tree/master/LAB3