进程同步问题

实验目的

  1. 系统调用的进一步理解。
  2. 进程上下文切换。
  3. 同步的方法。

实验内容

task1

1.1 实验要求

通过fork的方式,产生4个进程P1,P2,P3,P4,每个进程打印输出自己的名字,例如P1输出“I am the process P1”

要求:P1最先执行,P2、P3互斥执行,P4最后执行。通过多次测试验证实现是否正确。

1.2 知识准备

1.2.1 信号量相关

信号量是一种特殊的变量,访问具有原子性。只允许对它进行两个操作:

  1. 等待信号量
    当信号量值为0时,程序等待;当信号量值大于0时,信号量减1,程序继续运行。
  2. 发送信号量
    将信号量值加1。
    信号量的函数都以sem_开头,线程中使用的基本信号量函数有4个,它们都声明在头文件semaphore.h中。

sem_wait函数
该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。它的原型如下:
int sem_post(sem_t *sem)
sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.
sem_post函数
该函数用于以原子操作的方式将信号量的值加1。它的原型如下:
int sem_post(sem_t *sem)
与sem_wait一样,sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.
sem_init函数
该函数用于创建信号量
int sem_init(sem_t *sem,int pshared,unsigned int value)
该函数初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。 pshared控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享,value为sem的初始值。调用成功时返回0,失败返回-1.
sem_destroy函数
该函数用于对用完的信号量的清理
int sem_destroy(sem_t *sem)
成功时返回0,失败时返回-1.
sem_open函数
创建并初始化有名信号灯
参数:
name 信号灯的外部名字(不能为空,为空会出现段错误)
oflag 选择创建或打开一个现有的信号灯
mode 权限位
value 信号灯初始值
sem_t *sem sem_open(const char *name, int oflag, mode_t mode,unsinged int value)
成功时返回指向信号灯的指针,出错时为SEM_FAILED
sem_close函数
关闭有名信号灯。
一个进程终止时,内核还对其上仍然打开着的所有有名信号灯自动执行这样的信号灯关闭操作。不论该进程是自愿终止的还是非自愿终止的,这种自动关闭都会发生。但应注意的是关闭一个信号灯并没有将他从系统中删除。
int sem_close(const char *name)
若成功则返回0,否则返回-1。
sem_unlink函数
从系统中删除信号灯。有名信号灯用sem_unlink从系统中删除。每个信号灯有一个引用计数器记录当前的打开次数,sem_unlink必须等待这个数为0时才能把name所指的信号灯从文件系统中删除。也就是要等待最后一个sem_close发生
int sem_unlink(const char *name)
成功则返回0,否则返回-1
sem_open与sem_init的区别

  1. 创建有名信号量必须指定一个与信号量相关链的文件名称,这个name通常是文件系统中的某个文件。
    基于内存的信号量不需要指定名称
  2. 有名信号量sem 是由sem_open分配内存并初始化成value值
    基于内存的信号量是由应用程序分配内存,有sem_init初始化成为value值。如果shared为1,则分配的信号量应该在共享内存中。
  3. sem_open不需要类似shared的参数,因为有名信号量总是可以在不同进程间共享的,而基于内存的信号量通过shared参数来决定是进程内还是进程间共享,并且必须指定相应的内存
  4. 基于内存的信号量不使用任何类似于O_CREAT标志的东西,也就是说,sem_init总是初始化信号量的值,因此,对于一个给定的信号量,我们必须小心保证只调用sem_init一次,对于一个已经初始化过的信号量调用sem_init,结果是未定义的。
  5. 内存信号量通过sem_destroy删除信号量,有名信号量通过sem_unlink删除

1.2.2 进程关系

根据实验要求分析进程关系,绘制前趋图


image

由要求可知,进程必须是P1最先执行,之后P2、P3互斥执行,之后P4再执行。在本实验中,我们采取信号量完成这一实验。首先我们将题目要求拆分为两个部分:
1)P1执行后互斥执行P2,P3:故我们定义一个信号量t1_23,初始值为0,当P1进程执行完打印输出后,对t1_23进行signal操作,信号量值变为1。当执行P2和P3时,先对t1_23进行wait操作,维护前驱关系,同时保证P2和P3仅有一个进程可以执行,在P2,P3执行打印输出后,对t1_23进行signal操作,维护互斥关系。
2)P2、P3执行后执行P4:我们分别定义两个信号量P24和P34,初始值为0,P2进程执行完打印输出后对P24进行signal操作,P3进程执行完打印输出后对P34进行signal操作,在P4执行时先针对以上两个信号量进行wait操作,故P4会等待P2,P3都完成才进行,以维护P2、P3对于P4的前驱关系。

1.3 实验过程

  1. 程序流程图如下所示:


    image
  2. task1_fork.c 代码实现:
        #include <stdio.h>
        #include <stdlib.h>
        #include <unistd.h>
        #include <sys/ipc.h>
        #include <sys/types.h>
        #include <sys/sem.h>
        #include <pthread.h>
        #include <semaphore.h>
        #include <fcntl.h>
        
        int main()
        {
            pid_t p2,p3,p4; //创建子进程2,3,4,P1无需创建,进入主函数后的进程即为p1进程
            sem_t *t1_23,*t24,*t34;//创建信号量
    
        t1_23=sem_open("t1_23",O_CREAT,0666,0);//表示关系进程1执行完进程2,3中的一个可以执行
        t24=sem_open("t24",O_CREAT,0666,0);//表示关系进程2执行完进程4才可执行
        t34=sem_open("t34",O_CREAT,0666,0);//表示关系进程3执行完进程4才可执行
        
        p2=fork();//创建进程p2
        if(p2==0)
        {
            sem_wait(t1_23);//实现互斥
            printf("I am the process p2\n");
            sem_post(t1_23);
            sem_post(t24);//实现前驱
        }
        if(p2<0)
        {
            perror("error!");
        }
        if(p2>0)
        {
            p3=fork();//创建进程p3
            if(p3==0)
            {
                sem_wait(t1_23);//实现互斥
                printf("I am the process p3\n");
                sem_post(t1_23);
                sem_post(t34);//实现前驱
            }
            if(p3<0)
            {
                perror("error!");
            }
            if(p3>0)
            {
                p4=fork();//创建进程p4
                if(p4>0)
                {
                    printf("I am the process p1\n");
                    sem_post(t1_23);
                }
                if(p4==0)
                {
                    sem_wait(t24);
                    sem_wait(t34);
                    printf("I am the process p4\n");
                    sem_post(t24);
                    sem_post(t34);
                }
                if(p4<0)
                {
                    perror("error!");
                }
            }
            
        }
        sleep(1);
        sem_close(t1_23);
        sem_close(t24);
        sem_close(t34);
        sem_unlink("t1_23");
        sem_unlink("t24");
        sem_unlink("t34");
        return 0;
        }
  1. 编译运行该程序可得到符合要求的进程执行结果。(由于P2,P3是互斥,所以会有两种执行顺序)


    image

1.4 问题总结

  1. 运行时出现进程提前结束,通过添加sleep()函数延迟结束可以解决这个问题。


    image
  2. 运行结果多次测试仅有(P1->P3->P2->P4)一种情况,后发现是之前关闭了随机化导致,打开随机化后可以得到P2和P3先后是随机的。打开方式如下图:


    image
  3. 直接gcc -o 无法编译该程序,需要添加-pthread,正确的编译命令为:
    gcc -o task1 task1_fork.c -lpthread

task2

2.1 实验要求

火车票余票数ticketCount 初始值为1000,有一个售票线程,一个退票线程,各循环执行多次。添加同步机制,使得结果始终正确。要求多次测试添加同步机制前后的实验效果。

2.2 知识准备

2.2.1 pthread_yield()函数

作用:
主动释放CPU从而让另外一个线程运行
与sleep()的区别:
pthread_yield()函数可以使用另一个级别等于或高于当前线程的线程先运行。如果没有符合条件的线程,那么这个函数将会立刻返回然后继续执行当前线程的程序。
sleep()则函数是等待一定时间后等待CPU的调度,然后去获得CPU资源。

2.3 实验过程

  1. 程序task2.c代码,(全部代码,通过加入/取消注释完成不同测试)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <fcntl.h>
sem_t* Sem = NULL;

int ticketCount=1000;

void *back()
{
    int temp;
    int i;
    for(i=0;i<200;i++)
    {
        //sem_wait(Sem);
        int temp = ticketCount;
        printf("退票后的票数为:%d \n",ticketCount);
        //放弃CPU,强制切换到另外一个进程
        //pthread_yield();
        temp=temp+1;
        //pthread_yield();
        ticketCount=temp;
        //sem_post(Sem);
    }
}

void *sell()
{
    int temp;
    int i;
    for(i=0;i<200;i++)
    {
        //sem_wait(Sem);
        int temp = ticketCount;
        printf("售票后的票数为:%d \n",ticketCount);
        //放弃CPU,强制切换到另外一个进程
        //pthread_yield();
        temp=temp-1;
        //pthread_yield();
        ticketCount=temp;
        //sem_post(Sem);
    }

}

int main(int argc,char *argv[]){
    pthread_t p1,p2;
    printf("开始的票数为:%d \n",ticketCount);
    Sem = sem_open("sem", O_CREAT, 0666, 1);
        pthread_create(&p1,NULL,sell,NULL);
        pthread_create(&p2,NULL,back,NULL);
        pthread_join(p1,NULL);
        pthread_join(p2,NULL);
    sem_close(Sem);
    sem_unlink("sem");
    printf("最终的票数为: %d \n",ticketCount);
    return 0;
}
  1. 编译运行该程序,编译命令为gcc -o task2 task2.c -lpthread

  2. 测试不同情况运行结果:
    1) 不加pthread_yield();函数,测试“卖50张,退50张”的情况

    image

    可见结果显示正确
    2) 不加pthread_yield();函数,测试“卖100张,退50张”的情况
    image

    可见结果值不正确
    3) 不加pthread_yield();函数,测试“卖50张,退100张”的情况
    image

    可见结果值不正确
    4)添加pthread_yield();函数在售票ticketCount值被写回前,测试“卖200张,退200张”的情况
    image

    可见结果不正确且偏小
    5) 添加pthread_yield();函数在退票ticketCount值被写回前,测试“卖200张,退200张”的情况
    image

    可见结果不正确且偏大
    6) 添加信号量机制,并添加pthread_yield();函数,测试不同买票张数
    a. 卖200张退200张:
    image

    b. 卖80张退30张:
    image

    可见结果均正确

  3. 结果分析
    在并发执行多进程时,当循环次数很大是,会产生进程间的切换,而多进程的切换可能导致在一个进程在对票数ticketCount进行操作后还未写回,另外一个进程就读取该数据。产生读取脏数据及覆盖的问题,进而导致结果的不正确。
    在测试中,第2、3种情况是因改变两个进程循环次数而得到不同的值,这个值没有一定的规律。第4种情况是在售票后未及时写回,售票进程会在之后一段时间出现覆盖性写入。故而售票量多,剩余票结果较小。第5种情况是在退票后未及时写回,退票进程会在之后一段时间出现覆盖性写入。故而退票量多,剩余票结果较大。第6种情况加入了信号量就可保证售票进程和退票进程的的原子性,避免了脏数据读取和覆盖性写入等问题,结果正确,可保证进程同步。

task3

3.1 实验要求

一个生产者一个消费者线程同步。设置一个线程共享的缓冲区, char buf[10]。一个线程不断从键盘输入字符到buf,一个线程不断的把buf的内容输出到显示器。要求输出的和输入的字符和顺序完全一致。(在输出线程中,每次输出睡眠一秒钟,然后以不同的速度输入测试输出是否正确)。要求多次测试添加同步机制前后的实验效果。

3.2 知识准备

3.2.1 生产者-消费者问题

所谓生产者-消费者问题,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:
如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
如果共享数据区为空的话,阻塞消费者继续消费数据;

3.3 实验过程

  1. 程序task3.c代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <fcntl.h>

sem_t *empty=NULL;
sem_t *full=NULL;
char buffer[10];

void *worker1(void *arg)
{
    
    for(int i=0;i<10;i++)
    {
        sem_wait(empty);
        scanf("%c",&buffer[i]);
        sem_post(full);
        if(i==9)    i=-1;
    }   
    return NULL;
}
void *worker2(void *arg)
{
    for(int i=0;i<10;i++)
    {
        sem_wait(full);
        printf("%c",buffer[i]);
        sem_post(empty);
        sleep(1);
        if(i==9)    i=-1;
    }   
    return NULL;
}

int main(int argc,char *argv[])
{
    empty=sem_open("empty",O_CREAT,0666,10);//初始缓存空间为10
    full=sem_open("full",O_CREAT,0666,0);//初始没有字符
    
    pthread_t p1,p2;
    pthread_create(&p1,NULL,worker1,NULL);
    pthread_create(&p2,NULL,worker2,NULL);
    pthread_join(p1,NULL);
    pthread_join(p2,NULL);
    
    sem_close(empty);
    sem_close(full);
    sem_unlink("empty_");
    sem_unlink("full_");
    return 0;
}
  1. 编译运行程序,编译命令为:gcc -o task3 task3.c -lpthread
  2. 测试运行结果
    1)程序中输出函数中未加sleep函数时,可见结果正确


    image

    2)加入sleep函数后测试一次输入十个字符‘1234567890’,可见结果正确。


    image

    3)测试分别连续输入十个字符‘1234567890’,可见结果正确。
    image

    4)测试一次输入多于十个字符‘1234567890abcdef’,可见结果正确。
    image

3.4 问题总结

在一次性输入十个字符的时候,由于未超过缓存空间大小,会按照正常的情况读入缓存空间,empty陆续减至0,full陆续增至0,。但在一次性输入超过十个字符时,由于超过缓存空间的大小,empty减至0后就不可再写入缓存空间,必须输出后才可继续写入,但是为什么在用户角度看起来是一起写入一起输出的呢?因为其他的字符可以在I/O缓冲区等待,当拥有empty信号量时才可继续写入缓存空间,再被输出。

task4

4.1 实验要求

  1. 通过实验测试,验证共享内存的代码中,receiver能否正确读出sender发送的字符串?如果把其中互斥的代码删除,观察实验结果有何不同?如果在发送和接收进程中打印输出共享内存地址,他们是否相同,为什么?
  2. 有名管道和无名管道通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?
  3. 消息通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?

4.2 实验过程

4.2.1 共享内存

sender.c源码

/*
 * 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;
}

在sender的循环中:首先判断sem=0是否成立,即共享内存是否可用,如果为0,则写入数据到共享内存(阻塞读);写入完成后,sem=1,此时可以读取,不可以写入。
receiver.c源码

/*
 * 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;
}

在receiver的循环中:首先判断sem=1是否成立,即共享内存是否可读,如果为1,则读取数据;读取完成后,sem=0,此时只允许写入

  1. 验证共享内存的代码中,receiver能否正确读出sender发送的字符串


    image

    通过测试可以看出receiver能够正确读出sender发送的字符串。

  2. 把其中互斥的代码删除
    注释掉sender循环中的控制互斥的代码:


    image

    注释掉receiver循环中的控制互斥的代码:


    image

    再次编译运行两程序,在sender中第一次测试输入“test”,第二次重新打开后输入“hello”,“repeat”,sender在每次输入后就停留于等待输入的状态。
    image

    可见receiver刚开始的时候循环输出原共享内存中的内容,在sender向共享内存写入新的内容之后又循环输出新的内容。
    image
  3. 如果在发送和接收进程中打印输出共享内存地址,他们是否相同,为什么?
    修改编写sender_add.c,在其中编写输出共享内存地址
    image

    修改编写receiver_add.c,在其中编写输出共享内存地址
    image

    编译运行两程序,可得到两个共享内存地址不相同
    image

    针对此种现象,根据以往实验的经验,最先想到的原因是地址空间随机化的问题,尝试关闭地址随机化再次进行测试。
    image

    可见关闭地址空间随机化后得到的共享内存地址一致。
    image

    之后,我们再将地址空间随机化打开,避免此项操作的影响。
    image

    根据资料可知,由于在“共享内存映射”中shmat()作用是将共享内存空间挂载到进程中
    void *shmat(int shmid, const void *shmaddr, int shmflg)
    参数:
    shmid : shmget()返回值
    shmaddr: 共享内存的映射地址,一般为0(由系统自动分配地址)
    shmflg : 访问权限和映射条件
    返回值:
    成功:共享内存段首地址
    失败:NULL / (void *)-1
    所以考虑修改共享内存的映射地址为指定地址来保证共享内存地址的一致
    image

    结果可见此方法可以使得共享内存地址一致。
    image

    问题总结
    共享内存中删除互斥代码后,receiver循环输出速度非常快,通过在receiver.c的循环中添加sleep(5),使其以合适的速度输出。
    image

4.2.2 管道(pipe)

知识准备

  1. 无名管道
    无名管道是Linux中管道通信的一种原始方法,它具有以下特点:
    ① 它只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间);
    ② 它是一个半双工的通信模式,具有固定的读端和写端;
    ③ 管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read()、write()等函数。但它不是普通的文件,并不属于其他任何文件系统并且只存在于内存中。
  2. 有名管道(FIFO)
    有名管道是对无名管道的一种改进,它具有以下特点:
    ① 它可以使互不相关的两个进程间实现彼此通信;
    ② 该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当做普通文件一样进行读写操作,使用非常方便;
    ③ FIFO严格地遵循先进先出规则,对管道及FIFO的读总是从开始处返回数据,对它们的写则是把数据添加到末尾,它们不支持如 lseek()等文件定位操作。


    image
  3. 有名管道的特性是:当以只读方式打开管道时会一直阻塞到有其他地方以写打开的时候。利用这个特性可以实现进程同步。
    针对不同的读写情况,可以得到下表:
读进程 写进程 FIFO无数据 FIFO有数据
阻塞 阻塞
阻塞 阻塞
写入 读出(未满,读写同时)
√x 即写中途退出,读直接返回0 same
√x 读中途退出,写返回SIGPIPE same
  1. 有名管道和无名管道通信实现同步机制
    无名管道
    pipe.c源码
/*
 * Filename: pipe.c
 */
 
#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;
}

编译运行该程序可得结果

image

修改添加同步验证机制后的pipe_fork.c

#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    int fd[2];
    char buf[20];
    int real_read;
    pid_t pid;

    memset((void*)buf, 0, sizeof(buf));
    if(-1 == pipe(fd))
    {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

     pid=fork();
     if (pid == 0) /* 创建一个子进程 */
     {
  /* 子进程关闭写描述符,并通过使子进程暂停1s等待父进程已关闭相应的读描述符 */
            close(fd[1]);
            /* 子进程读取管道内容 */
    while(1)
    {       //系统调用
            if ((real_read = read(fd[0], buf, sizeof(buf))) > 0)
            {
                printf("%d bytes read from the pipe is '%s'\n", real_read, buf);
            }
        else
            printf("no data\n");sleep(1);
        if(strcmp(buf,"end")==0)
            break;
        memset(buf,0,sizeof(buf));
    }
     }
     if (pid > 0)
     {
  /* 父进程关闭读描述符,并通过使父进程暂停1s等待子进程已关闭相应的写描述符 */
            close(fd[0]);
        while(1)
    {
            printf("write into the pipe:\n");
        scanf("%s",buf);
       write(fd[1],buf,strlen(buf)); //系统调用
        if(strcmp(buf,"end")==0)
        break;
         }
     }
    return 0;
}

编译运行可得到正确的结果

image

有名管道
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;
}

编译运行以上程序,只有两进程都打开时,可得到正确输出

image

测试不同的阻塞情况并分析结果
通过加入 fd=open(FIFO_NAME,O_RDONLY | O_NONBLOCK)实现非阻塞,在程序中加入两种语句,通过不同注释实现不同情况
image

1)写阻塞,读非阻塞
先执行snd后执行rcv,结果正确;
image

先执行rcv后执行snd再执行rcv时,结果不正确。
image

2)读阻塞,写非阻塞
先执行snd后执行rcv,结果不正确
image

先执行rcv后执行snd,结果正确,接收很慢
image

3)读非阻塞,写非阻塞
先执行snd后执行rcv,结果不正确
image

先执行rcv后执行snd,结果不正确
image

4.2.3 消息队列

知识准备

key:用于创建ID值(ID值由一个进程创建的话,由于进程资源的私有性,另一个进程无法获取到该ID);采用统一key值创建的ID是相同的;
id:IPC机制的唯一标识
struct msqid_ds: 消息队列数据结构
struct msg: 单个消息的数据结构
struct msgbuf: 用户自定义消息缓冲区

发送/接收消息队列 - msgsnd()/msgrcv():
作用:发送消息到消息队列(添加到尾端)/接收消息
函数原型:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
参数:

msqid: 消息队列的ID值,msgget()的返回值
msgp: struct msgbuf,(发送/接收数据的暂存区,由用户自定义大小)
msgsz: 发送/接收消息的大小(范围:0~MSGMAP)
msgflg:当达到系统为消息队列所指定的界限时,采取的操作(一般设置为0)
length: 消息数据的长度
type: 决定从队列中返回哪条消息

msgtyp description
= 0 读取队列中的第一个消息
> 0 返回消息队列中等于mtype 类型的第一条消息
< 0 返回mtype<=type 绝对值最小值的第一条消息

返回值:

成功: 0 (for msgsnd());  实际写入到mtext的字符个数 (for msgrcv())
失败:-1

client.c源代码

#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;
}

sever.c源代码

#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;
}

编译运行结果正确

image

由消息队列相关知识及代码解释可以知道两个程序通过msgrcv和msgsnd两个函数的msgflg参数控制阻塞情况,msgflg 为0表示阻塞方式,设置IPC_NOWAIT 表示非阻塞方式msgrcv 调用成功返回0,不成功返回-1。
1)设置client阻塞,sever不阻塞
image

可见一直服务器循环接受消息队列中的空内容并发给客户端
2)设置client不阻塞,sever阻塞
image

可见服务端正常,客户端一直打印输出空内容

task5

5.1 实验要求

阅读Pintos操作系统,找到并阅读进程上下文切换的代码,说明实现的保存和恢复的上下文内容以及进程切换的工作流程。

5.2 实验过程

本次实验阅读了thread.h,tread.c,interrup.h,time.c这四个文件
pintos在thread.h中定义了一个结构体struct thread,这个结构体存放了有关进程的基本信息。

struct thread
  {
    tid_t tid;                          /* Thread identifier. */
    enum thread_status status;          /* Thread state. */
    char name[16];                      /* Name (for debugging purposes). */
    uint8_t *stack;                     /* Saved stack pointer. */
    int priority;                       /* Priority. */
    struct list_elem allelem;           /* List element for all threads list. */
 
    /* Shared between thread.c and synch.c. */
    struct list_elem elem;              /* List element. */
 
#ifdef USERPROG
    /* Owned by userprog/process.c. */
    uint32_t *pagedir;                  /* Page directory. */
#endif
 
    /* Owned by thread.c. */
    unsigned magic;                     /* Detects stack overflow. */
  };

其中enum thread_status这个枚举类型的变量,就是这个线程现在所处的状态。

enum thread_status
  {
    THREAD_RUNNING,     /* Running thread. */
    THREAD_READY,       /* Not running but ready to run. */
    THREAD_BLOCKED,     /* Waiting for an event to trigger. */
    THREAD_DYING        /* About to be destroyed. */
  };

还有一个重要的概念是中断。中断分两种:一种是IO设备向CPU发出的中断的信息,另一种是CPU决定切换到另一个进程时(轮换时间片)发出的指令。
pintos的中断在interrupt.h和interrupt.c之中。其中枚举类型intr_lverl会在之后提到

enum intr_level
  {
    INTR_OFF,             /* Interrupts disabled. */
    INTR_ON               /* Interrupts enabled. */
  };

intr_off表示关中断,on表示开中断。执行原子级别操作的时候,中断必须是关着的。而pintos是以ticks作为基本时间单位的,每秒有TIMER_FREQ个ticks

#define TIMER_FREQ 100 //系统默认这个宏为100

pintos默认每一个ticks调用一次时间中断。所以每一个线程最多可以占据CPU一个ticks的时长。
thread.c中有以下几个函数:

  1. thread_current():获取当前当前的线程的指针。
  2. thread_foreach(thread_action_func *func, void *aux):遍历当前ready queue中的所有线程,并且对于每一个线程执行一次func操作。注意到这里的func是一个任意给定函数的指针,参数aux则是你想要传给这个函数的参数,而该这个函数只能在中断关闭的时候调用。
  3. thread_block()和thread_unblock(thread *t)。 这是一对儿函数,区别在于第一个函数的作用是把当前占用cpu的线程阻塞掉(就是放到waiting里面),而第二个函数作用是将已经被阻塞掉的进程t唤醒到ready队列中。
  4. timer_interrupt (struct intr_frame *args UNUSED):这个函数在timer.c中,pintos在每次时间中断时(即每一个时间单位(ticks))调用一次这个函数。
  5. intr_disable ():这个函数在interrupt.c中,作用是返回关中断,然后返回中断关闭前的状态。

timer_sleep的作用是让此线程等待ticks单位时长,然后再执行。timer_sleep函数在devices/timer.c。系统现在是使用busy wait实现的,即线程不停地循环,直到时间片耗尽。

void timer_sleep (int64_t ticks) //想要等待的时间长度
{
  int64_t start = timer_ticks (); //记录开始时的系统时间
 
  ASSERT (intr_get_level () == INTR_ON);
  while (timer_elapsed (start) < ticks) //如果elapse(流逝)的时间>=ticks时就返回。否则将持续占用cpu。
    thread_yield ();
}

它使用的方法是利用一个while循环不断地请求CPU来判断是否经过了足够的时间长度。通常cpu在一个ticks时间内可以处理10000次这样的循环,而timer_elapsed()函数只会在ticks+1时更新一次,所以此处存在一些弊端。

在timer_sleep中调用了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;
}

在这里,intr_level的东西通过intr_disable返回了一个东西

/* Disables interrupts and returns the previous interrupt status. */
enum intr_level
intr_disable (void) 
{
  enum intr_level old_level = intr_get_level ();

  /* Disable interrupts by clearing the interrupt flag.
     See [IA32-v2b] "CLI" and [IA32-v3a] 5.8.1 "Masking Maskable
     Hardware Interrupts". */
  asm volatile ("cli" : : : "memory");

  return old_level;
}

这里intr_level代表能否被中断,而intr_disable做了两件事情:

  1. 调用intr_get_level()
  2. 直接执行汇编代码,调用汇编指令来保证这个线程不能被中断。

这里又出现了一个函数intr_get_level

/* Returns the current interrupt status. */
enum intr_level
intr_get_level (void) 
{
  uint32_t flags;

  /* Push the flags register on the processor stack, then pop the
     value off the stack into `flags'.  See [IA32-v2b] "PUSHF"
     and "POP" and [IA32-v3a] 5.8.1 "Masking Maskable Hardware
     Interrupts". */
  asm volatile ("pushfl; popl %0" : "=g" (flags));

  return flags & FLAG_IF ? INTR_ON : INTR_OFF;
}

这个函数调用了汇编指令,把标志寄存器的东西放到处理器棧上,然后把值pop到flags(代表标志寄存器IF位)上,通过判断flags来返回当前终端状态(intr_level)。
所以顺序为:intr_get_level返回了intr_level的值,intr_disable获取了当前的中断状态, 然后将当前中断状态改为不能被中断, 然后返回执行之前的中断状态。
所以最终实现的是:禁止当前行为被中断, 保存禁止被中断前的中断状态(用old_level储存)
之后,timer_ticks用t获取了一个全局变量ticks, 然后返回,其中调用了intr_set_level函数。

/* Enables or disables interrupts as specified by LEVEL and
   returns the previous interrupt status. */
enum intr_level
intr_set_level (enum intr_level level) 
{
  return level == INTR_ON ? intr_enable () : intr_disable ();
}

在这个函数中有个ASSERT断言了intr_context函数返回结果的false。

/* Returns true during processing of an external interrupt
   and false at all other times. */
bool
intr_context (void) 
{
  return in_external_intr;
}

这里直接返回了是否外中断的标志in_external_intr, 就是说ASSERT断言这个中断不是外中断而是操作系统正常线程切换流程里的内中断,即软中断。

实验总结

本次实验设计的内容非常丰富,也具有一定难度,在实践的过程中查询了大量资料,也在这个过程中学到了很多知识。对于前几个实验使我对于生产者-消费者问题有了更全面的掌握,能够理解明白其中逻辑,后几个实验使我对进程间通信有了较深入的了解,通过不同的方式在不同情况的测试也更加全面的了解了不同通信方式的关键点。当然在实验中也遇到了产生很多问题,通过对于已有代码的阅读和自己的改写能够更加理解源代码的含义,解决问题的过程也是加强理解的过程。另外对于csdn写博客的也更加的熟练,写完这篇报告也很有成就感,希望以后可以再接再厉,加油~

github实验代码链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。