进程间通信和同步
在linux下有过种进程间通信的方法:半双工管道,FIFO(命名管道),消息队列,信号量,共享内存,socket等
半双工管道
管道式Linux系统中最古老的进程间通信机制,这里所说的管道是指无名管道(PIPE),它可用于具有亲缘关系进程间的通信.有名管道(FIFO)克服了管道没有名字的限制,因此,除了具有管道所有具有的功能外,它还允许无亲缘关系进程间的通信.Linux的管道主要包括两种:无名管道和有名管道,本文主要介绍这两种管道.
把一个进程连接到另一个进程的一个数据流称为"管道".比如:ls|more.这条命令的作用是列出当前目录下的所有文件盒子目录,如果内容超过了一页则自动进行分页.符号"|"就是shell为ls和more命令简历的一条管道,它将ls的输出直接送进了more的输入.
无名管道特点
- 它只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间).
- 它是一个半双工的通信模式,具有固定的读端和写端.
- 管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的read(),write()等函数.但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
int result = -1;
int fd[2],nbytes;
pid_t pid;
char string[] = "1111111111111111";
char readbuffer[40];
memset(readbuffer,0,40);
int *write_fd = &fd[1];
int *read_fd = &fd[0];
//建立管道
result = pipe(fd);
if(result == -1)
{
//返回-1失败
printf("建立管道失败\n");
return -2;
}
pid = fork();//分叉进程,返回两次:0表示子进程;>0表示父进程
if(pid == -1)
{
//返回-1表示分叉进程失败
printf("分叉进程失败\n");
return -1;
}
if(pid == 0)
{
//0表示子进程
close(*read_fd);//关闭管道读那端
//写数据
write(*write_fd,string,strlen(string));
return 0;
}
else
{
//表示父进程
close(*write_fd);//关闭写端
nbytes = read(*read_fd,readbuffer,sizeof(readbuffer));
printf("读取字节总数:%d %s\n",nbytes,readbuffer);
}
return 0;
}
- 只有在管道的读端存在时,向管道写入数据才有意义.否则,向管道写入数据的进程将收到内核传来的SIGPIPE信号(通常为Broken pipe错误).
- 向管道写入数据时,Linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据.如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞.
- 父子进程在运行时,它们的先后次序并不能保证,因此,在为了保证父子进程已经关闭了相应的文件描述符,可在两个进程中调用sleep()函数,当然这种调用不是很好的解决方法,在后面学到进程之间的同步与互斥机制之后.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#define K 1024
#define WRITELEN (128*K)
int main(void)
{
int result = -1;
int fd[2],nbytes;
pid_t pid;
char string[WRITELEN] = "你好,管道";
char readbuffer[10*K];
memset(readbuffer,0,10*K);
int *write_fd = &fd[1];
int *read_fd = &fd[0];
//建立管道
result = pipe(fd);
if(result == -1)
{
//返回-1失败
printf("建立管道失败\n");
return -2;
}
pid = fork();//分叉进程,返回两次:0表示子进程;>0表示父进程
if(pid == -1)
{
//返回-1表示分叉进程失败
printf("分叉进程失败\n");
return -1;
}
if(pid == 0)
{
//0表示子进程
close(*read_fd);//关闭管道读那端
//写数据
int writeSize = WRITELEN;
result = 0;
while(writeSize>=0)
{
result = write(*write_fd,string,writeSize);
printf("result = %d\n",result);
if(result>0)
{
writeSize -= result;
printf("写入%d字节数据,剩余%d字节数据\n",result,writeSize);
}
else
{
sleep(10);
break;
}
}
return 0;
}
else
{
//表示父进程
close(*write_fd);//关闭写端
while(1)
{
memset(readbuffer,0,sizeof(readbuffer));
nbytes = read(*read_fd,readbuffer,sizeof(readbuffer));
if(nbytes <= 0)
{
printf("数据读取完毕\n");
break;
}
printf("读取字节总数:%d 内容为:%s\n",nbytes,readbuffer);
}
}
return 0;
}
ftok函数:
ftok原型如下:
key_t ftok( char * fname, int id )
当成功执行的时候,一个key_t值将会被返回,否则 -1 被返回。
在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。如指定文件的索引节点号为65538,换算成16进制为 0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。
#include "unpipc.h"
int main(int argc, char **argv)
{
struct stat stat;
if (argc != 2)
err_quit("usage: ftok ");
Stat(argv[1], &stat);
printf("st_dev: %lx, st_ino: %lx, key: %x/n",
(u_long) stat.st_dev,(u_long) stat.st_ino,
Ftok(argv[1], 0x57));
exit(0);
}
程序运行结果:
[cbs@linux svipc]$ ./ftok /tmp/mysql.sock
st_dev: 802, st_ino: 34219, key: 57024219
- pathname所在文件系统的信息(stat结构的st_dev成员)
- pathname在本文件系统内的索引节点号(stat结构的st_ino成员)
- id的低序8位(不能为0)
使用ftok()需要注意的问题:
- pathname指定的目录(文件)必须真实存在且调用进程可访问,否则ftok返回-1;
- pathname指定的目录(文件)不能在程序运行期间删除或创建。因为文件每次创建时由系统赋予的索引节点可能不一样。这样一来,通过同一个pathname与proj_id就不能保证生成同一个IPC键。
首先了解一下,信号量机概念是由荷兰科学家Dijkstr引入,值得一提的是,它提出的Dijksrtr算法解决了最短路径问题。
信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况,信号量是一个特殊的变量,并且只有两个操作可以改变其值:等待(wait)与信号(signal)。
因为在Linux与UNIX编程中,"wait"与"signal"已经具有特殊的意义了(暂不知这特殊意义是啥),所以原始概念:
用于等待(wait)的P(信号量变量) ;
用于信号(signal)的V(信号量变量) ;
这两字母来自等待(passeren:通过,如同临界区前的检测点)与信号(vrjgeven:指定或释放,如同释放临界区的控制权)的荷兰语。
P操作:负责把当前进程由运行状态转换为阻塞状态,直到另外一个进程唤醒它。
操作为:申请一个空闲资源(把信号量减1),若成功,则退出;若失败,则该进程被阻塞;
V操作:负责把一个被阻塞的进程唤醒,它有一个参数表,存放着等待被唤醒的进程信息。
操作为:释放一个被占用的资源(把信号量加1),如果发现有被阻塞的进程,则选择一个唤醒之。
补充:查看共享信息的内存的命令是ipcs [-m|-s|-q] (全部的话是ipcs -a) ;查看共享信息的内存的命令是ipcs [-m|-s|-q]。
(一)系统调用函数semget()
函数原型:int semget(key_t key,int nsems,int semflg);
功能描述: 创建一个新的信号量集,或者存取一个已经存在的信号量集。
当调用semget创建一个信号量时,他的相应的semid_ds结构被初始化。ipc_perm中各个量被设置为相应
值:
sem_nsems被设置为nsems所示的值;
sem_otime被设置为0;
sem_ctime被设置为当前时间
参数介绍:
- key:所创建或打开信号量集的键值,键值是IPC_PRIVATE,该值通常为0,创建一个仅能被进程进程给我的信号量, 键值不是IPC_PRIVATE,我们可以指定键值,例如1234;也可以一个ftok()函数来取得一个唯一的键值。
- nsems:创建的信号量集中的信号量的个数,该参数只在创建信号量集时有效。
- semflg:调用函数的操作类型,也可用于设置信号量集的访问权限,两者通过or表示:有IPC_CREAT,IPC_EXCL两种:
- IPC_CREAT如果信号量不存在,则创建一个信号量,否则获取。
- IPC_EXCL只有信号量不存在的时候,新的信号量才建立,否则就产生错误。
返回值说明:
- 如果成功,则返回信号量集的IPC标识符,其作用与信息队列识符一样。
- 如果失败,则返回-1,errno被设定成以下的某个值
- EACCES:没有访问该信号量集的权限
- EEXIST:信号量集已经存在,无法创建
- EINVAL:参数nsems的值小于0或者大于该信号量集的限制;或者是该key关联的信号量集已存在,并且nsems
- 大于该信号量集的信号量数
- ENOENT:信号量集不存在,同时没有使用IPC_CREAT
- ENOMEM :没有足够的内存创建新的信号量集
- ENOSPC:超出系统限制
每个信号量都有一些相关值:
- semval 信号量的值,一般是一个正整数,它只能通过信号量系统调用semctl函数设置,程序无法直接对它进行修改。
- sempid 最后一个对信号量进行操作的进程的pid.
- semcnt 等待信号量的值大于其当前值的进程数。
- semzcnt 等待信号量的值归零的进程数。
(二)信号量的控制 semctl()
原型:int semctl(int semid,int semnum,int cmd,union semun ctl_arg);
参数介绍:
- semid为信号量集引用标志符,即semget 的返回值。
- semnum第二个参数是信号量数目;
- cmd表示调用该函数执行的操作,其取值和对应操作如下:
标准的IPC函数 (注意在头文件<sys/sem.h>中包含semid_ds结构的定义) |
IPC_STAT 把状态信息放入ctl_arg.stat中 IPC_SET 用ctl_arg.stat中的值设置所有权/许可权 IPC_RMID 从系统中删除信号量集合 |
单信号量操作 (下面这些宏与sem_num指定的信号量合semctl返回值相关) |
GETVAL 返回信号量的值(也就是semval) SETVAL 把信号量的值写入ctl_arg.val中 GETPID 返回sempid值 GETNCNT 返回semncnt(参考上面内容) GETZCNT 返回semzcnt(参考上面内容) |
全信号量操作 |
GETALL 把所有信号量的semvals值写入ctl_arg.array SETALL 用ctl_arg.array中的值设置所有信号量的semvals |
union semun {
int val; //执行SETVAL命令时使用
struct semid_ds *buf; //在IPC_STAT/IPC_SET命令中使用
unsigned short *array; //使用GETALL/SETALL命令时使用的指针
}
功能:smctl函数依据command参数会返回不同的值。它的一个重要用途是为信号量赋初值,因为进程无法直接对信号量的值进行修改。
(三)信号量操作semop函数
在 Linux 下,PV 操作通过调用semop函数来实现,也只有它能对PV进行操作
调用原型:int semop(int semid,struct sembuf*sops,unsign ednsops);
返回值:0,如果成功。-1,如果失败:errno=E2BIG(nsops大于最大的ops数目)
EACCESS(权限不够)
EAGAIN(使用了IPC_NOWAIT,但操作不能继续进行)
EFAULT(sops指向的地址无效)
EIDRM(信号量集已经删除)
EINTR(当睡眠时接收到其他信号)
EINVAL(信号量集不存在,或者semid无效)
ENOMEM(使用了SEM_UNDO,但无足够的内存创建所需的数据结构)
ERANGE(信号量值超出范围)
参数介绍:
第一个参数semid 是信号量集合标识符,它可能是从前一次的semget调用中获得的。
第二个参数是一个sembuf结构的数组,每个 sembuf 结构体对应一个特定信号的操作,sembuf结构在,<sys/sem.h>中定义
struct sembuf{
usign short sem_num;/*信号量索引*/
short sem_op;/*要执行的操作*/
short sem_flg;/*操作标志*/
}
sem_num 存放集合中某一信号量的索引,如果集合中只包含一个元素,则sem_num的值只能为0。
----------------------------------------------------------------------------------------------
Sem_op取得值为一个有符号整数,该整数实际给定了semop函数将完成的功能。包括三种情况:
如果sem_op是负数,那么信号量将减去它的值,对应于p()操作。这和信号量控制的资源有关。如果没有使用IPC_NOWAIT,那么调用进程将进入睡眠状态,直到信号量控制的资源可以使用为止。
如果sem_op是正数,则信号量加上它的值。对应于v()操作。这也就是进程释放信号量控制的资源。
最后,如果sem_op是0,那么调用进程将调用sleep(),直到信号量的值为0。这在一个进程等待完全空闲的资源时使用。
----------------------------------------------------------------------------------------------
sem_flag是用来告诉系统当进程退出时自动还原操作,它维护着一个整型变量semadj(信号灯的计数器),可设置为 IPC_NOWAIT 或 SEM_UNDO 两种状态。若使用SEM_UNDO标志,则操作系统将自动释放该进程持有的信号量,从而使得另外一个进程可以继续工作。若没有这个标志,另外进程将P操作永远阻塞。因此,一般建议使用SEM_UNDo标志。
第三个参数是sembuf组成的数组中索引。参数sops指向由sembuf组成的数组,结构数组中的一员。
int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shm_id, const void *shm_addr, int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shm_id, int command, struct shmid_ds *buf);
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
- 首先子进程会进行V操作,V操作会等待父进程会将共享内存中进行赋值操作完成后的P操作
- 父进程P操作完成后,会进行V操作,等待子进程P操作,子进程会进行输出共享内存中的内容
- 在子进程P操作后,子进程退出,父进程V操作等待结束,进行销毁信号量后退出
#include <stdio.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <string.h>
#include <sys/stat.h>
static char msg[] = "你好,共享内存~~~\n";
typedef int sem_t;
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
}arg;
//创建信号量
int CreateSem(key_t key,int value)
{
union semun sem;//信号量结构体
sem_t semid;
sem.val = value;
semid = semget(key,1,IPC_CREAT|0666);//获得信号量的ID
if(semid == -1)
{
printf("获得信号量失败\n");
return -1;
}
semctl(semid,0,SETVAL,sem);
return semid;
}
//利用semop()构建基本的P操作
int Sem_V(sem_t semid)
{
struct sembuf sops = {0,1,SEM_UNDO};//对信号量0进行加1操作
return semop(semid,&sops,1);
}
//利用semop()构建基本的V操作
int Sem_P(sem_t semid)
{
struct sembuf sops = {0,-1,SEM_UNDO};//对信号量0进行减1操作
return semop(semid,&sops,1);
}
//销毁信号量
void DestroySem(sem_t semid)
{
union semun sem;//信号量结构体
sem.val = 0;
semctl(semid,0,IPC_RMID,sem);//命令IPC_RMID将给定的信号量销毁
}
int main(void)
{
key_t key;
int semid,shmid;
char i,*shms,*shmc;
struct semid_ds buf;
int value = 0;
char buffer[80];
pid_t p;
struct stat stat_info;
char path[256];
memset(path,0,256);
strcpy(path,"/etc/environment");
//sprintf(path, "/etc/environment", (char*)getenv("HOME"));
printf("path = %s\n",path);
if(stat(path,&stat_info)!=0)
{
printf("Error\n");
return -1;
}
key=ftok(path, 'a');//生成键值
printf("key = %d\n",key);
shmid = shmget(key,1024,IPC_CREAT|0604);//获得共享内存,大小1024
printf("共享内存地址为:0x%08x\n",shmid);
semid = CreateSem(key,0);//建立信号量
p = fork();
if(p>0)
{
//父进程
shms = (char*)shmat(shmid,0,0);//挂载共享内存
//Sem_V(semid);//减小信号量
memset(shms,0,1024);
memcpy(shms,msg,strlen(msg)+1);//复制内容
printf("11:信号量值=%d\n",semctl(semid,0,GETVAL));
sleep(10);
printf("12:信号量值=%d\n",semctl(semid,0,GETVAL));
Sem_V(semid);//获得共享内存信号量=======V操作1
shmdt(shms);//摘除共享内存
Sem_P(semid);//减小信号量=======P操作1 会等待子进程中V操作2
DestroySem(semid);//销毁信号量
printf("父进程End\n");
}
else if(p==0)
{
//子进程
printf("21:信号量值=%d\n",semctl(semid,0,GETVAL));
Sem_P(semid);//减小信号量=======P操作2 会等待父进程中V操作1
printf("22:信号量值=%d\n",semctl(semid,0,GETVAL));
shmc = (char*)shmat(shmid,0,0);//挂载共享内存
printf("共享内存的值为:%s\n",shmc);
shmdt(shmc);//摘除共享内存
printf("子进程End\n");
Sem_V(semid);//=======V操作2
return 0;
}
return 0;
}