linux系统编程之进程间通信(IPC)及相关API

文章目录

    • 无名管道
    • 命名管道FIFO
    • 消息队列MessageQueue
    • 共享存储SharedMemory
    • 信号量Semaphore
    • 信号 ( sinal )

无名管道

无名管道pipe是一种半双工的通信方式,不会创建一个实际的文件,相当于只是运行时在linux内核下有一个缓冲区来进行读写数据,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

 #include 

       int pipe(int pipefd[2]);

用pipe函数来创建一个管道,参数只有一个大小为2的一个整形数组,后续的读写操作的文件描述符就是根据这两个数组里的数,fd[0]是读,fd[1]是写。因为是半双工的,所以只能一端读一端写,而在某一端进行了某个操作时,相应的需要关闭掉另一种操作,这里可以举例说明,函数的返回值如果是-1则表示管道创建失败。

#include 
#include 
#include 
#include 

int main(){
	    int fd[2];
        pid_t pid;
        int data;
        if(pipe(fd)==-1){
                printf("创建管道失败\n");
        }
        printf("请输入一个数字,1是父进程发数据,0是子进程发数据\n");
        scanf("%d",&data);
        pid=fork();

        if(pid<0){
                printf("创建进程失败\n");
        }
        else if(pid>0){
                printf("这是父进程\n");
                if(data==1){
                        close(fd[0]);
                        write(fd[1],"message from father",strlen("message from father"));
                }
                else if(data==0){
                        close(fd[1]);
                        char readbuf[20]={0};
                        read(fd[0],readbuf,sizeof(readbuf));
                        printf("%s\n",readbuf);
                }
        }
        else {
                printf("这是子进程\n");
                if(data==0){
                        close(fd[0]);
                        write(fd[1],"message from child",strlen("message from child"));
                }
                else if(data==1){
                        close(fd[1]);
                        char readbuf[20]={0};
                        read(fd[0],readbuf,sizeof(readbuf));
                        printf("%s\n",readbuf);
                }

        }

        return 0;
}

linux系统编程之进程间通信(IPC)及相关API_第1张图片
linux系统编程之进程间通信(IPC)及相关API_第2张图片

命名管道FIFO

命名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信,并且是实际的创建了一个文件。

 #include 
       #include 

       int mkfifo(const char *pathname, mode_t mode);

有名管道使用mkfifo创建,pathname是管道的名字,mode是文件的属性,返回值如果是-1,则表示创建失败,失败的原因有可能会是管道已经存在导致的失败,所以在判断的时候可以加EEXIST来辅助判断是否是其他原因导致的失败,EEXIST是当管道存在时的错误信息,因为是半双工的,所以一边的open是WRONLY,另一边是RDONLY,在没接受到数据之前读端会一直阻塞知道写端发送数据,这里可以举例说明。

①读端

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(){

        if(mkfifo("./pipe",0600)==-1 && errno!=EEXIST){
                printf("管道创建失败\n");
                perror("why");
        }

        int fd=open("./pipe",O_RDONLY);
        char readBuf[128]={0};

        int n_read;
                while(1){
                        n_read=read(fd,readBuf,sizeof(readBuf));
                        printf("读了%ld大小的数据,内容是%s\n",strlen(readBuf),readBuf);
                }
        close(fd);

        return 0;
}

linux系统编程之进程间通信(IPC)及相关API_第3张图片

②写端

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(){


        int fd=open("./pipe",O_WRONLY);
        char readBuf[128]={0};
        FILE *fp;

        int n_write;
                while(1){
                        fp=popen("date","r");
                        fread(readBuf,128,1,fp);
                        n_write=write(fd,readBuf,strlen(readBuf));
                        printf("写了%d大小的数据,内容是%s\n",n_write,readBuf);
                        pclose(fp);
                        sleep(3);
                }
        close(fd);

        return 0;
}

linux系统编程之进程间通信(IPC)及相关API_第4张图片

消息队列MessageQueue

消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点,而且本质上就是一个存在于内核中的一个链表,每一个节点都是一个信息。

    #include 
       #include 
       #include 
       struct msgbuf {
               long mtype;
               char mtext[128];
           };

       int msgget(key_t key, int msgflg);
	   int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
	   ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
	   int msgctl(int msqid, int cmd, struct msqid_ds *buf);

结构体msgbuf是用来存放信息的,mtype是消息的类型,mtext是存放正文的

①msgget创建消息队列

key值是指某一条队列,可以自己指定,也可以用ftok函数来生成key值。
msgflag为所需要的操作和权限。值可以有 IPC_CREAT | IPC_EXCL(如果不存在key值的消息队列,则创建消息队列,并返回一个消息队列ID。如果存在,则返回-1),IPC_CREAT(如果不存在key值的消息队列,且权限不为0,则创建消息队列,并返回一个消息队列ID。如果存在,则直接返回消息队列ID)。

②msgsend发送消息

msgqid是msgget成功返回的ID值。
msgp:消息的结构体指针
msgsz:消息正文的大小
msgflg:可以选择0和IPC_NOWAIT,选择0时为当消息队列满时会阻塞,而IPC_NOWAIT为当消息队列满时会立即出错
成功返回0,失败返回-1

③msgrcv接收消息

msgid:为msgget成功返回的ID值
msgp:消息的结构体指针
msgsz:消息正文的大小
msgtyp:消息的类型
0:获取消息队列中的第一条消息
> 0:获取类型为 msgtyp 的第一条消息。
< 0: 获取队列中消息类型值小于等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息
msgflg:0时,阻塞式接收消息,没有该类型的消息一直阻塞等待
IPC_NOWAIT时,不等待立即获取消息,如果没有返回条件的消息调用立即返回
IPC_EXCEPT时,与msgtype配合使用返回队列中第一个类型不为msgtype的消息
成功返回消息的长度,失败返回-1

④msgctl控制消息队列

msqid:消息队列标识符
cmd:控制指令
IPC_RMID:可以删除消息队列
IPC_STAT:获得msgid的消息队列头数据到buf中
IPC_SET:设置消息队列的属性,要设置的属性需先存储在buf中
buf:消息队列管理结构体,如果不关心一些信息,可以设置为NULL
成功返回0,失败返回-1

这里可以举例说明,这里写的是两个进程都可以收发消息
#include 
#include 
#include 
#include 
#include 
#include 
/*  int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

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

       int msgflg);
*/
       struct msgbuf {
               long mtype;
               char mtext[128];
           };


int main(){

        struct msgbuf revBuf;
        struct msgbuf sendBuf={777,"i am fine"};
        key_t key;
        key=ftok("..",6);
        int msgId;
        msgId=msgget(key,IPC_CREAT|0777);
        if(msgId==-1){
                printf("id号获取失败\n");
        }


        msgrcv(msgId,&revBuf,sizeof(revBuf.mtext),666,0);
        printf("接受成功\n");
        printf("接收到的消息是%s\n",revBuf.mtext);



        msgsnd(msgId,&sendBuf,sizeof(sendBuf.mtext),0);
        printf("发送完成\n");
        msgctl(msgId,IPC_RMID,NULL);

        return 0;
}

#include 
#include 
#include 
#include 
#include 
#include 
/*  int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

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

       int msgflg);
*/
       struct msgbuf {
               long mtype;
               char mtext[128];
           };


int main(){

        struct msgbuf sendBuf={666,"how are you"};
        key_t key;
        key=ftok("..",6);
        int msgId;
        msgId=msgget(key,IPC_CREAT|0777);
        if(msgId==-1){
                printf("id号获取失败\n");
        }

        msgsnd(msgId,&sendBuf,sizeof(sendBuf.mtext),0);
        printf("发送完成\n");

        memset(sendBuf.mtext,0,128);
        msgrcv(msgId,&sendBuf,sizeof(sendBuf.mtext),777,0);
        printf("接受成功\n");
        printf("接收到的消息是%s\n",sendBuf.mtext);
        msgctl(msgId,IPC_RMID,NULL);
        return 0;
}


在这里插入图片描述

共享存储SharedMemory

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

#include 
       #include 

       int shmget(key_t key, size_t size, int shmflg);
		void *shmat(int shmid, const void *shmaddr, int shmflg);
		int shmdt(const void *shmaddr);
		int shmctl(int shmid, int cmd, struct shmid_ds *buf);

①shmget获取共享内存的ID号

key可以由ftok函数来生成
size为需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍
shmflg操作的权限,可以IPC_CREAT创建
成功返回共享内存id,失败返回-1

②shmat:共享内存的映射

shmaddr:如果等于0,则由linux内核自动选择一个可以用的地址来进行映射
shmflg:如果等于零则可读可写,也可以用SHME_RDONLY设置只读
成功返回内存的指针,错误返回-1

③shmdt 解除映射,直接传递内存指针进来

④shctl 共享内存的操作

cmd:操作的宏,一般用来销毁共享内存,用IPC_RMID
buf:共享内存cmd操作后返回的一些信息储存到该结构体中,如果不关心可以用NULL

举例说明

写端
#include 
#include 
#include 
 #include 

#include 
#include 
#include 

int main(){
        key_t key;
        char *shmaddr;
        key=ftok(".",6);

        int cnt=0;
        int shmId=shmget(key,4096,IPC_CREAT|0666);
		shmaddr=shmat(shmId,0,0);
        sleep(2);
        while(cnt<5){
                shmaddr[cnt]='A'+cnt;
                sleep(1);

                cnt++;

        }
        shmdt(shmaddr);
        sleep(2);
        shmctl(shmId,IPC_RMID,NULL);

        return 0;
}

读端
#include 
#include 
#include 
 #include 

#include 
#include 
#include 

int main(){
        key_t key;
        char *shmaddr;
        key=ftok(".",6);

        int cnt=0;
        int shmId=shmget(key,4096,0600);
        shmaddr=shmat(shmId,0,0);
        while(cnt<5){

                printf("%s\n",shmaddr);
                sleep(1);
                cnt++;
        }
        shmdt(shmaddr);
        return 0;
}

在这里插入图片描述

信号量Semaphore

信号量和其他IPC结构不同,它是一个计数器,可以用来控制多个进程对临界资源(当该资源被某个进程访问时,其他进程只能等待访问完成)的访问。它常作为一种锁机制,防止某进程正在访问临界资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

 #include 
 int semget(key_t key, int num_sems, int sem_flags);
 int semop(int semid, struct sembuf *sops, size_t nsops);
 int semctl(int semid, int sem_num, int cmd, ...);

①semget创建或获取一个信号量组

key:键值可用ftok生成
num_sems:信号量组中信号量的个数
sem_flags:可操作的权限,若没有则IPC_CREAT创建
若成功返回信号量集ID,失败返回-1

②semop对信号量组进行操作,可以改变信号量的值

semid:信号量组id
sops:结构体指针,实质是信号量结构体数组指针,结构体内有三个参数
sem_num 信号量的编号,只有一个信号量时,默认为0,多个时需要根据实际情况赋值
sem_op 对信号量的操作,当v操作后也就是访问完资源后+1以提示其他进程可以访问该临界资源了,当 p操作后-1提示其他进程该临界资源有其他进程在访问
sem_flg IPC_NOWAIT和SEM_UNDO,IPC_NOWAIT不等待,一般会用SEM_UNDO,该参数用后,在进程结束之后会自动解除锁
成功返回0,失败返回-1

③semctl:控制信号量的相关信息

有三个或者四个参数,取决于cmd,若有四个参数,第四个参数是一个联合体,sem_num是在信号量组里的哪一个信号量,若要设置信号量的锁,可以使cmd为SETVAL ,然后在联合体里把val的值设置成是否有进程访问临界资源

 以下举例说明,写的是父子进程访问资源的例子
#include 
#include 
#include 
#include 
#include 
#include 

     union semun {
               int              val;    /* Value for SETVAL */
               struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
               unsigned short  *array;  /* Array for GETALL, SETALL */
               struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
           };
void pGet(int semId){
        struct sembuf set;

        set.sem_num=0;
        set.sem_op=-1;
        set.sem_flg=SEM_UNDO;
                if(semop(semId, &set, 1)==-1){

                printf("操作失败\n");
        }

}
void vPut(int semId){
        struct sembuf set;
		set.sem_num=0;
        set.sem_op=1;
        set.sem_flg=SEM_UNDO;
        if(semop(semId, &set, 1)==-1){

                printf("操作失败\n");
        }

}
int main(){
        key_t key;
        key=ftok(".",66);
        union semun set;
        int semId=semget(key, 1, IPC_CREAT|0666);
        if(semId==-1){
                printf("获取失败\n");
        }
        set.val=0;
         semctl(semId, 0,SETVAL,set);

         int pid=fork();

         if(pid>0){
                pGet(semId);
                printf("父进程\n");
                vPut(semId);
         }
         if(pid==0){
		        printf("我就算等了三秒,你没有锁也不能访问\n");
				sleep(3);
                printf("子进程\n");
                vPut(semId);
         }
         if(pid<0){
                printf("fork失败\n");
         }

        return 0;
}

在这里插入图片描述
这里初始化时sem_num设置的是0也就是某个进程在访问资源的状态,只有解除此种状态也就是v操作后才能开始运行

信号 ( sinal )

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生,也就是接受某种信号,该信号可以携带一些消息给目标进程

 #include 

       typedef void (*sighandler_t)(int);
       sighandler_t signal(int signum, sighandler_t handler);
				
signal函数用于对信号的操作,signum是信号编号,可以用kill -l指令来看linux下有哪些信号
kill -编号 pid号  可以对该进程发送信号

linux系统编程之进程间通信(IPC)及相关API_第5张图片
handler为函数指针,定义一个该类型的函数,参数是整形也就是信号编号,在此函数中写出当收到对应信号时会做什么操作,这个是信号的捕获,如果想忽略对应的信号,可以把第二个值设置为SIG_IGN,而SIG_KILL,SIG_STOP是不能被捕获以及忽略的,因为如果可以,那么该程序就是没有能够被控制的方法的

举例说明
#include 
#include 
void handler(int signum){
        switch(signum){
                case 2:
                printf("signum=%d\n",signum);
                printf("就不退出\n");
                case 9:
                printf("signum=%d\n",signum);
                printf("就不退出\n");
        }
}
int main(){

        signal(SIGINT,SIG_IGN);//①
        signal(SIGKILL,handler);//②
        while(1);
        return 0;
}

①不能被捕获   ②程序会被杀死

除了用kill -9 -pid之外还可以自己写一个程序来实现发送信号的功能,使用了kill函数
 #include 
       #include 

       int kill(pid_t pid, int sig);

pid是目的进程的进程号,sig是信号编号,例子如下
#include 
#include 
#include 
#include 

//int kill(pid_t pid, int sig);
  int main(int argv,char **argc){

        pid_t pid=atoi(argc[2]);
        int sig=atoi(argc[1]);

        kill(pid,sig);
        printf("发送信号成功\n");
        return 0;

        }

在这里插入图片描述

以上是发送信号但不携带消息的,还有可以发送信号时可以携带信息的一套函数
  #include 

       int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
       int sigqueue(pid_t pid, int sig, const union sigval value);
			union sigval {
               int   sival_int;
               void *sival_ptr;
           };

		 	struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
           };

①sigaction是对信号处理的函数

signum参数指出要捕获的信号类型
act参数指定新的信号处理方式
oldact参数输出先前信号的处理方式,如果不为NULL的话

②sigaction结构体参数

sa_handler :只带一个int类型的参数的回调函数,效果和signal函数一样
sa_sigaction: int是信号编号,siginfo_t是结构体指针,存放着发送过来的一些数据,void参数一般可用来判断结构体指针中是否为空
sa_mask:用来指定在信号处理函数执行期间需要被屏蔽的信号
sa_flags用于指定信号处理的行为
SA_RESTART:使被信号打断的系统调用自动重新发起。
SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。
sa_restorer:不用使用

③sigqueue:另一进程发送信息时用到的函数

pid为目标进程pid号,sig为要发送的信号编号,value包含了需要发送的内容的联合体

④sigval联合体参数:sival_int就是发送信号时附带的参数

这里举例说明

①接受端
#include 
#include 
#include 
#include 
#include 
#include 
void handler(int signum,siginfo_t *rec,void *text){
        printf("信号是%d\n",signum);
        if(text!=NULL){ // 判断内容是否为空
                printf("发送的内容是:%d\n",rec->si_int);
                printf("发送信息的pid号是:%d\n",rec->si_pid);
        }
}
int main(){
	    struct sigaction rec;
        printf("进程pid号是:%d\n",getpid());
        rec.sa_sigaction=handler;
        rec.sa_flags=SA_SIGINFO;
	    sigaction(SIGUSR1,&rec,NULL);
	    printf("recive ok\n");
        while(1);
        return 0;
}

②发送端
#include 
#include 
#include 
#include 
int main(int argc,char **argv){
        int signum=atoi(argv[1]);
        int pid=atoi(argv[2]);
        union sigval value;
        value.sival_int=100;
        sigqueue(pid,signum,value);
        printf("send success\n");
        return 0;
}

在这里插入图片描述

你可能感兴趣的:(LINUX,c语言,linux,vim,多进程)