目录
进程控制原语
-
fork() // 创建进程
-
exec() // 执行程序
exec()前后实际用户的ID和实际组ID不变,而有效ID是否改变取决于所执行程序文件的设置用户ID和设置组ID为是否设置。
其中execve()为系统调用
-
exit() // 结束进程
-
wait() // 等待进程终止
-
waitpid()
进程间通信(IPC)
Linux进程间通信通信手段基本上是从UNIX平台继承下来的,而对UNIX做出重大贡献的两大主力AT&T的贝尔实验室和BSD在进程间通信的侧重点各有不同。前者对UNIX早期的进程间通信手段进行了系统的改进和补充,形成了“System V IPC”,其进程间通信局限在单个计算机中,后者则跳过了该限制,形成了基于套接字的进程间通信,而Linux将两者都继承了下来。
UNIX IPC 方式包括 管道、FIFO、信号
System V IPC 包括 System V 消息队列、System V 信号量以及 System V 共享内存
POSIX IPC 包括 Posix 信号量以及 Posix 共享内存区
- 管道(pipe)和有名管道(named pipe):管道可用于父子进程间的通信,有名管道允许非父子进程间的通信
- 信号(signal):信号是对中断机制的一种软件模拟,用于通知进程有某事件发生。
- 消息队列(message queue):消息队列是消息的链表,包括 System V 消息队列和 Posix 消息队列,可读可写
- 共享内存(shared memory):使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。但这种方式需要一定的同步互斥机制,如互斥锁或信号量
- 信号量(semaphore):主要作为进程间或同一进程的不同线程之间的同步与互斥
- 套接字(Socket):更广泛的进程间通信,可用于网络中不同机器之间的进程间通信。
管道
在管道内键入一个命令序列,让shell
执行时,shell
会为每一条命令单独创建一个进程,然后用管道将前一条命令进程的标准输出作为下一条命令的标准输入。
#include <unistd.h>
int pipe(int fd[2]); // fd[0]为读而打开,fd[1]为写而打开,fd[1]的输出是fd[0]的输入
// 先创建管道,然后进程fork创建子进程
// 父进程-->子进程 父端关闭读,子端关闭写
// 子进程-->父进程 父端关闭写,子端关闭读
-
是半双工的(数据只能朝一个方向流动),现在有些系统也支持全双工的。
-
只能在具有公共祖先的两个进程之间使用,或父子进程之间
FIFO(命名管道)
-
通过FIFO,不相关的进程也能进行通信
-
通过创建一个fifo,然后对其进行读写,像文件读写那样,因为fifo创建出来是有名字的,所以能够对任意两个进程进行通信
读写规则:
当要写入的数据量大于PIPE_BUF时,linux不能保证写入的原子性,在写满fifo空闲缓冲区后,写操作返回。
要写入的数据量不大于PIPE_BUF时,linux将保证写入原子性,如果当前缓冲区能容下要写的数据,则写完后返回,相反的话,返回EAGIN错误,提醒以后再写。
设置阻塞标志的读操作来说,造成阻塞的原因有两种:① 当前fifo内有数据,但有其他的进程在读这些数据。②fifo里面没有数据。解阻塞的原因则是fifo有新的数据写入不管新写入数据量的大小,也不论读操作请求多少数据。
#include <sys/stat.h>
int mkfifo(const char* path, mode_t mode);
int mkfifo(int fd, const char *path, mode_t mode);
// 这两个函数均为创建fifo的操作,返回文件描述符
// fd=open("fifo1",O_RDONLY);,打开fifo文件
// len=read(fd,buf,BUFES);从fifo中读取数据
// write(fd,buf,n+1);向fifo中写入数据
// mode 和文件读写相同
IPC对象--Message Queue、Shared Memory、Semaphore
由于消息队列、信号量和共享内存有很多相似之处,因此将他们统称为IPC对象
-
采用一个非负整数的标识符(identifier)加以引用,读写时只需要直到文件标识符即可
-
与文件描述符不同,IPC标识符不是小的整数。每次创建和删除一个IPC对象时,其标识符都会连续+1,直至达到一个整型数的最大正值,然后又转回0
-
创建IPC对象(
msgget, semget, shmget
)时需要指定一个键值,与IPC对象相关联。key_t
类型,键值由内核变换成标识符
一般可以指定键为IPC_PRIVATE创建一个新的IPC结构,将返回值的标识符存放在文件中 IPC_PRIVATE只用于创建新的结构
若不用IPC_PRIVATE创建结构时,要确保没有引用到已存在的标识符,必须在flag中同时指定IPC_CREAT和ICP_EXCL位,若已存在,则会报EEXIST错误
3个get函数(msgget, semget, shmget
)都有两个类似的参数,一个key
值和一个整型flag
创建新结构时,若key
是IPC_PRIVATE或当前某种类型的IPC结构无关,则需指定flag
的IPC_CREAT标志位
引用现有结构时,key
必须等于结构创建时指明的键值,并且IPC_CREAT不被指明
msgqid = msgget(IPC_PRIVATE, 0666); // 创建一个消息队列
msgqid = msgget((key_t)MAG_KEY, 0666 | IPC_CREAT | IPC_EXCL);
缺点
-
在进程结束后,IPC结构并不会消除,必须调用
msgctl(), semctl(), shmctl()
才能删除操作 -
IPC结构不像文件系统那样有名字,可调整
-
由于其不适用文件描述符,不能对他们使用多路转接I/O函数(select和poll)
-
System V 消息队列
-
消息的链接表,存储在内核中,由消息队列标识符标识。
-
System V 信号量
它是一个计数器,用于为多个进程提供对共享数据对象的访问。要使用该方法获得共享资源,则需要以下步骤:
-
测试控制该资源的信号量
-
若信号量为正值,则进程可以使用该资源,此时进程会将该资源信号量值减1,表示它使用了一个资源单位
-
若信号量值为0,则进程进入休眠状态,直至信号值大于0。此时进程被唤醒,然后返回步骤1
-
当进程使用完该共享资源后,该信号量值加1,此时若有进程正在休眠状态等待此信号量则会被唤醒
-
为了正确实现信号量,信号值的+1和-1操作均为原子操作。因此,信号量通常是在内核中实现的。
-
不足:
信号量并不是单个非负值,必需定义为含有一个或多个信号量值的集合,当创建信号量时需指定集合中的信号量值的数量
信号量的创建(semget())是独立于初始化(semctl())的,这样就不能原子地创建一个信号量集合,并且对该集合中的各个信号量值赋初值
有的程序在终止时并没有释放已经分配给它的信号量
-
-
System V 共享内存
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/sem.h>
用户读 0400
用户写 0200
组读 0040
组写 0020
其他读 0004
其他写 0002
1、消息队列
int msgget(key_t key, int flag); // 创建消息队列
// 用于修改其中ipc_perm中的用户id,组id,权限等
// 也可用于删除IPC结构
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// cmd: IPC_STAT:取出队列的msqid_ds结构,并把它放在buf中
// IPC_SET:设置msqid_ds结构
// IPC_RMID:删除消息队列,删除立即生效
// ptr 参数包括 消息类型 和 消息内容,ptr是一个执行struct mymesg的指针
// struct mymesg {
// long mytpe; // 区别消息类型,用来区别读取,可以实现非次序读取
// char mtext[size]; // 消息内容
// }
// nbytes为其中mtext的大小,并非整个结构体的大小
// flag : IPC_NOWAIT 类似于I/O系统的非阻塞I/O
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag); // 将消息添加到队尾
// ptr 指的是长整形消息类型和保存数据的缓冲区
// type 指定消息类型 ==0:返回队列中的第一个消息 >0 返回消息类型为type的第一个消息 <0 返回队列中消息类型小于等于type绝对值的消息
int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag); // 从队列中取消息,不一定按照FIFO顺序取,可以按照消息的类型字段取
2、信号量
int semget(key_t key, int nsems, int flag); // 获得信号量ID,并用nsems初始化ipc_perm结构, nsems表示集合中的信号量数
int semctl(int semid, int semnum, int cmd, .../*union semun arg*/);
/* union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
}
cmd:
IPC_STAT:去semid_ds结构并放入buf中
IPC_SET:按arg.buf指向的结构取值,是遏制sem_perm字段
IPC_RMID:从系统中删除信号量集合,删除是立即发生的
GETVAL
SETVAL
GETPID
GETNCNT
GETZCNT
GETALL
SETALL
*/
int semop(int semid, struct sembuf semoparray[], size_t nops);
// semoparray 指向一个由sembuf结构表示的信号量操作数组, npos指向操作的数量
/*
struct sembuf {
unsigned short sem_num;
short sem_op; // 信号量操作(negatice, 0, positive)
short sem_flg; // IPC_NOWAIT, SEM_UNDO
}
*/
3、共享内存
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag); // 创建共享内存段
int shmctl(int shmid, int cmd, struct shmid_ds *buf); // 对共享内存段进行操作
/*
cmd:
IPC_STAT:
IPC_SET:
IPC_RMID:
*/
void *shmat(int shmid, const void *addr, int flag); // 进程调用shmat将其连接到它的地址空间
// 若addr为0,则此段连接到由内核选择的第一个可用地址上。推荐使用
// 若addr != 0,且没有指定SHM_RND,则此段连接到指定的地址上
// 若addr != 0,且指定了SHM_RND,则连接到 (addr - (addr mod SHMLBA))所表示的地址
// 若flag = SHM_RDONLY,则以只读方式连接,否则以读写方式连接
// 返回值为该段所连接的实际地址,出错返回-1,若成功,则shmid中的shmid_ds结构中的shm_nattch计数器加1
int shmdt(const void *addr); // 操作结束后,调用shmdt分离内存段
// 分离内存段时并不删除shmid标识符,直至调用shmctl时cmd设置为IPC_RMID
// addr参数为shmat()的返回值。
套接字
前面几种通常局限于同一台主机的两个进程之间的IPC。套接字是仅有的支持不同主机上两个进程之间的IPC的方式。
POSIX 信号量
两种形式:
#include <semaphore.h>
// 创建新的命名信号量或使用一个已存在的信号量
sem_t *sem_open(const char *name, int oflag, ... /*mode_t mode, unsigned int value */);
// 使用现有信号量时,仅指定前两个参数,name与oflag的0值
// 当oflag = O_CREAT时,若信号量不存在则创建,若存在则使用
// oflag = O_CREAT | O_EXCL,若信号量存在,则会open失败
// 使用O_CREAT时,需要制定mode与value,mode表示读写权限,value指定信号量的初始值(0~SEM_VALUE_MAX)
// 释放任何信号量的相关资源,若没有调用close而退出,则内核会自动关闭任何打开的信号量,但不会影响信号量值的状态
int sem_close(sem_t *sem);
// 销毁一个命名信号量
int sem_unlink(const char *name);
// 可避免阻塞,对信号量减1操作
int trywait(sem_t *sem);
// 信号量计数为0时会阻塞
int wait(sem_t *sem);
// 阻塞一段确定的时间
int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict tsptr);
// 使信号量加1
int sem_post(sem_t *sem);
// 创建一个未命名信号量
// pshared表明是否可以被多个进程使用,若是,将其设置为非0值
// sem指向两个进程之间共享的内存范围
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 销毁未命名信号量
int sem_destroy(sem_t *sem);
// 检索信号量值
// 在我们想要读时,信号量的值可能已经变了。只能使用同步机制来避免这种竞争,否则getvalue只能用于调试
int sem_getvalue(sem_t *restrict sem, int *restrict valp);
-
性能更高
-
接口使用更简单,没有信号量集
-
命名的:可以通过名字访问,因此可以被任何已知其名字的进程中的线程使用
-
未命名的:只存在内存中。它只能应用在同一进程中的线程或不同进程中已经映射相同内存内容到他们地址空间中的线程
POSIX 与 System V 进程间通信
-
POSIX 是IEEE指定的标准,目的是为了在不同操作系统上提供统一的接口,而System V 是UNIX两大贡献者之一的贝尔实验室在进程间通信方面自己改进出来的结果,而另一方BSD在基于网络方面形成了套接字
-
信号量方面,POSIX信号量在无竞争条件下不会陷入内核,而System V则无论如何都要进入内核,性能会稍差一些
-
应用方面,可能会有一些操作系统没有实现POSIX标准,System V更加普遍,但可移植性的POSIX是趋势 在IPC,进程间的消息传递和同步上,POSIX较为普遍,在共享内存方面,POSIX尚未完善
-
POSIX 的sem_wait函数获取信号量之后,进程如果意外终止,将无法释放信号量
一般进程间通信使用System V,而线程间通信使用基于POSIX的接口函数
多线程使用System V时,每次调用都会陷入内核的接口,丧失线程的轻量优势
多进程也是可以使用POSIX 接口的
-
一般来说,只有信号量存在POSIX 和System V版本
进程间通信应用场景
- 进程状态监视
进程中调用了另外一个程序,此时需要等待该程序的结束 - 进程间直接通信
两个进程在运行,其中一个进程A接收来自服务器的数据并解密,再将解密后的数据通过进程通信的方式传递给另外一个进程B,B进程在处理好数据后再通信给A进程,A将数据加密后再发送给服务器。
进程间通信场景转自https://blog.csdn.net/Think88666/article/details/83662404