IPC 进程间通信

声明: 本篇博客的学习途径主要为以下网站和课堂讲解,发博客目的仅为学习使用,在该博客的基础上做了一定程序的简略和修改。
参考博客 :
原文链接:
https://blog.csdn.net/a987073381/article/details/52006729
https://www.cnblogs.com/biyeymyhjob/archive/2012/11/03/2751593.html
https://blog.csdn.net/zzymusic/article/details/4815142
https://blog.csdn.net/danelumax2/article/details/18503791
https://www.cnblogs.com/leeming0222/articles/3994125.html
https://blog.csdn.net/qq_27664167/article/details/81712887
https://blog.csdn.net/u013485792/article/details/50764224
https://blog.csdn.net/Fly_as_tadpole/article/details/81044096
https://blog.csdn.net/enjoymyselflzz/article/details/81603577
https://blog.csdn.net/ljianhui/article/details/10243617
《深入浅出Linux工具与编程》
https://blog.csdn.net/ypt523/article/details/79958188
https://www.cnblogs.com/wuyepeng/p/9748889.html

管道

匿名管道(PIPE)

【管道特点】

  • 半双工,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
  • 只能用于父子进程或者兄弟进程之间( 具有亲缘关系的进程)
  • 管道只能在本地计算机中使用,而不可用于网络间的通信
  • 写入与读取的顺序原则是 先进先出

【管道的实现机制】:

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

在这里插入图片描述
在这里插入图片描述

从原理上,管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。
最开始的时候,上面的两个箭头都连接在同一个进程Process 1上(连接在Process 1上的两个箭头)。
当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。
随后,每个进程关闭自己不需要的一个连接 (两个黑色的箭头被关闭; Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了如上图的PIPE。

int pipe(int file_descriptor[2]) 创建匿名管道
#include<unistd.h>
#include <unistd.h> 
int pipe(int file_descriptor[2]);
//建立管道,该函数在数组上填上两个新的文件描述符后返回0,失败返回-1。

【读写规则】

  • 当没有数据可读时
    • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
    • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
  • 当管道满的时候
    • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
    • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE

  • 当要写入的数据量不大于PIPE_BUF(Posix.1要求PIPE_BUF至少 512字节)时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

【实例】

#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>

void sys_err(const char *str)
{
    perror(str); //用来将上一个函数发生错误的原因输出到标准设备(stderr)
    exit(1);
}

int main(void)
{
    pid_t pid;
    char buf[1024];
    int fd[2];
    char *p = "test for pipe\n";
 
    if (pipe(fd) == -1)  //建立管道  建立成功:0 / 建立失败-1
    {                    //规定:fd[0] → r; fd[1] → w
        sys_err("pipe");   
    }
    
    pid = fork();
    if (pid < 0)  //创建子进程失败
    {
        sys_err("fork err");
    } 
    else if (pid == 0) //子进程
    {
        close(fd[1]);   //子进程关闭管道写端
        int len = read(fd[0], buf, sizeof(buf)); //子进程将管道中的数据读出
        write(STDOUT_FILENO, buf, len); //STDOUT_FILENO:向屏幕输出
        close(fd[0]);
    } 
    else  //父进程自己
    {
        close(fd[0]); //父进程关闭管道读端
        write(fd[1], p, strlen(p)); //父进程可以向管道中写入数据
        wait(NULL); //父进程等待自己的子进程(回收自己的子进程资源包括僵尸进程)
        close(fd[1]);
    }
    
    return 0;
}

在这里插入图片描述


【写法2】

int main(void)
{
    int n;
    int fd[2];
    pid_t pid;
    char line[MAXLINE];
   
    if(pipe(fd)  0){                 /* 先建立管道得到一对文件描述符 */
        exit(0);
    }

    if((pid = fork())  0)            /* 父进程把文件描述符复制给子进程 */
        exit(1);
    else if(pid > 0){                /* 父进程写 */
        close(fd[0]);                /* 关闭读描述符 */
        write(fd[1], "\nhello world\n", 14);
    }
    else{                            /* 子进程读 */
        close(fd[1]);                /* 关闭写端 */
        n = read(fd[0], line, MAXLINE);
        write(STDOUT_FILENO, line, n);
    }

    exit(0);
}

命名管道(FIFO)

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

int mkfifo(const char *filename,mode_t mode); 创建有名管道
#include<sys/types.h>
#include<sys/state.h>
#include <sys/types.h> 
#include <sys/stat.h> 
int mkfifo(const char *filename,mode_t mode); 

【实例】

#include <stdio.h>  
#include <stdlib.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
      
int main()  
{  
    int res = mkfifo("/tmp/my_fifo", 0777);  
    if (res == 0)  
    {  
        printf("FIFO created/n");  
    }  
     exit(EXIT_SUCCESS);  
}

编译这个程序:

gcc –o fifo1.c fifo

运行这个程序:

$ ./fifo1

用ls命令查看所创建的管道

$ ls -lF /tmp/my_fifo
prwxr-xr-x 1 root root 0 05-08 20:10 /tmp/my_fifo|

【注意】:
ls命令的输出结果中的第一个字符为p,表示这是一个管道。
最后的|符号是由ls命令的-F选项添加的,它也表示是这是一个管道。


信号(Signal):是进程间通信机制中唯一的异步通信机制

信号机制是unix系统中最为古老的进程之间的通信机制,用于一个或几个进程之间传递异步信号
信号可以有各种异步事件产生,比如键盘中断等。
shell也可以使用信号将作业控制命令传递给它的子进程。

Linux除了支持Unix早期信号
语义函数sigal外
还支持语义符合Posix.1标准的信号函数sigaction(实际上,用sigaction函数重新实现了signal函数,实现可靠信号机制,又
能够统一对外接口)

【信号种类】signum
在这里插入图片描述
【信号与中断】
就好像每个中断都有一个中断服务例程一样。大多数信号的默认操作是结束接收信号的进程
但是,信号和中断有所不同。

  • 中断的响应和处理都发生在内核空间
  • 信号的响应发生在内核空间,信号处理程序的执行却发生在用户空间

信号是进程间通信机制中唯一的异步通信机制


int sigaction() 查询或修改信号的处理方式

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

【解释】

  • signum:要操作的信号。(信号符号名)
  • act:要设置的对信号的新处理方式
  • oldact:原来对信号的处理方式

【返回值】:
0 表示成功,-1 表示有错误发生。


【涉及到的结构体】

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

【解释】

  • sa_handler 代表新的信号处理函数
  • sa_sigaction 则是另一个信号处理函数
  • sa_flags 成员的值
    • 包含了 SA_SIGINFO 标志时,系统将使用 sa_sigaction 函数作为信号处理函数
    • 否则使用 sa_handler 作为信号处理
  • sa_mask 指定在信号处理函数执行期间需要被屏蔽的信号
  • sa_flags 成员用于指定信号处理的行为,具体如表
sa_flag作用
SA_RESTART使被信号打断的系统调用自动重新发起。
SA_NOCLDSTOP使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
SA_NOCLDWAIT使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
SA_NODEFER使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
SA_RESETHAND信号处理之后重新设置为默认的处理方式。
SA_SIGINFO使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
  • re_restorer 成员则是一个已经废弃的数据域

【实例】

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

static void sig_usr(int signum)
{
    if(signum == SIGUSR1) 
    {
        printf("SIGUSR1 received\n");
    }
    else if(signum == SIGUSR2)
    {
        printf("SIGUSR2 received\n");
    }
    else
    {
        printf("signal %d received\n", signum);
    }
}
 
int main(void)
{
    char buf[512];
    int  n;
    struct sigaction sa_usr;
    sa_usr.sa_flags = 0;
    sa_usr.sa_handler = sig_usr;   //信号处理函数
    
    sigaction(SIGUSR1, &sa_usr, NULL); //为SIGUSR1 信号注册了处理函数
    sigaction(SIGUSR2, &sa_usr, NULL); //为SIGUSR2 信号注册了处理函数
    
    printf("My PID is %d\n", getpid());
    
    while(1)
    {
        if((n = read(STDIN_FILENO, buf, 511)) == -1) 
        //接收键盘的输入 0:数据读完了 / -1 读过程中遇见了中断
        {
            //errno 是记录系统的最后一次错误代码
            if(errno == EINTR)  // 错误类型为 中断错误EINTR
            {
                printf("read is interrupted by signal\n"); //读取过程被信号量打断
            }
        }
        else
        {
            buf[n] = '\0';
            printf("%d bytes read: %s\n", n, buf);
        }
    }
    
    return 0;
}

这时如果从另外一个终端向进程发送 SIGUSR1 或 SIGUSR2 信号,用类似如下的命令:

kill -USR1 5904

则程序将继续输出如下内容:

SIGUSR1 received
read is interrupted by signal

这说明用 sigaction 注册信号处理函数时,不会自动重新发起被信号打断的系统调用。
如果需要自动重新发起,则要设置 SA_RESTART 标志,
比如在上述例程中可以进行类似一下的设置:sa_usr.sa_flags = SA_RESTART;

【补充】EINTR错误的产生:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。


int kill() 向任何进程组或进程发送信号。

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int sig); 
//kill函数向进程号为pid的进程发送信号,信号值为sig。
//当pid为0时,向当前系统的所有进程发送信号sig。

【参数】

  • pid:可能选择有以下四种
    1. pid大于零时,pid是信号欲送往的进程的标识。
    2. pid等于零时,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
    3. pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。
    4. pid小于-1时,信号将送往以-pid为组标识的进程。
  • sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在执行

【返回值】

  • 成功执行时,返回0。
  • 失败返回-1
  • errno被设为以下的某个值
    • EINVAL:指定的信号码无效(参数 sig 不合法)
    • EPERM;权限不够无法传送信号给指定进程
    • ESRCH:参数 pid 所指定的进程或进程组不存在

【实例】

#include <sys/wait.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

int main( void )
{
    pid_t childpid;
    int status;
    int retval;

    childpid = fork();
    if ( -1 == childpid ) //创建子进程失败
    {
        perror( "fork()" );
        exit( EXIT_FAILURE );
    }
    else if ( 0 == childpid )  //子进程
    {
        puts( "In child process" );
        sleep( 100 );//让子进程睡眠,看看父进程的行为
        exit(EXIT_SUCCESS);
    }
    else    //父进程
    {
        if ( 0 == (waitpid( childpid, &status, WNOHANG ))) //waitpid会暂时停止目前进程的执行,等待子进程pid
        {
            retval = kill( childpid,SIGKILL );//父进程给子进程发送杀死子进程的信号

            if ( retval ) //信号发送失败 retval==-1
            {
                puts( "kill failed." );
                perror( "kill" );
                waitpid( childpid, &status, 0 ); 
                //父进程第二次调用waitpid,
                //保证他在子进程退出后再停止执行。
            }
            else //信号发送成功
            {
                printf( "%d killed\n", childpid );
            }

        }
    }

    exit(EXIT_SUCCESS);  
}
成功:
[root@localhost src]# gcc kill1.c
[root@localhost src]# ./a.out
In child process
4545 killed
失败:
[root@localhost src]# gcc kill2.c
[root@localhost src]# ./a.out
kill failed
kill:Succes

【解释】
在确信fork调用成功后
子进程睡眠100秒,然后退出。
同时父进程在子进程上调用waitpid函数,但使用了WNOHANG选项,
所以调用waitpid后立即返回。
父进程接着杀死子进程,如果kill执行成功返回0,失败返回-1
if(-1)程序继续运行,父进程第二次调用waitpid,
保证他在子进程退出后再停止执行。父进程显示一条成功消息后退出。


【问题】 失败的时候kill:Succes哪来的

答:perror()函数,把一个描述性错误消息输出到标准错误 stderr。
首先输出字符串 str,后跟一个冒号,然后是一个空格。

void perror(const char *str) 

【问题】为什么描述性错误信息是Succes
子进程休眠后返回的是SUCCESS

exit(EXIT_SUCCESS);

【问题】GDB调试过程中报错“linux gdb 没有符号表被读取。请使用 “file” 命令。”为什么?

其原因是生成的二进制可执行文件没有使用-g选项。

gcc中-g选项是为了获得有关调试信息,要用gdb进行调试,必须使用-g生成二进制可执行文件,

1.删除该程序原有的可执行文件
2.gcc -g kill1.c -o kill1.out

【问题】GDB常用调试过程
l:list 查看源文件
break+number :打断点
r:run 运行到断点处
n:next 单步调试

[root@localhost src]#gdb kill1.out
(gdb)l
(gdb)l
(gdb)break 27
(gdb)r
(gdb)n

waitpid()和wait()函数

父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:

1) 阻塞等待子进程退出

2) 回收子进程残留资源

3) 获取子进程结束状态(退出原因)
在这里插入图片描述
在这里插入图片描述

/* waitpid.c */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    pid_t pc, pr;

    pc = fork();
    if( pc < 0 )
    {
            printf("Error fork\n");
            exit(1);
    }
    else if( pc == 0 )    /* 子进程 */
    {
        /* 子进程暂停5s */
        sleep(5);
        /* 子进程正常退出 */
        exit(0);
    }
    else    /* 父进程 */
    {
        /* 循环测试子进程是否退出 */
        do
        {
            /* 调用waitpid,且父进程不阻塞 */
            pr = waitpid(pc, NULL, WNOHANG);

            /* 若子进程还未退出,则父进程暂停1s */
            if( pr == 0 )
            {
                printf("The child process has not exited\n");
                sleep(1);
            }
        }while( pr == 0 );

        /* 若发现子进程退出,打印出相应情况 */
        if( pr == pc )
        {
            printf("Get child exit code: %d\n",pr);
        }
        else
        {
            printf("Some error occured.\n");
        }
    }
}

信号通信其余常用函数

int raise(int sig);
//向当前进程中自举一个信号sig, 即向当前进程发送信号。

#include <unistd.h> 
unsigned int alarm(unsigned int seconds); //alarm()用来设置信号SIGALRM在经过参数seconds指定的秒数后传送给目前的进程。
//如果参数seconds为0,则之前设置的闹钟会被取消,并将剩下的时间返回
//使用alarm函数的时候要注意alarm函数的覆盖性,即在一个进程中采用一次alarm函数则该进程之前的alarm函数将失效。


int pause(void); //使调用进程(或线程)睡眠状态,直到接收到信号,要么终止,或导致它调用一个信号捕获函数。 

消息队列 Messge Queue

与命名管道相比,消息队列的优势在于

  1. 消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难
  2. 同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。
  3. 接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。

【实例】下面为一个简单的程序,一个service和一个client,service往消息队列里写数据,client从消息队列里读数据,当service输入QUIT时删除消息队列,并且俩程序都退出。

//service.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/msg.h>
#include<sys/ipc.h>
struct mymesg{  //消息数据结构
	long int mtype;
	char mtext[512];
};
int main()
{
	int id = 0;
	struct mymesg ckxmsg;   //消息数据实例化 ckxmsg
	key_t key = ftok("/tmp",66);   //生辰键值对:指定一个共享ID值key_t
	id = msgget(key,IPC_CREAT | 0666); //创建消息队列 设置当前用户,组用户,其他用户权限
	if(id == -1)   //创建队列失败
	{
		printf("create msg error \n");
		return 0;
	}
	while(1)  //创建队列成功
	{
		char msg[512];
		memset(msg,0,sizeof(msg));
        
		ckxmsg.mtype = 1; //消息数据的消息类型 应该为正数,为什么填1?
		printf("input message:");
		fgets(msg,sizeof(msg),stdin); //fgets从输入流stdin中读取数据到msg
		strcpy(ckxmsg.mtext,msg);   //msg的内容赋给消息数据的内容
 
		if(msgsnd(id,(void *)&ckxmsg,512,0) < 0) //发送数据,并判断是否发送成功
		{
			printf("send msg error \n");   //发送失败
			return 0;
		}
 
		if(strncmp(msg,"QUIT",4) == 0) //输入了QUIT,退出
			break;
	}
	if(msgctl(id,IPC_RMID,NULL) < 0) //删除消息队列,并判断是否操作成功
	{
		printf("del msg error \n");
		return 0;
	}
	return 0;
}

//Msg-client.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/msg.h>
#include<sys/ipc.h>
struct mymesg{  //消息数据结构
	long int mtype;
	char mtext[512];
};
int main()
{
	int id = 0;
	struct mymesg ckxmsg;   //消息数据实例化 ckxmsg
	key_t key = ftok("/tmp",66);    //生辰键值对:指定一个共享ID值key_t
	id = msgget(key,0666|IPC_CREAT);//创建消息队列 设置当前用户,组用户,其他用户权限
	if(id == -1)     //创建队列失败
	{
		printf("open msg error \n");
		return 0;
	}
	while(1)
	{
		if(msgrcv(id,(void *)&ckxmsg,512,1,0) < 0) //接受数据,并判断是否接受成功
		{
			printf("receive msg error \n");
			return 0;
		}
		printf("data:%s\n",ckxmsg.mtext);   //打印接收的数据
		if(strncmp(ckxmsg.mtext,"QUIT",4) ==0) //输入了QUIT,退出
			break;
	}
	return 0;
}
[root@localhost src]# ./msg_service
input message:hello
input message:hi
input message:QUIT
[root@localhost src]# ./msg_service
[root@localhost src]# ./msg_client
data:hello

data:hi

data:QUIT
[root@localhost src]# 

【问题】0666 | IPC_CREAT是什么意思
答:在创建或者消息队列或者共享内存的时候,会用到这个原语
如果:0666
从左向右:

  • 第一位:表示这是个八进制数 000
  • 第二位:当前用户的经权限:6=110(二进制),每一位分别对就 可读,可写,可执行,6说明当前用户可读可写不可执行
  • 第三位:group组用户,6的意义同上
  • 第四位:其它用户,每一位的意义同上,0表示不可读不可写也不可执行

【问题】结构体mymesg内的mtype属性。
long int mtype; //类,消息队列可以控制读取相应类型的数据,都有什么类型,mtype值不一样有什么区别?


ftok()函数生成键值

系统建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。

ftok函数是根据pathname和projid来创建一个关键字,此关键字在创建信号量,创建消息队列的时候都需要使用。

每一个消息队列都有一个对应的键值(key)相关联(共享内存、信号量也同样需要)。

#include<sys/ipc.h>
key_t ftok(const char *pathname ,int projid);

【参数】

  • path为一个已存在的路径名
  • id为0~255之间的一个数值,代表项目ID,自己取

【返回值】:

  • 成功返回键值(相当于32位的int)。
  • 出错返回-1

【举例】

key_t key = ftok(/tmp”, 66);

共享内存,消息队列,信号量它们三个都是找一个中间介质,来进行通信的,这种介质多的是。
就是怎么区分出来,就像唯一一个身份证来区分人一样。你随便来一个就行,就是因为这。只要唯一就行,就想起来了文件的设备编号和节点,它是唯一的,但是直接用它来作识别好像不太好,不过可以用它来产生一个号。ftok()就出场了

【问题】pathname是目录还是文件的具体路径,是否可以随便设置
答:ftok根据路径名,提取文件信息,再根据这些文件信息及project ID合成key,该路径可以随便设置。

【问题】pathname指定的目录或文件的权限是否有要求
答:该路径是必须存在的,ftok只是根据文件inode在系统内的唯一性来取一个数值,和文件的权限无关。

【问题】proj_id是否可以随便设定,有什么限制条件
答:proj_id是可以根据自己的约定,随意设置。这个数字,有的称之为project ID; 在UNIX系统上,它的取值是1到255;


msgget():用来创建和访问一个消息队列

#include<sys/msg.h>
int msgget(key_t key,int msgflg)

【参数】

  • key为ftok生成的键值
  • flag为所需要的操作和权限,可以用来控制创建一个消息队列。
    • flag的值为IPC_CREAT
      • 如果不存在key值的消息队列,且权限不为0,则创建消息队列,并返回一个消息队列ID。
      • 如果存在,则直接返回消息队列ID。
    • flag的值为 IPC_CREAT | IPC_EXCL
      • 如果不存在key值的消息队列,且权限不为0,则创建消息队列,并返回一个消息队列ID。
      • 如果存在,则产生错误。

【返回值】

  • 成功返回消息队列ID;
  • 出错返回-1

【举例】

int id = msgget(key,IPC_CREAT|IPC_EXCL|0666);

创建一个权限为0666(所有用户可读可写,具体查询linux权限相关内容)的消息队列,并返回一个整形消息队列ID,如果key值已经存在有消息队列了,则出错返回-1。

int id = msgget(key,IPC_CREAT|0666);

创建一个权限为0666(所有用户可读可写,具体查询linux权限相关内容)的消息队列,并返回一个消息队列ID,如果key值已经存在有消息队列了,则直接返回一个消息队列ID。


msgsnd():可以通过msqid对指定消息队列进行接收操作

#include<sys/msg.h>
int msgsnd(int msgid,const void *msgp,size_t msgsz,int msgflg)

【参数】

  • msgid:为msgget返回的消息队列ID值
  • ptr:为消息结构体mymesg指针
  • nbytes:为消息结构体mymesg里的字符数组mtext大小,sizeof(mtext)
  • flag:值可以为0、IPC_NOWAIT
    • 为0时,当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列或者消息队列被删除。
    • 为IPC_NOWAIT时,当消息队列满了,msgsnd函数将不会等待,会立即出错返回EAGAIN

【返回值】

  • 成功返回0;
  • 错误返回-1

【举例】

msgsnd(id,(void *)&ckxmsg,512,0);

msgrcv():允许我们把一条消息添加到消息队列中

#include<sys/msg.h>
int msgrcv(int msgid,void *msgp,size_t msgsz,long int msgtyp,int msgflg)

【参数】

  • msgid:为msgget返回的消息队列ID值
  • ptr:为消息结构体mymesg指针
  • nbytes:为消息结构体mymesg里的字符数组mtext大小,sizeof(mtext)
  • type:在结构体mymesg里我们定义了一个long int mtype,用于分别消息的类型
    • type ==0 返回队列中的第一个消息
    • type > 0 返回队列中消息类型为type的第一个消息
    • type < 0 返回队列中消息类型值小于等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息
  • flag:可以为0、IPC_NOWAIT、IPC_EXCEPT
    • 为0时,阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
    • 为IPC_NOWAIT时,如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
    • 为IPC_EXCEPT时,与msgtype配合使用返回队列中第一个类型不为msgtype的消息

【返回值】

  • 成功返回消息数据部分的长度;
  • 错误返回-1

【举例】

msgrcv(id,(void *)&ckxmsg,512,1,0);

srtuct msgseg/msgbuf{}消息缓冲区结构
struct msgseg{
    long mtype;
    char mtext[size_t];//柔性数组
}

在结构中有两个成员:

  • mtype为消息类型,用户可以给某个消息设定一个类型,可以在消息队列中正确地发送和接受自己的消息。必须为正数

  • mtext为消息数据,传递的数据放这里面,采用柔性数组,用户可以重新定义msgbuf结构。


msgctl():主要是一些控制操作如删除消息队列等操作

简单的操作就是删除消息队列了,也可以获取和改变消息队列的状态

#include<sys/msg.h>
int msgctl(int msgqid,int cmd,struct msgid_ds *buf)

【参数】

  • msgid就是msgget函数返回的消息队列ID
  • cmd有三个,
    • IPC_RMID:删除消息队列;
    • IPC_STAT:取此队列的msqid_ds结构,并将它存放在buf指向的结构中;
    • IPC_SET:改变消息队列的状态,把buf所指的msqid_ds结构中的uid、gid、mode复制到消息队列的msqid_ds结构内。(内核为每个消息队列维护着一个结构,结构名为msqid_ds,里面存放着消息队列的大小,pid,存放时间等一些参数)
  • buf就是结构体msqid_ds

【返回值】:

  • 成功返回0;
  • 错误返回-1

【例如】:

msgctl(id,IPC_RMID,NULL);删除id号的消息队列

struct msgqid_ds{}系统内核用来保存消息队列对象有关数据

内核为每个IPC对象维护了一个数据结构struct ipc_perm,用于标识消息队列,让进程知道当前操作的是哪个消息队列。
每一个msqid_ds表示一个消息队列,并通过msqid_ds.msg_first、msg_last维护一个先进先出的msg链表队列,当发送一个消息到该消息队列时,把发送的消息构造成一个msg的结构对象,并添加到msqid_ds.msg_first、msg_last维护的链表队列。
在内核中的表示如下:
在这里插入图片描述

struct msqid_ds
  {
    struct msqid_ds {
    struct ipc_perm msg_perm;
    struct msg *msg_first;      /* first message on queue,unused  */
    struct msg *msg_last;       /* last message in queue,unused */
    __kernel_time_t msg_stime;  /* last msgsnd time */
    __kernel_time_t msg_rtime;  /* last msgrcv time */
    __kernel_time_t msg_ctime;  /* last change time */
    unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */
    unsigned long  msg_lqbytes; /* ditto */
    unsigned short msg_cbytes;  /* current number of bytes on queue */
    unsigned short msg_qnum;    /* number of messages in queue */
    unsigned short msg_qbytes;  /* max number of bytes on queue */
    __kernel_ipc_pid_t msg_lspid;   /* pid of last msgsnd */
    __kernel_ipc_pid_t msg_lrpid;   /* last receive pid */
};

msg_perm成员保存了消息队列的存取权限以及其他一些信息(见上面关于ipc_perm
结构的介绍)。
msg_first成员指针保存了消息队列(链表)中第一个成员的地址。
msg_last成员指针保存了消息队列中最后一个成员的地址。
msg_stime 成员保存了最近一次队列接受消息的时间。
msg_rtime成员保存了最近一次从队列中取出消息的时间。
msg_ctime 成员保存了最近一次队列发生改动的时间。
wwait 和rwait 是指向系统内部等待队列的指针。
msg_cbytes 成员保存着队列总共占用内存的字节数。
msg_qnum 成员保存着队列里保存的消息数目。
msg_qbytes 成员保存着队列所占用内存的最大字节数。
msg_lspid成员保存着最近一次向队列发送消息的进程的pid。
msg_lrpid 成员保存着最近一次从队列中取出消息的进程的pid。


信号量(Semaphore)

信号量是一种计数器,用于控制对多个进程共享的资源进行的访问
它们常常被用作一个锁机制,在某个进程正在对特定的资源进行操作时,信号量可以防止另一个进程去访问它。
信号量是特殊的变量,它只取正整数值并且只允许对这个值进行两种操作:
等待(wait)和信号(signal)[P、V操作,P用于等待,V用于信号]

  • p(sv):如果sv的值大于0,就给它减1;如果它的值等于0,就挂起该进程的执行
  • V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有其他进程因等待sv而挂起,则给它加1

简单理解就是P相当于申请资源,V相当于释放资源


内核为每个信号量集合都维护一个semid_ds结构:

struct semid_ds{
    struct ipc_perm sem_perm;
    unsigned short sem_nsems;
    time_t sem_otime;
    time_t sem_ctime;
    ...
}

信号量数据结构:

union semun{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *__buf;
}

信号量操作sembuf结构:

struct sembuf{
    ushort sem_num;//信号量的编号
    short sem_op;//信号量的操作。如果为正,则从信号量中加上一个值,如果为负,则从信号量中减掉一个值,如果为0,则将进程设置为睡眠状态,直到信号量的值为0为止。
    short sem_flg;//信号的操作标志,一般为IPC_NOWAIT。
}

semget():函数用于创建一个新的信号量集合 , 或者访问一个现有的集合

在这里插入图片描述

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semget(key_t key,int nsems,int semflg)

【参数】

  • key_t key:信号量的键值,多个进程可以通过它访问同一个信号量
  • int nsems:需要创建的信号量数目,通常取值为1
  • int semflg:
    • 当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。
    • 而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
      【返回值】
  • 成功:0
  • 出错:-1

创 建 或 打 开 信 号 量 创建或打开信号量

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<sys/ipc.h>
  4 #include<sys/sem.h>
  5 int main()
  6 {
  7     int id=semget(123,1,IPC_CREAT|0644);
  8     if(id==-1)
  9     {
 10         perror("id");
 11         exit(1);
 12     }
 13     printf("semget success!\n");                                            
}

在这里插入图片描述


semctl():用于信号量集合执行控制操作

在这里插入图片描述

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semctl(int semid,int semnum,int cmd,union semun arg)

【参数】

  • int semid: semget函数返回的信号量标识符
  • int semnum:信号量编号,通常取值0,使用信号量集合时会被用到,指信号量集中的第几个信号量
  • int cmd:命令 ,指定对信号量的操作
    • SETVAL设初值,用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
    • IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
union semun{}
union semun{
    int val; //设定信号量的值
    struct semid_ds *buf;
    unsigned short *arry;
}

设置信号量初值

设 置 信 号 量 初 值 : 设置信号量初值:

  1 #include<stdio.h>
  2 #include<sys/ipc.h>
  3 #include<sys/sem.h>
  4 #include<stdlib.h>
  5 union semun{
  6     int val;
  7 };
  8 int main()
  9 {
 10     int id=semget(123,0,0);
 11     if(id==-1)
 12     {
 13         perror("id");
 14         exit(1);
 15     }
 16     union semun su;
 17     su.val=5;
 18     semctl(id,0,SETVAL,su); //模式:赋值,把su的值赋给semid为id的信号量                                                
}

查看信号量的值:

查 看 信 号 量 的 值 : 查看信号量的值:

  1 #include<stdio.h>
  2 #include<sys/ipc.h>
  3 #include<sys/sem.h>
  4 #include<stdlib.h>
  5 int main()
  6 {
  7     int id=semget(123,0,0);
  8     if(id==-1)
  9     {
 10         perror("id");
 11         exit(1);
 12     }
 13     int val=semctl(id,0,GETVAL,0); //模式为查看信号量值
 14     printf("val=%d",val);                                                   
 15 
 16 }

semnum:信号量集中的第几个信号量
cmd:命令,这里为GETVAL
0:无需再重复定义联合体

【返回值】为当前信号量的值


semop():用于改变信号量的值。

在这里插入图片描述

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semop(int semid,sturct sembuf *sops,size_t nsops)

【参数】

  • int semid:semget()函数返回的信号量标识符
  • struct sembuf* sops :指向信号量操作数组
  • size_t nsops:操作数组sops中的操作个数(数量)
struct sembuf{ }
struct sembuf{
    short sem_num;//除非使用一组信号量,否则它为0
    short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
                    //一个是+1,即V(发送信号)操作。
    short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
                    //并在进程没有释放该信号量而终止时,操作系统释放信号量
};

【参数】

  • short sem_num, //信号量的下标
  • short sem_op, // 1表示V操作,-1表示P操作
  • short sem_flg //一般填0,表示如果信号量为0就阻塞

P操作

【实例】

/***对信号量数组semnum编号的信号量做P操作***/
int P(int semid, int semnum)
{
	struct sembuf sops={semnum,-1, SEM_UNDO};
	return (semop(semid,&sops,1));
}

V操作
/***对信号量数组semnum编号的信号量做V操作***/

int V(int semid, int semnum)
{
	struct sembuf sops={semnum,+1, SEM_UNDO};
	return (semop(semid,&sops,1));
}

共享内存 : 高效、没有提供同步的机制

共享内存,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。
【提示】共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,
所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量

共享内存通信原理

在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过**内存管理单元(MMU)**进行管理。
两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。

在这里插入图片描述
对于一个共享内存,实现采用的是引用计数的原理,当进程脱离共享存储区后,计数器减一,挂架成功时,计数器加一,只有当计数器变为零时,才能被删除。
当进程终止时,它所附加的共享存储区都会自动脱离。


常 用 L i n u x 命 令 常用Linux命令 Linux
1.查看系统中的共享存储段

ipcs -m

2.删除系统中的共享存储段

ipcrm -m [shmid]

Shm-data.h

#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
#define TEXT_SZ 2048
struct shared_use_st
{  
    int written;//作为一个标志,非0:表示可读,0表示可写 
    char text[TEXT_SZ];//记录写入和读取的文本
};
#endif

Shm-write.c

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>
#include "shmdata.h"
int main()
{  
    int running = 1;   
    void *shm = NULL;  
    struct shared_use_st *shared = NULL;
    char buffer[BUFSIZ + 1];//用于保存输入的文本
    int shmid;  //创建共享内存
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if(shmid == -1)
    {  
        fprintf(stderr, "shmget failed\n");
        exit(EXIT_FAILURE);
    }   
    shm = shmat(shmid, (void*)0, 0);  //将共享内存连接到当前进程的地址空间
    if(shm == (void*)-1)
    {  
        fprintf(stderr, "shmat failed\n");     
        exit(EXIT_FAILURE);
    }  
    printf("Memory attached at %X\n", (int)shm);    //设置共享内存   
    shared = (struct shared_use_st*)shm;   
    while(running)//向共享内存中写数据  
    {  //数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本       
        while(shared->written == 1)     
        {          
            sleep(1);      
            printf("Waiting...\n");
        }       //向共享内存中写入数据       
        printf("Enter some text: ");       
        fgets(buffer, BUFSIZ, stdin);      
        strncpy(shared->text, buffer, TEXT_SZ);      //写完数据,设置written使共享内存段可读       
        shared->written = 1;     //输入了end,退出循环(程序)  
        if(strncmp(buffer, "end", 3) == 0)         
            running = 0;   
    }   //把共享内存从当前进程中分离
    if(shmdt(shm) == -1)   
    {      
        fprintf(stderr, "shmdt failed\n");     
        exit(EXIT_FAILURE);
    }  
    sleep(2);  
    exit(EXIT_SUCCESS);
}

Shm-read.c

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include "shmdata.h"
int main()
{  
    int running = 1;//程序是否继续运行的标志  
    void *shm = NULL;//分配的共享内存的原始首地址   
    struct shared_use_st *shared;//指向shm   
    int shmid;//共享内存标识符 //创建共享内存   
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if(shmid == -1)
    {      
        fprintf(stderr, "shmget failed\n");
        exit(EXIT_FAILURE);
    }   //将共享内存连接到当前进程的地址空间
    shm = shmat(shmid, 0, 0);
    if(shm == (void*)-1)   
    {  
        fprintf(stderr, "shmat failed\n"); 
        exit(EXIT_FAILURE);
    }  
    printf("\nMemory attached at %X\n", (int)shm);  //设置共享内存   
    shared = (struct shared_use_st*)shm;   
    shared->written = 0;
    while(running)//读取共享内存中的数据 
    {       //没有进程向共享内存定数据有数据可读取       
        if(shared->written != 0)
        {      
            printf("You wrote: %s", shared->text);      
            sleep(rand() % 3);          //读取完数据,设置written使共享内存段可写
            shared->written = 0;         //输入了end,退出循环(程序)  
            if(strncmp(shared->text, "end", 3) == 0)    
                running = 0;       
        }      
        else//有其他进程在写数据,不能读取数据     
            sleep(1);  
    }   //把共享内存从当前进程中分离
    if(shmdt(shm) == -1)   
    {      
        fprintf(stderr, "shmdt failed\n");     
        exit(EXIT_FAILURE);
    }   //删除共享内存   
    if(shmctl(shmid, IPC_RMID, 0) == -1)   
    {  
        fprintf(stderr, "shmctl(IPC_RMID) failed\n");  
        exit(EXIT_FAILURE);
    }  
    exit(EXIT_SUCCESS);
}
[root@localhost src]# ./write
Memory attached at 28125000
Enter some text:001
Waiting...
Waiting...
Enter some text:
[root@localhost src]# ./read
Memory attached at 19912000
You wrote:001

shmget ( ):创建共享内存

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

【参数】

  • [参数key]:由ftok生成的key标识,标识系统的唯一IPC资源
  • [参数size]:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍。
  • [参数shmflg]:如果要创建新的共享内存,需要使用IPC_CREAT,IPC_EXCL,如果是已经存在的,可以使用IPC_CREAT或直接传0。

【返回值】

  • 成功时返回一个新建或已经存在的的共享内存标识符,取决于shmflg的参数。
  • 失败返回-1并设置错误码。

shmat ( ):挂接共享内存

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);

【参数】

  • [参数shmid]:共享存储段的标识符。
  • [参数*shmaddr]:shmaddr = 0,则存储段连接到由内核选择的第一个可以地址上(推荐使用)。
  • [参数shmflg]:若指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。

【返回值】

  • 成功返回共享存储段的指针(虚拟地址),并且内核将使其与该共享存储段相关的shmid_ds结构中的shm_nattch计数器加1(类似于引用计数);
  • 出错返回-1。

shmdt ( ):去关联共享内存

当一个进程不需要共享内存的时候,就需要去关联。该函数并不删除所指定的共享内存区,而是将之前用shmat函数连接好的共享内存区脱离目前的进程。

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
int shmdt(const void *shmaddr);

【参数】

  • [参数*shmaddr]:连接以后返回的地址。

【返回值】

  • 成功返回0,并将shmid_ds结构体中的 shm_nattch计数器减1;
  • 出错返回-1。

shmctl ( ):销毁共享内存

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

【参数】

  • [参数shmid]:共享存储段标识符。
  • [参数cmd]:指定的执行操作,设置为IPC_RMID时表示可以删除共享内存。
  • [参数*buf]:设置为NULL即可。

【返回值】

  • 成功返回0
  • 失败返回-1。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值