首页 > 系统相关 >Linux - 内存间通信

Linux - 内存间通信

时间:2023-11-20 20:22:43浏览次数:39  
标签:信号量 int send 间通信 内存 Linux 进程 sem include

进程间通信

 

Linux下进程通信的方式有

  1. 管道:  管道用于有亲缘关系的进程间通信
  2. 有名管道:除了管道特性外还能在独立进程间进行通信
  3. 信号:  用于通知进程有某种事件发生
  4. 消息队列:用于进程间较多数据的通信,有读写权限的进程可以向队列中添加消息。只有读权限则只能读取队列中消息
  5. 共享内存:多个进程访问同一块内存空间,克服其他通信机制运行效率慢而设计的,一般和其他通信机制一起使用,如和信号量结合使用达到进程间同步或互斥
  6. 信号量:   用于进程间/同一进程不同线程之间同步
  7. 套接口:   不同机器之间的进程通信

 

什么是流控制?
  流控制:发送机与接受机数据传输速率不同导致接收缓冲区满造成后续数据丢失,这时就需要控制数据流。

 

管道

  无名管道:父子/兄弟进程间的通信
  有名管道:任意进程之间的通信,具有文件名和磁盘节点,位于文件系统,读写的内部实现和普通文件不同,而是和无名管道一样采用字节流的方式
  标准流管道:获取指令运行结果集(不太了解)

管道特点
  半双工,双向通信需要建立两个管道
  单独构成文件系统,对于管道两端的进程而言,管道就是一个文件。但它是一个特殊的文件,不属于任何文件系统,只存在于内存中。


管道通信的方法

无名管道


操作流程:
  1.pipe  -2.read(close)  -3close

1创建管道
int pipe(int fd[2])

  1 #include <unistd.h>
  2 int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1
  f[0]:用于管道read
  f[1]:用于管道write

返回值
0:成功
-1:失败

2管道读写
  读管道时:管道空则堵塞,直到另一端write写入数据,如果写端已经关闭则返回0
  写管道时:管道满则堵塞,直到另一端read取走数据,如果读端已关闭则写端返回21

3.管道关闭
close函数关闭管道
  创建管道时,写端关闭f[0],读端关闭f[1]
  关闭管道时,其它没关闭的描述符都需要关闭

使用注意:
管道是半双工,进程如果要相互传送数据需要开辟两条管道
pipe需要在fork()之前执行,子进程才能继承


实例代码

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(void)
{
    char buf[32] = { 0 };
    pid_t pid;          // 定义一个变量来保存文件描述符,管道也是一种特殊的文件
        // 因为一个读端, 一个写端, 所以数量为 2 个文件描述符
        int fd[2];
    // 创建无名管道
    pipe(fd);
    printf("fd[0] is %d\n", fd[0]);
    printf("fd[2] is %d\n", fd[1]);
    // 创建子进程
    pid = fork();
    if (pid < 0)
    {
        printf("error\n");
    }
    if (pid > 0)
    {
        int status;
        close(fd[0]);   //父进程关闭管道读端read
        write(fd[1], "hello", 5);
        close(fd[1]);
        wait(&status);  //等待子进程关闭,该过程会被阻塞
        exit(0);
    }
    if (pid == 0)
    {
        close(fd[1]);   //子进程关闭管道写端write
        read(fd[0], buf, 32);   //Linux中的文件操作,后面文件操作的笔记中会详细介绍
        //从fd[0](pipe)中读取32个字节数据到buf中,如果可供读取的字节数没有32个,返回的值将小于32
        printf("buf is %s\n", buf);
        close(fd[0]);
        exit(0);    //关闭当前进程并等待返回0
    }
    return 0;
}

 

有名管道

用在任意进程之间的通信,以文件的方式处理。

缺陷:没有解决半双工通信的缺点
通过管道通信:
读数据端以只读方式打开管道文件
写数据端以只写方式打开管道文件

fifo文件和普通文件区别
普通文件无法实现字节流方式管理,多进程访问共享资源会出现问题,fifo文件采用字节流管理

操作过程
1.mkfifo-2.open-3.read(write)-4.close-5.unlink(清除硬链接)

1创建有名管道:以文件流的形式操作


mkfifo()

    1. 函数功能:创建有名管道。
    2. 函数原型:int mkfifo(const char*filename,mode_t mode)
      参数1(filename):是将要在文件系统中创建的一个专用文件。
      参数2(mode):用来规定FIFO的读写权限。mode: 0666
    3. 函数返回值:成功返回0,失败返回-1。
    4. 函数所需头文件:#include <sys/types.h>, #include <sys/stat.h>

 

 

关于0666和0777,即可读可写可执行

 

 

mkfifo只会生成节点,并不会真正在内存里创建管道队列,在使用open函数打开该节点的时候,才会创建一个管道。

1、open FIFO文件阻塞的坑

与打开其他文件一样,FIFO文件也可以使用open调用来打开。注意,mkfifo函数只是创建一个FIFO文件,要使用命名管道还是将其打开

 

打开管道
open函数,默认为阻塞模式(改函数也是Linux中文件系统的操作函数)

open(const char *path, O_RDONLY);         //只读方式打开 
open(const char *path, O_RDONLY | O_NONBLOCK); // 非阻塞式只读
open(const char *path, O_WRONLY);         //阻塞式只写 
open(const char *path, O_WRONLY | O_NONBLOCK); // 非阻塞式只写

 

对于以读方式打开的有名通道,默认是管道的读取端

对于以写方式打开的有名通道,默认是管道的写入端

 

管道读写
read/write
阻塞模式:
读数据时,没有数据则一直等待,直到数据被写入。可能存在多个进程读取数据,当任意一个读取到数据时,其它进程也解除阻塞,,只不过返回0
写数据时,管道满则等待,直到数据被读走,多个进程写时,Linux保证写入原子性,采用互斥方式
原子性:要么全部写入,要么干脆不写入
非阻塞模式
读数据时,没数据直接返回0
写数据时,fifo缓冲区没空间则返回EAGAIN错误提醒以后写

管道关闭
close()
进程关闭前,关闭各自标识符

文件移除
unlink()函数,写端将临时文件移除
例子

namepipe1:创建管道并向其中写入数据

#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>    //管道头文件
#include <sys/stat.h>    //管道头文件
#include <stdio.h>
int main() {


    int res = mkfifo("./mypipe", 0777);    //创建一个有名管道文件,所有用户可读可写可执行
    if (res < 0) {
        printf("create pipe failed\n");
    }
    printf("wait for first open the pipe\n");
    int fd = open("./mypipe", O_WRONLY);    //只有打开了管道文件以后才会真正生成一条管道
    //0:成功    -1:失败
    if (fd < 0) {
        printf("open piple faied");
        return -2;
    }
    for (int i = 0; i < 20; i++) printf("process 1 run\n");
    close(fd);
    return 0;
}

 

标准流管道
是c函数,用于读取指令执行的结果
后面有需要再涉及

 

消息队列-消息的链表


进程可以按照规则添加新消息,另一个进程可以从消息队列中读消息
消息队列类型
POSIX消息队列
系统V消息队列-随内核持续,只有内核重启/人工删除才会被删除
消息队列是消息的一个链表,消息信息保存在结构体中,送到内核由内核管理。
消息的发送不是同步机制,只要消息没被清除则另一个程序任何时候都能读取消息

 

查看当前系统下的所有消息队列

 

 

多个进程从队列中读取信息。但FIFO需要读、写的两端事先都打开,才能够开始信息传递工作。而消息队列可以事先往队列中写信息,需要时再打开读取信息

 

消息的特点

1.消息结构体必须自己定义
2.消息队列中的消息是有类型的

3.消息队列中的消息是有格式的

4.消息队列可以实现消息的随机查询。消息不一定要以先进先出的次序读取,编程时可以按消息的类型读取,只要管道中拥有该类型的消息,就能够被进程读取到

5.消息队列的消息一旦被读出就会被清除

6.允许多个进程同时读取消息队列中的消息

7只有内核重启/人工删除,消息队列才会结束,否则一直存在

8.每一条消息队列都有属于它的消息队列标识符,系统唯一


键值


系统建立ipc通信(进程间通信)必须指定一个ID,通过ftok函数得到


key_t ftok()  ----------返回文件名对应键值

ftok原型如下:
key_t ftok( char * fname, int id )

fname:    你指定的文件名

id是子序号  虽然为int,但是只有8个比特被使用(0-255)

执行成功将会返回一个唯一的ID值

 

打开/创建消息队列

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

参数

(1) key:是唯一标识一个消息队列的关键字,如果为IPC_PRIVATE(值为0,用创建一个只有创建者进程才可以访问的消息队列),表示创建一个只由调用进程使用的消息队列,非0值的key(可以通过ftok函数获得)表示创建一个可以被多个进程共享的消息队列;

(2) msgflg:指明队列的访问权限和创建标志,创建标志的可选值为IPC_CREAT和IPC_EXC,如果单独指定IPC_CREAT,msgget要么返回新创建的消息队列id,要么返回具有相同key值的消息队列id;如果IPC_EXCL和IPC_CREAT同时指明,则要么创建新的消息队列,要么当队列存在时,调用失败并返回-1

/*1. 创建消息队列*/
int msgid = msgget((key_t)1235,0666 | IPC_CREAT);

 

发送消息/接收消息

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
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);

  从消息队列中读取一个消息,并将消息存储在msgbuf结构体中,读取了一条消息后,队列中的消息会被删除


msqid:已打开的消息队列ID,由前面的mssget函数获取
msgp:存放消息的结构,需要自己定义
msgsz:消息数据大小,每个消息体最大不要超过4K; 不含消息类型占用的4个字节,即mtext的长度
msgflg:发送标志(是否等待) 下面是可添加的参数

0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回
IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。

msgtyp:接收数据的形式,按需要选择

 msgtyp == 0 接收队列中的第1个消息,不区分消息类型
 msgtyp > 0 接收对列中的第1个类型等于msgtyp的消息
 msgtyp < 0 接收其类型小于或等于msgtyp绝对值的第1个最低类型消息

 

消息结构如下:

它的长度必须小于系统规定的上限,必须以一个长整型成员变量开始,接收函数将用这个成员变量来确定消息的类型
第一个类型绝对不允许被更改
struct msgbuf { long mtype; /* type of message */ char mtext[1]; /* message text */ };

 

消息控制

msgctl是消息队列的控制函数,常用来删除消息队列。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

对msqid标识的消息队列执行3种cmd操作

 

 

消息队列使用例子

利用消息队列设计抢答器

 

server.c

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


#define MY_TYPE 9527
#define server_TYPE 1        //
#define client1_TYPE 10        //主机发送的数据给客户端的数据类型
#define client2_TYPE 20
#define client3_TYPE 30
#define rec_type 40
#define MY_KEY 1314       //也可以使用ftok,避免冲突

// 定义消息内容
/*
    定义一个
    服务器端发送的消息类型是1
    客户端1对应的消息类型是10
    客户端2对应的消息类型是20
    客户端3对应的消息类型是30

*/
struct msgbuf
{
    long mtype;
    int number;
    char mtext[100];
};

int main(void)
{
    int msgid;
    int running = 1;
    struct msgbuf send_buff,rec_buff;
    msgid = msgget(MY_KEY, 0666 | IPC_CREAT);     // 创建消息队列
    while (running) 
    {
        send_buff.mtype = server_TYPE;
        printf("please enter s/x:\n");
        memset(send_buff.mtext, 0x00, 100);
        gets(send_buff.mtext);
        if (send_buff.mtext[0] == 's')
        {

            msgsnd(msgid, &send_buff, sizeof(send_buff) - sizeof(send_buff.mtype), 0); //msgsnd发送消息  指针  内容大小
            msgsnd(msgid, &send_buff, sizeof(send_buff) - sizeof(send_buff.mtype), 0); //msgsnd发送消息  指针  内容大小
            msgsnd(msgid, &send_buff, sizeof(send_buff) - sizeof(send_buff.mtype), 0); //msgsnd发送消息  指针  内容大小
            printf("msg has sended\n");
        }
        if (send_buff.mtext[0] == 'x')
        {
            printf("program over\n");
            
            msgsnd(msgid, &send_buff, sizeof(send_buff) - sizeof(send_buff.mtype), 0);
            msgsnd(msgid, &send_buff, sizeof(send_buff) - sizeof(send_buff.mtype), 0);
            msgsnd(msgid, &send_buff, sizeof(send_buff) - sizeof(send_buff.mtype), 0);
            printf("over msg send success\n");
            running = 0;



        }
        if (running) 
        {
            printf("waitting for client msg.........\n");
            //等待客户端发送消息给主机,这里客户端要发一个server_TYPE类型的数据过来
            if (msgrcv(msgid, &rec_buff, sizeof(rec_buff) - sizeof(rec_buff.mtype), rec_type, 0) == -1)        //接收队列中的第一个消息
            {
                printf("receive msg failed.....\n");
            }
            if (rec_buff.mtext[0] == 'q')
            {
                if(rec_buff.number == 1)
                    sprintf(send_buff.mtext, "client 1 rob success!\n", rec_buff.mtype);
                if (rec_buff.number == 2)
                    sprintf(send_buff.mtext, "client 2 rob success!\n", rec_buff.mtype);
                if (rec_buff.number == 3)
                    sprintf(send_buff.mtext, "client 3 rob success!\n", rec_buff.mtype);

                send_buff.mtype = client1_TYPE;
                msgsnd(msgid, &send_buff, sizeof(send_buff) - sizeof(send_buff.mtype), 0);
                send_buff.mtype = client2_TYPE;
                msgsnd(msgid, &send_buff, sizeof(send_buff) - sizeof(send_buff.mtype), 0);
                send_buff.mtype = client3_TYPE;
                msgsnd(msgid, &send_buff, sizeof(send_buff) - sizeof(send_buff.mtype), 0);
                memset(send_buff.mtext, 0x00, 100);

            }
        }


    }
    msgctl(msgid, IPC_RMID, NULL);  //删除消息队列,释放空间
    printf("talk over.........\n");
    exit(0);
    

}

 

client.c

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

#define MY_KEY 1314       //也可以使用ftok,避免冲突
#define MSGSIZE  64
#define client1_TYPE 10        //主机发送的数据给客户端的数据类型
#define client2_TYPE 20
#define client3_TYPE 30

#define rec_type 1
#define send_type 40

typedef struct
{
    long  mtype;
    int number;
    char  mtext[100];
} msgbuf;

#define  LEN   sizeof(msgbuf) - sizeof(long)

int  main()
{
    int msgid;
    int running = 1;
    msgbuf send_buf, rec_buf;
    send_buf.number = 0;
    msgid = msgget(MY_KEY, 0666 | IPC_CREAT);     // 创建消息队列

    printf("program start........\n");
    while(running)
    { 
        printf("wait for rob msg............\n");
        memset(rec_buf.mtext, 0x00, 100);
        msgrcv(msgid, &rec_buf, sizeof(rec_buf) - sizeof(rec_buf.mtype), rec_type, 0);

        //如果接收的是结束信号
        if (rec_buf.mtext[0] == 'x' || rec_buf.mtext[0] == 'X')
        {

            printf("client1 will quit!\n");
            break;
        }
        //如果接收的不是结束信号
        printf("start rob!!!\n");

        printf("input q to rob \n");
        gets(send_buf.mtext);
        send_buf.number = 1;
        send_buf.mtype = send_type;

        msgsnd(msgid, &send_buf, sizeof(send_buf) - sizeof(send_buf.mtype), 0);
        memset(rec_buf.mtext, 0x00, 100);
        msgrcv(msgid, &rec_buf, sizeof(rec_buf) - sizeof(rec_buf.mtype), client1_TYPE, 0);

        printf("waiting for rob result........\n");
        printf("rob result is:%s\n", rec_buf.mtext);


        
    }
    printf("program over........\n");
    return 0;

}

 

 

信号

 

信号是进程在运行中,由自身产生或由进程外部产生来通知进程发生了异步事件的通信机制

信号是事件发生时对进程的通知机制,有时也称之为软件中断。


包含在头文件<signal.h>中

一个进程(具有权限)能够向另一个进程发送信号。进程也可向自己发送信号。可以作为一种同步技术,甚至可作为 IPC (进程间通信)

 

信号种类:Linux总共有64种信号,其中32种比较重要

 

使用kill -l //查看所有信号
$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGEMT 8) SIGFPE 9) SIGKILL 10) SIGBUS 11) SIGSEGV 12) SIGSYS 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGURG 17) SIGSTOP 18) SIGTSTP 19) SIGCONT 20) SIGCHLD 21) SIGTTIN 22) SIGTTOU 23) SIGIO 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGINFO 30) SIGUSR1 31) SIGUSR2

多数信号可以忽略,但SIGSILSIGSTO不能被忽略

 

 

信号产生方式

  • 程序执行错误
  • 另一个进程(信号函数)发送过来
  • 用户控制中断产生信号
  • 子进程结束向父进程发送sigchld信号
  • 程序设计定时器产生sigalrm信号

信号处理方式

 

  • 默认处理,一般为消亡进程,同时也可能产生转储文件
  • 忽略信号,进程可以通过代码有意地忽略信号处理
  • 捕捉信号,进程可以事先注册信号处理函数,接收到信号时,信号处理函数自动捕捉并处理信号
  • 暂停进程
  • 唤醒进程

信号操作指令

 

发送信号

  • 键盘发送信号

  

 

  • 通过kill命令向进程发送信号
# kill -sig pid
$ kill -9 1782 # 给 1782 进程发送 9 号信号,也就是SIGKILL
$ kill -SIGKILL 1782 # 与上面相同

  默认发 SIGTERM(15 号)信号.

 

几个特殊的pid编码

  • pid > 0 发送给指定的进程。

  • pid = 0 发送给与 pid 同组的所有进程。

  • pid < -1 发送给 pid 绝对值的进程组内所有下属进程。

  • pid = -1 发送给有权限发送的所有进程,除 init

 

发送信号是可能失败的,比如杀死某些进程就需要高权限,不可能想杀就杀........

 

发送 kill -0 pid 可以确认某个进程是否存在

$ kill -0 1999
sh: kill: (1999) - No such process

其它发送信号的函数

#include <signal.h>

int kill(pit_t pid, int sig);
int raise(int sig); // 对自己发 sig 信号
int killpg(pid_t pgrp, int sig); // 对进程组发 sig 信号

#include <unistd.h>

unsigned int alarm(unsigned int s); // 过 s 秒后对自己发 SIGALRM 信号

#include <stdlib.h>

void abort(void) // 对自己发 SIGABRT 信号

 

int raise(int sig)  //参数:sig - 特定信号
作用:向当前进程发送信号

 

int pause(void)
作用:暂停当前进程直到接受到任何一个信号

 

unsigned int alarm(unsigned int seconds)  //second:时间(秒)
作用:倒计时到达时发出SIGALRm信号

 

abort函数
void abort()
作用:使异常程序终结

 

 

关于产生的转储文件

  该文件保留着操作系统最后执行的情况。一般程序都会自动退出,这种接收到信号退出的方式是不自然的,可能是程序发生了故障,可以通过转储文件了解出错原因

关于pkill命令    -能够通过指定进程名称杀死进程

pkill sleep

 

处理信号

 

处理信号的过程很类似单片机中的中断服务程序,同样支持递归中断

 

 

信号捕捉函数
signal函数  -参数1:特定信号  参数2:函数指针

#include <signal.h>
void (*signal(int sig, void (*handler)(int)))(int);
// sig 要修改的信号编号
// handler 接受到信号要调用的函数

// handler 函数一般如下
void handler(int sig) { // 收到的信号作为入参
    /* code */
}

 

等待信号pause:该函数会使进程等待信号并进入睡眠,避免占用CPU的性能

#include <unistd.h>

int pause(); // 暂停该进程执行。收到一个信号转而执行信号处理器,恢复进程执行。如果没有设置对应的信号处理器则会终止进程。

信号捕获例子

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

void handler1(int sig) {
    while (1) {
        printf("handler1, sig: %d\n", sig);
        sleep(3);
    }
}

void handler2(int sig) {
    printf("handler2, sig: %d\n", sig);    
}

int main() {
    signal(SIGINT, &handler1);  //信号1出现时进入中断处理函数1
    signal(SIGTSTP, &handler2);  //信号2出现时进入中断处理函数2
    pause();
}

 

补充
  memset(&act,0x00,sizeof(struct sigaction))  //内存清理函数,能够清除某一片地址的内存

 

 

信号量
  system-V机制,包括信号量,消息队列,共享内存。该机制的目的是为了保证多个进程之间的相互通信

信号量的分类

信号量分为

  1. 内核信号量,由内核控制路径使用
  2. 用户态进程使用的信号量,这种信号量又分为 POSIX 信号量和 SYSTEM V 信号量。其中POSIX信号量又分为无名信号量和有名信号量

 

  下面我重点解释POSIX信号量中的无名信号量和SYSTEM  V信号量

 

信号量原理

  信号量可以理解为一个计数器,它是用来描述临界资源的有效个数;

  信号量的本质是计数器,但不意味着只有计数器,信号量还包括一个等待队列,该等待队列会挂载所有需要信号量的进程。

  

 


信号量作用
多任务操作系统中
1.多个进程为完成一个任务相互协作实现进程同步
2.多个进程争夺有限资源的进程互斥关系

信号量使用操作

P操作:我们将申请信号量称为P操作,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减一,因此P操作的本质就是让计数器减一。

V操作:我们将释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加一,因此V操作的本质就是让计数器加一。

信号量的执行过程

 

POSIX的无名信号量

1.信号量初始化

//使用前必须包含信号量的头文件和句柄
#include <semaphore.h>//头文件 sem_t sem1;
//利用初始化函数对信号量进行初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数说明:

  • sem:需要初始化的信号量,由前面定义的信号量句柄提供。
  • pshared:一般给0,传入0值表示线程间共享,传入非零值表示进程间共享。
  • value:信号量的初始值(计数器的初始值)。

返回值说明:

  • 初始化信号量成功返回0,失败返回-1。

2.等待信号量(P操作)(当没有信号量时,该进程就会被挂起,直到有信号量来将其唤醒)

int sem_wait(sem_t* sem);  //sem:信号量的句柄  信号量-1
int sem_trywait(sem_t *sem); // 非阻塞的函数,和wait类似,但不同的是不会阻塞而是直接返回错误EAGAIN

3.发送信号量

int sem_post(sem_t* sem);    //信号量+1 并唤醒在该信号量等待队列中的进程

4.信号量销毁

int sem_destroy(sem_t *sem);
  • 销毁信号量成功返回0,失败返回-1。

5.获取当前信号量的值

int sem_getvalue(sem_t *sem, int *sval);

关于POSIX的有名信号量

  有名信号量和无名信号量不同的点在于有名信号量将信号量的值保存在文件当中,此时对信号量的操作类似操作文件。

  有名信号量的用途非常广:既可以用于线程,也可以用于相关进程间,甚至是不相关进程。

  

1.创建有名信号量

sem_t *sem_open(const char *name, int oflag, mode_t mode , int value);  //可以看到以文件的形式创建

name      是文件的路径名;

  这里的 name 不能写成 /tmp/aaa.sem 这样的格式,因为在 linux 下,sem 都是创建在 /dev/shm 目录下。你可以将 name 写成“/mysem”或“mysem”,创建出来的文件都是“/dev/shm/sem.mysem”,千万不要写路径。也千万不要写“/tmp/mysem”之类的。

Oflag      有 O_CREAT 或 O_CREAT | EXCL 两个取值;

  当 oflag = O_CREAT 时,若 name 指定的信号量不存在时,则会创建一个,而且后面的 mode 和 value 参数必须有效。若 name 指定的信号量已存在,则直接打开该信号量,同时忽略 mode 和 value 参数。

  当 oflag = O_CREAT | EXCL,当信号量已经存在时,该函数会直接返回 error

mode_t   控制新的信号量的访问权限;

Value      指定信号量的初始化值。

 

有名信号量的PV操作和无名信号量重用了,在此不再重复

2.删除有名信号量

int sem_close(sem_t *sem):  //关闭有名信号量,此时有名信号量文件并没有被删除

注意:在进程结束的时候,所有的信号量都会被迫关闭

int sem_unlink(const char *name); //输入前面定义的name可以真正删除有名信号量的文件
成功返回0,失败返回-1。必须保证当前信号量已经关闭,否则该操作会无效

下面来介绍SYSTEM -V 的信号量

 

关于IPC(进程间通信)

  SYSTEM V 信号量是 SYSTEM V IPC(即 SYSTEM V 进程间通信)的组成部分,其他的有SYSTEM V 消息队列,SYSTEM V 共享内存。而关键字和 IPC 描述符无疑是它们的共同点,也使用它们,就不得不先对它们进行熟悉

  IPC描述符相当于引用 ID 号,要想使用 SYSTEM V 信号量(或MSG、SHM),就必须用 IPC 描述符来调用信号量,它是用来标识信号量集的一个ID号,在Linux中常用ftok函数获取ID号,这在前面消息队列中已经有对该函数进行描述

 

信号量相关API

 

1.如何创建一个system V 信号量集

//#include <semaphore.h>
int semget(key_t key, int nsems, int oflag)  

参数介绍

key: 用来唯一标识该信号量集的值,一般由ftok函数获取,也可以用户自己定义,但并不建议,很可能出现某个键值已经被使用的情况

  解决办法

  一般来说,客户机和服务器至少共享一个头文件,所以一个比较简单的方法是避免使用 ftok,而只是在该头文件中存放一个大家都知道的关键字。

nsems:创建的信号量集中的信号量数量(信号量1,2,3,.....n)

nsems > 0 : 创建一个信的信号量集,指定集合中信号量的数量,一旦创建就不能更改。

nsems == 0 : 访问一个已存在的集合。

oflag :用于指定创建或获取信号量集合时的选项,例如是否创建新的信号量集合、是否要求信号量集合不存在、是否等待信号量集合可用等

IPC_CREAT:如果指定了这个选项,则表示如果指定的key值对应的信号量集合不存在,则创建一个新的信号量集合。如果信号量集合已经存在,则忽略这个选项。
IPC_EXCL:如果同时指定了IPC_CREAT和IPC_EXCL选项,则表示只有在指定的key值对应的信号量集合不存在时,才创建一个新的信号量集合。如果信号量集合已经存在,则semget()函数将返回一个错误。
IPC_NOWAIT:如果指定了这个选项,则表示在获取信号量集合时,如果信号量集合已经被其他进程占用,则semget()函数将立即返回一个错误,而不是等待信号量集合可用。

单独使用IPC_eXCL没有任何效果

返回值返回一个称为信号量标识符的整数,semop 和 semctl 函数将使用它,这个值独一无二,是用来区分信号量集的

(5)创建成功后信号量结构被设置,semget 函数执行成功后,就产生了一个由内核维持的类型为 semid_ds 结构体的信号量集,返回 semid 就是指向该信号量集的索引。(说实在的,我压根听不懂............)

 

2.设置信号量的值(初始化某个信号量)(PV操作)

int semop(int semid, struct sembuf *opsptr, size_t nops);

(1)semid: 是 semget 返回的 semid。

(2)opsptr: 指向信号量操作结构数组。

(3)nops : opsptr 所指向的数组中的 sembuf 结构体的个数。

sembuf结构体如下:

struct sembuf {
    short sem_num; // 要操作的信号量在信号量集里的编号,这个好像是程序员自己设置?
    short sem_op; // 信号量操作
    short sem_flg; // 操作表示符
};
关于sem_op

若 sem_op 是正数,其值就加到 semval 上,即释放信号量控制的资源,一般也就是用这个来初始化信号量的值

若 sem_op 是 0,那么调用者希望等到 semval 变为 0,如果 semval 是 0 就返回;

若 sem_op 是负数,那么调用者希望等待 semval 变为大于或等于 sem_op 的绝对值

 

 什么是semval?它在哪?

  存在于内核维护的信号量集(当然每个信号量的值即 semval 也是只由内核才能看得到了),用户能看到的就是返回的 semid,内核会根据sem_op的值做出响应的变化,说到底就是不用程序员去关心就是

 

  关于sem_flg

SEM_UNDO 由进程自动释放信号量,一般都是选这个选项

IPC_NOWAIT 不阻塞

 

改变某个信号量的semval值

int semctl(int semid, int semum, int cmd, ../* union semun arg */);

semid 是信号量集合;

semnum 是信号在集合中的序号,就是之前自己定义的结构体中的sem_num

semum 是一个必须由用户自定义的结构体,在这里我们务必弄清楚该结构体的组成:

 

union semun {
  int val; // cmd == SETVAL
  struct semid_ds *buf; // cmd == IPC_SET或者 cmd == IPC_STAT
  ushort *array; // cmd == SETALL,或 cmd = GETALL
};

为啥是联合体? ------》》》 因为不知道接下来的操作是啥

当要设置semval的值时

  val 只有 cmd ==SETVAL 时才有用,此时指定的 semval = 定义的联合体.val。

  注意:当  cmd == GETVAL 时,semctl 函数返回的值就是我们想要的 semval。千万不要以为指定的 semval 被返回到 arg.val 中。

当需要知道某一个信号量集的所有信息时

  array 指向一个数组,当 cmd == SETALL 时,就根据 定义的联合体.array 来将信号量集的所有值都赋值;

  当 cmd == GETALL 时,就将信号量集的所有值返回到 定义的联合体.array 指定的数组中。

关于buf

不常用,在这里不做讨论.........

 

 

多个进程实现信号量同步/互斥

该进程有两个进程,父进程等待子进程提供信号量,子进程3s后提供信号量

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

//定一个信号量的联合体,提供给semctl作为参数
union semun
{
    int val;
    struct semid_ds* buf;
    unsigned short* arry;
};

int sem_Init(int sem_id, int sem_value)
{
    /*
        函数介绍:信号量初始化
        参数:
            sem_id :semget函数返回的ID值
            sem_value:信号量的初始化值
    */
    union semun sem_union;
    sem_union.val = sem_value; //设置信号量的初始值

    //对信号量的操作,这里是对编号0信号量设置信号量的值为sem_value的值
    if (semctl(sem_id, 0, SETVAL, sem_union) == -1) //-1表示失败
    {
        printf("sem init failed........");
        return -1;
    }
    return 0;

}
int sem_del(int sem_id)
{
    /*
        函数内容:删除对应id的信号量
        参数    :sem_id 信号量编号
    */
    union semun sem_union;
    
    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
    {
        printf("sem delete failed.....");
        return -1;
    }
    return 0;

}
int sem_V(int sem_id)
{
    /*
        函数内容:信号量+1操作
    */
    struct sembuf sops;
    sops.sem_num = 0;   //当前信号量的编号
    sops.sem_op = 1;    //信号量 + sem_op
    sops.sem_flg = SEM_UNDO;    //系统自动清除残存的信号量

    if (semop(sem_id, &sops, 1) == -1)
    {
        printf("sem Add failed....");
        return -1;
    }
    return 0;
    
}
int sem_P(int sem_id)
{
    /*
        函数内容:信号量-1操作
    */
    struct sembuf sops;
    sops.sem_num = 0;   //当前信号量的编号
    sops.sem_op = -1;    //信号量 - sem_op
    sops.sem_flg = SEM_UNDO;    //系统自动清除残存的信号量

    if (semop(sem_id, &sops, 1) == -1)
    {
        printf("sem Add failed....");
        return -1;
    }
    return 0;

}

int main(int argc, char* argv[])
{
    pid_t result;
    int sem_id;     //用来保存semget返回的信号量集编号

    //创建一个信号量集合
    sem_id = semget((key_t)6666, 1, 0666 | IPC_CREAT);
    sem_Init(sem_id, 0);    //初始化信号量的值为0
    //创建子进程
    result = fork();
    if (result == -1)
    {
        printf("SUB PID create failed");
    }
    else if (result == 0)    //返回值 = 0,代表进入子进程
    {
        printf("Child process will wait for some seconds.....\n");
        sleep(3);
        printf("the Child process is running....\n");
        sem_V(sem_id);  //信号量+1

    }
    else  //信号量 > 0时,代表进入父进程
    {
        sem_P(sem_id);  //信号量-1
        printf("Father process is running.....\n");
        sem_V(sem_id);

        sem_del(sem_id);

    }

    exit(0);


}

 

 

七、信号量的牛刀小试 -- 生产者与消费者问题

1.问题描述

有一个长度为 N 的缓冲池为生产者和消费者所共有,只要缓冲池未满,生产者便可将消息送入缓冲池;只要缓冲池未空,消费者便可从缓冲池中取走一个消息。生产者往缓冲池放信息的时候,消费者不可操作缓冲池,反之亦然。

2.使用多线程和信号量解决该经典问题的互斥#include <pthread.h>

#include <stdio.h>
#include <semaphore.h>
#define BUFF_SIZE 10
char buffer[BUFF_SIZE];
char count; // 缓冲池里的信息数目
sem_t sem_mutex; // 生产者和消费者的互斥锁
sem_t p_sem_mutex; // 空的时候,对消费者不可进
sem_t c_sem_mutex; // 满的时候,对生产者不可进
void *Producer()
{
    while (1) {
    
//等待信号量
sem_wait(&p_sem_mutex); //当缓冲池未满时 sem_wait(&sem_mutex); //等待缓冲池空闲 count++; sem_post(&sem_mutex); if (count < BUFF_SIZE) //缓冲池未满 sem_post(&p_sem_mutex); if (count > 0) //缓冲池不为空 sem_post(&c_sem_mutex); } } void *Consumer() { while (1) {
    //等待信号量 sem_wait(&c_sem_mutex); //缓冲池未空时 sem_wait(&sem_mutex); //等待缓冲池空闲 count--; sem_post(&sem_mutex); if (count > 0) sem_post(c_sem_nutex); } } int main() { pthread_t ptid, ctid; //initialize the semaphores      //创建两个信号量 sem_init(&empty_sem_mutex, 0, 1);   //信号量初始值为1 sem_init(&full_sem_mutex, 0, 0);   //信号量初始值为0 //creating producer and consumer threads
  
    //创建生产者线程 if (pthread_create(&ptid, NULL, Producer, NULL)) { printf("\n ERROR creating thread 1"); exit(1); }
    //创建消费者线程 if (pthread_create(&ctid, NULL, Consumer, NULL)) { printf("\n ERROR creating thread 2"); exit(1); }
    //等待生产者线程结束 if (pthread_join(ptid, NULL)) /* wait for the producer to finish */ { printf("\n ERROR joining thread"); exit(1); }
    //等待消费者线程结束 if (pthread_join(ctid, NULL)) /* wait for consumer to finish */ { printf("\n ERROR joining thread"); exit(1); }
    //信号量销毁 sem_destroy(&empty_sem_mutex); sem_destroy(&full_sem_mutex); //exit the main thread pthread_exit(NULL); return 1; }

 


共享内存

 

通过将两个进程的虚拟内存地址映射到同一片物理地址空间,这样两个进程就能够高效地进行大批量数据传输。

共享内存允许两个或者多个进程共享给定的存储区域

共享内存的优缺点

优点:

  • 共享内存是进程间通信速度最快的通信方式,因为所有进程共享一片物理空间,所以不存在数据拷贝的过程。
  • 共享内存允许多个进程同时进行内存操作,可以方便地在各个进程之间实现通信。
  • 当虚拟地址映射以后就不需要再频繁地系统调用内存转移而能够像普通内存一样直接访问数据

缺点:

  • 共享内存本身并不支持互斥/同步机制,需要其他内核机制来辅助
  • 多个进程同时对内存资源进行访问,如果没协调好很容易造成数据错误
  • 只能用于单主机通信

 

共享内存的使用过程

相关API
共享内存对象创建

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
  • key:用于唯一标识共享内存段的键值。可以用ftok获取,也可以自己定义
  • size:指定共享内存段的大小。
  • shmflg:标志参数,用于指定权限和其他选项。

返回值:成功时返回共享内存段的标识符(shmid),失败时返回-1

 

共享内存映射

 

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:共享内存段的标识符。由上面shmget返回得到
shmaddr:指定共享内存连接的地址,通常设置为NULL,由系统自动选择。
shmflg:标志参数,用于指定访问权限和其他选项。
返回值:成功时返回共享内存段的起始地址,失败时返回-1

 

共享内存分离

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);

  shmaddr:共享内存段的起始地址。即shmat返回的地址
  • 返回值:成功时返回0,失败时返回-1。

 

共享内存控制

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存段的标识符。
cmd:控制操作命令,如IPC_RMID(删除共享内存段)。
buf:用于存储共享内存信息的结构体指针。
返回值:成功时返回0,失败时返回-1。

 共享内存使用例子

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

//共享内存所用的头文件
#include <sys/ipc.h>
#include <sys/shm.h>
#define TEXT_SZ 2048    //共享内存的空间

//定一个信号量的联合体,提供给semctl作为参数
union semun
{
    int val;
    struct semid_ds* buf;
    unsigned short* arry;
};

//信号量相关API
int sem_Init(int sem_id, int sem_value)
{
    /*
        函数介绍:信号量初始化
        参数:
            sem_id :semget函数返回的ID值
            sem_value:信号量的初始化值
    */
    union semun sem_union;
    sem_union.val = sem_value; //设置信号量的初始值

    //对信号量的操作,这里是对编号0信号量设置信号量的值为sem_value的值
    if (semctl(sem_id, 0, SETVAL, sem_union) == -1) //-1表示失败
    {
        printf("sem init failed........");
        return -1;
    }
    return 0;

}
int sem_del(int sem_id)
{
    /*
        函数内容:删除对应id的信号量
        参数    :sem_id 信号量编号
    */
    union semun sem_union;
    
    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
    {
        printf("sem delete failed.....");
        return -1;
    }
    return 0;

}
int sem_V(int sem_id)
{
    /*
        函数内容:信号量+1操作
    */
    struct sembuf sops;
    sops.sem_num = 0;   //当前信号量的编号
    sops.sem_op = 1;    //信号量 + sem_op
    sops.sem_flg = SEM_UNDO;    //系统自动清除残存的信号量

    if (semop(sem_id, &sops, 1) == -1)
    {
        printf("sem Add failed....");
        return -1;
    }
    return 0;
    
}
int sem_P(int sem_id)
{
    /*
        函数内容:信号量-1操作
    */
    struct sembuf sops;
    sops.sem_num = 0;   //当前信号量的编号
    sops.sem_op = -1;    //信号量 - sem_op
    sops.sem_flg = SEM_UNDO;    //系统自动清除残存的信号量

    if (semop(sem_id, &sops, 1) == -1)
    {
        printf("sem Add failed....");
        return -1;
    }
    return 0;

}

//共享内存相关API
struct shared_use_st
{
    int written;//作为一个标志,非0:表示可读,0表示可写 
    char text[TEXT_SZ];//记录写入和读取的文本
};
int main(int argc, char* argv[])
{
    pid_t result;
    int sem_id;     //用来保存semget返回的信号量集编号
    int shmid;      //共享内存标识符 //创建共享内存  
    void* shm = NULL;//分配的共享内存的原始首地址   
    struct shared_use_st* shared;//指向shm   
     
    //shmget函数 参数: key值  共享内存大小  模式
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
    if (shmid == -1)
    {
        //共享内存创建失败
        printf("shmget failed\n");
        exit(0);
    }  

    //创建一个信号量集合
    sem_id = semget((key_t)6666, 1, 0666 | IPC_CREAT);
    sem_Init(sem_id, 0);    //初始化信号量的值为0
    //创建子进程
    result = fork();
    if (result == -1)
    {
        printf("SUB PID create failed");
    }
    else if (result == 0)    //返回值 = 0,代表进入子进程
    {
        printf("the Child process is running....\n");
        //将子进程的虚拟地址映射到分配的共享内存中
        shm = shmat(shmid, 0, 0);
        if (shm == (void*)-1)
        {
            printf("Child shmat failed......\n");
            exit(0);
        }
        //用shared指针来操作这片空间
        shared = (struct shared_use_st*)shm;
        //从键盘中采集数据
        printf("waiting data input.........\n");
        gets(shared->text);
        sem_V(sem_id);  //信号量+1

    }
    else  //信号量 > 0时,代表进入父进程
    {
        sem_P(sem_id);  //信号量-1
        printf("Father process is running.....\n");
        //父进程共享同一片内存
        shm = shmat(shmid, 0, 0);
        printf("0x00\n");
        shared = (struct shared_use_st*)shm;
        printf("Father process get messages: %s\n", shared->text);
        //分离shm的地址
        shmdt(shm);
        //删除共享内存
        shmctl(shmid, IPC_RMID, NULL);  //最后一个参数是存储共享内存结构体信息的指针,在删除时用不到

        sem_del(sem_id);

    }

    exit(0);


}

 

 

标签:信号量,int,send,间通信,内存,Linux,进程,sem,include
From: https://www.cnblogs.com/sujian4412/p/17837292.html

相关文章

  • Linux设备树完全解析
    1.Linux中说的设备树是什么?对于搞Linux驱动开发和BSP的工程师来说,总是不可避免的接触到设备树,那么设备树指的是什么呢?其实通常所说的设备树是指Linux内核里面的设备树文件,以.dts结尾,也叫做设备树源文件,这个文件可以通过一个叫dtc的程序把他编译成.dtb文件,编译后的文件就是Linux......
  • Proj4:改进LiteOS中物理内存分配算法
    记录一下,操作系统课上老师讲的proj4做法给的参考资料LiteOS中的物理内存分配采用了TLSF算法,该算法较好地解决了最坏情况执行时间不确定(notbounded)或者复杂度过高(boundedwithatooimportantbound"),以及碎片化问题(fragmentation)两个问题。TLSF算法仍存在优化空间,Best-......
  • linux云服务器部署springboot项目
    第一次在linux云服务器上部署项目,经过非常坎坷的摸索之后终于部署完成了进行项目部署的一些默认条件默认你有一台linux操作系统的云服务器,博客中演示的linux的发行版本是centos7其次博客中部署的是gitee上的SpringBoot项目,后端打包完的格式是zip格式,默认你也是博客中前端的部署是使......
  • 20231119 mac 使用dd 命令 烧写 linux img到sd卡
    https://docs.radxa.com/rock5/official-images?model=ROCK+5B下载rock5b官方操作系统文件是一个.img.xz文件打开一个mac终端,ls/dev关注/dev/disk相关的,插入SD卡读卡器到macmini,再次ls/dev 把sd卡格式化sudoddif=/dev/zeroof=/dev/disk4bs=64Mcoun......
  • 使用Vmware虚拟机装载Linux系统如何联网
    在虚拟化技术的快速发展下,VMware提供了强大的虚拟化平台,为用户提供了在单一物理机上运行多个操作系统的便捷方式。本文将介绍如何在VMware虚拟机中运行Linux操作系统,并顺利配置网络,使其能够与外部网络通信。在VMware虚拟机中运行Linux系统,并使其能够联网,通常需要执行......
  • Linux虚拟机磁盘扩容
    Linux虚拟机磁盘扩容问题起源在使用linux系统开发时遇到文件无法创建的问题,根据提示发现是磁盘空间不足。使df-h 查看具体磁盘使用情况。  针对这个问题,有两种解决方案:使用du-sh./*可以查看当前工作目录下各文件的占用空间大小,然后可以删除一些比较大的且无用的文......
  • 【无为原创】万字图文详解java的堆内存及OOM的解决方案,看完还不懂,从此绝笔不写了!
      目录如下:什么是JVM的堆是不是所有的Java对象都放在堆上?线程和堆的关系堆的内部结构面试题新生代与老年代如何设置堆的大小?新生代与老年代的比例设置Eden、幸存者的比例常用参数对象分配金句:分配过程内存......
  • linux 系统shell脚本防止同一时间被多次重复执行
    前言当shell脚本中需要执行的步骤较多、耗时较长时,为了避免脚本被其它进程重复执行导致操作逻辑被打乱,需要使该脚本同一时间内只能被一个进程执行,类似C#中的lock代码块操作,确保线程安全代码#!/bin/bash#创建文件锁路径lock_file=/tmp/my_script.lock#信号处理函数fun......
  • linux查看每个cpu核心使用率
    Linux是一种开源的操作系统,它被广泛应用于各种计算机设备和服务器。在Linux系统中,我们可以使用一些命令来查看每个CPU核心的使用率。以下是一些常用的方法: 1.使用top命令:top命令是一个实时的系统监控工具,可以显示当前系统的各种信息,包括CPU的使用率。在终端中输入top命令后,......
  • Linux 用户和用户组管理
    一、Linux系统用户账号的管理1、添加新的用户账号使用useradd命令,其语法如下:useradd选项用户名参数说明:选项:-ccomment指定一段注释性描述。-d目录指定用户主目录,如果此目录不存在,则同时使用-m选项,可以创建主目录。-g用户组指定用户所属的用户组。-G用户组,用......