多进程编程之进程间通信-共享内存,信号量和套接字

1. 背景

本文将介绍进程通信中的信号量,共享内存和套接字方法。

2. 信号量

2.1 信号量的定义

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的,假设有一个信号量变量sv:
P(sv):如果sv的值大于零,就给它减1;如果它的值为0,就挂起该进程的执行。
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

1、semget函数
它的作用是创建一个新信号量或取得一个已有信号量,原型为:

int semget(key_t key, int num_sems, int sem_flags);

第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。
第二个参数num_sems指定需要的信号量数目,它的值几乎总是1。
第三个参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
semget函数成功返回一个相应信号标识符(正数非零),失败返回-1。

2、semop函数
它的作用是改变信号量的值,原型为:

int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops); 

第一个参数,sem_id是由semget返回的信号量标识符。第二个参数sem_ops是指向一个sembuf结构数组的指针,每个数组元素至少包含以下几个成员:

struct sembuf{  
    short sem_num;//该成员是信号编号,除非使用一组信号量,否则它为0  
    short sem_op;//该成员的值是信号量在一次操作中需要改变的。
    //信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,它等待信号量变为可用;一个是+1,即V(发送信号)操作,它发送信号表示信号量现在已可用。
    short sem_flg;//通常为SEM_UNDO,使操作系统跟踪当前进程对这个信号量的修改情况,
    //并在进程没有释放该信号量而终止时,操作系统释放信号量  
};  

semop成员的值数值
3、semctl函数
该函数用来直接控制信号量信息,它的原型为:

int semctl(int sem_id, int sem_num, int command, ...);  

前两个参数与前面一个函数中的一样,分别是信号量标识符和信号量编号。command表示将要采取的动作,通常是下面两个值中的其中一个:
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
如果有第四个参数,它通常是一个union semum结构,定义如下:

union semun{  
    int val;  
    struct semid_ds *buf;  
    unsigned short *arry;  
};  

semctl函数将根据command参数的不同而返回不同的值,对于SETVAL和IPC_RMID如果成功时,返回0,失败时返回-1。

2.2 信号量的使用

将两个不同字符的输出来表示进入和离开临界区域,如果程序启动时带有一个参数,它将在进入和退出临界区域时候打印字符X;而程序的其他运行实例将在进入和退出临界区域的时候打印字符O,因为在任一给定时刻,只能有一个进程可以进入到临界区域,所以字符X和字符O应该是成对出现的。

#include 
#include 
#include 
#include 


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

static int sem_id;
union semun
{
        int val;                    /* value for SETVAL */
        struct semid_ds *buf;       /* buffer for IPC_STAT, IPC_SET */
        unsigned short int *array;  /* array for GETALL, SETALL */
        struct seminfo *__buf;      /* buffer for IPC_INFO */
};

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

/* Then we have a loop which enters and leaves the critical section ten times.
 There, we first make a call to semaphore_p which sets the semaphore to wait, as
 this program is about to enter the critical section. */

    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);
        //离开临界区前再一次向屏幕输出数据 


/* After the critical section, we call semaphore_v, setting the semaphore available,
 before going through the for loop again after a random wait. After the loop, the call
 to del_semvalue is made to clean up the code. */
        //离开临界区,休眠随机时间后继续循环
        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);
}

/* The function set_semvalue initializes the semaphore using the SETVAL command in a
 semctl call. We need to do this before we can use the semaphore. */

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

/* The del_semvalue function has almost the same form, except the call to semctl uses
 the command IPC_RMID to remove the semaphore's ID. */

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");
}

/* semaphore_p changes the semaphore by -1 (waiting). */

static int semaphore_p(void)
{
    //对信号量做减1操作,即等待P(sv)
    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);
}

/* semaphore_v is similar except for setting the sem_op part of the sembuf structure to 1,
 so that the semaphore becomes available. */

static int semaphore_v(void)
{
    //这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
    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);
}

运行结果如下:
多进程编程之进程间通信-共享内存,信号量和套接字_第1张图片
同时运行一个程序的两个实例,注意第一次运行时,要加上一个字符作为参数,例如本例中的字符 1,它用于区分是否为第一次调用。因为每个程序都在其进入临界区后和离开临界区前打印一个字符,所以每个字符都应该成对出现,正如你看到的上图的输出那样。在main函数中循环中我们可以看到,每次进程要访问stdout(标准输出),即要输出字符时,每次都要检查信号量是否可用(即stdout有没有正在被其他进程使用)。所以,当一个进程A在调用函数semaphore_p进入了临界区,输出字符后,调用sleep时,另一个进程B可能想访问stdout,但是信号量的P请求操作失败,只能挂起自己的执行,当进程A调用函数semaphore_v离开了临界区,进程B马上被恢复执行。然后进程A和进程B就这样一直循环了10次。

另一个例子来说明一下,它实现的功能与前面的例子一样,运行方式也一样,都是两个相同的进程,同时向stdout中输出字符,只是没有使用信号量,两个进程在互相竞争stdout。它的代码如下:

#include 
#include 
#include 

int main(int argc, char *argv[])
{
    char message = 'X';
    int i = 0;  
    if(argc > 1)
        message = argv[1][0];
    for(i = 0; i < 10; ++i)
    {
        printf("%c", message);
        fflush(stdout);
        sleep(rand() % 3);
        printf("%c", message);
        fflush(stdout);
        sleep(rand() % 2);
    }
    sleep(10);
    printf("\n%d - finished\n", getpid());
    exit(EXIT_SUCCESS);
}

运行结果如下所示:
多进程编程之进程间通信-共享内存,信号量和套接字_第2张图片
从上面的输出结果,我们可以看到字符‘X’和‘O’并不像前面的例子那样,总是成对出现,因为当第一个进程A输出了字符后,调用sleep休眠时,另一个进程B立即输出并休眠,而进程A醒来时,再继续执行输出,同样的进程B也是如此。所以输出的字符就是不成对的出现。这两个进程在竞争stdout这一共同的资源。

2.3 信号量小结:

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。我们通常通过信号来解决多个进程对同一资源的访问竞争的问题,使在任一时刻只能有一个执行线程访问代码的临界区域,也可以说它是协调进程间的对同一资源的访问权,也就是用于同步进程的。

3. 共享内存

共享内存是最快的IPC(进程间通信)方式,它允许两个不相关进程访问同一个逻辑内存。共享内存是一个程序向内存写数据,另一个程序读数据,共享内存牵扯到同步的问题,一般有三种方案可以实现共享资源的同步。它们分别是信号量,记录锁和互斥量
使用信号量,首先服务端创建一个只含一个信号的信号量集合,并初始化为1。占据资源,则以sem_op=-1调用semop函数。释放资源,则则以sem_op=1调用semop函数。
使用记录锁,需要创建一个文件,并写入一个字节。分配资源,对文件获得写锁,释放资源,解锁。
使用互斥量,需要所有的进程将相同的文件映射到他们的地址空间,并使用PTHREAD_PROCESS_SHARED互斥量属性在文件中初始化互斥量。分配资源,对互斥量加锁,释放资源,解锁互斥量。
共享内存使用的函数类似信号量的函数:

#include  
int   shmget(key_t key, int size, int flag); 
void* shmat(int shmid,  const void *addr, int flag); 
int   shmdt(char *shmaddr);
int   shmctl(int shmid, int cmd, struct shmid_ds *buf);

3-1 shmget函数

用于开辟或指向一块共享内存,返回获得共享内存区域的ID(共享内存标识符),该标识符用于后续的共享内存函数。如果不存在指定的共享区域就创建相应的区域。 如果创建失败,则返回-1。
keyt key: 共享内存的标识符。如果是父子关系的进程间通信的话,这个标识符用IPC_PRIVATE来代替。
如果两个进程没有任何关系,所以就用ftok()算出来一个标识符(或者自己定义一个)使用了。
int size: 以字节为单位指定需要共享的内存容量。
int flag: 包含9个比特的权限标志,它是这块内存的模式(mode)以及权限标识。由IPC_CREAT定义的一个特殊比特必须和权限标志按位或才能创建一个新的共享内存段。设置IPC_CREAT标志的同时,给shmget函数传递一个已有共享内存段的键并不是一个错误,如果无需用到IPC_CREAT标志,该标志会被悄悄地忽略掉。
权限标志的存在,使得允许一个进程创建的共享内存可以被共享内存的创建者所拥有的进程写入,同时其他用户创建的进程只能读取该共享内存。可以利用这个功能提高一种有效的对数据进行只读访问的方法,通过将数据放入共享内存并设置它的权限,即可避免数据被其他用户修改。
模式可取如下值:
IPC_CREAT 新建(如果已创建则返回目前共享内存的id)
IPC_EXCL 与 IPC_CREAT结合使用,如果已创建则返回错误
将“模式” 和“权限标识”进行或运算,做为第三个参数。如:IPC_CREAT | IPC_EXCL | 0640
其中0640为权限标识,4/2/1 分别表示读/写/执行3种权限,第一个0是UID,第一个6(4+2)表示拥有者的权限,第二个4表示同组权限,第3个0表示他人的权限。
函数调用成功时返回共享内存的ID,失败时返回-1。
注:创建共享内存时,shmflg参数至少需要 IPC_CREAT | 权限标识,如果只有IPC_CREAT 则申请的地址都是k=0xffffffff,不能使用;

3-2 shmat函数

用来允许本进程访问一块共享内存的函数。
第一次创建共享内存时,它不能任何进程访问,要想启用对该共享内存的访问,必须将其连接到一个进程的地址空间中。
shmat函数就是用来完成此工作的。
int shmid : 共享内存的ID,即共享内存的标识。
char *shmaddr: 共享内存连接到进程中的起始地址,如果shmaddr为NULL,内核会把共享内存映射到系统选定的地址空间中;如果shmaddr不为NULL,内核会把共享内存映射到shmaddr指定的位置。
注:一般情况下我们很少需要控制共享内存连接的地址,通常都是让系统来选择一个地址,否则就会使应用程序对硬件的依赖性过高。所以一般把shmaddr设为NULL。
int shmflag : 本进程对该内存的操作模式,可以由两个取值:SHM_RND和SHM_RDONLY。SHM_RND为读写模式,
SHM_RDONLY是只读模式。需要注意的是,共享内存的读写权限由它的属主、它的访问权限和当前进程的属主共同决定。如果当shmflg & SM_RDONLY为true时,即使该共享内存的访问权限允许写操作,它也不能被写入。该参数通常会被设为0。
函数调用成功时,返回共享内存的起始地址,失败时返回-1。

3-3 shmdt函数

用于函数删除本进程对这块内存的使用。
shmdt()与shmat()相反,是用来禁止本进程访问一块共享内存的函数。
char *shmaddr 是那块共享内存的起始地址。
函数调用成功时返回0,失败时返回-1。

3-4shmctl函数

控制对这块共享内存的使用。
int shmid: 共享内存的ID,即共享内存标识。
int cmd : 控制命令,表示要采取的动作,可取值如下:
IPC_STAT 得到共享内存的状态:把shmid_ds结构中的数据设置为共享内存的当前关联值
IPC_SET 改变共享内存的状态:把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID 删除共享内存段
shmid_ds结构至少包含以下成员:
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
uid_t shm_perm.mode;
}
struct shmid_ds *buf: 一个结构体指针。IPC_STAT的时候,取得的状态放在这个结构体中。
如果要改变共享内存的状态,用这个结构体指定。
函数调用成功时返回0,失败时返回-1。
【未完待续!】

4. 套接口

【待补充】

你可能感兴趣的:(多线程和多进程)