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操作归还信号量。信号量不仅仅限于父子进程,任意进程都可以实现。