進程同步問題

實驗?zāi)康?/h1>
  1. 系統(tǒng)調(diào)用的進一步理解。
  2. 進程上下文切換。
  3. 同步的方法。

實驗內(nèi)容

task1

1.1 實驗要求

通過fork的方式,產(chǎn)生4個進程P1,P2,P3,P4,每個進程打印輸出自己的名字,例如P1輸出“I am the process P1”

要求:P1最先執(zhí)行,P2、P3互斥執(zhí)行,P4最后執(zhí)行。通過多次測試驗證實現(xiàn)是否正確。

1.2 知識準備

1.2.1 信號量相關(guān)

信號量是一種特殊的變量,訪問具有原子性。只允許對它進行兩個操作:

  1. 等待信號量
    當信號量值為0時,程序等待;當信號量值大于0時,信號量減1,程序繼續(xù)運行。
  2. 發(fā)送信號量
    將信號量值加1。
    信號量的函數(shù)都以sem_開頭,線程中使用的基本信號量函數(shù)有4個,它們都聲明在頭文件semaphore.h中。

sem_wait函數(shù)
該函數(shù)用于以原子操作的方式將信號量的值減1。原子操作就是,如果兩個線程企圖同時給一個信號量加1或減1,它們之間不會互相干擾。它的原型如下:
int sem_post(sem_t *sem)
sem指向的對象是由sem_init調(diào)用初始化的信號量。調(diào)用成功時返回0,失敗返回-1.
sem_post函數(shù)
該函數(shù)用于以原子操作的方式將信號量的值加1。它的原型如下:
int sem_post(sem_t *sem)
與sem_wait一樣,sem指向的對象是由sem_init調(diào)用初始化的信號量。調(diào)用成功時返回0,失敗返回-1.
sem_init函數(shù)
該函數(shù)用于創(chuàng)建信號量
int sem_init(sem_t *sem,int pshared,unsigned int value)
該函數(shù)初始化由sem指向的信號對象,設(shè)置它的共享選項,并給它一個初始的整數(shù)值。 pshared控制信號量的類型,如果其值為0,就表示這個信號量是當前進程的局部信號量,否則信號量就可以在多個進程之間共享,value為sem的初始值。調(diào)用成功時返回0,失敗返回-1.
sem_destroy函數(shù)
該函數(shù)用于對用完的信號量的清理
int sem_destroy(sem_t *sem)
成功時返回0,失敗時返回-1.
sem_open函數(shù)
創(chuàng)建并初始化有名信號燈
參數(shù):
name 信號燈的外部名字(不能為空,為空會出現(xiàn)段錯誤)
oflag 選擇創(chuàng)建或打開一個現(xiàn)有的信號燈
mode 權(quán)限位
value 信號燈初始值
sem_t *sem sem_open(const char *name, int oflag, mode_t mode,unsinged int value)
成功時返回指向信號燈的指針,出錯時為SEM_FAILED
sem_close函數(shù)
關(guān)閉有名信號燈。
一個進程終止時,內(nèi)核還對其上仍然打開著的所有有名信號燈自動執(zhí)行這樣的信號燈關(guān)閉操作。不論該進程是自愿終止的還是非自愿終止的,這種自動關(guān)閉都會發(fā)生。但應(yīng)注意的是關(guān)閉一個信號燈并沒有將他從系統(tǒng)中刪除。
int sem_close(const char *name)
若成功則返回0,否則返回-1。
sem_unlink函數(shù)
從系統(tǒng)中刪除信號燈。有名信號燈用sem_unlink從系統(tǒng)中刪除。每個信號燈有一個引用計數(shù)器記錄當前的打開次數(shù),sem_unlink必須等待這個數(shù)為0時才能把name所指的信號燈從文件系統(tǒng)中刪除。也就是要等待最后一個sem_close發(fā)生
int sem_unlink(const char *name)
成功則返回0,否則返回-1
sem_open與sem_init的區(qū)別

  1. 創(chuàng)建有名信號量必須指定一個與信號量相關(guān)鏈的文件名稱,這個name通常是文件系統(tǒng)中的某個文件。
    基于內(nèi)存的信號量不需要指定名稱
  2. 有名信號量sem 是由sem_open分配內(nèi)存并初始化成value值
    基于內(nèi)存的信號量是由應(yīng)用程序分配內(nèi)存,有sem_init初始化成為value值。如果shared為1,則分配的信號量應(yīng)該在共享內(nèi)存中。
  3. sem_open不需要類似shared的參數(shù),因為有名信號量總是可以在不同進程間共享的,而基于內(nèi)存的信號量通過shared參數(shù)來決定是進程內(nèi)還是進程間共享,并且必須指定相應(yīng)的內(nèi)存
  4. 基于內(nèi)存的信號量不使用任何類似于O_CREAT標志的東西,也就是說,sem_init總是初始化信號量的值,因此,對于一個給定的信號量,我們必須小心保證只調(diào)用sem_init一次,對于一個已經(jīng)初始化過的信號量調(diào)用sem_init,結(jié)果是未定義的。
  5. 內(nèi)存信號量通過sem_destroy刪除信號量,有名信號量通過sem_unlink刪除

1.2.2 進程關(guān)系

根據(jù)實驗要求分析進程關(guān)系,繪制前趨圖


image

由要求可知,進程必須是P1最先執(zhí)行,之后P2、P3互斥執(zhí)行,之后P4再執(zhí)行。在本實驗中,我們采取信號量完成這一實驗。首先我們將題目要求拆分為兩個部分:
1)P1執(zhí)行后互斥執(zhí)行P2,P3:故我們定義一個信號量t1_23,初始值為0,當P1進程執(zhí)行完打印輸出后,對t1_23進行signal操作,信號量值變?yōu)?。當執(zhí)行P2和P3時,先對t1_23進行wait操作,維護前驅(qū)關(guān)系,同時保證P2和P3僅有一個進程可以執(zhí)行,在P2,P3執(zhí)行打印輸出后,對t1_23進行signal操作,維護互斥關(guān)系。
2)P2、P3執(zhí)行后執(zhí)行P4:我們分別定義兩個信號量P24和P34,初始值為0,P2進程執(zhí)行完打印輸出后對P24進行signal操作,P3進程執(zhí)行完打印輸出后對P34進行signal操作,在P4執(zhí)行時先針對以上兩個信號量進行wait操作,故P4會等待P2,P3都完成才進行,以維護P2、P3對于P4的前驅(qū)關(guān)系。

1.3 實驗過程

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


    image
  2. task1_fork.c 代碼實現(xiàn):
        #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; //創(chuàng)建子進程2,3,4,P1無需創(chuàng)建,進入主函數(shù)后的進程即為p1進程
            sem_t *t1_23,*t24,*t34;//創(chuàng)建信號量
    
        t1_23=sem_open("t1_23",O_CREAT,0666,0);//表示關(guān)系進程1執(zhí)行完進程2,3中的一個可以執(zhí)行
        t24=sem_open("t24",O_CREAT,0666,0);//表示關(guān)系進程2執(zhí)行完進程4才可執(zhí)行
        t34=sem_open("t34",O_CREAT,0666,0);//表示關(guān)系進程3執(zhí)行完進程4才可執(zhí)行
        
        p2=fork();//創(chuàng)建進程p2
        if(p2==0)
        {
            sem_wait(t1_23);//實現(xiàn)互斥
            printf("I am the process p2\n");
            sem_post(t1_23);
            sem_post(t24);//實現(xiàn)前驅(qū)
        }
        if(p2<0)
        {
            perror("error!");
        }
        if(p2>0)
        {
            p3=fork();//創(chuàng)建進程p3
            if(p3==0)
            {
                sem_wait(t1_23);//實現(xiàn)互斥
                printf("I am the process p3\n");
                sem_post(t1_23);
                sem_post(t34);//實現(xiàn)前驅(qū)
            }
            if(p3<0)
            {
                perror("error!");
            }
            if(p3>0)
            {
                p4=fork();//創(chuàng)建進程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. 編譯運行該程序可得到符合要求的進程執(zhí)行結(jié)果。(由于P2,P3是互斥,所以會有兩種執(zhí)行順序)


    image

1.4 問題總結(jié)

  1. 運行時出現(xiàn)進程提前結(jié)束,通過添加sleep()函數(shù)延遲結(jié)束可以解決這個問題。


    image
  2. 運行結(jié)果多次測試僅有(P1->P3->P2->P4)一種情況,后發(fā)現(xiàn)是之前關(guān)閉了隨機化導致,打開隨機化后可以得到P2和P3先后是隨機的。打開方式如下圖:


    image
  3. 直接gcc -o 無法編譯該程序,需要添加-pthread,正確的編譯命令為:
    gcc -o task1 task1_fork.c -lpthread

task2

2.1 實驗要求

火車票余票數(shù)ticketCount 初始值為1000,有一個售票線程,一個退票線程,各循環(huán)執(zhí)行多次。添加同步機制,使得結(jié)果始終正確。要求多次測試添加同步機制前后的實驗效果。

2.2 知識準備

2.2.1 pthread_yield()函數(shù)

作用:
主動釋放CPU從而讓另外一個線程運行
與sleep()的區(qū)別:
pthread_yield()函數(shù)可以使用另一個級別等于或高于當前線程的線程先運行。如果沒有符合條件的線程,那么這個函數(shù)將會立刻返回然后繼續(xù)執(zhí)行當前線程的程序。
sleep()則函數(shù)是等待一定時間后等待CPU的調(diào)度,然后去獲得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("退票后的票數(shù)為:%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("售票后的票數(shù)為:%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("開始的票數(shù)為:%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("最終的票數(shù)為: %d \n",ticketCount);
    return 0;
}
  1. 編譯運行該程序,編譯命令為gcc -o task2 task2.c -lpthread

  2. 測試不同情況運行結(jié)果:
    1) 不加pthread_yield();函數(shù),測試“賣50張,退50張”的情況

    image

    可見結(jié)果顯示正確
    2) 不加pthread_yield();函數(shù),測試“賣100張,退50張”的情況
    image

    可見結(jié)果值不正確
    3) 不加pthread_yield();函數(shù),測試“賣50張,退100張”的情況
    image

    可見結(jié)果值不正確
    4)添加pthread_yield();函數(shù)在售票ticketCount值被寫回前,測試“賣200張,退200張”的情況
    image

    可見結(jié)果不正確且偏小
    5) 添加pthread_yield();函數(shù)在退票ticketCount值被寫回前,測試“賣200張,退200張”的情況
    image

    可見結(jié)果不正確且偏大
    6) 添加信號量機制,并添加pthread_yield();函數(shù),測試不同買票張數(shù)
    a. 賣200張退200張:
    image

    b. 賣80張退30張:
    image

    可見結(jié)果均正確

  3. 結(jié)果分析
    在并發(fā)執(zhí)行多進程時,當循環(huán)次數(shù)很大是,會產(chǎn)生進程間的切換,而多進程的切換可能導致在一個進程在對票數(shù)ticketCount進行操作后還未寫回,另外一個進程就讀取該數(shù)據(jù)。產(chǎn)生讀取臟數(shù)據(jù)及覆蓋的問題,進而導致結(jié)果的不正確。
    在測試中,第2、3種情況是因改變兩個進程循環(huán)次數(shù)而得到不同的值,這個值沒有一定的規(guī)律。第4種情況是在售票后未及時寫回,售票進程會在之后一段時間出現(xiàn)覆蓋性寫入。故而售票量多,剩余票結(jié)果較小。第5種情況是在退票后未及時寫回,退票進程會在之后一段時間出現(xiàn)覆蓋性寫入。故而退票量多,剩余票結(jié)果較大。第6種情況加入了信號量就可保證售票進程和退票進程的的原子性,避免了臟數(shù)據(jù)讀取和覆蓋性寫入等問題,結(jié)果正確,可保證進程同步。

task3

3.1 實驗要求

一個生產(chǎn)者一個消費者線程同步。設(shè)置一個線程共享的緩沖區(qū), char buf[10]。一個線程不斷從鍵盤輸入字符到buf,一個線程不斷的把buf的內(nèi)容輸出到顯示器。要求輸出的和輸入的字符和順序完全一致。(在輸出線程中,每次輸出睡眠一秒鐘,然后以不同的速度輸入測試輸出是否正確)。要求多次測試添加同步機制前后的實驗效果。

3.2 知識準備

3.2.1 生產(chǎn)者-消費者問題

所謂生產(chǎn)者-消費者問題,實際上主要是包含了兩類線程,一種是生產(chǎn)者線程用于生產(chǎn)數(shù)據(jù),另一種是消費者線程用于消費數(shù)據(jù),為了解耦生產(chǎn)者和消費者的關(guān)系,通常會采用共享的數(shù)據(jù)區(qū)域,就像是一個倉庫,生產(chǎn)者生產(chǎn)數(shù)據(jù)之后直接放置在共享數(shù)據(jù)區(qū)中,并不需要關(guān)心消費者的行為;而消費者只需要從共享數(shù)據(jù)區(qū)中去獲取數(shù)據(jù),就不再需要關(guān)心生產(chǎn)者的行為。但是,這個共享數(shù)據(jù)區(qū)域中應(yīng)該具備這樣的線程間并發(fā)協(xié)作的功能:
如果共享數(shù)據(jù)區(qū)已滿的話,阻塞生產(chǎn)者繼續(xù)生產(chǎn)數(shù)據(jù)放置入內(nèi);
如果共享數(shù)據(jù)區(qū)為空的話,阻塞消費者繼續(xù)消費數(shù)據(jù);

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. 測試運行結(jié)果
    1)程序中輸出函數(shù)中未加sleep函數(shù)時,可見結(jié)果正確


    image

    2)加入sleep函數(shù)后測試一次輸入十個字符‘1234567890’,可見結(jié)果正確。


    image

    3)測試分別連續(xù)輸入十個字符‘1234567890’,可見結(jié)果正確。
    image

    4)測試一次輸入多于十個字符‘1234567890abcdef’,可見結(jié)果正確。
    image

3.4 問題總結(jié)

在一次性輸入十個字符的時候,由于未超過緩存空間大小,會按照正常的情況讀入緩存空間,empty陸續(xù)減至0,full陸續(xù)增至0,。但在一次性輸入超過十個字符時,由于超過緩存空間的大小,empty減至0后就不可再寫入緩存空間,必須輸出后才可繼續(xù)寫入,但是為什么在用戶角度看起來是一起寫入一起輸出的呢?因為其他的字符可以在I/O緩沖區(qū)等待,當擁有empty信號量時才可繼續(xù)寫入緩存空間,再被輸出。

task4

4.1 實驗要求

  1. 通過實驗測試,驗證共享內(nèi)存的代碼中,receiver能否正確讀出sender發(fā)送的字符串?如果把其中互斥的代碼刪除,觀察實驗結(jié)果有何不同?如果在發(fā)送和接收進程中打印輸出共享內(nèi)存地址,他們是否相同,為什么?
  2. 有名管道和無名管道通信系統(tǒng)調(diào)用是否已經(jīng)實現(xiàn)了同步機制?通過實驗驗證,發(fā)送者和接收者如何同步的。比如,在什么情況下,發(fā)送者會阻塞,什么情況下,接收者會阻塞?
  3. 消息通信系統(tǒng)調(diào)用是否已經(jīng)實現(xiàn)了同步機制?通過實驗驗證,發(fā)送者和接收者如何同步的。比如,在什么情況下,發(fā)送者會阻塞,什么情況下,接收者會阻塞?

4.2 實驗過程

4.2.1 共享內(nèi)存

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的循環(huán)中:首先判斷sem=0是否成立,即共享內(nèi)存是否可用,如果為0,則寫入數(shù)據(jù)到共享內(nèi)存(阻塞讀);寫入完成后,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的循環(huán)中:首先判斷sem=1是否成立,即共享內(nèi)存是否可讀,如果為1,則讀取數(shù)據(jù);讀取完成后,sem=0,此時只允許寫入

  1. 驗證共享內(nèi)存的代碼中,receiver能否正確讀出sender發(fā)送的字符串


    image

    通過測試可以看出receiver能夠正確讀出sender發(fā)送的字符串。

  2. 把其中互斥的代碼刪除
    注釋掉sender循環(huán)中的控制互斥的代碼:


    image

    注釋掉receiver循環(huán)中的控制互斥的代碼:


    image

    再次編譯運行兩程序,在sender中第一次測試輸入“test”,第二次重新打開后輸入“hello”,“repeat”,sender在每次輸入后就停留于等待輸入的狀態(tài)。
    image

    可見receiver剛開始的時候循環(huán)輸出原共享內(nèi)存中的內(nèi)容,在sender向共享內(nèi)存寫入新的內(nèi)容之后又循環(huán)輸出新的內(nèi)容。
    image
  3. 如果在發(fā)送和接收進程中打印輸出共享內(nèi)存地址,他們是否相同,為什么?
    修改編寫sender_add.c,在其中編寫輸出共享內(nèi)存地址
    image

    修改編寫receiver_add.c,在其中編寫輸出共享內(nèi)存地址
    image

    編譯運行兩程序,可得到兩個共享內(nèi)存地址不相同
    image

    針對此種現(xiàn)象,根據(jù)以往實驗的經(jīng)驗,最先想到的原因是地址空間隨機化的問題,嘗試關(guān)閉地址隨機化再次進行測試。
    image

    可見關(guān)閉地址空間隨機化后得到的共享內(nèi)存地址一致。
    image

    之后,我們再將地址空間隨機化打開,避免此項操作的影響。
    image

    根據(jù)資料可知,由于在“共享內(nèi)存映射”中shmat()作用是將共享內(nèi)存空間掛載到進程中
    void *shmat(int shmid, const void *shmaddr, int shmflg)
    參數(shù):
    shmid : shmget()返回值
    shmaddr: 共享內(nèi)存的映射地址,一般為0(由系統(tǒng)自動分配地址)
    shmflg : 訪問權(quán)限和映射條件
    返回值:
    成功:共享內(nèi)存段首地址
    失敗:NULL / (void *)-1
    所以考慮修改共享內(nèi)存的映射地址為指定地址來保證共享內(nèi)存地址的一致
    image

    結(jié)果可見此方法可以使得共享內(nèi)存地址一致。
    image

    問題總結(jié)
    共享內(nèi)存中刪除互斥代碼后,receiver循環(huán)輸出速度非常快,通過在receiver.c的循環(huán)中添加sleep(5),使其以合適的速度輸出。
    image

4.2.2 管道(pipe)

知識準備

  1. 無名管道
    無名管道是Linux中管道通信的一種原始方法,它具有以下特點:
    ① 它只能用于具有親緣關(guān)系的進程之間的通信(也就是父子進程或者兄弟進程之間);
    ② 它是一個半雙工的通信模式,具有固定的讀端和寫端;
    ③ 管道也可以看成是一種特殊的文件,對于它的讀寫也可以使用普通的 read()、write()等函數(shù)。但它不是普通的文件,并不屬于其他任何文件系統(tǒng)并且只存在于內(nèi)存中。
  2. 有名管道(FIFO)
    有名管道是對無名管道的一種改進,它具有以下特點:
    ① 它可以使互不相關(guān)的兩個進程間實現(xiàn)彼此通信;
    ② 該管道可以通過路徑名來指出,并且在文件系統(tǒng)中是可見的。在建立了管道之后,兩個進程就可以把它當做普通文件一樣進行讀寫操作,使用非常方便;
    ③ FIFO嚴格地遵循先進先出規(guī)則,對管道及FIFO的讀總是從開始處返回數(shù)據(jù),對它們的寫則是把數(shù)據(jù)添加到末尾,它們不支持如 lseek()等文件定位操作。


    image
  3. 有名管道的特性是:當以只讀方式打開管道時會一直阻塞到有其他地方以寫打開的時候。利用這個特性可以實現(xiàn)進程同步。
    針對不同的讀寫情況,可以得到下表:
讀進程 寫進程 FIFO無數(shù)據(jù) FIFO有數(shù)據(jù)
阻塞 阻塞
阻塞 阻塞
寫入 讀出(未滿,讀寫同時)
√x 即寫中途退出,讀直接返回0 same
√x 讀中途退出,寫返回SIGPIPE same
  1. 有名管道和無名管道通信實現(xiàn)同步機制
    無名管道
    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;
}

編譯運行該程序可得結(jié)果

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) /* 創(chuàng)建一個子進程 */
     {
  /* 子進程關(guān)閉寫描述符,并通過使子進程暫停1s等待父進程已關(guān)閉相應(yīng)的讀描述符 */
            close(fd[1]);
            /* 子進程讀取管道內(nèi)容 */
    while(1)
    {       //系統(tǒng)調(diào)用
            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)
     {
  /* 父進程關(guān)閉讀描述符,并通過使父進程暫停1s等待子進程已關(guān)閉相應(yīng)的寫描述符 */
            close(fd[0]);
        while(1)
    {
            printf("write into the pipe:\n");
        scanf("%s",buf);
       write(fd[1],buf,strlen(buf)); //系統(tǒng)調(diào)用
        if(strcmp(buf,"end")==0)
        break;
         }
     }
    return 0;
}

編譯運行可得到正確的結(jié)果

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

測試不同的阻塞情況并分析結(jié)果
通過加入 fd=open(FIFO_NAME,O_RDONLY | O_NONBLOCK)實現(xiàn)非阻塞,在程序中加入兩種語句,通過不同注釋實現(xiàn)不同情況
image

1)寫阻塞,讀非阻塞
先執(zhí)行snd后執(zhí)行rcv,結(jié)果正確;
image

先執(zhí)行rcv后執(zhí)行snd再執(zhí)行rcv時,結(jié)果不正確。
image

2)讀阻塞,寫非阻塞
先執(zhí)行snd后執(zhí)行rcv,結(jié)果不正確
image

先執(zhí)行rcv后執(zhí)行snd,結(jié)果正確,接收很慢
image

3)讀非阻塞,寫非阻塞
先執(zhí)行snd后執(zhí)行rcv,結(jié)果不正確
image

先執(zhí)行rcv后執(zhí)行snd,結(jié)果不正確
image

4.2.3 消息隊列

知識準備

key:用于創(chuàng)建ID值(ID值由一個進程創(chuàng)建的話,由于進程資源的私有性,另一個進程無法獲取到該ID);采用統(tǒng)一key值創(chuàng)建的ID是相同的;
id:IPC機制的唯一標識
struct msqid_ds: 消息隊列數(shù)據(jù)結(jié)構(gòu)
struct msg: 單個消息的數(shù)據(jù)結(jié)構(gòu)
struct msgbuf: 用戶自定義消息緩沖區(qū)

發(fā)送/接收消息隊列 - msgsnd()/msgrcv():
作用:發(fā)送消息到消息隊列(添加到尾端)/接收消息
函數(shù)原型:
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)
參數(shù):

msqid: 消息隊列的ID值,msgget()的返回值
msgp: struct msgbuf,(發(fā)送/接收數(shù)據(jù)的暫存區(qū),由用戶自定義大小)
msgsz: 發(fā)送/接收消息的大小(范圍:0~MSGMAP)
msgflg:當達到系統(tǒng)為消息隊列所指定的界限時,采取的操作(一般設(shè)置為0)
length: 消息數(shù)據(jù)的長度
type: 決定從隊列中返回哪條消息

msgtyp description
= 0 讀取隊列中的第一個消息
> 0 返回消息隊列中等于mtype 類型的第一條消息
< 0 返回mtype<=type 絕對值最小值的第一條消息

返回值:

成功: 0 (for msgsnd()); ?實際寫入到mtext的字符個數(shù) (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;
}

編譯運行結(jié)果正確

image

由消息隊列相關(guān)知識及代碼解釋可以知道兩個程序通過msgrcv和msgsnd兩個函數(shù)的msgflg參數(shù)控制阻塞情況,msgflg 為0表示阻塞方式,設(shè)置IPC_NOWAIT 表示非阻塞方式msgrcv 調(diào)用成功返回0,不成功返回-1。
1)設(shè)置client阻塞,sever不阻塞
image

可見一直服務(wù)器循環(huán)接受消息隊列中的空內(nèi)容并發(fā)給客戶端
2)設(shè)置client不阻塞,sever阻塞
image

可見服務(wù)端正常,客戶端一直打印輸出空內(nèi)容

task5

5.1 實驗要求

閱讀Pintos操作系統(tǒng),找到并閱讀進程上下文切換的代碼,說明實現(xiàn)的保存和恢復的上下文內(nèi)容以及進程切換的工作流程。

5.2 實驗過程

本次實驗閱讀了thread.h,tread.c,interrup.h,time.c這四個文件
pintos在thread.h中定義了一個結(jié)構(gòu)體struct thread,這個結(jié)構(gòu)體存放了有關(guān)進程的基本信息。

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這個枚舉類型的變量,就是這個線程現(xiàn)在所處的狀態(tài)。

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設(shè)備向CPU發(fā)出的中斷的信息,另一種是CPU決定切換到另一個進程時(輪換時間片)發(fā)出的指令。
pintos的中斷在interrupt.h和interrupt.c之中。其中枚舉類型intr_lverl會在之后提到

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

intr_off表示關(guān)中斷,on表示開中斷。執(zhí)行原子級別操作的時候,中斷必須是關(guān)著的。而pintos是以ticks作為基本時間單位的,每秒有TIMER_FREQ個ticks

#define TIMER_FREQ 100 //系統(tǒng)默認這個宏為100

pintos默認每一個ticks調(diào)用一次時間中斷。所以每一個線程最多可以占據(jù)CPU一個ticks的時長。
thread.c中有以下幾個函數(shù):

  1. thread_current():獲取當前當前的線程的指針。
  2. thread_foreach(thread_action_func *func, void *aux):遍歷當前ready queue中的所有線程,并且對于每一個線程執(zhí)行一次func操作。注意到這里的func是一個任意給定函數(shù)的指針,參數(shù)aux則是你想要傳給這個函數(shù)的參數(shù),而該這個函數(shù)只能在中斷關(guān)閉的時候調(diào)用。
  3. thread_block()和thread_unblock(thread *t)。 這是一對兒函數(shù),區(qū)別在于第一個函數(shù)的作用是把當前占用cpu的線程阻塞掉(就是放到waiting里面),而第二個函數(shù)作用是將已經(jīng)被阻塞掉的進程t喚醒到ready隊列中。
  4. timer_interrupt (struct intr_frame *args UNUSED):這個函數(shù)在timer.c中,pintos在每次時間中斷時(即每一個時間單位(ticks))調(diào)用一次這個函數(shù)。
  5. intr_disable ():這個函數(shù)在interrupt.c中,作用是返回關(guān)中斷,然后返回中斷關(guān)閉前的狀態(tài)。

timer_sleep的作用是讓此線程等待ticks單位時長,然后再執(zhí)行。timer_sleep函數(shù)在devices/timer.c。系統(tǒng)現(xiàn)在是使用busy wait實現(xiàn)的,即線程不停地循環(huán),直到時間片耗盡。

void timer_sleep (int64_t ticks) //想要等待的時間長度
{
  int64_t start = timer_ticks (); //記錄開始時的系統(tǒng)時間
 
  ASSERT (intr_get_level () == INTR_ON);
  while (timer_elapsed (start) < ticks) //如果elapse(流逝)的時間>=ticks時就返回。否則將持續(xù)占用cpu。
    thread_yield ();
}

它使用的方法是利用一個while循環(huán)不斷地請求CPU來判斷是否經(jīng)過了足夠的時間長度。通常cpu在一個ticks時間內(nèi)可以處理10000次這樣的循環(huán),而timer_elapsed()函數(shù)只會在ticks+1時更新一次,所以此處存在一些弊端。

在timer_sleep中調(diào)用了timer_ticks函數(shù)

/* 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. 調(diào)用intr_get_level()
  2. 直接執(zhí)行匯編代碼,調(diào)用匯編指令來保證這個線程不能被中斷。

這里又出現(xiàn)了一個函數(shù)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;
}

這個函數(shù)調(diào)用了匯編指令,把標志寄存器的東西放到處理器棧上,然后把值pop到flags(代表標志寄存器IF位)上,通過判斷flags來返回當前終端狀態(tài)(intr_level)。
所以順序為:intr_get_level返回了intr_level的值,intr_disable獲取了當前的中斷狀態(tài), 然后將當前中斷狀態(tài)改為不能被中斷, 然后返回執(zhí)行之前的中斷狀態(tài)。
所以最終實現(xiàn)的是:禁止當前行為被中斷, 保存禁止被中斷前的中斷狀態(tài)(用old_level儲存)
之后,timer_ticks用t獲取了一個全局變量ticks, 然后返回,其中調(diào)用了intr_set_level函數(shù)。

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

在這個函數(shù)中有個ASSERT斷言了intr_context函數(shù)返回結(jié)果的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斷言這個中斷不是外中斷而是操作系統(tǒng)正常線程切換流程里的內(nèi)中斷,即軟中斷。

實驗總結(jié)

本次實驗設(shè)計的內(nèi)容非常豐富,也具有一定難度,在實踐的過程中查詢了大量資料,也在這個過程中學到了很多知識。對于前幾個實驗使我對于生產(chǎn)者-消費者問題有了更全面的掌握,能夠理解明白其中邏輯,后幾個實驗使我對進程間通信有了較深入的了解,通過不同的方式在不同情況的測試也更加全面的了解了不同通信方式的關(guān)鍵點。當然在實驗中也遇到了產(chǎn)生很多問題,通過對于已有代碼的閱讀和自己的改寫能夠更加理解源代碼的含義,解決問題的過程也是加強理解的過程。另外對于csdn寫博客的也更加的熟練,寫完這篇報告也很有成就感,希望以后可以再接再厲,加油~

github實驗代碼鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。