1--信号量的实践到内核--信号量的实验

来源:互联网 发布:ubuntu的启动过程 编辑:程序博客网 时间:2024/06/04 18:18
前边几节中我们谈到了关于进程间通讯的消息队列和共享内存,今天我们研究一下信号量,这里所说的信号量是“用户空间进程通讯的信号量”与内核进程的信号量是不同的,还是与以前的学习方法一样,我们先来看一个例子,在这个例子中通过semget创建一个信号量,它返回一个信号量的ID号,我们看代码

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#include <sys/sem.h>

#include "semun.h"

static int set_semvalue(void);
static void del_semvalue(void);
static int semaphore_p(void);
static int semaphore_v(void);

static int sem_id;


int main(int argc, char *argv[])
{
    int i;
    int pause_time;
    char op_char = 'O';

    srand((unsigned int)getpid());
    
    sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);

    if (argc > 1) {
        if (!set_semvalue()) {
            fprintf(stderr, "Failed to initialize semaphore/n");
            exit(EXIT_FAILURE);
        }
        op_char = 'X';
        sleep(2);
    }

同以前的例子一样我们用1234做为创建信号量的标识符,可以看出与以前讲到的消息队列和共享内存几乎一模一样它是通过semget()来创建一个信号量。如果程序执行时有参数的话就通过set_semvalue()来初始化

for(i = 0; i < 10; i++) {

        if (!semaphore_p()) exit(EXIT_FAILURE);
        printf("%c", op_char);fflush(stdout);
        pause_time = rand() % 3;
        sleep(pause_time);
        printf("%c", op_char);fflush(stdout);

然后是进入一个for循环,首先是调用semaphore_p()来进入临界区域,不知道临界区域的朋友可以在网上查阅其概念,后边是一些延时等待。

if (!semaphore_v()) exit(EXIT_FAILURE);
        
        pause_time = rand() % 2;
        sleep(pause_time);
    }

    printf("/n%d - finished/n", getpid());

    if (argc > 1) {
        sleep(10);
        del_semvalue();
    }
        
    exit(EXIT_SUCCESS);
}

到这里已经进入了临界区域之后调用semaphore_v()设置信号量的值,for循环之后如果程序执行时带参数就调用del_semvalue()删除信号量。下面是调用的几个子函数

 

下面这个函数是初始信号量的值,可以看出初始化的值为1,然后使用semctl系统调用参数中使用了SETVAL。这个系统调用是必须的,随后几节我们来分析他在内核的过程。

static int set_semvalue(void)
{
    union semun sem_union;

    sem_union.val = 1;
    if (semctl(sem_id, 0, SETVAL, sem_union) == -1) return(0);
    return(1);
}

下面这个函数与上面几乎相同,只不过这里系统调用中传递的参数是IPC_RMID,我们在前边的消息队列和共享内存看到过这个宏参数,他的作用是删除信号量。

static void del_semvalue(void)
{
    union semun sem_union;
    
    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
        fprintf(stderr, "Failed to delete semaphore/n");
}

这就是信号量的P操作了,改变信号量的值为-1,使进程在操作信号量时等待。同时我们看到使用了一个系统调用semop()。稍后我们在内核中分析它。

static int semaphore_p(void)
{
    struct sembuf sem_b;
    
    sem_b.sem_num = 0;
    sem_b.sem_op = -1; /* P() */
    sem_b.sem_flg = SEM_UNDO;
    if (semop(sem_id, &sem_b, 1) == -1) {
        fprintf(stderr, "semaphore_p failed/n");
        return(0);
    }
    return(1);
}

下面函数就是V操作了,改变信号量的值为1,也就是使进程可以操作信号量,同样使用了semop()系统调用来完成的。

static int semaphore_v(void)
{
    struct sembuf sem_b;
    
    sem_b.sem_num = 0;
    sem_b.sem_op = 1; /* V() */
    sem_b.sem_flg = SEM_UNDO;
    if (semop(sem_id, &sem_b, 1) == -1) {
        fprintf(stderr, "semaphore_v failed/n");
        return(0);
    }
    return(1);
}

这几个子函数其实非常简单,主要是完成对信号量的设置及删除。

cc sem1.c -o sem1
[root@localhost wumingxiaozu]# ./sem1 1 &
[1] 3605
[root@localhost wumingxiaozu]# ./sem1
OOXXOOOOXXOOXXOOXXOOXXOOXXOOXXOOXXXXOOXX
3605 - finished

3606 - finished

你可以在调用试验程序时输入一个任意的参数,这样就会创建一个信号量,然后再次调用这个程序时就会出现上面的O和X的交叉显示的情况,看一下上边的代码可以注意到O和X分别代表二次调用程序执行。这就表示了先是信号量的程序会打印出自己的字符,所以我们看到O和X交替打印在屏幕上。同一时刻只能有一个执行程序即进程得到信号量进入临界区域去完成打印自己的字符,这个例子让我们明白了信号量的作用。我们下边进入内核追踪一下吧,与以前一样我们首先来到的是系统调用的总入口处sys_ipc()函数代码部分,我们只看重要的部分

    case SEMGET:
        return sys_semget (first, second, third);

我们可以根据上边的试验中创建函数到达sys_semget()函数,这个函数的过程与我们先前讲到的sys_msgget()以及sys_shmget()一模一样

asmlinkage long sys_semget(key_t key, int nsems, int semflg)
{
    struct ipc_namespace *ns;
    struct ipc_ops sem_ops;
    struct ipc_params sem_params;

    ns = current->nsproxy->ipc_ns;

    if (nsems < 0 || nsems > ns->sc_semmsl)
        return -EINVAL;

    sem_ops.getnew = newary;
    sem_ops.associate = sem_security;
    sem_ops.more_checks = sem_more_checks;

    sem_params.key = key;
    sem_params.flg = semflg;
    sem_params.u.nsems = nsems;

    return ipcget(ns, &sem_ids(ns), &sem_ops, &sem_params);
}

总之,都象前边二节提到的那样,会执行红色部分赋值的函数入口newary()这个函数在ipc/sem.c的234行处。

static int newary(struct ipc_namespace *ns, struct ipc_params *params)
{
    int id;
    int retval;
    struct sem_array *sma;
    int size;
    key_t key = params->key;
    int nsems = params->u.nsems;
    int semflg = params->flg;

    if (!nsems)
        return -EINVAL;
    if (ns->used_sems + nsems > ns->sc_semmns)
        return -ENOSPC;

    size = sizeof (*sma) + nsems * sizeof (struct sem);
    sma = ipc_rcu_alloc(size);
    if (!sma) {
        return -ENOMEM;
    }
    memset (sma, 0, size);

    sma->sem_perm.mode = (semflg & S_IRWXUGO);
    sma->sem_perm.key = key;

    sma->sem_perm.security = NULL;
    retval = security_sem_alloc(sma);
    if (retval) {
        ipc_rcu_putref(sma);
        return retval;
    }

    id = ipc_addid(&sem_ids(ns), &sma->sem_perm, ns->sc_semmni);
    if (id < 0) {
        security_sem_free(sma);
        ipc_rcu_putref(sma);
        return id;
    }
    ns->used_sems += nsems;

    sma->sem_base = (struct sem *) &sma[1];
    /* sma->sem_pending = NULL; */
    sma->sem_pending_last = &sma->sem_pending;
    /* sma->undo = NULL; */
    sma->sem_nsems = nsems;
    sma->sem_ctime = get_seconds();
    sem_unlock(sma);

    return sma->sem_perm.id;
}

按照以前的分析方法我们还是从参数先看一下,再次把试验中的创建代码部分贴在下边

semget((key_t)1234, 1, 0666 | IPC_CREAT);

对照一下,与消息队列和共享内存的实验完全一样。只不过我们在上边看到多了一个关键的地方int nsems = params->u.nsems;这里是从sys_semget()函数中设置的sem_params.u.nsems = nsems;而这个参数看上边方框中的调用界面是设置为1,这里也就是只创建一个信号量,但是从上面的size = sizeof (*sma) + nsems * sizeof (struct sem);这句代码中我们可以看出可以同时创建几个信号量,上面newary()函数中出现一个数据结构,这个结构我们看一下

/* One sem_array data structure for each set of semaphores in the system. */
struct sem_array {
    struct kern_ipc_perm    sem_perm;    /* permissions .. see ipc.h */
    time_t            sem_otime;    /* last semop time */
    time_t            sem_ctime;    /* last change time */
    struct sem        *sem_base;    /* ptr to first semaphore in array */
    struct sem_queue    *sem_pending;    /* pending operations to be processed */
    struct sem_queue    **sem_pending_last; /* last pending operation */
    struct sem_undo        *undo;        /* undo requests on this array */
    unsigned long        sem_nsems;    /* no. of semaphores in array */
};

其中sem_base是一个指向一个结构数组,数组中的每个sem都是一个信号量

/* One semaphore structure for each semaphore in the system. */
struct sem {
    int    semval;        /* current value */
    int    sempid;        /* pid of last operation */
};

这个结构数组的大小由我们上边接到的nsems来决定,在应用程序中我们看到传递过来的参数值是1,所以这个数组只会有一个元素,即一个信号量。这就是我们上面看到的size = sizeof (*sma) + nsems * sizeof (struct sem);决定的。可以看到sem数组紧跟在sem_array的后边,紧跟着sma->sem_base = (struct sem *) &sma[1];这句决定了二者的挂钩,即sem_base指向信号量的超始地址。为什么这里能够建立一组信号量而不是一个呢,想一下操作系统理论中的“哲学家与刀叉”的问题就明白了,信号量的获得要么是全有要么是全无,不然就会死锁了。明白了这个道理这里的newary()也就很简单了。今天先到这里,明天继续看下如何操作的。