六、信号量(Semaphore)
同步和互斥:在多任务的操作系统下,多个进程会同时运行,并且一些进程间可能会存在一定的关联。多个进程可能为了完成同一个任务相互协作,这就形成了进程间的同步关系;在不同进程间为了争夺有限的系统资源(硬件或软件资源)会进入竞争状态,这就是进程间的互斥关系。
进程间的互斥、同步关系存在的根源在于临界资源。临界资源是在同一时刻只允许有限个(通常一个)进程可以访问(读)或修改(写)的资源,通常包括硬件资源(处理器、内存、存储器以及其他外围设备等)和软件资源(共享代码段、共享结构和变量等)。访问临界资源的代码叫做临界区,临界区本身也会成为临界资源。信号量是用来解决进程间的同步与互斥问题的一种进程间的通信机制,包括一个称为信号量的变量在该信号量下等待资源的进程等待序列,以及对信号量进行的两个原子操作(P/V操作)。其中,信号量对应于某一种资源,取一个非负的整型值。信号量值(常用sem_id表示)指的是当前可用的该资源的数量,若等于0则意味着目前没有可用的资源。
PV原子操作的具体定义如下:
P操作:如果有可用的资源(信号量值>0),则此操作所在的进程占用一个资源(此时信号量值-1,进入临界代码区);如果没有可用的资源(信号量值=0),则此操作所在的进程被阻塞直到系统将资源分配给该进程(进入等待序列,一直等到资源轮到该进程);
V操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程;如果没有等待他,则释放一个资源(即信号量值+1);
总而言之,P操作负责分配资源,没有资源的时候就等着(进入阻塞队列)。V操作负责释放资源,在阻塞队列不为空的时候唤醒某个进程进入临界区。
在Linux系统中,使用信号量通常分为以下4个步骤:
(1)创建信号量或获得在系统中已存在的信号量,此时需要调用semget()函数。不同进程通过使用同一个信号量键值来获得同一个信号量;
(2)初始化信号量,此时使用semctl()函数的AETVAL操作。当使用互斥信号量时,通常将信号量初始化为1;
(3)进行信号量的PV操作,此时,调用semop()函数。这一步是实现进程间的同步和互斥的的核心工作部分;
(4)如果不需要信号量,则从系统中删除他,此时使用semctl()函数的IPC_RMID操作。需要注意的是,在程序中不应该出现对已经被删除的信号量的操作;
ftok()获取IPC关键字
#include<sys/types.h>
#include<sys/ipc.h>
key_t ftok(const char *pathname,int proj_id);
该函数根据pathname指定的文件或目录的索引节点号和proj_id计算并返回一个key_t类型的ID值,如果失败则返回-1;
pathname:系统中必须存在的文件或文件夹路径,会使用该文件的索引节点;
proj_id:用户指定的子序号,他是一个8bit的整数,取值范围是1~255;
共享内存、消息队列、信号量三种进程间的通信方式都需要key_t类型的关键字ID值 ,就像我们需要一个唯一的身份证号来区分一样,有时我们可以直接指定一个固定的整数值作为该ID值,但是通常情况下会通过ftok()函数获取该值。在一般的UNIX系统中,该函数是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。如指定文件的索引节点号为65538,换成16进制为0x010002,这时我们指定的ID值为38,换成16进制为0x26,则最后key_t的返回值为0x26010002。查询文件索引节点号的方法是:ls -i filename
semget()函数创建或者获取信号量
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semget(key_t key,int nsems,int semflg);
该函数用来创建一个信号集,或者获取已经存在的信号集。成功返回信号量集的标识符,失败返回-1;
key:ftok()的返回值,
nsems:指定需要的信号量数目,它的值几乎总为1;
semflg:用于指定信号量集合的访问权限和其他选项,通常使用位掩码来指定不同的选项;
semctl()函数
#include<sys/types.h>
#include<ysy/ipc.h>
#include<sys/sem.h>
int semctl(int semid,int semun,int cmd,...);
该函数用来初始化信号集,或者删除信号集。成功返回一个正数,失败返回-1;
semid:semget()函数返回的信号量键值;
semun:是操作信号在信号集中的编号,第一个信号是0;
cmd:是在semid指定的信号量集合上执行此命令,可以是:
SETVAL:设置信号量集合中的一个单独的信号量的值,此时需要传入第四个参数;
IPC_RMID:从系统中删除该信号量集合;
IPC_SEAT:对此集合取semid_ds结构,并存放在由arg.buf指向的结构中;
第四个参数是可选的,如果使用该参数,则其类型是semun,它是多个特定明命令参数的联合,该联合不在任何系统头文件中定义,需要我们自己在代码中定义:
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
semop()函数
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semop(int semid,struct sembuf* sops,unsigned nsops);
PV操作。成功执行,返回0;失败返回-1,并设置errno错误信息;
semid:是semget()函数返回的信号量键值;
sops:是一个指针,指向一个信号量操作数组。信号量操作由结构体sembuf结构,表示如下:
struct sembuf
{
unsigned short sem_num;// 操作信号在信号集中的编号,第一个信号的编号是0,最后一个编号是nsems-1
short sem_op; // 操作为负——P操作,其绝对值大于信号的现有值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权;操作为正——V操作,该值会加到现有的信号内值上,通常用于释放所控制资源的使用权;为0,如果后面的sem_flag没有设置IPC_NOWAIT,则调用该操作的进程或线程将暂时休眠,直到信号量的值为0,否则进程或线程会返回错误EAGAIN;
short sem_flg;// 操作标识符,IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息;SEM_UNDO:程序结束时,保证信号值会被重设为semop()调用前的值,避免程序在异常情况下结束时未解锁锁定的资源,造成死锁;
};
nsops:信号操作结构的数量,大于等于1;
下面通过PV操作来实现父子进程同步运行的例子。在初始化信号量时将信号量初始值设为0,如果父进程先运行的话将会调用semaphore_p(semid),这时因为资源为0所以父进程会堵塞。之后子进程会执行semaphore_v(semid)将资源+1,之后父进程就可以运行了
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>#define FTOK_PATH "/lizhao/APUE"
#define FTOK_PROJID 0x22
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};
int semaphore_init(void);
int semaphore_p(int semid);
int semaphore_v(int semid);
void semaphore_term(int semid);
int main(int argc,char **argv)
{
int semid;
pid_t pid;
int i;if( (semid = semaphore_init()) < 0 )
{
printf("semaphore initial failure : %s\n",strerror(errno));
return -1;
}
if( (pid = fork()) < 0 )
{
printf("fork() failure : %s\n",strerror(errno));
return -2;
}/*child process*/
else if( 0 == pid )
{
printf("Child process start running and do something now...\n");
sleep(3);
printf("Child process do something over...\n");
semaphore_v(semid);sleep(1);
printf("child process exit now\n");
exit(0);
}
/*前面的semaphore_init()函数里将信号量的值设置为0,如果这时候父进程先执行的话,P操作会阻塞;
* 直到子进程执行V操作后,父进程的P操作才能返回继续执行*/printf("Parent process P operator wait child process over\n");
semaphore_p(semid);
printf("Parent process destroy semaphore and exit\n");
sleep(2);
printf("Child process exit amd ");
semaphore_term(semid);
return 0;
}int semaphore_init(void)
{
key_t key;
int semid;
union semun sem_union;if( (key = ftok(FTOK_PATH,FTOK_PROJID)) < 0 )
{
printf("ftok() get IPC token failure : %s\n",strerror(errno));
return -1;
}semid = semget(key,1,IPC_CREAT|0644);
if( semid < 0 )
{
printf("semget() get semid failure : %s\n",strerror(errno));
return -2;
}sem_union.val = 0;
if( semctl(semid,0,SETVAL,sem_union) < 0 )
{
printf("semctl() set initial value failure : %s\n",strerror(errno));
return -3;
}printf("Semaphore get key_t[0x%x] and semid[%d]\n",key,semid);
return semid;
}
void semaphore_term(int semid)
{
union semun sem_union;if( semctl(semid,0,IPC_RMID,sem_union) < 0 )
{
printf("semctl() delete semaphore ID failure : %s\n",strerror(errno));
}return ;
}
int semaphore_p(int semid)
{
struct sembuf _sembuf;
_sembuf.sem_num = 0;
_sembuf.sem_op = -1;
_sembuf.sem_flg = SEM_UNDO;// IPC_NOWAIT SEM_UNDOif( semop(semid,&_sembuf,1) < 0 )
{
printf("semop P operator failure : %s\n",strerror(errno));
return -1;
}return 0;
}
int semaphore_v(int semid)
{
struct sembuf _sembuf;
_sembuf.sem_num = 0;
_sembuf.sem_op = 1;
_sembuf.sem_flg = SEM_UNDO;// IPC_NOWAIT SEM_UNDOif( semop(semid,&_sembuf,1) < 0 )
{
printf("semop V operator failure: %s\n",strerror(errno));
return -1;
}return 0;
}
七、共享内存(Shared Memory)
共享内存,就是两个或多个进程可以访问同一块内存空间,一个进程对这块空间的内容的修改可为其他县域通信的进程所看到的。显然,为了创建共享内存需要两个步骤:一是在内存划出一块区域作为共享区;二是把这个区域映射到参与通信的各个进程空间。共享内存是最快的IPC形式。一旦建成共享内存,进程就不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存会使用到的函数:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
key_t ftok(const char *pathname,size_t size,int shmflg);//上文已经讲过
/* 该函数用来创建共享内存,成功返回一个非负整数,即该共享内存段的标识码;失败返回-1;*/
int shmget(key_t key,size_t size,int shamflg);
key:ftok()返回的key_t类型的键值;
size:以字节为单位指定需要共享内存的内存容量;
shmflg:是权限标志;
/*该函数用于对共享内存的启用,并把共享内存连接到当前进程的地址空间,调用成功时返回一个指向共享内存第一个字节指针,如果调用失败返回-1*/
void *shmat(int shmid,const void *shmaddr,int shmflg);
shmid:shmget()函数返回的共享内存标识;
shmaddr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址;
shmflg:一组标志位,通常为0;
/* 该函数用于将共享内存从当前进程中分离(并不是删除),使该共享内存对当前进程不再可用。成功返回0,失败返回-1*/
int shmdt(const void *shmaddr);
shmaddr:是shmat函数返回的地址指针;
/*该函数用于控制共享内存*/
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
shmid:是shmget()函数返回的共享内存标识符;
cmd:是要采取的操作,他可以采取下面的三个值:
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值;
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中 给出的值;
IPC_RMID:删除共享内存段;
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
}
下面两个进程,使用共享内存的方式共享一个结构体变量的示例,其中shared_mem_write.c用来创建一个student结构体共享内存并更改更新里面的成员内容,而shared_mem_read.c则在另外一个毫无关系的进程中同步访问该结构体里的内容
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define FTOK_PATH "/dev/zero"
#define FTOK_PROJID 0x22typedef struct st_student
{
char name[64];
int age;
}t_student;
int main(int argc,char **argv)
{
key_t key;
int shimd;
int i;
t_student *student;if( (key = (FTOK_PATH,FTOK_PROJID)) < 0 )
{
printf("ftok() get IPC token failure : %s\n",strerror(errno));
return -1;
}shimd = shmget(key,sizeof(t_student),IPC_CREAT|0666);
if( shimd < 0 )
{
printf("shmget() create shared memory failure : %s\n",strerror(errno));
return -2;
}student = shmat(shimd,NULL,0);
if( (void *)-1 == student )
{
printf("shmat() alloc shared memory failure : %s\n",strerror(errno));
return -2;
}strncpy(student->name,"zhangsan",sizeof(student->name));
student->age = 18;for(i=0;i<4;i++)
{
student->age ++;
printf("Student '%s' age [%d]\n",student->name,student->age);
sleep(1);
}shmdt(student);
shmctl(shimd,IPC_RMID,NULL);
return 0;
}
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define FTOK_PATH "/dev/zero"
#define FTOK_PROJID 0x22typedef struct st_student
{
char name[64];
int age;
}t_student;
int main(int argc,char **argv)
{
key_t key;
int shimd;
int i;
t_student *student;if( (key = (FTOK_PATH,FTOK_PROJID)) < 0 )
{
printf("ftok() get IPC token failure : %s\n",strerror(errno));
return -1;
}shimd = shmget(key,sizeof(t_student),IPC_CREAT|0666);
if( shimd < 0 )
{
printf("shmget() create shared memory failure : %s\n",strerror(errno));
return -2;
}student = shmat(shimd,NULL,0);
if( (void *)-1 == student )
{
printf("shmat() alloc shared memory failure : %s\n",strerror(errno));
return -2;
}strncpy(student->name,"zhangsan",sizeof(student->name));
student->age = 18;for(i=0;i<4;i++)
{
student->age ++;
printf("Student '%s' age [%d]\n",student->name,student->age);
sleep(1);
}shmdt(student);
shmctl(shimd,IPC_RMID,NULL);
return 0;
}
八、消息队列(MQ,Message Queue)
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法,每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型。每个消息的最大长度是有上限的,每个消息队列的总得字节数是有上限的,系统上消息队列的总数也有一个上限。
MQ的API操作与共享内存几乎是相同的,分为下面四个步骤:
1.创建和访问MQ;
2.发送消息;
3.接受消息;
4.删除MQ;
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
key_t ftok(const char *pathname,int proj_id);
/*该函数用来创建消息队列ID,成功返回一个非负数,即该共享内存段的标识码,失败返回-1*/
int msgget(key_t key,int msgflg);
key:是ftok()函数的返回值
msgflg:是创建标志;
/*用来发送一个消息,必须要有写消息队列的权限,成功返回0;失败返回-1,并设置errno*/
int msgsnd(int msqid,const void *msgp,size_t msgsz,int msgflg);
msgid:由msgget()返回的消息队列ID;
msgp:是一个指针,它指向要发送的消息结构体类型的变量。消息结构在两方面受到制约。首先,它必须小于系统规定的上限值;其次,它必须以一个long int 长整数开始,接收者函数将利用这个长整数确定消息的类型,其参考的类型如下:
typedef struct s_msgbuf
{
long mtype;
char mtext[512];
} t_msgbuf;
msgsz:要发送的消息的长度;
msgflg:控制当前消息队列满或达到系统上限将要发生的事情;
/*用来从一个消息队列中接收消息,成功返回实际放到接收缓冲区里去的字符个数,失败返回-1,并设置errno*/
ssize_t msgrcv(int msqid,void *msgp,size_t msgsz,long msgtyp,int msgflg);
msgid:msgget()返回的消息队列ID;
msgp:是一个指针,指向准备接受的消息;
msgsz:msgp指向的消息的长度;
msgtype:是消息的类型,他可以实现接收优先级的简单形式:
msgtype = 0,返回队列第一条信息;
msgtype>0 ,返回队列第一条类型等于msgtype信息;
msgtype<0,返回队列第一条类型小于等于msgtype绝对值的消息;
msgflg:控制队列中没有相应类型的消息可供接收时将要发生的事
/*该函数用于控制消息队列*/
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
msqid
:消息队列的标识符,是由msgget()
函数返回
cmd
:要执行的操作类型,可以是以下之一:
IPC_STAT
:获取消息队列的状态信息,并将其存储在struct msqid_ds
结构体中。
IPC_SET
:设置消息队列的状态信息,通过struct msqid_ds
结构体中提供的信息进行设置。
IPC_RMID
:从系统中删除消息队列。
buf
:一个指向struct msqid_ds
结构体的指针,用于存储或传递消息队列的状态信息。
示例程序:
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#define FTOK_PATH "/dev/zero"
#define FTOK_PROJID 0x22typedef struct s_msgbuf
{
long mtype;
char mtext[512];
}t_msgbuf;
int main(int argc,char **argv)
{
key_t key;
int msgid;
t_msgbuf msgbuf;
int msgtype;
int i;
if( (key = ftok(FTOK_PATH,FTOK_PROJID)) < 0 )
{
printf("ftok() get IPC token failure : %s\n",strerror(errno));
return -1;
}
msgid = msgget(key,IPC_CREAT|0666);if(msgid < 0 )
{
printf("shmget() create shared memory failure : %s\n",strerror(errno));
return -2;
}
msgtype = (int)key;
printf("key[%d] msgid[%d] msgypte[%d]\n",(int)key,msgid,msgtype);for(i=0;i<4;i++)
{
msgbuf.mtype = msgtype;
strcpy(msgbuf.mtext,"Ping");if(msgsnd(msgid,&msgbuf,sizeof(msgbuf.mtext),IPC_NOWAIT) < 0 )
{
printf("msgnd() send message failure : %s\n",strerror(errno));
break;
}
printf("Send message : %s\n",msgbuf.mtext);
sleep(1);}
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>#define FTOK_PATH "/dev/zero"
#define FTOK_PROJID 0x22typedef struct s_msgbuf
{
long mtype;
char mtext[512];
}t_msgbuf;
int main(int argc,char **argv)
{
key_t key;
int msgid;
t_msgbuf msgbuf;
int msgtype;
int i;if( (key = ftok(FTOK_PATH,FTOK_PROJID)) < 0 )
{
printf("ftok() get IPC token failure : %s\n",strerror(errno));
return -1;
}msgid = msgget(key,IPC_CREAT|0666);
if( msgid < 0 )
{
printf("shmget() create shared memory failure : %s\n",strerror(errno));
return -2;
}msgtype = (int)key;
printf("key[%d], msgid[%d] msgypte[%d]\n",(int)key,msgid,msgtype);for(i=0;i<4;i++)
{
memset(&msgbuf,0,sizeof(msgbuf));if( msgrcv(msgid,&msgbuf,sizeof(msgbuf.mtext),msgtype,IPC_NOWAIT) < 0 )
{
printf("msgsnd() receive message failure : %s\n",strerror(errno));
break;
}printf("Receive message : %s\n",msgbuf.mtext);
sleep(1);
}msgctl(msgid,IPC_RMID,NULL);
return 0;
}