解决进程间通信需要借助双方都可以访问的第三方
第三方可以是:
1:普通文件:P1进程<---文件--->P2进程
2.管道
3.信号
4.消息队列
5.信号量
6.共享内存
7.网络---socket
1.管道
管道:利用了文件系统接口(操作管道时:open read write close),只是文件内容(实体)存在内核/内存中(而不是存在与硬盘中)。
管道有两种:无名管道和有名管道
1.1无名管道
无名管道:没有名字,本质是没有i_node——i_no:de是一个文件的唯一标识,没有i_node在文件系统中就找不到这个文件。
无名管道的创建:pipe
NAME
pipe - create pipe
SYNOPSIS
#include <unistd.h>
int pipe(int pipefd[2]);
pipefd[2]:2个整型元素得数组--存储管道文件描述符
保存了管道的两端:pipefd[0] 读端 pipefd[1] 写端
返回值:
成功,返回0
失败,返回-1,并且errno被设置
PS:
-
无名管道在使用过程中,适用于亲缘进程之间的通信
-
管道的通信方式为半双工,同时进行收发容易造成自收自发。所以如果双方需要同时进行通信(全双工)则需要使用两根管道
-
通信方式:
全双工:通信双方可以同时进行数据接收和发送,比如:视频电话
半双工:通信双方,同一时刻,只能有一方进行发送,另一方接收,比如:对讲机
单工:通信双方中,一方固定为发送端,一方则固定为接收端。 信息只能沿一个方向传输,比如:广播
1.2 有名管道
有名管道:有名字,本质就是在文件系统存在i_node<能够通过路径进行访问>,可以用于非亲缘进程之间的通信。但是有名管道的内容(实体)仍然存在于内核中。
创建一个有名管道:mkfifo
NAME
mkfifo - make a FIFO special file (a named pipe) -- 命名/有名管道
SYNOPSIS
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname:带路径的文件名 --- 你要创建的有名管道 名字以及创建位置
"/home/china/test.pipe" OK
"./test.pipe" 不一定行 有名管道不能够放在共享文件夹中
mode:权限
①:S_IRUSR | S_IWUSR
S_IRGRP
S_IROTH
②:0664
返回值:
成功,返回0
失败,返回-1,并且errno被设置
有名管道 和 无名管道 数据收发方式类似,只是有名管道可以用于非亲缘进程之间的通信。同样的是不能同时进行收发。
/*
FIFO特殊文件(命名管道)类似于管道,不同之处在于它是作为文件系统的一部分访问的。它可以被多个进程打开,用于读或写。当进程通过FIFO交换数据时,内核在内部传递所有数据,而不将其写入文件系统。因此,FIFO特殊文件在文件系统上没有内容;文件系统条目仅仅用作参考点,以便进程可以使用文件系统中的名称访问管道。
内核为每个至少由一个进程打开的FIFO特殊文件维护一个管道对象。在数据可以传递之前,FIFO必须在两端打开(读和写)。通常,打开FIFO块,直到另一端也被打开。
进程可以在非阻塞模式下打开FIFO。在这种情况下,即使没有人在写端打开,为只读打开也会成功,而在ENXIO(没有这样的设备或地址)下,为只写打开失败,除非另一端已经打开。
在Linux下,为读写打开FIFO将在阻塞和非阻塞模式下都成功。POSIX不定义此行为。这可以用来打开一个FIFO写,而没有可用的读者。使用连接的两端与自己通信的进程应该非常小心,以避免死锁。
*/
/*
fifo 的两端都必须同时打开,fifo才能正常使用
以非阻塞的形式打开 读端,打开会成功,即使写端未打开
以非阻塞的形式打开 写端,打开会失败,并且 errno == ENXIO,除非读端被打开
*/
2.信号
信号也是IPC 通信手段之一,他不是用来传递数据,它的本质是一个整数,不同的信号值代表不同的含义。
# 可发送的信号
china@ubuntu:~$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
信号:
信号名 信号值 信号行为 具体内容
Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
控制终端 的挂起操作 或者控制终端进程死亡时
控制终端 上的所有进程都会收到 SIGHUP
SIGINT 2 Term Interrupt from keyboard
键盘输入 中断信号 cltr + c
SIGQUIT 3 Core Quit from keyboard
键盘输入 退出信号 cltr + \
SIGILL 4 Core Illegal Instruction
使用了非法指令
SIGABRT 6 Core Abort signal from abort(3)
调用 abort函数产生
SIGFPE 8 Core Floating-point exception
浮点运算异常
SIGKILL 9 Term Kill signal 干掉相应进程
SIGSEGV 11 Core Invalid memory reference
非法内存访问 段错误
SIGPIPE 13 Term Broken pipe: write to pipe with no
readers; see pipe(7)
往管道写入数据时 没有读端
SIGALRM 14 Term Timer signal from alarm(2)
闹钟信号 由alarm函数产生
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at terminal
ctrl+z
SIGTTIN 21,21,26 Stop Terminal input for background process
SIGTTOU 22,22,27 Stop Terminal output for background process
对信号的处理,通常有三种方式:
1.捕获这个信号:把一个信号和用户自定义的信号处理函数联系起来。
2.默认行为:收到这个信号,采用系统默认的行为。
3.忽略:收到这个信号不进行处理
2.2信号相关的API
捕获信号:关键是将原先系统默认的行为(函数地址)替换为用户自定义的函数——signal
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
/*
sighandler_t 新的类型 函数指针类型 可以定义指针变量 用于指向一个 没有返回值,.
且参数为int类型的函数
*/
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum:信号值 --- 你要捕获的信号
handler:函数地址 对应的函数 需要是无返回值 且参数为int类型
①:自定义的处理函数
②:SIG_IGN 忽略该信号
③:SIG_DFL 默认
返回值:
成功,返回之前的信号处理方式
失败,返回SIG_ERR,errno被设置
/* The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored SIGKILL SIGSTOP 不能被捕获 被阻塞 被忽略 */
kill:发送一个信号给对应的进程
NAME
kill - send signal to a process
SYNOPSIS
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
pid:pid 指定信号的接收进程
pid > 0 指定的进程
pid == 0 和调用进程同组的所有进程
pid == -1 发送给所有进程(有权限发送的)
pid < -1 发送给 进程组id == abs(pid)的那个组的所有进程
sig:指定发送的信号值
返回值:
成功,返回0
失败,返回-1,并且errno被设置
kill -9 1456 <=等价于=> kill(1456, 9);
alarm:闹钟 设置一个闹钟
NAME
alarm - set an alarm clock for delivery of a signal
SYNOPSIS
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
seconds:设置闹钟时长
返回值:
返回上一个闹钟的剩余秒数
PS:一个进程只有一个闹钟
raise:发送信号给自己 pause:等待一个信号
NAME
raise - send a signal to the caller
发送一个信号给自己
SYNOPSIS
#include <signal.h>
int raise(int sig);
sig:你要发送的那个信号的值
NAME
pause - wait for signal
等待一个信号
SYNOPSIS
#include <unistd.h>
int pause(void);
pause() returns only when a signal was caught and the signal-catching
function returned. In this case, pause() returns -1, and errno is set
to EINTR.
3.共享内存
共享内存:其实就是通过映射的方式使得多个进程可以同时访问同一块内存,换而言之就是将一块内存同时映射到多个进程的进程地址空间中,借此这些进程就可以通过这块内存进行数据交流。这块内存就称之为共享内存
管道和共享内存:
共享内存效率比管道要高
共享内存实现方式:
1.POSIX标准共享内存——mmap
2.System V标准 共享内存
3.1System V操作流程
1.创建一个System V IPC对象的Key——>ftok
2.创建/打开相应的IPC对象(msg、shm、sem)
3.映射到进程中
4.通过System V读写接口,进行数据传递
5.解除映射和关闭这个IPC对象
3.2相应API函数
ftok
NAME
ftok - convert a pathname and a project identifier to a System V IPC
key
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
pathname:路径名 --- 在文件系统中要存在
proj_id:工程ID号
返回值:
成功,返回一个 system V IPC唯一的key
失败,返回-1,并且errno被设置
shmget
NAME
shmget - allocates a System V shared memory segment
创建/打开一个共享内存
SYNOPSIS
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key:system V ipc对象的key
size:共享区域的大小
创建时,size != 0,size的大小,一般是4096的整数倍 PAGE_SIZE
打开一个已存在的共享内存时,size == 0
shmflg:打开方式
①:创建打开 IPC_CREAT | 权限位(0664/S_IRUSR)
②:打开 0
返回值:
成功,返回一个共享内存的id,这个id是共享内存的唯一标识
失败,返回-1,并且errno被设置
shmat/shmadt
NAME
shmat, shmdt - System V shared memory operations
映射/解除映射
SYNOPSIS
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:你要映射的那个共享区域的ID
shmaddr:你要映射到进程的哪个区域,一般为NULL 让系统自行分配
shmflg:对映射区域的操作方式
SHM_RDONLY 只读
0 读写
返回值:
成功,返回映射区域的首地址
失败,返回 (void *)-1,errno 被设置
int shmdt(const void *shmaddr);
shmaddr:要解除的映射区域的首地址
返回值:
成功,返回0
失败,返回-1,errno被设置
shmctl
NAME
shmctl - System V shared memory control
SYNOPSIS
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:你要控制的那个共享内存的ID
cmd:你要做什么操作
IPC_RMID 删除指定的共享内存
IPC_SET 设置共享内存的属性
IPC_STAT 获取共享内存的属性
buf:存储第二个参数的相关内容
if cmd == IPC_RMID buf == NULL
返回值:
成功,返回0
失败,返回-1,errno被设置
4.信号量
引入:对共享内存的操作:假定有一块共享内存PSHM指针指向了这块共享内存,有两个进程p1,p2,将其映射到了进程地址空间。此时,p1进程和p2进程对共享内存的操作(int *pSHM)有这样的一部分代码:
for(int i=0; i<100000; i++)
(*pSHM)++;
// 最终得到的结果不是20w ---》对共享内存的操作 (*pSHM)++不是原子操作(原子操作:一定会执行完的操作),由于时间片的原因 会在任意的时刻结束对共享内存的操作
4.1 信号量机制
什么是信号量?作用是什么?
信号量(semphore)是一种用于提供不同 进程间/线程间 同步的一种机制。
进程 / 线程 : 并发的实体
同步:并发实体之间 相互等待 相互制约 ,有条件的访问
信号量的目的是 为了保护共享资源,使得共享资源有序访问。
使用信号量时,需要明确谁是共享资源,必须要存在共享资源(产生竞争),才需要用到信号量。
信号量机制:其实是一种约定,进程之间需要按照规则去访问共享资源。
约定(规则):
进程A 和 进程B 都需要访问一个 互斥(同时只有由一个进程访问)设备,可以使用信号量来表示这个设备是否可以被访问。
约定:只要有进程访问 这个设备,先访问信号量,如果信号量的访问标志为 YES,则先设置信号量的访问标志为NO,然后再去访问设备,访问完设备之后,将信号量置为YES。
PS:使用信号量来访问共享内存时 一定要遵守约定,否则会出现异常
4.2 信号量操作流程
①:创建一个信号量 / 信号灯 / 锁
信号量 实际上就是一个整数,信号量的创建时可以指定信号量的初始值:
sem(信号量) 的初始值 == 5,表示该信号量所保护的共享资源可以同时被5个进程/线程 访问
sem(信号量) 的初始值 == 1,表示该信号量所保护的共享资源是互斥资源,也称这个信号量为互斥信号量
YES ---> sem value > 0
NO ---> sem value == 0
②:等待一个信号量 上锁
等待一个信号量:等待这个信号量是否可以被访问 把这个操作 称之为 P操作 (荷兰语 proberen),也可以称之为上锁
该操作 会测试这个信号量的值,如果值 == 0,就会等待,如果这个值>0表示可以访问,访问时会将信号量的值-1(上锁),继续往下执行 操作临界区《操作共享资源的代码块》的代码
③:释放一个信号量 解锁
释放一个信号量:称之为 V操作 荷兰语-verhogen,或者解锁,该操作将信号量的值+1
4.3 相关API
4.3.1 System V信号量
操作流程:获得key--->创建/打开信号量---》P/V操作--》删除/控制
system V:所设计的信号量 叫做信号量集
semget:
NAME
semget - get a System V semaphore set identifier
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
key:system V对应的IPC对象的key
nsems:你要创建的信号量集中的信号量的个数
如果是打开一个已存在的信号量集,nsem 给0 即可
信号量集中的信号量的个数 一旦创建就不能被改变
semflg:方式
①:创建 IPC_CREAT | 权限(S_IRUSR / 0664)
②:打开 0
返回值:
成功,返回信号量的id,id是该信号量的唯一标识
失败,返回-1,并且errno被设置
semctl:信号量被创建之后应当 立即赋予初值
NAME
semctl - System V semaphore control operations
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
semid:你要操作的信号量的id
semnum:指定你要操作的信号量集中的那一信号量
cmd:你要对该信号量作何种操作
cmd == GETVAL 获取 semnum 指定的那个信号量的值
SETVAL 设置 semnum 指定的那个信号量的值
GETALL 获取 semid 指定的那个信号量集中所有信号量的值
SETALL 设置 semid 指定的那个信号量集中所有信号量的值
IPC_RMID 删除该信号量集
...
...:
if cmd == IPC_RMID 第四个参数就不要了
cmd == GETVAL 第四个参数就不要了,可以通过函数返回值返回
cmd == SETVAL 第四个参数是要设置的值
cmd == GETALL 第四个参数用来接收所有信号量的值
unsigned short getvals[nsems];
cmd == SETALL 第四个参数用来保存所有待设置的信号量的值
unsigned short setvals[nsems];
返回值:
if cmd == GETVAL
返回值的含义就是相应的信号量的值
if cmd == IPC_RMID
成功,返回0
失败,返回-1,并且errno被设置
P/V操作:semop
NAME
semop, semtimedop - System V semaphore operations
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
struct sembuf{
unsigned short sem_num; /* semaphore number */
信号量的编号
short sem_op; /* semaphore operation */
对信号量的操作
> 0 V操作
==0 尝试操作
< 0 P操作
short sem_flg; /* operation flags */
操作方式
== 0 默认方式,P操作 如果获取不了,阻塞
== IPC_NOWAIT 非阻塞版本,如果P操作获取不了 直接退出
== SEM_UNDO 撤销,为了防止进程退出 带锁退出
使用 SEM_UNDO 这个标志 内核会额外记录该进程对信号量的所有PV操作,然后在进程退出时,会还原相应的信号量操作
}
int semop(int semid, struct sembuf *sops, size_t nsops);
semid:你要进行p操作的信号量集
sops:指定对信号量集中的哪一个信号量做P/V操作
nsops:指定第二个参数的个数
返回值:
成功,返回0
失败,返回-1,并且errno被设置
timed:超时等待
int semtimedop(int semid, struct sembuf *sops, size_t nsops,
const struct timespec *timeout);
前三个参数都是和semop一样的
timeout:设置超时时间
struct timespec{
time_t tv_sec;
long tv_nsec;
};
示例:超时等待 5s
// 超时5s
struct timespec timedout = {.tv_sec = 5, .tv_nsec = 0};
// p操作 阻塞版本 第0号信号量
struct sembuf sem = {.sem_num = 0, .sem_op = -1, .sem_flg = 0};
semtimedop( semid, &sem, 1, &timedout);
4.3.2 死锁
哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。 哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。
死锁问题的必要条件:
-
互斥性:一个资源职能被一个任务访问
-
请求和保持条件:一个任务对请求的资源被占用阻塞时,已经占用资源不释放
-
不可剥夺:一个任务在释放之前,其他的任务无法剥夺它所占用的资源
-
循环等待:当发生死锁时,任务进入死循环,永久阻塞
解决死锁问题:破坏死锁形成的必要条件(只要破坏其中一个即可),有一个经典的算法---银行家算法
system V标准 信号量集 ---》对于信号量集中的信号量<5个信号量> ---》获取其中的两个信号量,对于system V的做法是 必要要全部获取,否则就等待(并且不会保持某个信号量)
4.3.2 POSIX信号量
POSIX信号量 分为 :有名信号量 和 无名信号量。
有名信号量:在文件系统中 有一个文件接口(i_node),内容存在于内核。可以用于进程间的同步。
无名信号量:没有名字,可以存在于进程的地址空间中 / 共享内存中。
如果 无名信号量存在于 进程的地址空间中,那么只能用于进程内部的任务的同步。
如果 无名信号量存在于 共享内存中,那么能用于进程之间任务的同步。
创建一个POSIX信号量:sem_open
NAME
sem_open - initialize and open a named semaphore
SYNOPSIS
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
sem_t *sem_open(const char *name,int oflag);
sem_t *sem_open(const char *name,int oflag,mode_t mode,unsigned in value);
name:带路径的文件名,要创建/打开的那个有名信号量
路径要求:以/开头, 并且只能由一个 /
"/POSIX.SEM" OK
"/home/china/POSIX.sem" ERR
oflag:打开方式
O_CREAT 创建
0 打开
mode:权限 --- 创建时才需要指定
0664
S_IRUSR | S_IWUSR
value:指定信号量的初始值
返回值:
成功,返回一个信号量指针
失败,返回 SEM_FAILED,errno被设置
PS:有名信号量创建成功之后,可以在 /dev/shm 目录下查看有名信号
无名信号量的创建:sem_init
NAME
sem_init - initialize an unnamed semaphore
SYNOPSIS
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:指向一个要初始化的无名信号量
pshared:该信号量的共享方式
0 进程内部共享 sem 指向进程内部的空间
1 进程间共享 sem 指向内核共享区域
value:初始化的信号量的值
返回值:
成功,返回0
失败,返回-1,并且errno被设置
Link with -pthread.
PV操作:sem_wait / sem_post
NAME
sem_wait, sem_timedwait, sem_trywait - lock a semaphore
SYNOPSIS
#include <semaphore.h>
// 阻塞版本的p操作
int sem_wait(sem_t *sem);
// 非阻塞版本的p操作
int sem_trywait(sem_t *sem);
// (绝对时间的)超时等待的p操作
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
sem:你要对哪个信号量进行p操作
abs_timeout:绝对时间的超时等待
返回值:
成功,返回0
失败,返回-1,并且errno被设置
相对时间:相对于当前时间来说
2个小时之后就下课了
绝对时间:绝对时间
2024-7-12-17-30 下课
5s钟超时等待:
先要获取当前的系统时间:
struct timespec timedout;
int clock_gettime(clockid_t clk_id, struct timespec *tp);
timeout.tv_sec+=5;
Link with -pthread.
NAME
sem_post - unlock a semaphore
SYNOPSIS
#include <semaphore.h>
int sem_post(sem_t *sem);
sem:你要进行V操作的信号量
返回值:
成功,返回0
失败,返回-1,并且errno被设置
其他操作:sem_getvalue\sem_close\sem_unlink\sem_destroy
NAME
sem_getvalue - get the value of a semaphore
SYNOPSIS
#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);
sem:表示你要获取的那个信号量
sval:用来保存获取到的信号量的值
返回值:
成功,返回0
失败,返回-1,并且errno被设置
NAME
sem_close - close a named semaphore
关闭一个被打开的有名信号量
SYNOPSIS
#include <semaphore.h>
int sem_close(sem_t *sem);
NAME
sem_unlink - remove a named semaphore
删除一个有名信号量
SYNOPSIS
#include <semaphore.h>
int sem_unlink(const char *name);
name:删除一个文件 带路径的文件名
NAME
sem_destroy - destroy an unnamed semaphore
销毁一个无名信号量
SYNOPSIS
#include <semaphore.h>
int sem_destroy(sem_t *sem);
PS:使用POSIX信号量时,编译时需要加上编译选项 -pthread
例如:gcc posixsem.c -o posixsem -pthread