进程的空间地址是各自独立的,因此进程间进行数据交流就需要特定的通信机制,在大型应用中往往需要多个进程同时工作,这就需要进程间的数据交流和配合。
进程间的通信方式有:
管道是一种两个进程间进行通信的机制,它有一些局限性:
#include
int pipe(int fd[2]);
管道两端分别用描述符fd[0],fd[1]来描述。需要注意的是管道两端的任务是固定的,一端只能用于读,由描述符fd[0]表示,为读端;另一端只能用于写,由描述符fd[1]表示,为写端;如果试图从读端写入会出错。管道是一种文件,所以可以使用文件的I/O函数,如read(),write()等;
管道的使用方法:
(1).创建一个管道;
(2).创建子进程;
(3).父进程关闭管道读端,负责写入数据;
(4).子进程关闭管道写端,负责读出数据;
例:pipe.c
/*************************************************
* 用管道创建单向传递功能
* **********************************************/
#include
#include
#include
#include
#include
void read_from_pipe(int fd) //读管道
{
char message[100];
read(fd, message, 100);
printf("read from pipe:%s",message);
}
void write_to_pipe(int fd) //写管道
{
char *message = "Hello, pipe!\n";
write(fd, message,strlen(message)+1);
}
int main()
{
int fd[2];
pid_t pid;
int stat_val;
if(pipe(fd))
{
printf("create pipe failed!\n");
exit(1);
}
pid = fork();
switch(pid)
{
case -1:
{
printf("fork error!\n");
exit(1);
}
case 0:
{
close(fd[1]); //子进程关闭fd1
read_from_pipe(fd[0]);
exit(0);
}
default:
{
close(fd[0]); //父进程关闭fd2
write_to_pipe(fd[1]);
wait(&stat_val); //等待子进程
exit(0);
}
}
return 0;
}
dual_pipe.c
/*************************************************
* 用两个管道创建双向传递功能
* **********************************************/
#include
#include
#include
#include
#include
#include
void child_rw_pipe(int readfd, int writefd)
{
char *message1 = "from child process!\n";
write(writefd, message1,strlen(message1)+1); //写入管道
char message2[100];
read(readfd, message2,100); //读管道
printf("child process read from pipe:%s",message2);
}
void parent_rw_pipe(int readfd, int writefd)
{
char *message1 = "from parent process!\n";
write(writefd, message1,strlen(message1)+1); //写管道
char message2[100];
read(readfd, message2,100);
printf("parent process read from pipe:%s",message2); //读管道
}
int main()
{
int pipe1[2],pipe2[2];
pid_t pid;
int stat_val;
printf("realize full-duplex communication:\n\n");
if(pipe(pipe1)) //创建管道pipe1 父写子读
{
printf("pipe1 failed!\n");
exit(1);
}
if(pipe(pipe2)) //创建管道pipe2 父读子写
{
printf("pipe2 failed!\n");
exit(1);
}
pid = fork();
switch(pid)
{
case -1:
{
printf("fork error!\n");
exit(1);
}
case 0:
{
//子进程
close(pipe1[1]); //关闭写
close(pipe2[0]); //关闭读
child_rw_pipe(pipe1[0],pipe2[1]);
exit(0);
}
default :
{
close(pipe1[0]); //关闭读
close(pipe2[1]); //关闭写
parent_rw_pipe(pipe2[0],pipe1[1]);
wait(&stat_val);
exit(0);
}
}
}
管道有一个局限性是没有名字,因此只能在具有亲缘关系的进程间才可以使用,有名管道(named pipe或FIFO)的出现客服了这一限制。FIFO与管道不同之处在于它提供了一个路径名与之关联,以FIFO的文件形式储存于文件系统中。有名管道是一个设备文件,因此可以不局限与具有亲缘关系才能访问。
有名管道的创建和读写
创建有名管道的方法有两种,一种是在shell下创建,另一种是在程序中用函数创建。shell中使用mknod或者mkfifo命令,例如:
mknod namedpipe
创建有名管道的函数有两个:mknod和mkfifo。函数原型为:
#include
#include
int mknod(const char *path, mode_t mod, dev_t dev);
int mkfifo(const char *path, mode_t mode);
函数mknod中path参数为创建的有名管道的全路径名;mod为创建有名管道的模式,指名其权限;dev为设备值,该值文件创建的类型,它只在创建设备文件时才会用到。这两个函数调用成功返回0,失败返回-1。mkfifo的参数与mknod的前两个参数一样。
使用时如下:
//1.0
umask(0);
if(mknod("/tmp/fifo",S_IFIFO | 0666, 0) == -1)
{
perror("mknod error!");
exit(1);
}
//2.0
umask(0);
if(mkfifo("/tmp/fifo",S_IFIFO | 0666) == -1)
{
perror("mknod error!");
exit(1);
}
umask(0)用于在创建新文件时屏蔽掉不应该有的访问允许权限。S_IFIFO | 0666指创建一个有名管道且存取权限为0666。
下面我们用一个有名管道实现两个无亲缘关系的进程间的通信:
procread.c
/*************************************
* 先运行procwrite.c文件,写入有名管道
* 后运行procread.c文件,读出有名管道中的内容
* 如果已经有了有名管道myfifo则先删除它
* **********************************/
#include
#include
#include
#include
#include
#include
#define FIFO_NAME "myfifo"
#define BUF_SIZE 1024
int main()
{
int fd;
char buf[BUF_SIZE];
umask(0); //给予最大权限
fd = open(FIFO_NAME, O_RDONLY); //以只读的方式打开文件myfifo
read(fd, buf, BUF_SIZE); //读取文件myfifo
printf("Read content: %s\n", buf);
close(fd); //关闭文件
return 0;
procwrite.c
/*************************************
* 先运行procwrite.c文件,写入有名管道
* 后运行procread.c文件,读出有名管道中的内容
* 如果已经有了有名管道myfifo则先删除它
* **********************************/
#include
#include
#include
#include
#include
#include
#include
#define FIFO_NAME "myfifo"
#define BUF_SIZE 1024
int main()
{
int fd;
char buf[BUF_SIZE] = "有名管道";
char A[100] = "ABC";
umask(0);
if(mkfifo(FIFO_NAME, S_IFIFO | 0666) == -1) //创建一个有名管道
{
perror("mkfifo error!");
exit(1);
}
if((fd = open(FIFO_NAME, O_WRONLY)) == -1) //以写的方式打开一个有名管道
{
perror("open error!");
exit(1);
}
write(fd, buf,strlen(buf)+1); //向管道内写入内容,并等待na_pipe进程读出myfifo中的内容
write(fd,A,strlen(A)+1); //该内容没有被写入或者读出
printf("na_pipe 读出完毕\n");
close(fd); //关闭
return 0;
}
如果要实现双向通信需要使用两个有名管道。
概念
消息队列是一个存放在内核中的消息链表,每个消息队列由消息队列标识符标识。与管道不同的是消息队列存放在内核中,只有在内核重启(即操作系统重启)或者显式地删除-一个消息队列时,该消息队列才会被真正删除。
操作消息队列时,需要用到- -些数据结构,熟悉这些数据结构是掌握消息队列的关键。下面介绍几个重要的数据结构。
1.消息缓冲结构
向消息队列发送消息时,必须组成合理的数据结构。Linux系统定义了-一个模板数据结构msgbuf:
#include
struct msgbuf{
long mtype;
char mtext[1];
}
结构体中的mtype字段代表消息类型。给消息指定类型,可以使得消息在一一个队列中重复使用。nmtext字段指消息内容。
注意: mtext虽然定义为char类型,并不代表消息只能是一个字符,消息内容可以为任意类型,由用户根据需要定义。如下面就是用户定义的一个消息结构:
struct mymsgbuf{
long mtype;
struct stdudent stu;
};
消息队列的大小受到MSGMAX的限制,MSGMAX为最大值。
2.msqid_ds内核数据结构
每个消息队列都有一个msqid_ds结构体,此结构体保存着该消息队列当前的状态信息。
struct msqid_ds{
struct ipc_perm msg_perm; //保存了消息队列的存取权限和队列用户ID,组ID等信息
struct msg *msg_first; //指向队列中第一条消息
struct msg *msg_last; //指向队列中最后一条消息
__kernel_time_t msg_stime; //向消息队列发送最后一条信息的时间
__kernel_time_t msg_rtime; //从消息队列中读取最后一条消息的时间
__kernel_time_t msg_ctime; //最后一个变更消息队列时间
unsigned long msg_lcbytes;
unsigned long msg_lqbytes;
unsigned short msg_cbytes; //消息队列中所有消息所占的字节数
unsigned short msg_qnum; //消息队列中消息的数目
unsigned short msg_qbytes; //消息队列的最大字节数
__kernel_ipc_pid_t msg_lspid; //向消息队列发出最后一条消息的进程ID
__kernel_ipc_pid_t msg_lrpid; //从消息队列读出最后一条消息的进程ID
};
#include
struct ipc_perm{
__kernel_key_t key; //创建消息队列用到的健值key
__kernel_uid_t uid; //消息队列的用户ID
__kernel_gid_t gid; //消息队列的组ID
__kernel_uid_t cuid; //创建消息队列的进程用户ID
__kernel_gid_t cgid; //创建消息队列的进程组ID
__kernel_ mode_t mode;
__unsigned_short seq;
}
消息队列的创建
消息队列是随着内核的存在而存在的,每个消息队列在系统范围内对应唯一的键值。要获取一个消息队列的描述符,只需提供该消息队列的键值及可,该键值通常由函数ftok返回,该函数原型为:
#include
#include
key_t ftok(const char *pathname, int proj_id);
参数pathname为路径,且一定要有访问权限;参数proj_id的值为1~255。
例如:
ftok.c
#include
#include
#include
int main()
{
int i;
for(i = 1; i <= 5; i++)
{
printf("key[%d] = %ul\n", i, ftok(".",i));
//ftok函数根据键值生成唯一的键值
}
return 0;
}
有了键值我们就可以根据键值创建或打开一个消息队列,函数为msgget,该函数调用成功返回一个消息队列描述符,错误返回-1,函数原型为:
int msgget(key_t key, int msgflg);
key参数为键值,msgflg为一个标志参数,它的取值有:
IPC_CREATE:如果内核中不存在键值与key相等的消息对列,则新建一-个消息队列:如果存在这样的消息队列,返回该消息队列的描述符。
IPC_ EXCL: 和IPC _CREATE -起使用,如果对应键值的消息队列已经存在,则出错。
注意:IPC_EXCL需要和IPC_CREATE一起使用否者无意义。
写消息队列
用函数msgsnd向消息队列发送(写)数据,该函数调用成功返回0,错误返回-1,该函数原型为:
#include
int msgsnd(int msqid, struct msgbuf *msgp, size_t msgsz, int magflg);
各参数含义如下:
/***********************************
* 创建一个消息队列,并写入消息
* 编译运行该文件
* 在shell下输入ipcs命令
* 系统内部生成了一个消息队列Message Queues,其中含有一条消息
* ********************************/
#include
#include
#include
#include
#include
#define BUF_SIZE 256
#define PROJ_ID 32
#define PATH_NAME "."
int main()
{
struct mymsgbuf{
long msgtype;
char ctrlstring[BUF_SIZE];
}msgbuffer;
int qid; //消息队列标识符
int msglen;
key_t msgkey;
if((msgkey = ftok(PATH_NAME, PROJ_ID)) == -1) //获取键值
{
perror("ftok error!\n");
exit(1);
}
if((qid = msgget(msgkey, IPC_CREAT | 0660)) == -1) //创建消息队列
{
perror("msgget error!\n");
exit(1);
}
msgbuffer.msgtype = 3;
strcpy(msgbuffer.ctrlstring, "Hello,message queue");
msglen = sizeof(struct mymsgbuf) - 4;
if(msgsnd(qid,&msgbuffer, msglen, 0) == -1) //将msgbuffer结构中的信息写入消息中
{
perror("msgget error!\n");
exit(1);
}
return 0;
}
读消息队列
将消息写入消息队列或就可以用函数msgecv将消息读出,该函数调用成功返回读出的实际字节数,错误返回-1,函数原型为:
#include
int msgrcv(int msqid, struct msgbuf *msgp, size_t msgsz, long int msgtyp, int msgflg);
参数含义为:
/**************************************
* 从消息队列中读取消息
* ***********************************/
#include
#include
#include
#include
#include
#define BUF_SIZE 256
#define PROJ_ID 32
#define PATH_NAME "."
int main()
{
struct mymsgbuf{
long msgtype;
char ctrlstring[BUF_SIZE];
}msgbuffer;
int qid; //消息队列标识符
int msglen;
key_t msgkey;
if((msgkey = ftok(PATH_NAME, PROJ_ID)) == -1) //获取键值
{
perror("ftok error!\n");
exit(1);
}
if((qid = msgget(msgkey, IPC_CREAT | 0660)) == -1) //获取消息队列标识符
{
perror("msgget error!\n");
exit(1);
}
msglen = sizeof(struct mymsgbuf) - 4;
if(msgrcv(qid, &msgbuffer, msglen, 3, 0) == -1) //将消息读入到msgbuffer结构中
{
perror("msgrcv error!\n");
exit(1);
}
printf("Get message %s\n",msgbuffer.ctrlstring);
return 0;
}
获取和设置消息队列的属性
消息队列的属性在结构体msqid_ds中,用户可以通过函数msgctl来获取和设置消息队列的属。该函数原型为:
#include
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
该函数对msqid为标识符的消息队列执行cmd操作:
概念
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程在访问共享资源时,其他进程也在访问,因此主要作为进程间或同一进程间不同线程之间的同步机制。与消息队列一样,每个信号集都有一个semid_ds数据结构.
#include
struct semid_ds{
struct ipc_perm sem_perm; //对信号进行操作的许可权
__kernel_time_t sem_otime; //对信号进行操作的最后时间
__kernel_time_t em_ctime; //对信号进行修改的最后时间
struct sem *sembase; //指向第一个信号
struct sem_queue sem_pending; //等待处理的挂起操作
struct sem_queue **sem_pending_last; //最后一个正在挂起的操作
struct sem_undo *undo; //撤销的请求
ushort sem_nsems; //数组中的信号数
}
注意:这里的信号与之前学的信号不一样,该函数调用成功返回一个信号集的标识符,失败返回-1,上面semid_ds结构体不懂也无妨。
信号集的创建和打开
我们可以使用semget函数创建或打开信号集,函数原型为:
#include
int semget(key_t key, int nsems, int semflg);
参数key为ftok获取的键值,nsems为要创建信号集所包含的个数,如果只打开一个信号集则为0,semflg为操作标志,可取以下值:
#include
int semop(int semid, struct sembuf *sops, size_t nsops);
semid参数为信号集标识符; sops参数为指向进行操作的结构体数组首地址;参数nsops为要进行操作的信号的个数;该函数调用成功返回0,失败返回-1.
#include
struct sembuf{
ushort sem_num; //信号在信号集中的索引
short sem_op; //操作类型
short sem_flg; //操作标志
}
//P操作函数 信号-1
int sem_P(int semid, int index) //index = 0
{
struct sembuf buf = {0, -1, IPC_CREAT}; //信号在信号集中的索引->0
//操作类型->-1 信号加上-1即-1
//操作标志->IPC_CREAT
if(index < 0)
{
perror("index of array cannot equals a minus value!");
return -1;
}
buf.sem_num = index; //信号在信号集中的索引->0
if(semop(semid , &buf, 1) == -1) //信号-1
{
perror("a wrong operation to semaphore occurred!");
return -1;
}
return 0;
}
V操作:将信号量+1,表示有一个进程使用完了资源;
//V操作函数 信号+1
int sem_V(int semid, int index) //index = 0
{
struct sembuf buf = {0, +1, IPC_CREAT}; //信号在信号集中的索引->0
//操作类型->+1 信号加上+1即+1
//操作标志->IPC_CREAT
if(index < 0)
{
perror("index of array cannot equals a minus value!\n");
return -1;
}
buf.sem_num = index; //信号在信号集中的索引->0
if(semop(semid,&buf, 1) == -1) //信号+1
{
perror("a wrong operation to semaphore occurred!");
return -1;
}
return 0;
}
信号集的控制
使用信号集时需要对信号集进行一些控制,例如:删除一个信号集,对semid_ds进行设置,获取信号集中信号量等;这些操作都需要函数semctl进行,该函数的原型为:
#include
int semctl(int semid, int semnum, int cmd,...);
semid为信号集标识符,semnum标识一个特定信号,cmd为操作类型,‘…'表示参数是可选的。他依赖于cmd,通过共用体来选取参数。
int val;
struct semid_ds *buf;
ushort *array;
struct seminfo *buf;
void *pad;
前面说过信号量一般和其他通信类型共同使用,所以我们吧它和共享内存结合使用来理解。
共享内存是分配一块能被其他进程访问的内存。同时美国共享内存都有一个shmid_ds结构.
struct shmid_ds{
struct ipc_perm shm_perm;/* 操作权限*/
int shm_segsz; /*段的大小(以字节为单位)*/
__kernel_time_t shm_atime; /*最后一个进程附加到该段的时间*/
__kernel_time_t shm_dtime; /*最后一个进程离开该段的时间*/
__kernel_time_t shm_ctime; /*最后一个进程修改该段的时间*/
unsigned short shm_cpid; /*创建该段进程的pid*/
unsigned short shm_lpid; /*在该段上操作的最后1个进程的pid*/
short shm_nattch; /*当前附加到该段的进程的个数*/
/*下面是私有的*/
unsigned short shm_npages; /*段的大小(以页为单位)*/
unsigned long *shm_pages; /*指向frames->SHMMAX的指针数组*/
};
共享内存区的创建
使用shmget函数来创建一个共享内存区,或访问一个已经存在的共享内存区,该函数的函数原型为:
#include
int shmget(key_t key, size_t size, int shmflg);
参数key为键值,size为以字节为单位指定内存的大小,shmflg为操作标志位,它的取值有:
#include
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数shmid为shmget的返回值,为贡献内存区的描述符;shmflg为存取权限;shmaddr为共享内存的附加点,取值可为:
在使用完共享内存区时需要通过函数shmdt断开与共享内存区的链接。该函数的原型为:
#include
int shmdt(const void *shmadder);
参数shmadder为函数shmat的返回值,调用成功返回0,失败返回-1.
共享内存区的控制
对贡献内存区的控制是使用函数shmctl实现的,该函数的原型为:
#include
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid为标识符;buf为指向shmid_ds结构体的指针,cmd为操作位:
例:
sharemem.h
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SHM_SIZE 1024
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
};
//创建信号量函数
int createsem(const char *pathname, int proj_id, int members, int init_val)
{
key_t msgkey; //键值
int index,sid;
union semun semopts;
if((msgkey = ftok(pathname,proj_id)) == -1) //获取键值
{
perror("ftok error!\n");
return -1;
}
if((sid = semget(msgkey, members, IPC_CREAT|0666)) == -1) //创建或打开信号集
{
perror("semget call failed!\n");
return -1;
}
semopts.val = init_val;
for(index = 0; index < members; index++)
{
semctl(sid, index, SETVAL, semopts);
}
return (sid);
}
//P操作函数 信号-1
int sem_P(int semid, int index) //index = 0
{
struct sembuf buf = {0, -1, IPC_CREAT}; //信号在信号集中的索引->0
//操作类型->-1 信号加上-1即-1
//操作标志->IPC_CREAT
if(index < 0)
{
perror("index of array cannot equals a minus value!");
return -1;
}
buf.sem_num = index; //信号在信号集中的索引->0
if(semop(semid , &buf, 1) == -1) //信号-1
{
perror("a wrong operation to semaphore occurred!");
return -1;
}
return 0;
}
//V操作函数 信号+1
int sem_V(int semid, int index) //index = 0
{
struct sembuf buf = {0, +1, IPC_CREAT}; //信号在信号集中的索引->0
//操作类型->+1 信号加上+1即+1
//操作标志->IPC_CREAT
if(index < 0)
{
perror("index of array cannot equals a minus value!\n");
return -1;
}
buf.sem_num = index; //信号在信号集中的索引->0
if(semop(semid,&buf, 1) == -1) //信号+1
{
perror("a wrong operation to semaphore occurred!");
return -1;
}
return 0;
}
//等待信号为1
int wait_sem(int semid, int index) //index = 0
{
while(semctl(semid, index, GETVAL,0) == 0)
{
sleep(1);
}
return 1;
}
//创建共享内存函数
int createshm(char *pathname, int proj_id, size_t size)
{
key_t msgkey; //键值
int sid;
if((msgkey = ftok(pathname, proj_id)) == -1) //获取键值
{
perror("ftok error!\n");
return -1;
}
if((sid = shmget(msgkey, size, IPC_CREAT|0666)) == -1) //创建或访问共享内存区
{
perror("shmget call failed!\n");
return -1;
}
return sid;
}
//删除信号集函数
int sem_deleta(int semid)
{
return (semctl(semid,0,IPC_RMID));
}
//打开信号量函数
int opensem(const char *pathname, int proj_id)
{
key_t msgkey;
int sid;
if((msgkey = ftok(pathname, proj_id)) == -1) //获取键值
{
perror("ftok error!\n");
return -1;
}
if((sid = semget(msgkey,0, IPC_CREAT|0666)) == -1) //创建或打开信号集
{
perror("semget call failed!\n");
return -1;
}
return (sid);
}
reader.c
/*************************************************
* 先运行writer.c文件,在writer:后输入内容到共享内存区
* 再运行reader.c文件,读出共享内存区数据
* **********************************************/
#include "sharemem.h"
int main()
{
int semid,shmid;
char *shmaddr;
if((shmid = createshm(".",'m',SHM_SIZE)) == -1) //打开共享内存区
{
exit(1);
}
if((shmaddr = shmat(shmid,(char *)0,0)) == (char *)-1) //返回指向共享内存区的指针
{
perror("attach shared memory error!\n");
exit(1);
}
if((semid = opensem(".",'s')) == -1) //打开信号集
{
exit(1);
}
while(1)
{
printf("reader: ");
wait_sem(semid, 0); //等待信号为1
sem_P(semid, 0); //信号-1
printf("%s\n",shmaddr);
sleep(10);
sem_V(semid, 0); //信号+1
sleep(10);
}
}
writer.c
/*************************************************
* 先运行writer.c文件,在writer:后输入内容到共享内存区
* 再运行reader.c文件,读出共享内存区数据
* **********************************************/
#include "sharemem.h"
int main()
{
int semid,shmid; //semget和shmget的返回值
char *shmaddr; //shmat的返回值
char write_str[SHM_SIZE];
if((shmid = createshm(".",'m', SHM_SIZE)) == -1) //创建共享内存区
{
exit(1);
}
if((shmaddr = shmat(shmid, (char *)0, 0)) == (char *)-1) //返回指向共享内存区的指针
{
perror("attach shared memory error!\n");
exit(1);
}
if((semid = createsem(".",'s',1,1)) == -1) //创建(打开)信号集
{
exit(1);
}
while(1)
{
wait_sem(semid, 0); //等待信号为1
sem_P(semid, 0); //信号-1
printf("writer: ");
fgets(write_str,1024,stdin); //输入内容
int len = strlen(write_str)-1;
write_str[len] = '\0';
strcpy(shmaddr,write_str); //写入共享内存区
sleep(10);
sem_V(semid, 0); //信号+1
sleep(10);
}
}