进程间通信IPC

1.管道(无名管道,命名管道)

两者都是通过文件描述符来读写管道,交换数据

无名管道(pipe)

相关函数    int pipe(int pipefd[2])

        fd[0],fd[1]是区分读写进程的文件标识符。

        只能用于父子进程或者兄弟进程之间( 具有亲缘关系的进程)。比如fork或exec创建的新进程, 在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。

        管道是半双工的,同时只能一人发一人收,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;

        当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

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

int main()
{
	int fd[2];
	int pid;
	if(pipe(fd)==-1)
	{
		printf("pipe creat failed!\n");
	}
	pid=fork();
	if(pid<0)
	{
		printf("creat child failed!\n");
	}
	if(pid>0)
	{	
		char *s="mes from father";
		close(fd[0]);
		write(fd[1],s,strlen(s));
	}
	if(pid==0)
	{
		char readBuf[128]={0};
		close(fd[1]);
		read(fd[0],readBuf,128);
		printf("rec mes:%s\n",readBuf);
	}
	return 0;
}

命名管道(fifo)

first in first out     总是按照先进先出的原则工作,第一个被写入的数据将首先从管道中读出。

相关函数  int mkfifo(const char *pathname, mode_t mode)

        命名管道是一种特殊类型的文件,它在系统中以文件形式存在。这样克服了无名管道的弊端,他可以允许没有亲缘关系的进程间通信。 

        当你用O_RDONLY or O_WRONLY的方式打开一个命名管道时,会存在open阻塞,如果同时用读写方式O_RDWR打开,则一定不会导致阻塞;如果不希望命名管道操作的时候发生阻塞,可以在open的时候使用O_NONBLOCK标志,以关闭默认的阻塞操作。

 当前路径下已经通过mkfifo file创建了管道文件,也可以在c程序中使用mkfifo()函数创建。

read.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>


int main()
{
	int fd;
	char readBuf[128]={0};

//  mkfifo(./file,0666); 

	fd=open("./file",O_RDONLY);
	printf("open success\n");
	read(fd,readBuf,128);
	printf("read context:%s\n",readBuf);
	close(fd);
	return 0;
} 

write.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>


int main()
{
	int fd;
	char *context="mes from file-write.c";
	fd=open("./file",O_WRONLY);
	printf("open success\n");
	write(fd,context,strlen(context));
	printf("write success\n");
	close(fd);
	return 0;
} 

命名管道的write具有原子性,与无名管道不同。无名管道是看到缓存区有空间就写入,而命名管道写入的数据不会是不完整的,会先判断缓存区大小是否够写入,够才写入,不够不写入。

2.消息队列 

通过msgget这个函数创建消息队列queen。再通过msgsnd和msgrcv这两个函数进行收发消息。

相关函数

int msgget(key_t key, int msgflg) 

通过这个函数可以获得操作某个消息队列的句柄msqid,后面的读写操作都是基于此qid的,这个key可以通过手动赋值,也可以通过函数  key_t ftok(const char *pathname, int proj_id)  来获得key_t。msgflg=IPC_CREAT|0777  如果不存在则创建并赋予权限

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg)

msgflg=0阻塞读取 msgflg=IPC_NOWAIT非阻塞读取。MSG_NOERROR截断消息(

  • 在默认情况下,当消息的 mtext 字段的大小超过了可用空间时(由 maxmsgsz 参数定义),msgrcv()调用会失败。
  • 如果指定了 MSG_ NOERROR 标记,那么 msgrcv()将会从队列中删除消息并将其 mtext 字段的大小截短为maxmsgsz 字节,然后将消息返回给调用者。被截去的数据将会丢失

)

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)

msgflg=0阻塞发送 msgflg=IPC_NOWAIT非阻塞发送。

int msgctl(int msqid, int cmd, struct msqid_ds *buf)

cmd:

-IPC_STAT 获取消息队列的属性放在struct msqid_ds结构体里

-IPC_SET 通过结构体设置消息队列的属性

-IPC_RMID 删除消息队列

相关结构体(mtext大小可变)

struct msgbuf {
           long mtype;       /* message type, must be > 0 */
           char mtext[1];    /* message data */
};

在结构中有两个成员,mtype为消息类型,用户可以给某个消息设定一个类型,可以在消息队列中正确地发送和接受自己的消息。mtext为消息数据,采用柔性数组,用户可以重新定义msgbuf结构。当然用户不可随意定义msgbuf结构,因为在linux中消息的大小是有限制的,在linux/msg.h中#define MSGMAX 8192  消息总的大小不能超过8192个字节,包括mtype成员(4个字节)。

下列程序中666代表消息类型,0x1234代表自定义的key_t key。

read.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>


struct msgbuf 
{
	long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
};


int main()
{
	int msgID;
	struct msgbuf readBuf;
	msgID=msgget(0x1234,IPC_CREAT|0777);
	if(msgID==-1)printf("failed to getID\n");	
	msgrcv(msgID,&readBuf,sizeof(readBuf.mtext),666,0);
	printf("read:%s\n",readBuf.mtext);
	return 0;
}

write.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>




struct msgbuf 
{
	long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
};

//0x1234 like a linkedlist's addr,666 is one of the node,which contain the msg

int main()
{
	int msgID;
	struct msgbuf sendBuf={666,"msg from write.c"};
	msgID=msgget(0x1234,IPC_CREAT|0777);
	if(msgID==-1)printf("failed to getID\n");	
	msgsnd(msgID,&sendBuf,sizeof(sendBuf.mtext),0);
	printf("write over\n");
	return 0;
}

消息队列的本质
        Linux的消息队列(queue)实质上是一个链表,它有消息队列标识符(queue ID)。 msgget创建一个新队列或打开一个存在的队列;msgsnd向队列末端添加一条新消息;msgrcv从队列中取消息, 取消息是不一定遵循先进先出的, 也可以按消息的类型字段取消息。

消息队列与命名管道的比较
        消息队列跟命名管道有不少的相同之处,通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read,则在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。
        与命名管道相比,消息队列的优势在于,1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。

3.共享内存

        通过shmget这个函数开辟一块共享内存sharememory。然后通过shmat映射到进程中。接着可以直接操作这块内存区域。shmdt是在进程中删除映射。shmctl是释放这快内存。

        共享内存是 IPC最快捷的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换。共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅映射到各进程的地址不同而已,因此不需要进行复制,可以直接使用此段空间。

         注意:共享内存本身并没有同步机制,需要程序员自己控制

相关函数

key_t ftok(const char *pathname, int proj_id);


int shmget(key_t key, size_t size, int shmflg);

shmflg=IPC_CREATE|0666 如果不存在则创建并赋予权限


void *shmat(int shmid, const void *shmaddr, int shmflg);

shmaddr=NULL为linux系统自动分配地址

shmflg=0可读可写 shmflg=SHM_RDONLY只读

返回共享内存的首地址


int shmdt(const void *shmaddr);


int shmctl(int shmid, int cmd, struct shmid_ds *buf);

cmd

-IPC_STAT 获取共享内存的属性放在struct shmid_ds结构体里

-IPC_SET 通过结构体设置共享内存的属性

-IPC_RMID 删除共享内存

write.c

#include <stdio.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/types.h>


/*firs creat a sharememory,second use shmat reflect the sharememory 
to this process,and then operate the addr*/
int main()
{
	int shmID;
	char *shmaddr;
	key_t key=ftok(".",1);
	shmID=shmget(key,1024*4,IPC_CREAT|0666);//size must be M
	if(shmID==-1)printf("shmget failed\n");
	shmaddr=shmat(shmID,0,0);//the sec para to 0 means Linux auto distribute memory,the third para means WR and RE
	printf("reflect finished\n");
	strcpy(shmaddr,"write.c has wrote msg to shm");
	printf("write ok\n");
	sleep(5);
	shmdt(shmaddr);
	shmctl(shmID,IPC_RMID,0);
	return 0;
}

read.c

#include <stdio.h>
#include <sys/ipc.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/types.h>


/*firs creat a sharememory,second use shmat reflect the sharememory 
to this process,and then operate the addr*/
int main()
{
	int shmID;
	char *shmaddr;
	key_t key=ftok(".",1);
	shmID=shmget(key,1024*4,0);//size must be M
	if(shmID==-1)printf("shmget failed\n");
	shmaddr=shmat(shmID,0,0);//the sec para to 0 means Linux auto distribute memory,the third para means WR and RE
	printf("reflect finished\n");
	printf("msg from write.c:%s\n",shmaddr);
	shmdt(shmaddr);
	return 0;
}

消息队列、信号量以及共享内存的相似之处:
        它们被统称为XSI IPC,它们在内核中有相似的IPC结构(消息队列的msgid_ds,信号量的semid_ds,共享内存的shmid_ds),而且都用一个非负整数的标识符加以引用(消息队列的msg_id,信号量的sem_id,共享内存的shm_id,分别通过msgget、semget以及shmget获得),标志符是IPC对象的内部名,每个IPC对象都有一个键(key_t key)相关联,将这个键作为该对象的外部名。

XSI IPC和PIPE、FIFO的区别:
        1、XSI IPC的IPC结构是在系统范围内起作用,没用使用引用计数。如果一个进程创建一个消息队列,并在消息队列中放入几个消息,进程终止后,即使现在已经没有程序使用该消息队列,消息队列及其内容依然保留。而PIPE在最后一个引用管道的进程终止时,管道就被完全删除了。对于FIFO最后一个引用FIFO的进程终止时,虽然FIFO还在系统,但是其中的内容会被删除。
        2、和PIPE、FIFO不一样,XSI IPC不使用文件描述符,所以不能用ls查看IPC对象,不能用rm命令删除,不能用chmod命令删除它们的访问权限。只能使用ipcs和ipcrm来查看可以删除它们。

4.信号

kill -l 查看所有信号

相关函数

typedef void (*sighandler_t)(int)
sighandler_t signal(int signum, sighandler_t handler)

sighandler_t handler有3种类型

-SIG_IGN 忽略

-SIG_DFL 默认

-typedef void (*sighandler_t)(int) 自定义信号处理函数

int kill(pid_t pid, int sig)

给指定进程 pid 发送指定信号

int raise(int sig)

自举信号,即给自己,当前进程发送指定信号

#include <stdio.h>
#include <signal.h>


void handler(int signalNum)
{
	printf("signalNum=%d\n",signalNum);
}

int main()
{
	signal(SIGINT,handler);//SIGINT is the signal of ctrl+c
	while(1);
	return 0;
}
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>

int main(int argc,char **argv)
{
	int sigNum,pid;
	sigNum=atoi(argv[1]);
	pid=atoi(argv[2]);
//	int kill(pid_t pid, int sig);
	kill(pid,sigNum);
	return 0;
}
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>

int main(int argc,char **argv)
{
	int sigNum,pid;
	char cmd[128]={0};
	sigNum=atoi(argv[1]);
	pid=atoi(argv[2]);
	sprintf(cmd,"kill -%d %d",sigNum,pid);
	system(cmd);
	return 0;
}

以上的signal函数可以用,但也可以使用更健壮的sigaction函数,可以更灵活,参数传递的更丰富

参考博文:

信号编程之sigaction函数和sigqueue函数_小帅比simon的博客-CSDN博客

相关函数

int sigqueue(pid_t pid, int sig, const union sigval value)
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact)
 

read.c

#include <signal.h>
#include <stdio.h>
//       int sigaction(int signum, const struct sigaction *act,
//                   struct sigaction *oldact);

void handler(int sigNum, siginfo_t *info, void *context)
{
	printf("sigNum=%d",sigNum);
	if(context!=NULL)
	{
		printf("data=%d\n",info->si_int);
		printf("data=%d\n",info->si_value.sival_int);
		printf("from:%d\n",info->si_pid);
	}
}

int main()
{
	struct sigaction act;
	printf("receive process pid:%d\n",getpid());
	act.sa_sigaction=handler;
	act.sa_flags=SA_SIGINFO;//be able to get msg
	sigaction(SIGUSR1,&act,NULL);
	while(1);
	return 0;
}

write.c

#include <signal.h>
#include <stdio.h>
// int sigqueue(pid_t pid, int sig, const union sigval value);
int main(int argc,char **argv)
{	
	int sigNum,pid;
	printf("proces-ssend pid:%d",getpid());
	sigNum=atoi(argv[1]);
	pid=atoi(argv[2]);
	union sigval value;
	value.sival_int=100;
	sigqueue(pid,sigNum,value);
	return 0;
}

屏蔽信号集


用于屏蔽某些信号。64位位图。

进程接收到某个信号号,回去查看这个信号集,

若信号未被屏蔽,那么进程就能马上去处理这些信号。

屏蔽方式:

手动,调用信号集api函数去设置
自动,进程在处理某信号时,可能会屏蔽它接收到的其他信号,处理完当前信号再去处理这些信号


未处理信号集


信号如果被屏蔽,则记录在未处理信号集中,

屏蔽信号解除以后,进程才能提取这些信号进行处理。

被手动或自动屏蔽的信号类型:

非实时信号(1 ~ 31),不排队,只留一个(如输入多个ctrl+c时,被自动屏蔽多余的信号,后面进行未处理信号时,只留一个)
实时信号(34 ~ 64),保留全部(被自动屏蔽多余的信号,会一个不落的处理)

可以通过api函数进行信号的实时处理,即可以手动解除对多余ctrl+c信号的屏蔽,这样对多余的ctrl+c信号也可以一个不落的处理了

参考文章:

linux 之信号集处理函数_你板子冒烟了的博客-CSDN博客

5.信号量(Semaphore)

        信号量是一种计数器,用于控制对多个进程共享的资源进行的访问。它们常常被用作一个锁机制,在某个进程正在对特定的资源进行操作时,信号量可以防止另一个进程去访问它。 
        信号量是特殊的变量,它只取正整数值并且只允许对这个值进行两种操作:等待(wait)和信号(signal)。(P、V操作,P用于等待,V用于信号) 
        p(sv):如果sv的值大于0,就给它减1;如果它的值等于0,就挂起该进程的执行 
        V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有其他进程因等待sv而挂起,则给它加1。简单理解就是P相当于申请资源,V相当于释放资源 

相关函数:

int semget(key_t key, int nsems, int semflg)

nsems:产生的信号量数量

semflg:IPC_CREAT|0666 不存在创建并且赋予权限

成功返回信号量ID

int semctl(int semid, int semnum, int cmd,union semnum arg)

功能:设置获取属性,还可以设置初始值

semid:信号量ID

semnum:信号量编号,一般通过semget产生的信号量数量nsems的值为1时,信号量编号为0

cmd:

-IPC_STAT 获取信号量的属性放在struct semid_ds结构体里

-IPC_SET 通过结构体struct semid_ds设置信号量的属性

-IPC_RMID 删除信号量

-SETVAL 设置信号量的值

第四个参数联合体取决于cmd的设置

union semun {
               int              val;    /* Value for SETVAL */
               struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */

};
 

int semop(int semid, struct sembuf *sops, size_t nsops)

功能:pv操作
nsops:信号量数量


struct sembuf{
        unsigned short sem_num;  /* semaphore number */ //信号量编号
           short          sem_op;   /* semaphore operation */  //PV操作
           short          sem_flg;  /* operation flags */   //一般设置flag为SEM_UNDO 若运行结束后未释放信号量,使用这个宏,操作系统会帮我们释放
};

e.g.

#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>


union semun {
	int val;
	struct semid_ds *buf;

};

/*struct sembuf{
	unsigned short sem_num;
	short sem_op;
	short sem_flg;
};*/

int sem_Init(int semID,int val)
{
	union semun us;
	us.val=val;
	if(-1==semctl(semID,0,SETVAL,us))//0代表第一个信号量,即信号量编号
	{
		perror("init err");
		return -1;
	}
	return 0;
}

int sem_Del(int semID)
{
	if(-1==semctl(semID,0,IPC_RMID,NULL))//0代表第一个信号量,即信号量编号
	{
		perror("del err");
		return -1;
	}
	return 0;
}

int p_op(int semID)
{
	struct sembuf p;
	p.sem_num=0; //信号量编号
	p.sem_op=-1; //信号量p/v操作
	p.sem_flg=SEM_UNDO;
	if(-1==semop(semID,&p,1))//操作信号量的数量为1
	{
		perror("p operation err");
		return -1;
	}
	return 0;
}

int v_op(int semID)
{
	struct sembuf v;
	v.sem_num=0;
	v.sem_op=1;
	v.sem_flg=SEM_UNDO;
	if(-1==semop(semID,&v,1))
	{
		perror("v operation err");
		return -1;
	}
	return 0;
}

int main()
{
	pid_t retpid;
	key_t key;
	int semID;

	key=ftok("./",666);
	semID=semget(key,1,IPC_CREAT|0666);
	sem_Init(semID,0); //0代表初始化信号量的值为0,没有资源,要通过v操作加1
		
	retpid=fork();
	if(-1==retpid)
	{
		perror("fork err");
	}
	else if(0==retpid) //child pro
	{
		printf("child pro sleep 3 sec\n");
		sleep(3);
		printf("child pro is running...\n");
		v_op(semID);
	}
	else if(retpid>0) //father pro
	{
		p_op(semID);
		printf("father pro is running..\n.");
		v_op(semID);

		sem_Del(semID);
	}
	else
       	{
	
	}
	
	return 0;
}

fork创建的父子进程执行顺序不固定,通过信号量的控制可以让子进程先于父进程执行。一开始子进程休眠3s,父进程虽然没有休眠,但是信号量的值初始化为0,p操作阻塞,需要等待子进程的v操作,子进程可以在这之前执行所需代码段,等待大约3s后,再执行父进程的代码段,最后父进程v操作归还信号量。信号量不仅仅限于父子进程,任意进程都可以实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值