银行家算法以及工厂生产实验报告

实验内容:进程同步

  • 编译运行课件Lecture 17示例代码:alg.17-1-bankers-6;编译运行课件Lecture 18示例代码:alg.18-1~alg.18-5。指出你认为不合适的地方并加以改进。

  • 编译运行课件Lecture 18示例代码:alg.18-6~alg.18-8,讨论其中的生产者-消费者问题,共享内存保护,进程同步机制经加以改进。

实验环境

  • 架构:Intel x86_64(双系统)

  • 操作系统:Ubuntu 20.04

  • 汇编器:gas(GNU Assembler) in AT&T mode

  • 编译器:gcc

技术日志

alg.17-1-banckers-6分析

实验原理分析

全局数据变量设置

假设:在系统中有m种资源以及n个进程需要资源。

available向量:指出每种资源现在可用的数量。available[j]==k意味着有k个实例的R_j​资源当前是可用的。

int available[m];
int work[n];/*the pretending of available[],用于验证*/

max矩阵:定义了每个进程所需要的最大需求量。max[i][j]==k表示第i个进程最多需要k个实例的j型资源。

int max[n][m];

allocation矩阵:定义了对每一个资源在这么多个需求进程中的分配方案。allocation[i][j]==k意味着第i个进程获得了k个实例的j型资源。

int allocation[n][m]={0};

need矩阵:指出目前对每个进程而言还有多少需要用到的资源。need[i][j]==k表明第i个进程可能还需要kj型资源来完成它的任务。

int need[n][m]={0}
need[i][j]==max[i][j]-allocation[i][j]

request向量:记录发出新的资源需求的进程所需的每种资源实例的列表。

安全状态定义以及函数判定方法

当前操作系统的状态可以在当前的资源分配中得到分配。因此,状态显示在avaliable向量,needallocation矩阵中。一个安全的状态是至少可以有一个分配方案其结果不会导致死锁(资源分配图中不会出现圈,且该圈覆盖所有节点)。

为了确定同时需要同种资源的两个进程哪个先得到该种资源,此处使用字典测序的方法对进程的需求进行排序,只有进程需求在字典测序上严格的小于当前资源数才能得到分配。其定义是,假设XY是两个长度为m的向量,X<=Y当且仅当对于所有i=1,2,..,mX[i]<=Y[i]。因此每次判断需要O(m)的时间复杂度。

for (i = 1; i <= n; i++) { /* to label n processes */
    for (j = 0; j < n; j++) { /* select one process to test */
        if (finish[j] == FALSE) { /* Pj has not been tested */
            if (need[j] <= work) { /* Pj can terminate */
                work = work + allocation[j];
                /* after Pj released its resource(s) */
            finish[j] = TRUE; /* Pj has been tested */
            break;
            } /* else select the next process to test */
        } 
    } 
}
if (i > n) 
return EXIT_SECCESS;
else
return EXIT_FAILURE;

此处判定该种分配是否会导致死锁的判定算法。首先看第一个循环,它定义在判定过程中会进行n轮测试,如果所有进程都可以顺利执行完毕则表明该分配方案是安全的(因此,每轮测试都会至少完成一个进程)。在进入第二个循环之后,其j的值代表进程号;如果finish[j]==FALSE表明j进程还没有接收过测试。随后,如果当前剩余的资源数足以满足j进程的需求(此处使用字典测序,因此此处消耗O(m)的时间),则表明进程j可以顺利执行,假设其完成了执行过程,操作系统重新获得之前分配给他的资源work=work+allocation[j],并且表明进程j完成了执行过程finish[j]=True,然后跳出此次循环(因为该轮循环中已有一个进程完成了执行)。如果发现当前剩余的资源不足以满足当前进程或者当当前进程已完成执行,则跳过该进程,测试下一个进程。

资源分配算法

input request from process Pi
if (request > need)
    exit(1); /* Pi has exceeded its maximum claim */
if (request > available)
    exit(1); /* resources are unavailable */
​
/* pretending allocation */
work = available - request; /* remainder resources */
allocation[i] = allocation[i] + request;
need[i] = need[i] - request;
​
ret = safetycheck();
return ret; 
/* roll back the allocation if EXIT_FAILURE */

假设此处有一个进程P_i申请了资源(一个行向量,长度为m),首先进行一个检测其是否超过了预先设置的该进程的最大资源数(request > need)或者超过了当前的资源剩余数(requeat>available)。如果条件为真,则不可能满足该进程的资源条件,返回1异常。如果资源数可以满足该进程的需求,则首先满足该进程的需求,将该进程需要的资源分配给该进程。随后,调用safetycheck()查看是否有满足当前需求条件的分配方案(此处应用了只要某进程其需求可满足,则其分配顺序无差异(由于等待与释放))。

实验细节解释

得到安全分配序列的方案safecheck()

int safecheck(int pro_i)
{
    int finish[N] = {FALSE};
    int safe_seq[N] = {0};
    int i, j, k, l;
​
    printf("\nsafecheck starting >>>>>>>>\n");
​
    for (i = 0; i < N; i++) { /* we need to select N processes, i is just a counter */
        for (j = 0; j < N; j++) { /* check the j-th process */
            if(finish[j] == FALSE) {
                if(j == pro_i) {
                    for (k = 0; k < M; k++) {
                        if(need[pro_i][k] - request[k] > work[k]) { /* if pretending need[pro_i] > work[] */
                            break; /* to check next process */
                        }
                    } 
                } else {
                    for (k = 0; k < M; k++) {
                        if(need[j][k] > work[k]) {
                            break; /* to check next process */
                        }
                    }
                }
​
                if(k == M) { /* the j-th process can finish its task */
                    safe_seq[i] = j;
                    finish[j] = TRUE;
                    printf("safe_seq[%d] = %d\n", i, j);
                    printf("new work vector: ");
                    if(j == pro_i) {
                        for (l = 0; l < M; l++) { /* process pro_i releasing pretending allocated resources */
                            work[l] = work[l] + allocation[pro_i][l] + request[l];
                            printf("%d, ", work[l]);
                        }
                    } else {
                        for (l = 0; l < M; l++) { /* another process releasing allocated resource */
                            work[l] = work[l] + allocation[j][l];
                            printf("%d, ", work[l]);
                        }
                    }
                    printf("\n");
​
                    break; /* to select more process */
                }
            }
        }
​
        if(j == N) {
            break; /* not enough processes can pass the safecheck */
        }
    }
​
    if(i == N) { /* all N processes passed the safecheck */
        printf("A safty sequence is: ");
        for (j = 0; j < N; j++) {
            printf("P%d, ", safe_seq[j]);
        }
        printf("\n");
        return EXIT_SUCCESS;
    }
    else {
        printf("safecheck failed, process %d suspended\n", pro_i);
        return EXIT_FAILURE;
    }
}

首先给出该函数内本地变量的定义finish[N]={FALSE}声明当前所有进程的均未完成执行,由于未分配资源(除了新输入的进程需求)。随后定义一个存储一个可行方案的资源分配序列safe_seq,初始值全为0。随后进行N(进程数)次循环,每次循环完成一个进程的执行。

在第二轮循环内,每次循环进行一个进程的检查,查看其资源供给是否可以让其完成执行任务。该检查通过一个字典测序的循环实现,如果当前进程是发出新需求的进程,则需要查看其当前请求(除去已分配的资源)是否超过了现在拥有的资源。

                    for (k = 0; k < M; k++) {
                        if(need[j][k] > work[k]) {
                            break; /* to check next process */
                        }
                    }

如果在此次循环中有可以完成一个进程的执行,则将该进程加入到进程序列执行表中safe_seq[i] = j;finish[j] = TRUE;。如果,该进程恰好为发出请求的进程,则需要释放到他本身申请的资源与其新申请的资源work[l] = work[l] + allocation[pro_i][l] + request[l];。若为其他进程,则只需要释放掉其本身申请的资源work[l] = work[l] + allocation[j][l];。否则,表明此次循环中没有资源可以完成执行,出现死锁,退出该循环if(j==N) break;

	if(i == N) { /* all N processes passed the safecheck */
        printf("A safty sequence is: ");
        for (j = 0; j < N; j++) {
            printf("P%d, ", safe_seq[j]);
        }
        printf("\n");
        return EXIT_SUCCESS;
    }
    else {
        printf("safecheck failed, process %d suspended\n", pro_i);
        return EXIT_FAILURE;
    }

如果第一层循环可以执行N次,表明这k个进程均可以完成执行任务。则返回可以正确执行的序列。否则,则返回不能正常执行。

资源分配算法

int resource_request(int pro_i)
{
    int j, k, ret;
    
    for (k = 0;  k < M; k++) {
        if(request[k] > need[pro_i][k]) {
            printf("request[%d] > need[%d][%d]: request[%d] = %d, need[%d][%d] = %d\n", k, pro_i, k, k, request[k], pro_i, k, need[pro_i][k]);
            return EXIT_FAILURE;
        }
    }

    for (k = 0; k < M; k++) {
        if(request[k] > available[k]) {
            printf("request[%d] > available[%d]: request[%d] = %d, available[%d] = %d\n", k, k, k, request[k], k, available[k]);
            return EXIT_FAILURE;
        }
    }

    for (k = 0; k < M; k++) {
        work[k] = available[k] - request[k]; /* work[] as the pretending available[] */
    }

    ret = safecheck(pro_i); /* check whether the pretending state is safe */

    if(ret == EXIT_SUCCESS) { /* confirm the pretending state as the new state */
        for (k = 0; k < M; k++) {
            available[k] -= request[k];
            need[pro_i][k] -= request[k];
            allocation[pro_i][k] += request[k];
        }
    }

    return ret;
}

此处操作的顺序与技术原理出操作的描述相同,关键之处是需要在得到正确的执行序列之后改变系统资源分配available[k] -= request[k];need[pro_i][k] -= request[k];allocation[pro_i][k] += request[k];,将新申请的资源加入已分配资源中。

执行结果

Lecture 18示例代码:alg.18-1~alg.18-5分析

实验原理

type __sync__???(type *ptr,type value)原子运算家族。

  1. _fetch_and family实现先抓取信息再对该信息进行运算改造。由于其实现的是原子操作,因此这些函数的操作不可中断。因此这个操作可以保证只有一个进程获得锁。

type __sync_???(type *ptr, type value);
/* type: uint8_t, unt16_t uint32_t, unt64_t */
 _fetch_and_ familly
type __sync_fetch_and_add(type *ptr, type value);
/* oldval = *ptr; *ptr += value; return oldval; */
type __sync_fetch_and_sub(type *ptr, type value);
/* oldval = *ptr; *ptr -= value; return oldval; */
type __sync_fetch_and_or(type *ptr, type value);
/* oldval = *ptr; *ptr = (*ptr) | value; return oldval; */
type __sync_fetch_and_and(type *ptr, type value);
/* oldval = *ptr; *ptr = (*ptr) & value; return oldval; */
type __sync_fetch_and_xor(type *ptr, type value);
/* oldval = *ptr; *ptr = (*ptr) ^ value; return oldval; */
type __sync_fetch_and_nand(type *ptr, type value);
/* oldval = *ptr; *ptr = ~((*ptr)&value); return oldval; */`
  1. _and_fetch family实现先进行输入信息进行操作再将运算完的结果进行返回。由于其实现的是原子操作,因此这些函数的操作不可中断。因此这个操作可以保证只有一个进程获得锁。

    type __sync_add_and_fetch(type *ptr, type value);
    /* *ptr += value; return *ptr; */
    type __sync_sub_and_fetch(type *ptr, type value);
    /* *ptr -= value; return *ptr; */
    type __sync_or_and_fetch(type *ptr, type value);
    /* *ptr = (*ptr) | value; return *ptr; */
    type __sync_and_and_fetch(type *ptr, type value);
    /* *ptr = (*ptr) & value; return *ptr; */
    type __sync_xor_and_fetch(type *ptr, type value);
    /* *ptr = (*ptr) ^ value; return *ptr; */
    type __sync_nand_and_fetch(type *ptr, type value);
    /* *ptr = ~((*ptr)&value); return *ptr; */
  2. 一些其他的原子函数

    bool __sync_bool_compare_and_swap(type *ptr, type oldval, typ
    e newval, ...);
    /* if(*ptr == oldval) {*ptr = newval; return TRUE;} 
    else return FALSE; */
    type __sync_val_compare_and_swap(type *ptr, type oldval, type
    newval, ...);
    /* if(*ptr == oldval) {*ptr = newval; return oldvl;}
    else return *ptr; */
    __sync_synchronize(...)
    /* set a full memory barrier */
    type __sync_lock_test_and_set(type *ptr, type newval, ...)
    /* oldval = *ptr; *ptr = newval; lock(ptr); return 
    oldval; */
    /* try spinlock */
    void __sync_lock_release (type *ptr, ...)
    /* *ptr = 0; unlock(ptr); no return val */
    /* unlock spinlock */

    实验细节分析

    alg.18-1-syn-fetch-demo.c分析

    使用原子函数的子线程执行函数

    void *thread_func1(void *arg)
    {
        for (int i = 0; i < 20000; ++i) {
            __sync_fetch_and_add(&count_1, 1);
        }
    
        pthread_exit(NULL);
    }

    由于第一个进程使用原子命令。因此,不会出现指令交织,其每次对count的每次操作都是不可中断。所以,每个子线程访问到的count的值均是正确经历过前面的自加操作。根据递归的证明,可以说明其可以计算出正确的结果。

    未使用原子函数的子线程执行函数

    void *thread_func2(void *arg)
    {
        for (int i = 0; i < 20000; ++i) {
            count_2++;
        }
    
        pthread_exit(NULL);
    }

    由于下一个进程未使用原子命令。因此,会出现指令交织(多次寻址),其计算出的count的值与正确的值不同,而且每次结果均不同。

    int main(void)
    {
        pthread_t ptid_1[MAX_N];
        pthread_t ptid_2[MAX_N];
    
        for (int i = 0; i < MAX_N; ++i) {
            pthread_create(&ptid_1[i], NULL, &thread_func1, NULL);
        }
    
        for (int i = 0; i < MAX_N; ++i) {
            pthread_create(&ptid_2[i], NULL, &thread_func2, NULL);
        }
    
        for (int i = 0; i < MAX_N; ++i) {
            pthread_join(ptid_1[i], NULL);
        }
    
        for (int i = 0; i < MAX_N; ++i) {
            pthread_join(ptid_2[i], NULL);
        }
    
        printf("result conut_1 = %d\n", count_1);
        printf("result conut_2 = %d\n", count_2);
    
        return 0;
    }

    在主线程中,其主要作用是派生大量的子线程,并且让这些子线程对同一个count变量进行操作,查看是否有指令交织的现象出现。

    执行结果

alg.18-2-syn-compare.c分析

这个实验主要用于测试可用于比较,测试的一些原子家族函数。

int main(void)
{
    uint8_t i, oldval, newval;
    
    i = 30; oldval = 30; newval = 40;
    printf("__sync_bool_compare_and_swap(&i, oldval, newval)\n");
    printf("i = %d, oldval = %d, newval = %d\n", i, oldval, newval);
    printf("ret = %d, ", __sync_bool_compare_and_swap(&i, oldval, newval));
    printf("new i = %d\n", i);
    i = 30; oldval = 10; newval = 40;
    printf("__sync_bool_compare_and_swap(&i, oldval, newval)\n");
    printf("i = %d, oldval = %d, newval = %d\n", i, oldval, newval);
    printf("ret = %d, ", __sync_bool_compare_and_swap(&i, oldval, newval));
    printf("new i = %d\n\n", i);
 
    i = 30; oldval = 30; newval = 40;
    printf("__sync_val_compare_and_swap(&i, oldval, newval)\n");
    printf("i = %d, oldval = %d, newval = %d\n", i, oldval, newval);
    printf("ret = %d, ", __sync_val_compare_and_swap(&i, oldval, newval));
    printf("new i = %d\n", i);
    i = 30; oldval = 10; newval = 40;
    printf("__sync_val_compare_and_swap(&i, oldval, newval)\n");
    printf("i = %d, oldval = %d, newval = %d\n", i, oldval, newval);
    printf("ret = %d, ", __sync_val_compare_and_swap(&i, oldval, newval));
    printf("new i = %d\n\n", i);

    i = 30; newval = 10;
    printf("__sync_lock_test_and_set(&i, newval)\n");
    printf("i = %d, newval = %d\n", i, newval);
    printf("ret = %d, ", __sync_lock_test_and_set(&i, newval));
    printf("new i = %d\n", i);

    i = 30;
    printf("__sync_lock_release(&i)\n");
    printf("i = %d\n", i);
    __sync_lock_release(&i); /* no return value */
    printf("new i = %d\n", i);

    return 0;
}

执行结果

alg.18-3-syn-pthread-mutex.c

使用原子函数的子线程执行函数

void *thread_func1(void *arg)
{
    for (int i = 0; i < 20000; ++i) {
        pthread_mutex_lock(&mutex);
        count_1++;
        pthread_mutex_unlock(&mutex);
    }

    pthread_exit(NULL);
}

由于第一个进程使用pthread的互斥锁,同一时间只会有一个线程进入临界区。因此,不会出现指令交织,其每次对count的每次操作都是不可中断(由于只有一个线程获得锁)。所以,每个子线程访问到的count的值均是正确经历过前面的自加操作。根据递归的证明,可以说明其可以计算出正确的结果。

未使用原子函数的子线程执行函数

void *thread_func2(void *arg)
{
    for (int i = 0; i < 20000; ++i) {
        count_2++;
    }

    pthread_exit(NULL);
}

由于下一个进程未使用原子命令。因此,会出现指令交织(多次寻址),其计算出的count的值与正确的值不同,而且每次结果均不同。

int main(void)
{
    pthread_t ptid_1[MAX_N];
    pthread_t ptid_2[MAX_N];

//    pthread_mutex_init(&mutex, NULL);

    for (int i = 0; i < MAX_N; ++i) {
        pthread_create(&ptid_1[i], NULL, &thread_func1, NULL);
    }

    for (int i = 0; i < MAX_N; ++i) {
        pthread_create(&ptid_2[i], NULL, &thread_func2, NULL);
    }

    for (int i = 0; i < MAX_N; ++i) {
        pthread_join(ptid_1[i], NULL);
    }

    for (int i = 0; i < MAX_N; ++i) {
        pthread_join(ptid_2[i], NULL);
    }

    pthread_mutex_destroy(&mutex);

    printf("result count_1 = %d\n", count_1);
    printf("result count_2 = %d\n", count_2);

    return 0;
}return 0;
}

在主线程中,其主要作用是派生大量的子线程,并且让这些子线程对同一个count变量进行操作,查看是否有指令交织的现象出现。

执行结果

alg.18-4-syn-pthread-sem-named.c

使用原子函数的子线程执行函数

void *thread_func1(void *arg)
{
    for (int i = 0; i < 20000; ++i) {
        sem_wait(&unnamed_sem);
        count_1++;
        sem_post(&unnamed_sem);
    }

    pthread_exit(NULL);
}

由于第一个进程使用pthread的有名信号量sem_init(&unnamed_sem,0,x)(用于有亲缘关系之间的进程(1)/线程(0)进行使用),同一时间只会有一个(最大允许数目)线程进入临界区。因此,不会出现指令交织,其每次对count的每次操作都是不可中断(由于只有一个线程获得锁)。所以,每个子线程访问到的count的值均是正确经历过前面的自加操作。根据递归的证明,可以说明其可以计算出正确的结果。

未使用原子函数的子线程执行函数

void *thread_func2(void *arg)
{
    for (int i = 0; i < 20000; ++i) {
        count_2++;
    }

    pthread_exit(NULL);
}

由于下一个进程未使用保证互斥的措施。因此,会出现指令交织(多次寻址),其计算出的count的值与正确的值不同,而且每次结果均不同。

int main(void)
{
    pthread_t ptid_1[MAX_N];
    pthread_t ptid_2[MAX_N];

    int i, ret;

    named_sem = sem_open("MYSEM", O_CREAT, 0666, 1);
        /* a file named "sem.MYSEM" is created in /dev/shm/ 
           to be shared by processes who can sense the file name */

    if(named_sem == SEM_FAILED) {
        perror("sem_open()");
        return EXIT_FAILURE;
    }

    for (int i = 0; i < MAX_N; ++i) {
        pthread_create(&ptid_1[i], NULL, &thread_func1, NULL);
    }

    for (int i = 0; i < MAX_N; ++i) {
        pthread_create(&ptid_2[i], NULL, &thread_func2, NULL);
    }

    for (i = 0; i < MAX_N; ++i) {
        pthread_join(ptid_1[i], NULL);
    }

    for (i = 0; i < MAX_N; ++i) {
        pthread_join(ptid_2[i], NULL);
    }

    printf("result count_1 = %d\n", count_1);
    printf("result count_2 = %d\n", count_2);

    sem_close(named_sem);
    sem_unlink("MYSEM"); /* remove sem.MYSEM from /dev/shm/ when its references is 0 */

    return 0;
}

在主线程中,其主要作用是派生大量的子线程,并且让这些子线程对同一个count变量进行操作,查看是否有指令交织的现象出现。

执行结果

alg.18-4-syn-pthread-sem-named.c

使用原子函数的子线程执行函数

void *thread_func1(void *arg)
{
    for (int i = 0; i < 20000; ++i) {
        sem_wait(named_sem);
        count_1++;
        sem_post(named_sem);
    }

    pthread_exit(NULL);
}

由于第一个进程使用pthread的信号量,同一时间只会有一个(最大允许数目)线程进入临界区。因此,不会出现指令交织,其每次对count的每次操作都是不可中断(由于只有一个线程获得锁)。所以,每个子线程访问到的count的值均是正确经历过前面的自加操作。根据递归的证明,可以说明其可以计算出正确的结果。

未使用原子函数的子线程执行函数

void *thread_func2(void *arg)
{
    for (int i = 0; i < 20000; ++i) {
        count_2++;
    }

    pthread_exit(NULL);
}

由于下一个进程未使用保证互斥的措施。因此,会出现指令交织(多次寻址),其计算出的count的值与正确的值不同,而且每次结果均不同。

int main(void)
{
    pthread_t ptid_1[MAX_N];
    pthread_t ptid_2[MAX_N];

    int i, ret;

    ret = sem_init(&unnamed_sem, 0, 1);
    if(ret == -1) {
        perror("sem_init()");
        return EXIT_FAILURE;
    }
    
    for (int i = 0; i < MAX_N; ++i) {
        pthread_create(&ptid_1[i], NULL, &thread_func1, NULL);
    }

    for (int i = 0; i < MAX_N; ++i) {
        pthread_create(&ptid_2[i], NULL, &thread_func2, NULL);
    }

    for (i = 0; i < MAX_N; ++i) {
        pthread_join(ptid_1[i], NULL);
    }

    for (i = 0; i < MAX_N; ++i) {
        pthread_join(ptid_2[i], NULL);
    }

    printf("result count_1 = %d\n", count_1);
    printf("result count_2 = %d\n", count_2);

    sem_destroy(&unnamed_sem);

    return 0;
}

在主线程中,其主要作用是派生大量的子线程,并且让这些子线程对同一个count变量进行操作,查看是否有指令交织的现象出现。此处使用sem_destroy()来销毁已创建的信号量

执行结果

alg.18-6~alg.18-8及其中的生产者-消费者问题,共享内存保护,进程同步机制

alg.18-6-syn-pc-con-7.h

#define BASE_ADDR 10
/* the shared memory is organized by data-pc-st, 
   the first BASE_ADDR units are reserved for ctl_pc_st,
   all item data start from the unit indexed BASE_ADDR,
   circular data queue is indicated by (enqueue | dequeue) % buffer_size + BASE_ADDR */

struct ctl_pc_st
{
    int BUFFER_SIZE;      // number of data units in the shared memory
    int MAX_ITEM_NUM;     // number of items to be produced
    int THREAD_PROD;      // number of producers, old version: THREAD_PRO
    int THREAD_CONS;      // number of consumers
    sem_t sem_mutex;      // semophore for mutex, type of long int */
    sem_t sem_stock;      // semophore for number of stocks in BUFFER
    sem_t sem_emptyslot;  // semophore for number of empty units in BUFFER
    int prod_num;         // total number of items having produced, old version item_num
    int cons_num;         // total number of items having consumed, old version: consume_num
    int enqueue;          // current position of PROD in buffer
    int dequeue;          // current positions of CONS in buffer
    int END_FLAG;         // producers met MAX_ITEM_NUM, finished their works 
}; /* 60 bytes */

struct data_pc_st
{
    int item_sn;      // the item's serial number when it is made
    int prod_code;       // reserved for producer ID
    long int prod_tid; // tid of the producer who made the item 
}; /* 16 bytes */

在定义的结构体中,首先用了buffer_size表明现在临界区中存在的产品的大小;MAX_ITEM_NUM表明了可以接受的最大产品生产个数;THREAD_PRODTHREAD_CONS表明了最大接收可以操作临界区资源的生产者数量以及消费者数量。sem_mutex,sem_srock,sem_emptyslot是用于保证临界区资源正确操作的信号量,第一个信号量表明了每次操作期间只有一个线程可以加入临界区;第二个信号量表明现在临界区内的产品是否为满,用于控制生产者进入临界区;第三个信号量表明临界区内产品是否为空,用于控制消费者进入临界区。prod_num,cons_num表明了当前操作临界区资源的生产者与消费者的数量。enqueue,dequeue表明当前产品的读下标与写下标,用于判断当前产品数量为满还是为空。END_FLAG用于广播是否关闭临界区资源。

对于每一个工厂,需要定义他的序号item_sn,工厂号prod_code,以及工厂对应线程的线程号prod_tid(用于控制哪个线程进入临界区)。

alg.18-6.syn-pc-comn-7.c分析

    int buffer_size, max_item_num, thread_prod, thread_cons;
    char code_str[10], pathname[80];
    int fd;
    key_t key;
    int ret;
    struct ctl_pc_st *ctl_ptr = NULL;
    struct data_pc_st *data_ptr = NULL;
    pid_t childpid, prod_pid, cons_pid;
		
    while (1) { /* env. initialization */
        printf("Pls input the number of items as the buffer bound(1-100, 0 quit): ");
        scanf("%d", &buffer_size);
        if(buffer_size <= 0)
            return 0;
        if(buffer_size > 100)
            continue;
        printf("Pls input the max number of items to be produced(1-10000, 0 quit): ");
        scanf("%d", &max_item_num);
        if(max_item_num <= 0)
            return 0;
        if(max_item_num > 10000)
            continue;
        printf("Pls input the number of producers(1-500, 0 quit): ");
        scanf("%d", &thread_prod);
        if(thread_prod <= 0)
            return 0;
        printf("Pls input the number of consumers(1-500, 0 quit): ");
        scanf("%d", &thread_cons);
        if(thread_cons <= 0)
            return 0;

        break;
    }

首先,程序在终端中读取到生产者-消费者模式下的基本数据(判断产品满的条件,最大的容许接入的线程数量),如果此时用户在终端输入的数据不符合预设的范围,则进入下一个循环再次读取数据(0<=buffer_size<100,0<=max_item_num<10000,thread_prod>0,thread_cons>0)。

	sprintf(code_str, "%d", random_code(4));
    strcpy(pathname, "/tmp/shm-");
    strcat(pathname, code_str);
    printf("shm pathname: %s\n", pathname);
    fd = open(pathname, O_CREAT);
	if(fd == -1) {
        perror("pathname fopen()");
        return EXIT_FAILURE;
    }

    if((key = ftok(pathname, 0x28)) < 0) { 
        perror("ftok()");
        exit(EXIT_FAILURE);
    }
    
      /* get the shared memoey
         'invalid argument' if the size exceeds the current shmmax.
	     when the shmid exists, its size is defined when it was firstly declared and can not be changed.
	     if you want a lager size, you have to alter a new key for a new shmid.
      */
    shmid = shmget((key_t)key, (buffer_size + BASE_ADDR)*sizeof(struct data_pc_st), 0666 | IPC_CREAT);
    if(shmid == -1) {
        perror("shmget()");
        exit(EXIT_FAILURE);
    }

      /* attach the created shared memory to user space */
    shm = shmat(shmid, 0, 0);
    if(shm == (void *)-1) {
        perror("shmat()");
        exit(EXIT_FAILURE);
    }

随后,通过自己定义的一个生成四位随机码的函数random_code(4)得到一个四位的随机数并且把这个数字与/tmp/shm-进行拼接,得到一个共享内存文件地址,随后将以此为临界资源作为写入产品与读出产品的载体。open(pathname,O_CREAT)作用是创建一个在之前限定好文件位置的文件,ftok()得到这个文件在文件系统中的编码信息,可用于让子线程同时感知到该文件。随后调用shmget通过内核的调用,在这个文件中创建一个大小为struct data_pc_st的内存空间并且设置该主线程及其子线程对于这个文件的权限是可读可写。然后,调用shmat()函数将这个文件与当前的线程进行链接(让这个文件地址出现在其PVM中)。

	ctl_ptr = (struct ctl_pc_st *)shm;
    data_ptr = (struct data_pc_st *)shm;

    ctl_ptr->BUFFER_SIZE = buffer_size;
    ctl_ptr->MAX_ITEM_NUM = max_item_num;
    ctl_ptr->THREAD_PROD = thread_prod;
    ctl_ptr->THREAD_CONS = thread_cons; 
    ctl_ptr->prod_num = 0;
    ctl_ptr->cons_num = 0;
    ctl_ptr->enqueue = 0;
    ctl_ptr->dequeue = 0;
    ctl_ptr->END_FLAG = 0;

在通过上面的参数有效性测试之后,则将控制单元的结构体信息赋以输入的数据。

	ret = sem_init(&ctl_ptr->sem_mutex, 1, 1); /* pshared set to non-zero for inter process sharing */
    if(ret == -1) {
        perror("sem_init-mutex");
        return detachshm();
    }
    ret = sem_init(&ctl_ptr->sem_stock, 1, 0); /* initialize to 0 */
    if(ret == -1) {
        perror("sem_init-stock");
        return detachshm();
    }
    ret = sem_init(&ctl_ptr->sem_emptyslot, 1, ctl_ptr->BUFFER_SIZE); /*initialize to BUFFER_SIZE */
    if(ret == -1) {
        perror("sem_init-emptyslot");
        return detachshm();
    }

    printf("\nsyn-pc-con console pid = %d\n", getpid());

完成参数初始化设置以及临界资源创建之后,需要定义信号量信息来控制操作临界资源的线程数目。首先,设置互斥锁的信号量sem_mutex,由于这里设计到生产者与消费者的控制(二者中只能有一种类型可以进入临界区),因此此处的三个参数选择1,表明这个信号量是用于进程之间的通信。随后,定义判断目前缓存中的产品数量是否为满的信号量sem_stock,由于此处的合作进程均为生产者的子线程,因此选择0作为第三个参数,进行线程之间的临界区管理。最后,随后,定义判断目前缓存中的产品数量是否为空的信号量sem_emptyslot,由于此处的合作进程均为消费者的子线程,因此选择0作为第三个参数,进行线程之间的临界区管理。

	char *argv1[3];
    char execname[] = "./";
    char shmidstring[10];
    sprintf(shmidstring, "%d", shmid);
    argv1[0] = execname;
    argv1[1] = shmidstring;
    argv1[2] = NULL;
	childpid = vfork();
    if(childpid < 0) {
        perror("first fork");
        detachshm();
        unlink(pathname);
        return 0;
    } 
    else if(childpid == 0) { /* call the producer */ 
        prod_pid = getpid();
        printf("producer pid = %d, shmid = %s\n", prod_pid, argv1[1]);
        execv("./alg.18-7-syn-pc-producer-7.o", argv1);
    }
    else {
        childpid = vfork();
        if(childpid < 0) {
            perror("second vfork");
            detachshm();
            unlink(pathname);
            return 0;
        } 
        else if(childpid == 0) { /* call the consumer */
            cons_pid = getpid();
            printf("consumer pid = %d, shmid = %s\n", cons_pid, argv1[1]);
            execv("./alg.18-8-syn-pc-consumer-7.o", argv1);
        }
    }

在信号量创建完毕之后,说明一切工厂与消费者所需的资源创建完毕,现在需要派生出生产者与消费者的子进程。此处的创建使用vfork()方法,此处需要将共享内存的shmid作为第二个参数传入子进程中,从而让工厂与消费者都可以感知到共享内存信息。

	if(waitpid(prod_pid, 0, 0) != prod_pid) {/* block wait */
        perror("wait prod");
    } else {
        printf("waiting prod_pid %d success.\n", prod_pid);
    }

    if (waitpid(cons_pid, 0, 0) != cons_pid) {
        perror("wait cons");
    } else {
        printf("waiting cons_pid %d success.\n", cons_pid);
    }

    ret = sem_destroy(&ctl_ptr->sem_mutex);
    if(ret == -1) {
        perror("sem_destroy sem_mutex");
    }
    ret = sem_destroy(&ctl_ptr->sem_stock); /* sem_destroy() will not affect the sem_wait() calling process */
    if(ret == -1) {
        perror("sem_destroy stock");
    }
    ret = sem_destroy(&ctl_ptr->sem_emptyslot);
    if(ret == -1) {
        perror("sem_destroy empty_slot");
    }

    detachshm();
    unlink(pathname);
    return EXIT_SUCCESS;

完成子进程创建之后,在主进程中使用阻塞等待的方法,在等待所有生产者以及消费者完成执行之后,主进程才继续其执行过程。随后,主进程中销毁掉创建的三个匿名信号量sem_mutex,sem_stock,sem_emptyslot。之后,调用创建的detachshm()取消对共享文件的连接,并且删除掉这个创建的文件,最后成功退出。

alg.18-7-syn-pc-producer-7.c

void *producer(void *arg)
{
    while (ctl_ptr->prod_num < ctl_ptr->MAX_ITEM_NUM) {
        //can not be changed the order
        sem_wait(&ctl_ptr->sem_emptyslot);
        sem_wait(&ctl_ptr->sem_mutex);
        if(ctl_ptr->prod_num < ctl_ptr->MAX_ITEM_NUM) {
            ctl_ptr->prod_num++;		
            //(data_ptr + ctl_ptr->enqueue + BASE_ADDR)->prod_code = ;
            (data_ptr + ctl_ptr->enqueue + BASE_ADDR)->prod_tid = gettid();
            (data_ptr + ctl_ptr->enqueue + BASE_ADDR)->item_sn = item_sn++;
            printf("producer tid %ld prepared item_sn %d, now enqueue = %d\n", (data_ptr + ctl_ptr->enqueue + BASE_ADDR)->prod_tid, (data_ptr + ctl_ptr->enqueue + BASE_ADDR)->item_sn, ctl_ptr->enqueue);
            ctl_ptr->enqueue = (ctl_ptr->enqueue + 1) % ctl_ptr->BUFFER_SIZE;
            if(ctl_ptr->prod_num == ctl_ptr->MAX_ITEM_NUM) {
                ctl_ptr->END_FLAG = 1;
		    }
            sem_post(&ctl_ptr->sem_stock);
        } else {
            sem_post(&ctl_ptr->sem_emptyslot);
        }
        sem_post(&ctl_ptr->sem_mutex);
        usleep(random_code(6));
    }

    pthread_exit(NULL);
}

首先定义每一个工厂的业务代码,首先需要查看现在生产的总数是否小于最大允许生产数,如果不是则直接跳出循环结束工厂的生产,如果是则继续生产产品。随后,需要先等待目前缓存中产品数量是否为空,如果为空则唤醒一个等待中的工厂获得第一个锁;而后等待当前临界区中的线程为空,被唤醒的线程才能进入临界区;(注意:此处的顺序不能颠倒,如果颠倒,可能会导致progress条件得不到满足)。顺利进入临界区之后,如果目前生产的总数小于最大生产数,则继续生产;否则,停止生产并释放得到的锁。随后,如果可以继续生产,则先得到该工厂的tid,并且记录现在这件生产产品的序号。打印出刚刚得到产品信息(此处信息存储在一个队列中)。如果在改建产品生产之后刚好到达最大的产品生产数,则设置终止END_FLAG=1。完成操作后,释放获得的判断当前缓存是否有产品的锁,让其他工厂可以进入临界区。然后,释放互斥锁,表明该轮操作完毕并且睡眠几毫秒(随机产生)。

int main(int argc, char *argv[])
{
    int shmid;
    void *shm = NULL;
    int i, ret;

    shmid = strtol(argv[1], NULL, 10); /* shmid delivered */
    shm = shmat(shmid, 0, 0);
    if(shm == (void *)-1) {
        perror("\nproducer shmat()");
        exit(EXIT_FAILURE);
    }

    ctl_ptr = (struct ctl_pc_st *)shm;
    data_ptr = (struct data_pc_st *)shm;

    pthread_t ptid[ctl_ptr->THREAD_PROD];

    item_sn = random_code(8);

    for (i = 0; i < ctl_ptr->THREAD_PROD; ++i) {
        ret = pthread_create(&ptid[i], NULL, &producer, NULL);
        if(ret != 0) {
            perror("producer pthread_create()");
            break;
        }
    }    

    for (i = 0; i < ctl_ptr->THREAD_PROD; ++i) {
        pthread_join(ptid[i], NULL);
    }

    for (i = 0; i < ctl_ptr->THREAD_CONS - 1; ++i) {
      /* all producers stop working, in case some consumer takes the last stock
         and no more than THREAD_CON-1 consumers stick in the sem_wait(&stock) */
        sem_post(&ctl_ptr->sem_stock);
    }
    
    if(shmdt(shm) == -1) {
        perror("producer shmdt()");
        exit(EXIT_FAILURE);
    }

    exit(EXIT_SUCCESS);
}

首先,负责生产的主线程会获取到从主进程传入的shmid并且将该共享内存文件绑定到当前的线程上。随后,通过pthread_create的方法,派生出由用户指定的工厂数目。然后,阻塞的等待所有子线程执行完毕。为了防止有些消费者会由于此时缓存中无产品会进入无线等待,需要释放消费者个数减1次的存品锁。最后,取消与共享内存的连接。

alg.18-7-syn-pc-consumer-7.c

void *consumer(void *arg)
{
    while ((ctl_ptr->cons_num < ctl_ptr->prod_num) || (ctl_ptr->END_FLAG == 0))  { 
        sem_wait(&ctl_ptr->sem_stock);  /* if stock is empty and all producers stop working at this point, one or more consumers may wait forever */
        sem_wait(&ctl_ptr->sem_mutex);
        if (ctl_ptr->cons_num < ctl_ptr->prod_num) { 
            printf("\t\t\t\tconsumer tid %ld taken item_sn %d by tid %ld, now dequeue = %d\n", gettid(), (data_ptr + ctl_ptr->dequeue + BASE_ADDR)->item_sn, (data_ptr + ctl_ptr->dequeue + BASE_ADDR)->prod_tid, ctl_ptr->dequeue);
            ctl_ptr->dequeue = (ctl_ptr->dequeue + 1) % ctl_ptr->BUFFER_SIZE;
            ctl_ptr->cons_num++;
            sem_post(&ctl_ptr->sem_emptyslot);
        } else {
            sem_post(&ctl_ptr->sem_stock);
        }
        sem_post(&ctl_ptr->sem_mutex);
        usleep(random_code(6));
    }
    
    pthread_exit(0);
}

首先定义每一个消费者的业务代码,首先需要查看现在消费者的拿取总数是否小于工厂生产总数数或者是否工厂已经完成全部生产,如果不是则直接跳出循环结束工厂的生产,如果是则继续拿取产品。随后,需要先等待目前缓存中产品数量是否有存品,如果不为空则唤醒一个等待中的工厂获得第一个锁;而后等待当前临界区中的线程为空,被唤醒的线程才能进入临界区;(注意:此处的顺序不能颠倒,如果颠倒,可能会导致progress条件得不到满足)。顺利进入临界区之后,如果目前消费者的拿取总数小于工厂生产总数,则继续拿取;否则,释放得到的锁。随后,如果可以继续拿取,则让共享内存区中队列执行出队操作。完成操作后,释放获得的判断当前缓存是否为空的锁,让其他消费者可以进入临界区。然后,释放互斥锁,表明该轮操作完毕并且睡眠几毫秒(随机产生)。

int main(int argc, char *argv[])
{
    int shmid;
    void *shm = NULL;
    int i, ret;

    shmid = strtol(argv[1], NULL, 10); /* shmnid delivered */
    shm = shmat(shmid, 0, 0);
    if (shm == (void *)-1) {
        perror("consumer shmat()");
        exit(EXIT_FAILURE);
    }

    ctl_ptr = (struct ctl_pc_st *)shm;
    data_ptr = (struct data_pc_st *)shm;

    pthread_t ptid[ctl_ptr->THREAD_CONS];

    for (i = 0; i < ctl_ptr->THREAD_CONS; ++i) {
        ret = pthread_create(&ptid[i], NULL, &consumer, NULL); 
        if (ret != 0) {
            perror("consumer pthread_create()");
            break;
        }
    } 
	
    for (i = 0; i < ctl_ptr->THREAD_CONS; ++i) {![2022-05-11 17-32-40 的屏幕截图](D:/os/os_labweek14/2022-05-11%2017-32-40%20%E7%9A%84%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE.png)
        pthread_join(ptid[i], NULL);
    }

    if (shmdt(shm) == -1) {
        perror("consumer shmdt()");
        exit(EXIT_FAILURE);
    }
    
    exit(EXIT_SUCCESS);
}

首先,负责生产的主线程会获取到从主进程传入的shmid并且将该共享内存文件绑定到当前的线程上。随后,通过pthread_create的方法,派生出由用户指定的消费者数目。然后,阻塞的等待所有子线程执行完毕。最后,取消与共享内存的连接。

执行结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值