首页 > 系统相关 >Linux进程的通信

Linux进程的通信

时间:2024-06-02 09:21:45浏览次数:22  
标签:IPC int 通信 信号量 信号 Linux 进程 include

IPC(Inter-process communication (IPC))

进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。在这方面,有几种常见的通信方式,让我们一一介绍:

  1. 管道(Pipe)

    • 管道是一种半双工的通信方式,数据只能单向流动,通常用于具有亲缘关系的进程之间,例如父子进程或兄弟进程。
    • 无名管道(Pipe):只能在具有亲缘关系的进程间使用。
    • 命名管道(FIFO):允许无亲缘关系进程间的通信。
  2. 信号(signal):

    • 信号是一种异步的通信方式,用于通知接收进程某个事件已经发生。信号可以用来实现进程间的同步,处理异步事件等。
  3. 消息队列(Message Queue)

    • 消息队列是由消息的链表组成,存放在内核中,并由消息队列标识符标识。
    • 克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  4. 共享内存(Shared Memory)

    • 共享内存允许多个进程共享一段内存,这段内存可以被映射到各个进程的地址空间中。
    • 是最快的 IPC 方式,专门设计用于提高其他进程间通信方式的运行效率。
  5. 信号量(Semaphore)

    • 信号量是一个计数器,用于控制多个进程对共享资源的访问。
    • 主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 套接字(Socket)

    • 套接字是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机上的进程通信。

管道

image-20240531191341811

无名管道

  1. 没有名字,因此无法使用open()。
  2. 只能用于亲缘进程间(比如父子进程、兄弟进程、祖孙进程……)通信。
  3. 半双工工作方式:读写端分开。
  4. 写入操作不具有原子性,因此只能用于一对一的简单通信情形。
  5. 不能使用lseek( )来定位

pipe

功能 创建无名管道:PIPE
头文件 #include <unistd.h>
原型 int pipe(int pipefd[2]);
参数 pipefd:一个至少具有2个int型数据的数组,用来存放PIPE的读写端描述符
返回值 成功:0
失败:-1
备注 pipefd[0]记录管道读取端的文件描述符,pipefd[1]记录管道写入端的文件描述符。

缺点:读写操作不作保护,如果多个进程或线程同时操作PIPE进行读写,数据会相互践踏。

注意:内核的缓冲区大小是固定的(linux系统下默认是4M大小),如果写入的速度快于读取的速度,则可能会发生缓冲区满的情况,如果读取的速度快于写入的速度,如果管道内部没有数据,则读取数据会出现阻塞等待的情况。

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

int main(int argc, char const *argv[])
{
	//1.创建一条匿名管道  pipe   pipefd[0] 读操作   pipefd[1]  写操作
	int pipefd[2] ={0};

	int ret = pipe(pipefd);
	if (ret == -1)
	{
		fprintf(stderr, "pipe error,errno:%d,%s\n",errno,strerror(errno));
		exit(1); //exit函数可以终止进程,并把进程的终止状态提供给该进程的父进程
	}

	//2.此时可以创建子进程  fork  子进程会拷贝父进程的数据段、代码段、堆栈段
	int child_pid = fork();

	//3.分析当前的进程空间是父进程 or 子进程 or 创建失败
	if (child_pid > 0)
	{
		//说明是父进程,父进程需要从匿名管道中读取数据
		char recvbuf[128] ={0};

		read(pipefd[0],recvbuf,sizeof(recvbuf)); //父进程会阻塞
		printf("my is parent,read from pipe data is [%s]\n",recvbuf);	

		wait(NULL); //父进程需要回收子进程的资源,也会阻塞
	}
	else if(child_pid == 0)
	{
		//说明是子进程,子进程向匿名管道中写入字符串
		char sendbuf[128] = "my is child,hello parent";
		write(pipefd[1],sendbuf,strlen(sendbuf));
	}
	else
	{
		fprintf(stderr, "fork error,errno:%d,%s\n",errno,strerror(errno));
		exit(2); //exit函数可以终止进程,并把进程的终止状态提供给该进程的父进程
	}

	return 0;
}

有名管道

  1. 有名字,存储于普通文件系统之中。
  2. 任何具有相应权限的进程都可以使用open()来获取FIFO的文件描述符。
  3. 跟普通文件一样:使用统一的read()/write( )来读写。
  4. 跟普通文件不同:不能使用lseek( )来定位,原因同PIPE。
  5. 具有写入原子性,支持多写者同时进行写操作而数据不会互相践踏。
  6. First In First Out,最先被写入FIFO的数据,最先被读出来。

mkfifo

功能 创建有名管道:FIFO
头文件 #include <sys/types.h>
#include <sys/stat.h>
原型 int mkfifo(const char *pathname, mode_t mode);
参数 pathname:FIFO的文件名
mode:文件权限
返回值 成功:0
失败:-1
备注

练习:在当前目录下创建一条命名管道,命名管道的名称用户决定,然后设计两个程序,要求进程A获取当前系统时间(time-->ctime)并写入到命名管道,进程B从命名管道中读取数据并存储在一个名字叫做log.txt的文本中。

fiforead.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

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

#include <time.h>

#define FIFO "./fifo4test"  //有名管道的名字

int main(int argc, char const *argv[])
{
    
    if(access(FIFO,F_OK))
    {
        mkfifo(FIFO,0644);
    }

    int fifo = open (FIFO,O_RDONLY);  //以只写方式打开FIFO

    char msg[400]={0};
    //bzero(msg,400);

    read(fifo,msg,400);//将数据从FIFO中读出
    printf("fromFIFO:%s",msg);

    return 0;
}

fifowrite.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

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

#define FIFO "./fifo4test"  // 有名管道的名字
#define LOG_FILE "log.txt"  // 存储数据的文本文件名

int main(int argc, char const *argv[])
{
    int fifo = open(FIFO, O_RDONLY);  // 以只读方式打开 FIFO

    char buffer[200] = {0};
    read(fifo, buffer, sizeof(buffer));

    // 打开 log.txt 文件以追加方式写入数据
    FILE *logFile = fopen(LOG_FILE, "a");
    if (logFile == NULL) {
        perror("Error opening log file");
        return 1;
    }

    fprintf(logFile, "%s", buffer);  // 写入数据到 log.txt
    fclose(logFile);  // 关闭文件

    close(fifo);  // 关闭 FIFO

    return 0;
}

思考:如果进程A向命名管道中写入了数据之后就关闭了命名管道,而进程B从命名管道中读取了进程A写入的部分数据之后就关闭了命名管道,请问下次打开命名管道之后是否可以继续读取上一次遗留的数据?

在Linux系统中,命名管道(FIFO)是一种特殊的文件类型,它存在于文件系统中,但其内容实际上存储在内存中。当一个进程向命名管道写入数据后,即使该进程关闭了管道,数据仍然会保留在管道的缓冲区中,直到被完全读取。因此,如果进程A向命名管道写入数据后关闭了管道,进程B读取了部分数据后也关闭了管道,那么下次打开命名管道时,只要管道的写端没有被所有相关进程关闭,就可以继续读取上一次遗留在管道中的数据。

这是因为命名管道的数据是在内存中的缓冲区里,而不是写入磁盘。只有当管道的所有写端都被关闭时,读取操作才会返回0,表示没有更多的数据可读。如果还有进程持有管道的写端,管道中的数据就不会丢失,可以在下次打开管道时继续读取

信号

信号(signal)是Unix系统、类Unix系统(比如Linux系统)以及其他POSIX兼容的操作系统中用于实现进程间通信的一种方式。信号采用的是一种异步通信机制。

思考:请问什么是异步通信?异步通信和同步通信的区别是什么?相比于同步通信,异步通信的优势是什么?

同步指的是当进程发起一个请求,但是该请求并未马上响应,则进程就会阻塞等待,直到请求被响应。

而异步指的是当进程发起一个请求,如果该请求并未马上响应,则进程会继续执行其他的任务,过来一段时间请求得到了响应,则会通知该进程,该进程得到通知再去对请求做出处理。

也就是说相比于同步通信,异步通信可以提高程序的执行效率。

kill命令

描述

  • kill 命令的默认信号是 TERM
  • 使用 -l-L 列出可用的信号。特别有用的信号包括 HUPINTKILLSTOPCONT0
  • 可以通过三种方式指定替代信号:-9-SIGKILL-KILL
  • 负的 PID 值可以用来选择整个进程组;请参阅 ps 命令输出中的 PGID 列。
  • PID-1 是特殊的;它表示所有进程,除了 kill 进程本身和 init

选项

  • <pid> [...]:向列出的每个 <pid> 发送信号。
  • -<signal>-s <signal>--signal <signal>:指定要发送的信号。信号可以通过名称或数字指定。信号的行为在 signal(7) 手册页中有解释。
  • -q, --queue value:使用 sigqueue(3) 而不是 kill(2)value 参数用于指定一个整数,该整数将与信号一起发送。如果接收进程使用 sigaction(2)SA_SIGINFO 标志安装了此信号的处理程序,则它可以通过 siginfo_t 结构的 si_value 字段获取此数据。
  • -l, --list [signal]:列出信号名称。此选项有可选参数,将信号编号转换为信号名称,反之亦然。
  • -L, --table:以表格形式列出信号名称。

信号的分类

image-20240529012655676

可以发现Linux系统中的信号编号为1 ~ 64,其中编号为1 ~ 31的信号为普通信号,编号为34 ~ 64的信号为实时信号。

(1) 普通信号

Linux系统中的普通信号也被称为不可靠信号,指的是当进程接收到了很多的信号请求但是又不能及时处理时,不会把信号形成队列,而是把其余未被处理的信号直接丢弃,只留下一个信号。Linux系统中的普通信号是从Unix系统继承过来的。

特点:

  1. 非实时信号不排队,信号的响应会相互嵌套。
  2. 如果目标进程没有及时响应非实时信号,那么随后到达的该信号将会被丢弃。
  3. 每一个非实时信号都对应一个系统事件,当这个事件发生时,将产生这个信号。
  4. 如果进程的挂起信号中含有实时和非实时信号,那么进程优先响应实时信号并且会从大到小依此响应,而非实时信号没有固定的次序。

(2) 实时信号

Linux系统中的实时信号也被称为可靠信号,指的是当进程接收到了很多信号请求但是又不能及时处理时,会把未处理的信号形成队列,然后按照顺序依次处理,不会丢弃信号。Linux系统中的实时信号是新增加的。

特点:

  1. 实时信号的响应次序按接收顺序排队,不嵌套。
  2. 即使相同的实时信号被同时发送多次,也不会被丢弃,而会依次挨个响应。
  3. 实时信号没有特殊的系统事件与之对应。

普通信号的默认动作

在操作系统中,信号是进程间通信的一种方式,用于告知进程某个事件已经发生。每个信号都有一个默认的处置行为,决定了进程接收到该信号时的反应。以下是各个信号的默认处置行为:

  • Term(终止): 默认行为是终止进程。当进程接收到这类信号时,它会结束运行。
  • Ign(忽略): 默认行为是忽略信号。进程会忽视这类信号,不会有任何反应。
  • Core(核心转储): 默认行为是终止进程并转储核心。这意味着进程会被终止,并且系统会创建一个核心转储文件,通常用于调试和分析进程终止的原因。
  • Stop(停止): 默认行为是停止进程。这类信号会使进程暂停执行,但进程并未完全终止,可以被重新启动。
  • Cont(继续): 默认行为是如果进程当前被停止了,则继续执行。这类信号用于恢复被停止的进程,让它继续运行。
信号值 缺省动作 备注
SIGHUP 1 终止 控制终端被关闭时产生
SIGINT 2 终止 从键盘按键产生的中断信号(比如Ctrl+C)
SIGQUIT 3 终止并产生转储文件 从键盘按键产生的退出信号(比如Ctrl+\)
SIGILL 4 终止并产生转储文件 执行非法指令时产生
SIGTRAP 5 终止并产生转储文件 遇到进程断点时产生
SIGABRT 6 终止并产生转储文件 调用系统函数abort()时产生
SIGBUS 7 终止并产生转储文件 总线错误时产生
SIGFPE 8 终止并产生转储文件 处理器出现浮点运算错误时产生
SIGKILL 9 终止 系统杀戮信号
SIGUSR1 10 终止 用户自定义信号
SIGSEGV 11 终止并产生转储文件 访问非法内存时产生
SIGUSR2 12 终止 用户自定义信号
SIGPIPE 13 终止 向无读者的管道输入数据时产生
SIGALRM 14 终止 定时器到点时产生
SIGTERM 15 终止 系统终止信号
SIGSTKFLT 16 终止 已废弃
SIGCHLD 17 忽略 子进程暂停或终止时产生
SIGCONT 18 恢复运行 系统恢复运行信号
SIGSTOP 19 暂停 系统暂停信号
SIGTSTP 20 暂停 由控制终端发起的暂停信号
SIGTTIN 21 暂停 后台进程发起输入请求时控制终端产生该信号
SIGTTOU 22 暂停 后台进程发起输出请求时控制终端产生该信号
SIGURG 23 忽略 套接字上出现紧急数据时产生
SIGXCPU 24 终止并产生转储文件 处理器占用时间超出限制值时产生
SIGXFSZ 25 终止并产生转储文件 文件尺寸超出限制值时产生
SIGVTALRM 26 终止 由虚拟定时器产生
SIGPROF 27 终止 profiling定时器到点时产生
SIGWINCH 28 忽略 窗口大小变更时产生
SIGIO 29 终止 I/O变得可用时产生
SIGPWR 30 终止 启动失败时产生
SIGUNUSED 31 终止并产生转储文件 同SIGSYS

"缺省"这个词在计算机领域中通常对应英文单词 “default”。在中文里,"缺省"的意思是在没有特殊设置时系统或程序会采取的预设行为。而"默认"在中文中也有类似的含义,指的是在没有明确指定时所采用的预设选项或行为。

之所以使用"缺省"而不是"默认",主要是由于历史翻译的习惯。"default"这个词在英文中不仅有"默认"的意思,还有违约、拖欠等含义。在计算机领域的早期翻译中,由于翻译水平的限制,“default"被直译为了"缺省”。尽管这可能是一个翻译上的误差,但由于长期的使用,"缺省"这个词已经被广泛接受并用于表示默认的设置或操作1

  • 上表中罗列出来的信号的“值”,在x86、PowerPC和ARM平台下是有效的,但是别的平台的信号值也许跟这个表的不一致。

  • “备注”中注明的事件发生时会产生相应的信号,但并不是说该信号的产生就一定发生了这个事件。事实上,任何进程都可以使用函数kill()来产生任何信号。

  • 信号SIGKILLSIGSTOP是两个特殊的信号,他们不能被忽略、阻塞或捕捉, 只能按缺省动作来响应。

  • 换句话说,除了这两个信号之外的其他信号,接收信号的目标进程按照如下顺序来做出反应(进程对信号的3种处理方式):

    • A) 如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。否则进入B。
    • B) 如果该信号被捕捉,那么进一步判断捕捉的类型:
      • B1) 如果设置了响应函数,那么执行该响应函数。
      • B2) 如果设置为忽略,那么直接丢弃该信号。 否则进入C。
      • C) 执行该信号的缺省动作。

信号处理过程

  1. 接收信号
  2. 中断执行
  3. 执行信号处理函数/指令
  4. 恢复执行

信号的产生

首先明确信号的4种产生条件:

1.通过终端按键(组合键)产生信号

2.硬件异常产生的信号

3.调用系统函数向进程发信号

4.由软件条件产生信号

————通过终端按键(组合键)产生信号————

Ctrl + C 对应 信号2 SIGINT 默认动作为终止进程

ctrl + \对应 信号3 SIGQUIT 默认动作为终止进程并且Core Dump

这里解释一下核心转储 Core Dump

概念:当⼀个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。也叫核心转储,帮助开发者进行调试,在程序崩溃时把内存数据dump到硬盘上,让gdb识别 。

一个进程允许产生多大的core文件取决于进程的 Resource Limit(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。

用ulimit -a命令查看系统中的软硬件资源限制

其中core file size = 0,也就印证了上边的说法。

但我们在开发调试阶段可以用ulimit命令改变这个限制, 允许产生core文件:ulimit -c 1024,允许core⽂件最大为1024K

更改后再次运行程序就可以看到core文件,其文件名后边的数字就是进程的pid号。

进程异常终止通常是因为有 Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做 Post-mortem Debug(事后调试)。 此时就可以使用core-file core.xxxx 就可以定位到是什么引起的程序退出原因。

Ctrl + C 为例子,运行程序在命令后面加一个"&"符号,程序运行命令后面加个&可以放到后台运行,形成后台进程。

  1. 用命令查看时,发现后台进程STAT状态栏是R,所以有+表示前台进程,无+表示后台进程。
  2. Ctrl + C 产生的信号只能发给前台进程。因为后台进程使Shell不必等待进程结束就可以接受新的命令,启动新的进程。而前台进程运行时占用SHELL,它运行的时候SHELL不能接受其他命令。
  3. Shell可以同时运行一个前台进程和任意多个后台进程。

————-硬件异常产生的信号————

硬件异常产生的信号会由系统硬件进行检测,比如进程中执行除以0的指令会导致ALU异常,或者进程中访问了非法内存地址会导致MMU异常,此时内核会发送给进程相关的信号。

————调用系统函数向进程发信号————

kill

功能 向指定进程或者进程组发送一个指定的信号
头文件 #include <sys/types.h>
#include <signal.h>
原型 int kill(pid_t pid, int sig);
参数 pid
小于-1:信号将被发送给组ID等于-pid的进程组里面的所有进程
-1:信号将被发送给所有进程(如果当前进程对其有权限)
0:信号将被发送给与当前进程同一个进程组内的所有进程
大于0:信号将被发送给PID等于pid的指定进程
sig:要发送的信号
返回值 成功:0
失败:-1
备注

signal

功能 捕捉一个指定的信号,即预先为某信号的到来做好准备
头文件 #include <signal.h>
原型 void (*signal(int sig, void (*func)(int)))(int);
参数 - sig:要捕捉的信号
- func
- SIG_IGN:捕捉动作为忽略该信号
- SIG_DFL:捕捉动作为执行该信号的缺省动作
- void (*p)(int):捕捉动作为执行由p指向的信号响应函数
返回值 成功:返回最近一次调用该函数时第二个参数的值
失败:SIG_ERR
备注 对一个信号的“捕捉”可以重复进行

raise

功能 自己给自己发送一个指定的信号
头文件 #include <signal.h>
原型 int raise(int sig);
参数 sig:要唤醒(发送)的信号
返回值 成功:0
失败:非0
备注

pause

功能 将本进程挂起,直到收到一个信号
头文件 #include <unistd.h>
原型 int pause(void);
参数
返回值 收到非致命信号或者已经被捕捉的信号:-1
收到致命信号导致进程异常退出:不返回
备注

信号集操作函数簇

功能 信号集操作函数簇:
1. sigemptyset():将信号集清空
2. sigfillset():将所有信号添加到信号集中
3. sigaddset():将指定的一个信号添加到信号集中
4. sigdelset():将指定的一个信号从信号集中剔除
5. sigismember():判断一个指定的信号是否被信号集包含
头文件 #include <signal.h>
原型 int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
参数 set:信号集
signum:要添加,剔除,或判断的信号
返回值 成功:sigismember()返回1,其余函数返回0
失败:sigismember()返回0,其余函数返回-1
备注

sigprocmask

功能 阻塞或者解除阻塞一个或者多个信号
头文件 #include <signal.h>
原型 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数 how
- SIG_BLOCK:在原有阻塞的信号基础上,再添加set中的信号
- SIG_SETMASK:将原有阻塞的信号,替换为set中的信号
- SIG_UNBLOCK:在原有阻塞的信号基础上,解除set中的信号
set:信号集
oldset:原有的信号集
返回值 成功:0
失败:-1
备注 对原有的阻塞信号不感兴趣,可以将oldset设置为NULL。

————通过终端发送指令产生信号————

用户除了在程序中调用函数发送信号之外,还可以直接在终端中使用shell命令。

————由软件条件产生信号 ————

当内核检测到某种软件条件发生时也可以通过信号通知进程,例如内核检测到闹钟超时则会产生SIGALRM信号,或者当内核检测到进程向读端已关闭的管道写数据时就产生SIGPIPE信号。

Linux系统下提供了一个叫做alarm()的函数接口,alarm翻译为中文是闹钟的意思,也就是说该函数可以设置内核定时器的时间,时间是以秒为单位的,当设置的秒数到达时,就相当于闹钟时间达到,此时内核检测到闹钟到达之后就会向调用该函数的进程发送SIGALRM,该信号的默认含义是终止进程。

alarm

功能 设置一个闹钟,经过指定的时间后发送SIGALRM信号给调用进程
头文件 #include <unistd.h>
原型 unsigned int alarm(unsigned int seconds);
参数 seconds:设置闹钟时间(以秒为单位),0表示取消之前设置的闹钟
返回值 返回距离上一个闹钟剩余的秒数,如果没有设置过闹钟则返回0
备注 当指定的时间到达时,系统会向调用进程发送SIGALRM信号。

sysstem -V IPC

消息队列、共享内存和信号量被统称为system-VIPC,V是罗马数字5,是UnixAT&T分支的其中一个版本,一般习惯称呼他们为IPC对象,这些对象的操作接口都比较类似,在系统中他们都使用一种叫做key的键值来唯一标识,而且他们都是“持续性”资源——即他们被创建之后,不会因为进程的退出而消失,而会持续地存在,除非调用特殊的函数或者命令删除他们。

跟文件类型,进程每次“打开”一个IPC对象,就会获得一个表征这个对象的ID,进而再使用这个ID来操作这个对象。IPC对象key是唯一的,但是ID是可变的。key类似于文件的路径名,ID类似于文件的描述符。

ipcmk命令

用于创建System V IPC资源,包括共享内存段、消息队列和信号量数组。

选项:

  • -M, --shmem size:创建指定大小的共享内存段。
  • -Q, --queue:创建消息队列。
  • -S, --semaphore number:创建包含指定数量元素的信号量数组。
  • -p, --mode mode:设置资源的访问权限,默认是0644。
  • -h, --help:显示帮助文本并退出。
  • -V, --version:打印版本信息并退出。

示例:

ipcmk -M 1024 # 创建一个1024字节的共享内存段
ipcmk -Q      # 创建一个消息队列
ipcmk -S 10   # 创建一个包含10个信号量的信号量数组

ipcs命令

用于显示系统中当前存在的IPC资源信息。

选项:

  • -m:查看共享内存信息。
  • -q:查看消息队列信息。
  • -s:查看信号量信息。
  • -a:显示所有IPC资源信息。
  • -c:查看IPC的创建者和所有者。
  • -l:查看IPC资源的限制信息。
  • -p:查看IPC资源的创建者和使用的进程ID。
  • -t:查看最新调用IPC资源的详细时间。
  • -u:查看IPC资源状态汇总信息。

示例:

ipcs -m # 列出共享内存信息
ipcs -q # 列出消息队列信息
ipcs -s # 列出信号量信息
ipcs -a # 列出所有IPC资源信息

ipcrm命令

用于删除IPC资源,释放相关的资源。

选项:

  • -M shmkey:移除用shmkey创建的共享内存段。
  • -m shmid:移除用shmid标识的共享内存段。
  • -S semkey:移除用semkey创建的信号量。
  • -s semid:移除用semid标识的信号量。
  • -Q msgkey:移除用msgkey创建的消息队列。
  • -q msgid:移除用msgid标识的消息队列。
  • -a:删除所有IPC资源

ftok

功能 获取当前一个未用的IPC的key
原型 key_t ftok(const char *pathname, int proj_id);
头文件 #include <sys/types.h>
#include <sys/ipc.h>
参数 pathname - 一个存在的文件系统路径
proj_id - 一个非零的整数
返回值 成功 - 返回一个合法的、当前未使用的IPC键值
失败 - 返回-1
备注 ftok函数是用来生成一个System V IPC键值的标准方法。该键值可以用于msggetsemgetshmget函数,以创建或访问IPC对象。
  • 如果两个参数相同,那么产生的key值也相同。

  • 系统中只有一套key标识,也就是说,不同类型的IPC对象也不能重复。

  • 在man手册中提到ftok()函数生成的键值key的组成:proj_id的低8位+ 设备编号的低8位+ inode编号的低16位。

ftok()函数的第一个参数指的是系统中已经存在并且可以访问的一个文件的路径,用户可以指定一个文件,但是该文件必须存在且可以被访问,其实就是为了得到文件的属性信息中的inode编号和设备编号,因为Linux系统中一个文件的inode编号是唯一的。

ftok()函数的第二个参数指的是项目ID,这个可以由用户进行指定,虽然参数proj_id的类型是整型int,但是只会用到最低8it,所以这个参数的范围其实是1~255,因为这个参数的值必须是非0值。

消息队列

消息队列提供一种带有数据标识的特殊管道,使得每一段被写入的数据都变成带标识的 消息,读取该段消息的进程只要指定这个标识就可以正确地读取,而不会受到其他消息的干 扰,从运行效果来看,一个带标识的消息队列,就像多条并存的管道一样。

image-20240528105036710

消息队列的使用方法一般是:

发送者:

  • 获取消息队列的ID

  • 将数据放入一个附带有标识的特殊的结构体,发送给消息队列。

接收者:

  • 获取消息队列的ID

  • 将指定标识的消息读出。

msgget

功能 获取消息队列的ID
头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
原型 int msgget(key_t key, int msgflg);
参数 key - 消息队列的键值
msgflg - 操作标志:
IPC_CREAT - 如果key对应的消息队列不存在,则创建该对象
IPC_EXCL - 如果该key对应的消息队列已经存在,则报错
mode - 消息队列的访问权限(八进制,如0644)
返回值 成功 - 返回该消息队列的ID
失败 - 返回-1
备注 如果key指定为IPC_PRIVATE,则会自动产生一个随机未用的新键值
  • 其中IPC_CREAT指的是如果消息队列不存在则创建,IPC_EXCL指的是如果消息队列存在则表示函数调用失败。 一般是进行位或操作。

  • 可以指定消息队列的权限,权限的结构和open函数的mode类型,采用八进制表示,只不过消息队列不需要设置执行权限,所以权限设置为0644即可。

msgsnd msgrcv

功能 发送、接收消息
头文件 <sys/types.h>
<sys/ipc.h>
<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);
参数 - msqid:发送、接收消息的消息队列ID
- msgp:要发送的数据、要接收的数据的存储区域指针
- msgsz:要发送的数据、要接收的数据的大小
- msgtyp:这是 msgrcv 独有的参数,代表要接收的消息的标识
- msgflg
- IPC_NOWAIT:非阻塞读出、写入消息
- MSG_EXCEPT:读取标识不等于 msgtyp 的第一个消息
- MSG_NOERROR:消息尺寸比 msgsz 大时,截断消息而不报错
- 0:默认接收模式,在MSG中无指定类型消息时阻塞
返回值 成功:
- msgsnd:0
- msgrcv:真正读取的字节数
失败:-1
备注 消息队列默认的属性是阻塞的,也就是当待写入的消息的长度大于消息队列剩余空间时,默认阻塞,直到消息队列的容量足够容纳时会解除阻塞。
  • 用户创建的消息队列是有默认容量的,默认容量是16384字节,可以通过msg.h得到,用户在向消息队列写入数据的时候要考虑消息队列的容量

    image-20240528130059744

  • msgsnd()函数的第二个参数msgp指的是一个指向struct msgbuf类型的结构体指针,该结构体中有两个成员,其中一个成员mtype指的是消息类型,必须是一个大于0的正整数,另一个成员mtext指的是消息正文,类型可以是数组或者其他结构。

    • 发送消息时,消息必须被组织成以下形式:

      structmsgbuf 
      {
          longmtype;		 //消息的标识 
          charmtext[1]; 	 //消息的正文 
      };   //发送出去的消息必须以一个long型数据打头,作为该消息的标识,后面的数据则没有要求。
      
    • 消息的标识可以是任意长整型数值,但不能是0

    • 参数msgsz是消息中正文的大小,不包含消息的标识,可以设置为0,表示消息正文的长度为0

  • msgtyp:这是 msgrcv 独有的参数,代表要接收的消息的标识

  • 等于0:指的是不区分类型,直接读取MSG中的第一个消息。

  • 大于0:读取类型为指定msgtyp的第一个消息(若msgflg被配置了MSG_EXCEPT则读取除了类型为msgtyp的第一个消息)。

  • 小于0:读取类型小于等于msgtyp绝对值的第一个具有最小类型的消息。

    例如当MSG对象中有类型为3、1、5类型消息若干条,当msgtyp为-3时,类型为3的第一个消息将被读取。

示例代码

要求进程A创建一条消息队列之后向进程B发送SIGUSR1信号,进程B收到该信号之后打开消息队列并把进程的PID作为消息写入到消息队列中,要求进程B在写入消息之后,发SIGUSR2信号给进程A,进程A收到该信号则从消息队列中读取消息并输出消息正文的内容。

msg_A.c

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/msg.h>

struct msgbuf {   //带标识的结构体
    long mtype;
    int  mtext;
};

volatile sig_atomic_t flags = 0;

void handle_signal(int sig) {
    flags = 1;
}

int main(int argc, char** argv) {

    //1.打开一个消息队列
    key_t key = ftok(".", 3);
    int msgid = msgget(key, IPC_CREAT | 0644); //获取消息队列ID
    if (msgid == -1) {
        fprintf(stderr, "msgget error, errno: %d, %s\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }

    //2.捕捉SIGUSR2信号
    signal(SIGUSR2, handle_signal); // 将SIGUSR2的响应动作设置为执行函数handle_signal()

    //3.阻塞等待输入B的pid,等待发送
    printf("please input pid of process B: ");
    int a;
    scanf("%d", &a);
    kill(a, SIGUSR1);

    //4.等待信号到来
    while (!flags) {
        pause(); // 暂停进程,等待信号到来...
    }

    //5.从消息队列读取信息
    struct msgbuf buf;
    bzero(&buf, sizeof(buf));
    if (msgrcv(msgid, &buf, sizeof(buf.mtext), 1, 0) == -1) {
        fprintf(stderr, "msgrcv error, errno: %d, %s\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }
    printf("msg is %d\n", buf.mtext);

    flags = 0;

    return 0;
}

msg_B.c

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/msg.h>

struct msgbuf {   // 带标识的结构体
    long mtype;
    int  mtext;
};

volatile sig_atomic_t flags = 0;

void handle_signal(int sig) {
    flags = 1;
}

int main(int argc, char** argv) {
    
    // 1. 打开一个消息队列
    key_t key = ftok(".", 3);
    int msgid = msgget(key, IPC_CREAT | 0644); // 获取消息队列ID
    if (msgid == -1) {
        fprintf(stderr, "msgget error, errno: %d, %s\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }

    // 2. 捕捉SIGUSR1信号
    signal(SIGUSR1, handle_signal); // 将SIGUSR1的响应动作设置为执行函数handle_signal()

    // 3. 暂停进程,等待信号到来...
    pause();

    // 4. 发送队列信息
    if (flags) {
        // 从消息队列发送信息
        struct msgbuf buf;
        buf.mtype = 1;
        buf.mtext = getpid();

        if (msgsnd(msgid, &buf, sizeof(buf.mtext), 0) == -1) {
            fprintf(stderr, "msgsnd error, errno: %d, %s\n", errno, strerror(errno));
            exit(EXIT_FAILURE);
        }

        // 发送SIGUSR2信号
        // 阻塞等待输入A的pid,等待发送
        printf("please input pid of process A: ");
        int a;
        scanf("%d", &a);
        kill(a, SIGUSR2);
        printf("kill is ok\n");
        flags = 0;
    }

    return 0;
}

msgctl

功能 设置或者获取消息队列的相关属性
头文件 <sys/types.h>
<sys/ipc.h>
<sys/msg.h>
原型 int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数 - msqid:消息队列ID
- cmd
- IPC_STAT:获取该消息队列的信息,储存在结构体 msqid_ds
- IPC_SET:设置该消息队列的信息,储存在结构体 msqid_ds
- IPC_RMID:立即删除该消息队列,并唤醒所有阻塞在该消息队列上的进程,忽略第三个参数
- IPC_INFO:获得关于当前系统中消息队列的限制值信息
- MSG_INFO:获得关于当前系统中消息队列的相关资源消耗信息
- MSG_STAT:同 IPC_STAT,但 msgid 为该消息队列在内核中记录所有消息队列信息的数组的下标,因此通过迭代所有的下标可以获得系统中所有消息队列的相关信息
- buf:相关信息结构体缓冲区
返回值 成功:
- IPC_STATIPC_SETIPC_RMID:0
- IPC_INFO:内核中记录所有消息队列信息的数组的下标最大值
- MSG_INFOMSG_STAT:返回消息队列的ID
失败:-1
备注
  • IPC_STAT获得的属性信息以及权限相关的信息结构体

    //`IPC_STAT`获得的属性信息存在以下结构体内
    struct msqid_ds {
        struct ipc_perm msg_perm;   /* 权限相关信息 */
        time_t msg_stime;           /* 最后一次发送消息的时间 */
        time_t msg_rtime;           /* 最后一次接收消息的时间 */
        time_t msg_ctime;           /* 最后一次状态变更的时间 */
        unsigned long __msg_cbytes; /* 当前消息队列中的数据尺寸 */
        msgqnum_t msg_qnum;         /* 当前消息队列中的消息个数 */
        msglen_t msg_qbytes;        /* 消息队列的最大数据尺寸 */
        pid_t msg_lspid;            /* 最后一个发送消息的进程PID */
        pid_t msg_lrpid;            /* 最后一个接收消息的进程PID */
    };
    //权限相关的信息结构体
    struct ipc_perm {
        key_t __key;          /* 当前消息队列的键值key */
        uid_t uid;            /* 当前消息队列所有者的有效UID */
        gid_t gid;            /* 当前消息队列所有者的有效GID */
        uid_t cuid;           /* 当前消息队列创建者的有效UID */
        gid_t cgid;           /* 当前消息队列创建者的有效GID */
        unsigned short mode;  /* 消息队列的读写权限 */
        unsigned short __seq; /* 序列号 */
    };
    
  • 当使用IPC_INFO时,需要定义一个如下结构体来获取系统关于消息队列的限制值信息,并且将这个结构体指针强制类型转化为第三个参数的类型。

    struct msginfo
     {
     int msgpool; /* 系统消息总尺寸(千字节为单位)最大值 */
     int msgmap; /* 系统消息个数最大值 */
     int msgmax; /* 系统单个消息尺寸最大值 */
     int msgmnb; /* 写入消息队列字节数最大值 */
     int msgmni; /* 系统消息队列个数最大值 */
     int msgssz; /* 消息段尺寸 */
     int msgtql; /* 系统中所有消息队列中的消息总数最大值 */
    unsigned short int msgseg; /* 分配给消息队列的数据段的最大值 */
     };
    
  • 当使用选项MSG_INFO时,跟IPC_INFO一样也是获得一个msginfo结构体的 信息,但是有如下几点不同:

    • 成员msgpool记录的是系统当前存在的MSG的个数总和
    • 成员msgmap记录的是系统当前所有MSG中的消息个数总和
    • 成员msgtql记录的是系统当前所有MSG中的所有消息的所有字节数总和

示例代码

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

int main() {
    key_t key;
    int msqid;
    struct msqid_ds buf;

    // 生成唯一键值
    key = ftok("progfile", 65);

    // 创建消息队列
    msqid = msgget(key, IPC_CREAT | 0666);
    if (msqid < 0) {
        perror("msgget");
        exit(1);
    }

    // 获取消息队列信息
    if (msgctl(msqid, IPC_STAT, &buf) == -1) {
        perror("msgctl IPC_STAT");
        exit(1);
    } else {
        printf("Permissions: %o\n", buf.msg_perm.mode);
    }

    // 设置消息队列信息
    buf.msg_perm.mode = 0644; // 修改权限
    if (msgctl(msqid, IPC_SET, &buf) == -1) {
        perror("msgctl IPC_SET");
        exit(1);
    }

    // 删除消息队列
    if (msgctl(msqid, IPC_RMID, NULL) == -1) {
        perror("msgctl IPC_RMID");
        exit(1);
    }

    return 0;
}

共享内存

共享内存是效率最高的IPC,但这些进程必须小心谨慎地操作这块裸露的共享内存,做好诸如同步互斥等工作。

共享内存一般不能单独使用,而要配合信号量、 互斥锁等协调机制,让各个进程在高效交换数据的同时,不会发生数据践踏、破坏等意外。

当进程P1向其虚拟内存中的区域1写入数据时,进程2就能同时在其虚拟内存空间的区域2看见这些数据,中间没有经过任何的转发,效率极高。

image-20240528140335341

使用共享内存的一般步骤是:

  • 获取共享内存对象的ID
  • 将共享内存映射至本进程虚拟内存空间的某个区域
  • 当不再使用时,解除映射关系
  • 当没有进程再需要这块共享内存时,删除它。

shmget

功能 获取共享内存的ID
头文件 #include <sys/ipc.h>
#include <sys/shm.h>
原型 int shmget(key_t key, size_t size, int shmflg);
参数 参数名称
key
size
shmflg
返回值 成功:该共享内存的ID
失败:-1
备注 如果key指定为IPC_PRIVATE,则会自动产生一个随机未用的新键值

shmat shmdt

功能 对共享内存进行映射,或者解除映射
头文件 #include <sys/types.h>
#include <sys/shm.h>
原型 void* shmat(int shmid, const void* shmaddr, int shmflg);
int shmdt(const void* shmaddr);
参数
shmid 共享内存ID
shmaddr shmat()
1. 如果为NULL,则系统会自动选择一个合适的虚拟内存空间地址去映射共享内存。
2. 如果不为NULL,则系统会根据shmaddr来选择一个合适的内存区域。
shmdt()
共享内存的首地址
shmflg SHM_RDONLY 以只读方式映射共享内存
SHM_REMAP 重新映射,此时shmaddr不能为NULL
SHM_RND 自动选择比shmaddr小的最大页对齐地址
返回值 成功
共享内存的首地址
失败
-1
备注

shmctl

功能 获取或者设置共享内存的相关属性
头文件 #include <sys/ipc.h>
#include <sys/shm.h>
原型 int shmctl(int shmid, int cmd, struct shmid_ds* buf);
参数
shmid 共享内存ID
cmd
IPC_STAT 获取属性信息,放置到buf中
IPC_SET 设置属性信息为buf指向的内容
IPC_RMID 将共享内存标记为“即将被删除”状态
IPC_INFO 获得关于共享内存的系统限制值信息
SHM_INFO 获得系统为共享内存消耗的资源信息
SHM_STAT 同IPC_STAT,但shmid为该SHM在内核中记录所有SHM信息的数组的下标,因此通过迭代所有的下标可以获得系统中所有SHM的相关信息
SHM_LOCK 禁止系统将该SHM交换至swap分区
SHM_UNLOCK 允许系统将该SHM交换至swap分区
buf 属性信息结构体指针
返回值
成功 IPC_INFO 内核中记录所有SHM信息的数组的下标最大值
SHM_INFO 下标值为shmid的SHM的ID
失败 -1
备注
  1. IPC_STAT 获得的属性信息被存放在 struct shmid_ds 结构体中。

    struct shmid_ds {
        struct ipc_perm shm_perm; /* 权限相关信息 */
        size_t shm_segsz;         /* 共享内存尺寸(字节) */
        time_t shm_atime;         /* 最后一次映射时间 */
        time_t shm_dtime;         /* 最后一个解除映射时间 */
        time_t shm_ctime;         /* 最后一次状态修改时间 */
        pid_t shm_cpid;           /* 创建者PID */
        pid_t shm_lpid;           /* 最后一次映射或解除映射者PID */
        shmatt_t shm_nattch;      /* 映射该 SHM 的进程个数 */
    };
    //权限相关信息结构体
    struct ipc_perm {
        key_t __key;                 /* 该 SHM 的键值 key */
        uid_t uid;                   /* 所有者的有效 UID */
        gid_t gid;                   /* 所有者的有效 GID */
        uid_t cuid;                  /* 创建者的有效 UID */
        gid_t cgid;                  /* 创建者的有效 GID */
        unsigned short mode;         /* 读写权限 + SHM_DEST + SHM_LOCKED 标记 */
        unsigned short __seq;        /* 序列号 */
    };
    
    
  2. 当使用 IPC_RMID 后,上述结构体 struct ipc_perm 中的成员 mode 将可以检测出 SHM_DEST,但 SHM 并不会被真正删除,要等到 shm_nattch 等于 0 时才会被真正删除。IPC_RMID 只是为删除做准备,而不是立即删除。

  3. 当使用 IPC_INFO 时,需要定义一个如下结构体来获取系统关于共享内存的限制值信息,并且将这个结构体指针强制类型转化为第三个参数的类型。

    struct shminfo {
        unsigned long shmmax; /* 一块 SHM 的尺寸最大值 */
        unsigned long shmmin; /* 一块 SHM 的尺寸最小值(永远为 1) */
        unsigned long shmmni; /* 系统中 SHM 对象个数最大值 */
        unsigned long shmseg; /* 一个进程能映射的 SHM 个数最大值 */
        unsigned long shmall; /* 系统中 SHM 使用的内存页数最大值 */
    };
    
  4. 使用选项 SHM_INFO 时,必须保证宏 _GNU_SOURCE 有效。获得的相关信息被存放在如下结构体当中:

    struct shm_info {
        int used_ids;                /* 当前存在的 SHM 个数 */
        unsigned long shm_tot;       /* 所有 SHM 占用的内存页总数 */
        unsigned long shm_rss;       /* 当前正在使用的 SHM 内存页个数 */
        unsigned long shm_swp;       /* 被置入交换分区的 SHM 个数 */
        unsigned long swap_attempts; /* 已废弃 */
        unsigned long swap_successes;/* 已废弃 */
    };
    
  5. 注意:选项 SHM_LOCK 不是锁定读写权限,而是锁定 SHM 能否与 swap 分区发生交换。一个 SHM 被交换至 swap 分区后如果被设置了 SHM_LOCK,那么任何访问这个 SHM 的进程都将会遇到页错误。进程可以通过 IPC_STAT 后得到的 mode 来检测 SHM_LOCKED 信息。

示例

/*  如下为进程A
练习:设计三个程序,要求三个程序申请一块共享内存,并分别映射到各自进程的地址空间,
进程A和进程B对共享内存段中的数据进行修改,然后进程C不断输出共享内存段中的数据,
并观察效果,要求实现进程间的互斥,避免竞争。提示:信号
*/

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

int flag = 0;

void handle_signal(int sig) {
    flag = 1;
}

int main(int argc, char** argv) {

    //1.打开共享内存段,如果不存在则创建,如果存在则打开
    key_t key = ftok(".", 3);
    int shmid = shmget(key,256, IPC_CREAT | 0644); //获取共享内存ID
    if (shmid == -1) {
        fprintf(stderr, "shmid error, errno: %d, %s\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }

	//2.把共享内存段连接到进程空间  shmat()  默认是可读可写  默认会初始化为0
    char *p = shmat(shmid,NULL,0); 
    if (p == (void *)-1)
	{
		fprintf(stderr, "shmat error,errno:%d,%s\n",errno,strerror(errno));
		return -1;
	}
		
    bzero(p,256);

    //3.捕捉SIGUSR1信号
    signal(SIGUSR1, handle_signal); // 将SIGUSR1的响应动作设置为执行函数handle_signal()


    while (1)
    {
        if(flag)
        {
        strcpy(p,"HELLO WORLD !!!");
        printf("Process A wrote: %s\n", p); // 添加调试信息
        flag = 0; 
        }

    }

    return 0;
}

信号量

信号量的本质是数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。

信号量在Linux系统中的作用不是为了实现进程间信息交互,而是为了实现资源协同而设计的。

  • 临界资源(critical resources):多个进程或线程共同访问的资源

  • 临界区(criticalzone):访问临界资源的代码段

    • 进入临界区:开始执行临界区代码段
    • 退出临界区:临界区的代码段执行结束后
    • 死锁(Dead Lock):只申请资源,不释放资源,就会导致所有的进程阻塞。
      • 申请资源及时归还避免死锁。
  • 互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

  • 同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源

把申请信号量的操作称为P(Passeren)操作,把归还信号量的操作称为V(vrijgeven)操作。P\V都属于原子操作

  • P操作

P操作指的是请求分配资源,也就是说每完成一次P操作,则信号量资源会减一,当信号量资源为0时,如果有进程或者线程请求分配资源,则会导致阻塞,直到有进程释放信号量。

  • V操作

V操作指的是请求释放资源,也就是说每完成一次V操作,则信号量资源会加一,使用完资源之后应该及时释放,否则其他进程申请资源时如果资源不够,则会导致阻塞。

image-20240528190944166

semget

功能 获取信号量的ID
头文件 <sys/types.h>
<sys/ipc.h>
<sys/sem.h>
原型 int semget(key_t key, int nsems, int semflg);
参数 - key: 信号量的键值
- nsems: 信号量元素的个数
- semflg:
- IPC_CREAT: 如果 key 对应的信号量不存在,则创建之
- IPC_EXCL: 如果 key 对应的信号量已存在,则报错
- mode: 信号量的访问权限(八进制,如0644)
返回值 成功:该信号量的ID
失败:-1
备注 创建信号量时,还受到以下系统信息的影响:
1,SEMMNI:系统中信号量的总数最大值。
2,SEMMSL:每个信号量中信号量元素的个数最大值。
3,SEMMNS:系统中所有信号量中的信号量元素的总数最大值。
Linux中,以上信息在/proc/sys/kernel/sem中可查看。

semop

功能 对信号量进行P/V操作,或者等零操作
头文件 <sys/types.h>
<sys/ipc.h>
<sys/sem.h>
原型 int semop(int semid, struct sembuf sops[], unsigned nsops);
参数 - semid: 信号量ID
- sops: 信号量操作结构体数组
- nsops: 结构体数组元素个数
返回值 成功:0
失败:-1
备注

信号量集合

image-20240528191804698

System V信号量集中,每个信号量都具有以下关联的值:

unsigned short semval;   /* 信号量值 */
unsigned short semzcnt;  /* 等待值为零的进程数 */
unsigned short semncnt;  /* 等待值增加的进程数 */
pid_t sempid;            /* 上次操作信号量的进程的PID */

struct sembuf 是一个结构体,它指定了要在单个信号量上执行的操作。这个结构体的元素是 semop() 函数使用的类型,semop() 函数在由 semid 指示的信号量集合中选定的信号量上执行操作。数组中的每个 nsops 元素都指向 struct sembuf

struct sembuf {
    unsigned short int sem_num;  // 信号量的序号从0~nsems-1(数组下标)
    short int sem_op;            // 对信号量的操作,>0, 0, <0
    short int sem_flg;           // 操作标识:0,IPC_WAIT,SEM_UNDO
};
  • sem_num 标识信号量集中的第几个信号量,其中 0 表示第一个,1 表示第二个,nsems - 1 表示最后一个。
  • sem_op标识对信号量进行的操作类型:
    • sem_op > 0:执行挂出操作,将 sem_op 的值加到信号量的当前值上。
      • 如果SEM_UNDO被设置了,那么该V操作将会被系统记录。V操作永远不会导致进程阻塞。
    • sem_op < 0:执行等待操作,当信号量的当前值 semval >= -sem_op 时,减去 sem_op 的绝对值,为线程分配对应数目的资源。
      • 如果IPC_NOWAIT被设置,则立即出错返回并将errno设置为 EAGAIN,否则将使得进程进入睡眠,直到以下情况发生:
        • semval变为0。
        • 信号量被删除。(将导致semop( )出错退出,错误码为EIDRM)
        • 收到信号。(将导致semop( )出错退出,错误码为EINTR)
    • sem_op = 0:希望信号量变为0,如果为0则立即返回,否则阻塞线程。
      • 如果SEM_UNDO被设置 了,那么该P操作将会被系统记录。
      • 如果semval小于sem_op的绝对值并且设置了 IPC_NOWAIT,那么semop( )将会出错返回且将错误码置为EAGAIN,否则将使得进程进入睡眠,直到以下情况发生:
        • semval的值变得大于或者等于sem_op的绝对值。
        • 信号量被删除。(将导致semop()出错退出,错误码为EIDRM)
        • 收到信号。(将导致semop( )出错退出,错误码为EINTR)
  • sem_flg信号量操作的属性标志:
    • 如果为0,表示正常操作。
    • 如果为 IPC_NOWAIT,使对信号量的操作非阻塞。
    • 如果为 SEM_UNDO,维护进程对信号量的调整值,以便进程结束时恢复信号量的状态。

与单个信号量相关的值

  • semval:信号量的当前值。
  • semncnt:等待信号量变为大于当前值的线程数。
  • semzcnt:等待信号量变为0的线程数。

semctl

功能 获取或者设置信号量的相关属性
头文件 <sys/types.h>
<sys/ipc.h>
<sys/sem.h>
原型 int semctl(int semid, int semnum, int cmd, ...);
参数 - semid: 信号量ID
- semnum: 信号量元素序号(数组下标)
- cmd: 指令
指令 - IPC_STAT: 获取属性信息
- IPC_SET: 设置属性信息
- IPC_RMID: 立即删除该信号量,参数semnum将被忽略
- IPC_INFO: 获得关于信号量的系统限制值信息
- SEM_INFO: 获得系统为信号量消耗的资源信息
- SEM_STAT: 同IPC_STAT,但shmid为该信号量在内核中记录所有信号量信息的数组的下标,通过迭代所有的下标可以获得系统中所有信号量的相关信息
- GETALL: 返回所有信号量元素的值,参数semnum将被忽略
- GETNCNT: 返回正阻塞在对该信号量元素P操作的进程总数
- GETPID: 返回最后一个对该信号量元素操作的进程PID
- GETVAL: 返回该信号量元素的值
- GETZCNT: 返回正阻塞在对该信号量元素等零操作的进程总数
- SETALL: 设置所有信号量元素的值,参数semnum将被忽略
- SETVAL: 设置该信号量元素的值
返回值 - 成功:
- GETNCNT: semncnt
- GETPID: sempid
- GETVAL: semval
- GETZCNT: semzcnt
- IPC_INFO: 内核中记录所有信号量信息的数组的下标最大值
- SEM_INFO: 同IPC_INFO
- SEM_STAT: 内核中记录所有信号量信息的数组,下标为semid的信号量的ID
- 其他:0
失败:-1
备注
  • 根据cmd的不同,可能需要第四个参数,该参数为一个联合体

    union semun

    union semun {
        int val;                  /* 当cmd为SETVAL时使用 */
        struct semid_ds *buf;     /* 当cmd为IPC_STAT或IPC_SET时使用 */
        unsigned short *array;    /* 当cmd为GETALL或SETALL时使用 */
        struct seminfo *__buf;    /* 当cmd为IPC_INFO时使用 */
    };
    
  • IPC_STAT和IPC_SET需要用到以下属性信息结构体

    struct semid_ds

    struct semid_ds {
        struct ipc_perm sem_perm; /* 权限相关信息 */
        time_t sem_otime;         /* 最后一次semop()的时间 */
        time_t sem_ctime;         /* 最后一次状态改变时间 */
        unsigned short sem_nsems; /* 信号量元素个数 */
    };
    
    
    • 权限结构体如下struct ipc_perm
    struct semid_ds {
        struct ipc_perm sem_perm; /* 权限相关信息 */
        time_t sem_otime;         /* 最后一次semop()的时间 */
        time_t sem_ctime;         /* 最后一次状态改变时间 */
        unsigned short sem_nsems; /* 信号量元素个数 */
    };
    
    
  • IPC_INFO使用提供以下结构体,使用SEM_INFO时,semusz和semaem含义发生改变

    struct seminfo

    //当使用IPC_INFO时
    struct seminfo {
        int semmap;   /* 当前系统信号量总数 */
        int semmni;   /* 系统信号量个数最大值 */
        int semmns;   /* 系统所有信号量元素总数最大值 */
        int semmnu;   /* 信号量操作撤销结构体个数最大值 */
        int semmsl;   /* 单个信号量中的信号量元素个数最大值 */
        int semopm;   /* 调用semop()时操作的信号量元素个数最大值 */
        int semume;   /* 单个进程对信号量执行连续撤销操作次数的最大值 */
        int semusz;   /* 撤销操作的结构体的尺寸 */
        int semvmx;   /* 信号量元素的值的最大值 */
        int semaem;   /* 撤销操作记录个数最大值 */
    };
    //当使用SEM_INFO时
    struct seminfo {
        int semmap;   /* 此时代表系统当前存在的信号量的个数 */
        int semmni;   /* 系统信号量个数最大值 */
        int semmns;   /* 系统所有信号量元素总数最大值 */
        int semmnu;   /* 信号量操作撤销结构体个数最大值 */
        int semmsl;   /* 单个信号量中的信号量元素个数最大值 */
        int semopm;   /* 调用semop()时操作的信号量元素个数最大值 */
        int semume;   /* 单个进程对信号量执行连续撤销操作次数的最大值 */
        int semusz;   /* 此时代表系统当前存在的信号量中信号量元素的总数 */
        int semvmx;   /* 信号量元素的值的最大值 */
        int semaem;   /* 此时代表系统当前存在的信号量的个数 */
    };
    

示例

练习:设计一个程序,作为进程A,进程A专门创建一个信号量集,要求信号量集中有1个信号量,对信号量集合中的信号量进行设置,要求集合中的信号量的初值为1,然后再设计2个程序,分别是进程B和进程C,要求进程B和进程C使用进程A创建的信号量集合中的信号量实现互斥访问。 提示:进程A、进程B、进程C需要使用共享内存作为临界资源的访问。

image-20240528204925815

套接字

待补齐------

标签:IPC,int,通信,信号量,信号,Linux,进程,include
From: https://www.cnblogs.com/san39/p/18226780

相关文章

  • 系统编程——管道通信
    管道通信Linux系统提供了一种通信方式,名字叫做管道通信,顾名思义,管道是单向的,比如水管、燃气管道等,换个说法就是管道是采用半双工通信的,也就是同一时刻只能完成发送数据或者接收数据。匿名管道匿名管道的特点是没有名称,所以用户无法使用open来创建和打开,但是匿名管道进行数据读......
  • 【Linux】Linux环境基础开发工具的使用
    文章目录前言Linux软件包管理器-yum什么是软件包关于yum查看软件包如何安装软件卸载软件Linux编辑器-vimvim的基本概念vim不同模式的切换vim正常模式命令集vim底行模式命令集简单vim配置Linux编译器-gcc/g++gcc/g++的作用gcc/g++语法格式gcc如何完成-gcc执行编译步......
  • 利用Linux系统提供的和调度器相关的接口让进程或线程对某个处理器进行绑定
    目录设置进程与CPU的亲和性设置线程与CPU的亲和性设置进程与CPU的亲和性taskset命令允许你查看或设置运行中的进程的CPU亲和性(即该进程可以在哪些CPU上运行)。要将一个已经运行的进程(例如PID为1234的进程)绑定到CPU0和CPU1上,你可以使用:taskset-cp0,11234如果你正在启动一......
  • Linux编译——基于oebuild编译openEuler系统——新手向
    环境:Ubuntu20.04(镜像来自清华源)、已换源(来自清华源)、python3.8.10、pip3、Docker、oebuild。除此之外,只安装了Makefile、vim、net-tools、openssh-server、open-vm-tools、open-vm-tools-desktop,比较纯净的镜像。细则:Oebuildversion:v0.0.45.16Python3.8.10Dockerversi......
  • 嵌入式Linux shell编程实例
    1.输入两个数,实现两个数的相加(1)具体实现代码如下1#!/bin/bash2reada3readb4sum=$(($a+$b))5echo"$sum"(2)编辑完内容后按Esc键再输入:wq保存,回车退出,执行结果如下图;(3)上图示例为输入两个数,实现两个数相加和,输出结果。2.计算1~100的和(1)具体实现代码......
  • 嵌入式Linux命令基础
    一、命令概述1.命令本质 命令的特性:一般就是对应shell命令,每一个命令代表一个可执行程序,运行一个命令就相当于运行一个可执行代码。2.打开终端方法第一种方法:通过鼠标右键选择打开终端第二种方法:利用Ctrl+Alt+T快捷键的方式3.普通用户/超级用户切换方法(1)普通用户......
  • 给Linux的虚拟机设置别名,同事设置免密登录
    我这里有四台虚拟机:192.168.75.147192.168.75.148192.168.75.149192.168.75.150分别修改对应的/etc/hosts文件,每个文件中都添加上以下内容192.168.75.147node1192.168.75.148node2192.168.75.149node3192.168.75.150node4免密登录配置:要在Linux系统中实现多台服务器之间......
  • Linux上的redis的安装和配置
    上传redis压缩包到linux上解压文件命令: tar-zxvfredis-6.2.4.tar.gz 进入解压好的redis文件编译make安装到指定目录makeinstallPREFIX=/usr/local/redis从解压文件中复制配置文件到reids中cp/root/redis-6.2.4/redis.conf/usr/local/redis/bin/修改配置文件re......
  • Linux体系结构
    Linux的体系结构源自于Unix,主要分为三层,从内到外依次是:内核-->系统调用–>应用层。内核(kernel)负责两个功能:管理计算机硬件资源;为上层应用程序提供运行环境。系统调用(systemcalls):内核给上层应用程序提供的接口。库函数(libraryroutines):通常我们会把系统调......
  • Linux上安装MySQL
    1).准备工作在安装MySQL数据库之前,我们需要先检查一下当前Linux系统中,是否安装的有MySQL的相关服务(很多linux安装完毕之后,自带了低版本的mysql的依赖包),如果有,先需要卸载掉,然后再进行安装。A.通过rpm相关指令,来查询当前系统中是否存在已安装的mysql软件包,执行指令如下:rpm-qa......