操作系统-进程

一、进程

1、进程介绍

进程与程序

  • 程序是存储在磁盘上的可执行文件,里面包含可执行的机器指令和数据的静态实体;进程是处于活跃状态的计算机程序,也就是正在运行中的程序
  • 一个运行中的程序,可能由多个进程组成,但至少要有一个进程,称为主进程,同时可以通过系统调用创建出若干个子进程同时进行任务
  • 一个程序也可以同时运行出若干个进程

进程的分类
​ 根据进程的功能不同一般分为三类:交互进程、批处理进程、守护进程

  • 交互进程:由一个shell终端启动的进程,在执行过程中,需要与用户进行交互操作,可以运行在前台,也可以运行在后台
  • 批处理进程:该进程是一个进程指令集合,负责按顺序去启动其他进程
  • 守护进程:一般都处于活跃状态,运行在后台,由系统在开机时通过脚本自动创建并运行。
查看进程:

简单形式
ps:以简略的形式显示出当前用户有控制终端控制的进程信息
复杂形式
ps auxw:以更宽大的列表形式详细地列出所有用户的进程信息

参数 详细
a 所有用户的有终端控制的进程
x 包括无终端控制的进程
u 以更详细的内容显示
w 以更大的列宽显示
e 显示所有进程
f 显示出其他信息字段

进程的信息列表

进程信息 详细
USER 进程属主
PID 进程ID
%CPU CPU使用率
%MEM 内存使用率
VSZ 占用虚拟内存大小(Kb)
RSS 占用物理内存大小(Kb)
TTY 控制终端设备号,?表示无终端控制
STAT 进程状态
START 进程启动的时间点
TIME 进程运行的耗时时间
COMMAND 启动进程的指令

进程状态列表

进程状态 详细
O 就绪态 ,表示等待被调度
R 运行态,Linux下没有O状态,就绪态也用R表示
S 可被唤醒睡眠态。当系统中断、获得资源、收到信号等都可以被唤醒转入回运行态
D 不可被唤醒睡眠态。只能被wake_up系统调用唤醒
T 暂停态。收到停止类信号转入暂停态,当收到SIGCONT(18)转入运行态
Z 僵尸态。已经停止运行,但是父进程尚未回收相关资源
X 死亡态。不可见
N 低优先级
< 高优先级
s 进程组的领导
l 多线程化的进程
+ 在前台的进程组中的
L 有被锁入内存的分页
# 查看指定进程  
ps aux | grep bash		# 过滤出包含bash关键字的进程信息
# 分页查看进程
ps aux | more
# 查看指定用户进程
ps -u 用户名 uw

父进程与子进程

  • 一个进程可以创建出另一个进程,创建者称为被创建者的父进程,被创建者称为创建者的子进程
  • 父进程创建出子进程后,子进程在操作系统的调度下与父进程同时运行

孤儿进程与僵尸进程

  • 子进程先于父进程结束,子进程一定会向父进程发送SIGCHLD(17)信号,父进程负责回收子进程的相关资源
  • 如果父进程先于子进程结束,此时子进程称为孤儿进程,同时会被孤儿院进程收养,就成为了孤儿院进程的子进程
    • 早期孤儿院进程init pid是1
    • 现在孤儿院进程不是1了,在图形化界面中是/sbin/upstart --user
  • 子进程先于父进程结束,但是父进程没有去回收子进程相关资源,该子进程就成为僵尸进程

进程标识符

  • 每个进程都有一个以非负整数表示的唯一标识,称为进程ID,简称PID
  • 进程ID在任意时刻内是唯一的,但是可以重用,当一个进程结束后,它的进程ID就会被分配个后面创建的其他进程使用
  • 延时重用:当进程结束后,它的ID不会立即被系统重新分配,会隔一段时间后再重新分配
/**
 * 功能: 获取当前进程的ID
*/
pid_t getpid(void);

/**
 * 功能: 获取当前进程的父进程ID
*/
pid_t getppid(void);

2、fork创建子进程

/**
 * 功能: 创建一个子进程
 * 返回值: 创建失败返回-1,创建成功会返回两次
 * 父进程: 返回子进程的pid
 * 子进程: 返回0
*/
pid_t fork(void);

注意:总进程数或者实际拥有pid的进程数量超过了系统的限制,该函数失败
注意:子进程创建出来后,父子进程会同时各自运行代码,因此可以通过分支判断返回值,来让父子进程执行不同的程序代码

父子进程的运行顺序:通过fork系统调用创建出来的子进程与它父进程会各自往下运行,但是其先后顺序不确定,可以通过睡眠等系统调用确定让哪个进程先执行
子进程是父进程的副本:由fork创建的子进程会获得拷贝出父进程的data段、bss段、heap段、stack段、I/O流缓冲区。

子进程会共享父进程的代码段、文件描述符fd

  • 通过fork创建的子进程会共享父进程的代码段,fork之前的代码只有父进程执行,fork之后的代码父子进程都有机会执行,主要受到逻辑的控制进入不同的分支
  • 不同的程序之间,文件描述符是不能共享的
  • 但是由fork创建的父子进程之间,是把父进程内核中的文件描述符的表格拷贝给了子进程,此时两者共享父进程的已打开的文件描述符

fork子进程会继承父进程的信号处理方式:通过fork创建子进程会继承父进程的信号处理方式,是因为子进程共享了父进程的代码段

3、vfork和exec系列函数创建进程

vfork介绍
/**
 * 功能:创建一个子进程,返回值特点与fork没有区别
*/
pid_t vfork(void);

vfork的特点

  • 当调用vfork系统调用时,父进程会进入阻塞状态,子进程一定先返回执行
  • 子进程返回时,先临时使用父进程的相关资源,然后等待exec系列函数执行加载一个可执行文件,从而让子进程去启动另一个程序,把那个程序的资源替换自己原来的资源。
  • 当子进程调用完exec系列函数,替换完原来的所有资源后,子进程才算真正创建完毕,此时父进程才会接触阻塞状态,返回子进程的pid
  • 如果子进程不调用exec系列函数后果:
    • 情况1:子进程一直没有创建成功,导致父进程一直处于阻塞状态,无法返回
    • 情况2:子进程直接结束并释放相关资源,此时子进程使用的还是父进程的资源,父进程会返回,但会产生段错误,因为它的相关资源已经被子进程错误释放掉了
  • vfork不能单独创建出子进程,必须与exec系列函数中某个函数配合使用

fork和vfork的区别

  • vfork调用后,子进程先返回,而fork调用,谁先返回不确定
  • vfork不会复制、共享父进程的相关资源,而是去加载其他程序,替换原来的临时资源
  • 以exec系列函数创建的子进程不会继承父进程的信号处理方式,但是可以继承父进程的信号屏蔽
exec系列函数:
/**
 * 功能: 都是为了与vfork配额创建子进程的函数
 * @path: 要加载的程序的路径
 * @arg: 命令行参数,最起码第一个是执行可执行文件的命令,最后一个以NULL结尾
*/
int execl(const char *path, const char *arg, ...
               /* (char  *) NULL */);

/**
 * @file: 只需要被加载程序的文件名,系统会根据环境变量PATH中的路径去查找该文件
*/
int execlp(const char *file, const char *arg, ...
               /* (char  *) NULL */);

/**
 * @envp: 环境变量表,相当于父进程把自己的环境变量表拷贝给子进程
*/
int execle(const char *path, const char *arg, ...
               /*, (char *) NULL, char * const envp[] */);

/**
 * @argv: 把命令行参数以字符串指针数组方式提供,注意:一定要以NULL结尾
*/
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
               char *const envp[]);

4、进程的正常结束

main函数中执行了return结束进程

int main(...) {
    ...
    return x;	//	该返回值可以被父进程接收
}
//	等价于
int main(...) {
    ...
    exit(x)	//	该返回值可以被父进程接收
}

调用标准C的exit函数结束进程

/**
 * @status: 进程的结束状态码
*/
void exit(int status);
  • 该函数一旦调用就不会返回,其父进程通过wait\ waitpid函数可以获取到status的低8位数据
  • 进程正常退出前会先调用事先通过atexit\ on_exit函数注册过的函数,然后冲刷并关闭所有处于打开状态下的标准I/O流
  • 可以用 EXIT_SUCCESSEXIT_FAILURE常量作为exitreturn的结束状态码,表示进程是正常结束,还是出问题结束的
  • 该函数底层调用了 _exit\ _Exit函数
/**
 * @funciton: 函数指针 进程退出前要执行该函数
*/
int atexit(void (*function)(void));

/**
 * @funciton: 函数指针   
 *	第一个参数: 来自return的n或者exit的参数 status
 *	第二个参数: 来自on_exit的arg参数
 * @arg: 任意类型指针
*/
int on_exit(void (*function)(int , void *), void *arg)

调用_exit/ _Exit 函数结束进程

void _exit(int status);
// 该函数有一个完全等价的标准C版本
void _Exit(int status);
  • 该函数一旦调用就不会返回,其父进程通过wait\ waitpid函数可以获取到status的低8位数据
  • 在进程退出前,先关闭所有打开状态下的文件描述符,并把所有的子进程托付给孤儿院进程收养(init\upstart--user),并把当信号SIGCHLD(17)发送给其父进程

其它正常结束进程的方式

  • 进程的最后一个线程执行完毕,进程也结束
  • 进程的最后一个线程调用pthread_exit函数,进程也结束

5、进程的异常终止

  • 进程收到了某些信号,他杀
  • 进程自己调用abort函数,产生了SIGABRT(6)信号,自杀
  • 进程的最后一个线程收到了"取消"操作,并且做出响应
  • 如果进程是异常结束的,atexit\on_exit它们事先注册的遗言函数不会被调用,也不会冲刷标准IO流
  • 但是依然会给父进程发送信号SIGCHLD(17),关闭所有打开状态下的文件描述符

6、子进程的资源回收

  • 对于子进程任何方式的结束,都希望父进程能够知道,可以通过wait\waitpid函数可以知道子进程是如何结束的以及它结束状态码
/**
 * 功能: 以阻塞状态等待任意一个子进程的结束,并回收它的相关资源,获取到结束状态码
 * @status: 获取结束的子进程的结束状态码 是输出型参数
 * 返回值: 成功返回结束的子进程的pid,失败的返回-1
 *   1、如果所有子进程都在运行中,则阻塞等待
 *   2、如果有一个子进程结束,则立即返回该子进程的状态码和pid
 *   3、如果当前没有子进程运行,返回-1
*/
pid_t wait(int *status);

对于子进程的结束状态码status可借助宏函数解析判断

宏函数 作用
WIFEXITED(status) 子进程是否正常结束
WEXITSTATUS(status) 当子进程是正常结束时,该宏可以获取到正确的结束状态码的低8位数据
WTERMSIG(status) 如果进程是异常终止的,该宏可以获取到杀死该子进程的信号编号

注意:由于wait函数可能会阻塞父进程的执行,因此不适合在父进程的主业务逻辑中调用,可以通过给SIGCHLD信号注册信号处理函数,在信号处理函数中调用wait

/**
 * 功能: 等待子进程结束
 *	与wait不同的是,可以选择等待哪些\哪个子进程结束,并且可以选择是否阻塞
 * @pid: 要等待的子进程的pid
 *	< -1 等待abs(pid)编号的进程组中的任意进程结束
 *    -1	等待任意进程结束,等待范围等同于wait
 *    0 	等待同组的任意进程结束
 *    >0	等待pid进程结束
 *    
 * @status: 获取结束的子进程的结束状态码 是输出型参数 等同wait
 * @options:
 *	0	表示忽略该参数 功能与wait一致
 *	WNOHANG	如果此时没有子进程结束会立即返回,不再阻塞
 *	WUNTRACED 如果有子进程转入暂停态也会立即返回
 *	WCONTINUED	如果有子进程从暂停态转回继续执行也会立即返回
 * 返回值:
 *	如果有子进程,且子进程状态改变 返回pid
 *	如果有子进程 且都没改状态 返回0
 *	如果没有子进程  返回-1
*/
pid_t waitpid(pid_t pid, int *status, int options);

7、system的实现原理

/**
 * 功能: 执行command程序,成功返回值commad对应程序的终止状态码,失败-1
 *    如果command给NULL,返回非零表示shell终端可用,返回0表示shell终端不可用
*/
int system(const char *command);
  • 该函数的实现,底层调用了vforkexecwaitpid函数,该返回值:
    • 如果vfork创建失败,则返回-1
    • 如果exec函数执行出错,则会在子进程中执行exit(127)
    • 如果都成功,则返回执行command程序的最终的结束状态码
  • vfork的区别:system会等待加载的程序执行完后才返回
  • 可以使用system替代vfork+exec的功能,好处是system会把错误处理都处理好

二、进程间通信介绍

进程间通信(Interprocess communication 也叫IPC):指两个或多个进程之间进行数据交互的过程。

需要进程间通信的原因:由于进程采用的是虚拟空间+用户态/内核态机制,所以就导致进程与进程之间、进程与内核之间是互相独立的,如果想让多个进程协同工作解决复杂问题,就需要进程之间进行通信。

进程的运行机制:从人类的感知来讲,所有进程是并行执行,但实际情况是所以进程轮流使用CPU,只是轮转的速度比较快,人类感受不到,CPU的一个内核就是一个运算单位,可以供一个进行使用(这也是CPU厂商不断追求CPU内核的数量原因),CPU内核越多,同时执行的进程就越多。

需要多进程协同工作的问题

  • CPU密集型问题:主要特点是需要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力,负责运算的进程越多,竞争得到的执行时间就越长。但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,是一种损人不利已的做法。所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
  • IO密集型问题:主要涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度,因为创建进程也需要相关的资源,比如代码段、栈、文件描述符表、环境变量表。所以处理CPU密集型程序时可以选择多进程实现,有效的利用多核提升效率;而IO密集型的由于99%的时间都花在IO上,花在CPU上的时间很少,所以多线程也能提高很大效率。

三、简单的进程间通信

命令行参数:

情况1:在终端执行程序时,给子进程传递命令行参数。
情况2:使用vfork + exec创建进程时,给子进程传递命令行参数。
​ 只能在父子进程之间,在父进行创建子进程时使用,只能由父进行传递给子进程,单向通信,并且只能传递一些简单的字符串数据。

环境变量表:

情况1:使用fork创建子进程时,子进程会拷贝一份父进程的环境变量表。
情况2:使用vfork + exec创建进程时,给子进程传递一份父进程的环境变量表,子进程拿到环境表后会进行拷贝。
​ 只能在父子进程之间,父进行创建子进程时使用,只能由父进行传递给子进程,单向通信,并且只能传递一些简单的字符串数据。

信号:

情况1:使用kill向指定的进程发送信号进行通信。
情况2:使用sigqueue向指定的进程发送信号,也可以附带一些简单的数据。
​ 可以在任意进程之间进行通信,但也只能是互相告知某某事件发生了,即使用能使用sigqueue带一些数据,但只能传递一个int类型整数,只有通过fork创建的子进程才能传递内存地址。

文件:

情况1:使用文件+信号,让任意两个进程之间传递大量数据,但要各自控制好位置指针,协调好读写时间。
情况2:使用文件+文件锁,也可以让任意两个进程之间传递大量数据,但文件锁的操作比较麻烦,也有可能陷入死锁的情况。
​ 单纯的使用文件进行通信,无法协调读取和写入的时间,空间造成文件内的数据混乱,所以必须配合使用信号或文件锁。

四、传统的进程间通信——管道通信

  • 管道是UNIX系统中最古老的进程间通信方式,是一种特殊文件读写机制
  • 当进程从管道文件中读取数据时,如果管道中没有数据则进程会进入阻塞状态,直到有数据读取出来才返回,因此不需要借助信号、文件锁来协调读写时间
  • 管道中的数据一旦读取完毕就会消失,因此也不需要管理文件的位置指针,所以使用管道文件比普通文件的进程间通信要方便很多
  • 古老的好处是所有系统都支持,早期的管道文件是半双工,现在有些系统支持管道文件的全双工,现在绝大多数情况已经不使用管道来通信了

有名管道:

  • 在文件系统中创建出一个实体的有文件名的管道文件,然后通过系统I/O的相关API来进行相关操作

使用函数创建

/**
 * 功能: 创建一个有名管道文件
 * @pathname: 管道文件的名字
 * @mode: 管道文件的权限
 * 返回值: 成功返回0 失败-1
*/
int mkfifo(const char *pathname, mode_t mode);

使用命令创建

mkfifo <file>

管道单向通信的编程模型

进程A		 ->			进程B
创建有名管道		
打开管道			      打开管道
写数据				   读数据
关闭管道			      关闭管道
删除管道				

匿名管道:

  • 只在内核中创建的管道文件对象并返回该对象的文件描述符,然后使用系统IO进行相关操作,匿名管道文件不会在文件系统中显示,在创建时不需要提供路径,也不会占用磁盘空间,只是使用内核空间来临时存储数据,当关闭文件描述符后会自动回收
  • 注意:只适合fork创建的有关系的父子进程之间进行通信
相关API:
/**
 * 功能: 创建出匿名管道文件对象,并返回该对象的文件描述符
 * @pipefd: 输出型参数,用于存储文件描述符的数组,其中
 *	pipefd[0]	用于读操作
 *	pipefd[1]	用于写操作
 *
 * 使用步骤:
 *	1、调用该函数在内核中创建出管道文件,并获取到该文件的两个文件描述符
 *	2、通过fork创建出子进程,子进程可以直接拷贝父进程的pipefd描述符
 *	3、写数据的进程要关闭读端,读数据的进程要关闭写端
 *	4、发送完毕后,父子进程分别关闭文件描述符
*/
int pipe(int pipefd[2]);

编程模型

	父进程		->		子进程
   创建匿名管道			
   创建子进程		       拷贝一对fd
   关闭读端(fd[0])	     关闭写端(fd[1])
   写数据(fd[1])	        读数据(fd[0])
   关闭写				关闭读

五、XSI机制的进程间通信

1、XSI介绍:

​ X/Open国际联盟有限公司是一个欧洲基金会,它的建立是为了向UNIX环境提供标准,XSI是X/Open System Interface的缩写,也就是X/Open设计的系统接口。

​ X/Open的主要的目标是促进对UNIX系统、接口、网络和应用的开放式系统协议的制定。它还促进在不同的UNIX环境之间的应用程序的互操作性,以及支持对电气电子工程师协会对UNIX的可移植操作系统接口规范。

IPC对象:
  • 它用于进程间通信的XSI-IPC内核对象,类似于匿名管道、文件内核对象一样,通过使用XSI方式进行进程间通信时,系统会在内核中创建出一个XSI-IPC内核对象,让通信的进程能够共同访问该内核对象
  • XSI-IPC对象只存在于内核空间,不会在文件系统中显示,该对象需要通过IPC键值来创建和获取
IPC键值:
  • 用于创建\获取 IPC对象的凭证,是一个无符号的整数,相当于IPC对象的名字,类似于文件名
  • 在创建IPC对象时,需要创建者提供一个IPC键值,有点类似给文件取名字,但是所有已经存在的IPC对象都在同一个作用域下,因此所有进程都可以访问到,所有有很大重名的风险,所以不建议创建者自己瞎想一个IPC键值去创建IPC对象,而应该使用操作系统提供的一个自动生成IPC键值的API
/**
 * 功能: 根据项目路劲和项目编号 自动生成一个ipc键值
 * @pathname: 建议提供当前项目的路径
 * @proj_id: 项目编号
 * 返回值:会根据项目路径和编号来计算出一个IPC键值,但是只是根据字符串内容来计算,不会检查路径是否为假
*/ 
key_t ftok(const char *pathname, int proj_id);

注意:使用时,建议提供当前的正确的工作路径,以及不同的项目编号,就可以获得不同的IPC键值,通过不同的IPC键值来创建不同的IPC对象,但是获取时,需要拿到相同的IPC对象,因此需要拿相同的IPC键值,因此提供相同路径和编号即可获取

IPC描述符:类似于文件描述符,是一个非负整数,它是IPC内核对象的给用户空间来访问的凭证

ICP对象命令:

显示IPC对象命令

ipcs -m  # 查看共享内存IPC对象	memory
ipcs -q  # 查看消息队列IPC对象	queue
ipcs -s  # 查看信号量IPC对象     sem
ipcs -a  # 查看所有的IPC对象		   

删除IPC对象命令

ipcrm -m  id	# 删除共享内存IPC对象
ipcrm -q  id	# 删除消息队列IPC对象
ipcrm -s  id	# 删除信号量IPC对象

2、共享内存:

基本原理

  • 在内核中开辟一块内存,可以让其它进程的虚拟地址与这块内存进行映射,从而达到让多个进程共享一块内存的目的,当一个进程向这块内存写数据时,其它进程就都可以读取到,这就达到了通信的目的
  • 优点:这种通信方式不存在数据的复制,是最快的进程间通信方式
  • 缺点:需要考虑同步访问的问题(用信号)

使用方式:当一个进程向共享内存写入数据时,内核不会通知其他进程,进程从共享内存读取数据时,也无法分辨是否是通信的对方新写入的数据,为解决该问题,有以下方式:

  • 读写:一个进程只负责写,另一个只负责读,这要负责读的进程使用的是最新即可,是一种单向通信
  • 轮询:配合定时器或者闹钟,每隔一段时间就读取一次
  • 中断:配合信号,进程只要往共享内存中写入数据,就给对方发送一个约定好的信号,其它进程如果已经读取完共享内存也可以发送一个约定好的信号

相关API

/**
 * 功能: 创建\获取一个共享内存的IPC对象
 * key: IPC键值,类似于文件名
 * @size: 共享内存的字节数,建议取内存页字节数的整数倍,默认1页=4096字节
 *	如果是想要创建,则必须指定size的大小
 *	如果是获取已有的共享内存,这size可取0即可
 * @shmflg: 
 *	0		-表示该参数无效,获取共享内存,size也无效了
 *	IPC_CREAT	-创建共享内存
 *	IPC_EXCL	-如果已经存在同一个IPC对象,返回失败
 *	mode		-共享内存的权限,当创建共享内存时必须加上
 *		例如:IPC_CREAT|0644
 * 返回值: IPC描述符,失败-1
*/
int shmget(key_t key, size_t size, int shmflg);

/**
 * 功能: 加载共享内存(把进程中的虚拟地址与内核中的共享内存建立映射关系)
 * @shmid: IPC描述符,要映射哪条共享内存
 * @shmaddr: 想要映射的虚拟地址,可以给NULL由操作系统自动分配
 * @shmflg:
 *	0				- 以读写方式映射共享内存
 *	SHM_RDONLY		- 以只读方式映射共享内存
 * 返回值: 映射成功后的虚拟内存首地址,失败返回-1
*/ 
void *shmat(int shmid, const void *shmaddr, int shmflg);

/**
 * 功能: 取消虚拟内存与共享内存的映射,也叫卸载共享内存    
 * @shmaddr: 要取消映射的虚拟内存地址
*/
int shmdt(const void *shmaddr);

/**
 * 功能: 销毁共享内存、获取共享内存属性、设置共享内存的属性
 * @shmid: IPC描述符
 * cmd:
 *	IPC_SET		-设置共享内存的属性  buf输入型参数,只有uid、gid、mode可设置
 *	IPC_STAT	-获取共享内存属性	buf输出型参数
 *	IPC_RMID	-删除共享内存	buf给NULL即可,
 *    			-删除并非真正直接删除,而是对共享内存的使用计数-1,如果该计数被-1为0后,意味着系统中没有任何的进程映射这块共享内存,才会从内核中真正销毁它
*/  
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    			
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;  //	当前映射的进程数的计数器
   ...
};

struct ipc_perm {
   key_t          __key;    //	IPC键值
   uid_t          uid;      //	属主id
   gid_t          gid;      //	属主组id
   uid_t          cuid;     //	创建者id
   gid_t          cgid;     //	创建者组id
   unsigned short mode;     //	权限
   unsigned short __seq;    //	序列号
};

编程模型

	进程A				进程B
  创建共享内存		获取共享内存
  映射共享内存		映射共享内存
  写数据并通知对方	  接收到通知后读取数据
  接收到通知后读取数据 写数据并通知对方
  取消映射		     取消映射
  删除共享内存

3、消息队列:

基本原理

  • 由系统内核维护的一个链式队列,每个节点称为一条消息,每条消息由消息类型、数据、长度信息组成
  • 和管道类似,可以双向通信,并且从中读取一个消息后,会自动出队,而且不是按照顺序读取,是按照消息类型读取

相关API

/**
 * 功能: 创建\获取消息队列
 * @key: IPC键值
 * @msgflg:
 *	0			-获取,不存在则失败
 *	IPC_CREAT	-创建,不存在则创建,存在会获取,除非
 *  IPC_EXCL	-排斥,已存在则创建失败
 *  mode		-消息队列权限,创建时必给
 * 返回值: IPC描述符,失败-1
*/
int msgget(key_t key, int msgflg);
    
/**
 * 功能: 向消息队列发送消息包
 * @msqid: IPC描述符
 * @msgp: 提供一个包含有消息类型和消息内容的消息包内存块,参考格式如下:
 *	struct msgbuf {
 *      long mtype;       //	消息类型
 *      char mtext[1];    //	消息内容,也可以使用柔性数组
 *  };
 * @msgsz: 提供消息包中消息内容的字节数,不包含消息类型的字节数
 * @msgflg:
 *	0		-如果当消息队列中没有空闲空间,该函数会一直阻塞
 *	IPC_NOWAIT	-当消息队列中没有空闲空间,不会阻塞,返回-1
 * 返回值: 成功0 失败-1
*/
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

/**
 * 功能: 从消息读取消息包
 * @msqid: IPC描述符
 * @msgp: 接收消息包的结构体首地址,也是由消息类型+消息内容组成,输出型参数
 * @msgsz: 消息包的字节数,如果实际消息包的大小>msgsz,会立即返回-1
 * @msgtyp: 要接收的消息类型
 *	0	-获取消息队列中的第一条消息
 *	>0	-获取指定消息类型的消息
 *	<0  -获取消息队列中,消息类型小于或等于abs(msgtyp)的消息,如果有多个,则接收最小值的
 * @msgflg:
 *	0			阻塞等待
 *	IPC_NOWAIT	如果消息队列中没有该消息类型的消息,直接返回
 *	MSG_EXCEPT  获取消息队列中第一个不等于msgtyp的消息,msgtyp>0
 *   MSG_NOERROR 如果包含它,则当消息包的大小>msgsz时,不会报错,并接收msgsz字节
 * 返回值: 成功接收到的消息的字节数
*/
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

/**
 * 功能: 销毁消息队列、获取消息队列属性、设置消息队列属性
 * @shmid: IPC描述符
 * @cmd:
 *	IPC_SET		-设置消息队列的属性  buf输入型参数,只有uid、gid、mode可设置
 *	IPC_STAT	-获取消息队列属性	buf输出型参数
 *	IPC_RMID	-删除消息队列	buf给NULL即可
*/
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

struct msqid_ds {
   struct ipc_perm msg_perm;     //	与shmctl的一致
   time_t          msg_stime;    /* Time of last msgsnd(2) */
   time_t          msg_rtime;    /* Time of last msgrcv(2) */
   time_t          msg_ctime;    //	最后属性修改时间
   unsigned long   __msg_cbytes; // 消息队列中的字节数
   msgqnum_t       msg_qnum;     //	消息队列中的消息数
   msglen_t        msg_qbytes;   //	消息队列运行的最大字节数
   pid_t           msg_lspid;    /* PID of last msgsnd(2) */
   pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
};

编程模型

		进程A				进程B
    创建消息队列		  获取消息队列
    发送消息(type:a)	接收消息(type:a)
    接收消息(type:b)    发送消息(type:b) 
    销毁消息队列		

4、信号量:

基本原理

  • 所谓信号量就是内核中维护的一个全局变量,当做计数器,用于计数多进程工作中的共享资源数,也是为了限制多进程对共享资源的使用
  • 信号量是一种数据操作锁,本身是不具备数据交换通信功能的,而是通过控制其他进程的通信资源来协助实现进程间通信
    • 假设操作系统中有n个共享资源,需要把信号量的值设置为n
    • 当有m个进程需要以独占的形式使用k个共享资源,并且m*k>n,需要使用信号量
    • 每个进程在使用共享资源之前先尝试执行信号量-1操作
    • 如果信号量>0时,说明剩余共享资源够用,则-1后拿到共享资源去执行,当执行完毕后,需要把共享资源归还,信号量+1操作
    • 如果信号量<=0时,说明共享资源不足,则进程阻塞等待,直到信号量的值>0,才重新唤醒后,继续尝试-1
  • 注意:进程之间资源几乎都是相互独立的,共享资源很少,所以很少用到信号量

相关API

/**
 * 功能: 创建或获取信号量集合
 * @key: IPC键值
 * @nsems: 表示信号量集合中信号量的数量,一般写1即可
 * @semflg:
 *	0			-获取,不存在则失败
 *	IPC_CREAT	-创建,不存在则创建,存在会获取,除非
 *  IPC_EXCL	-排斥,已存在则创建失败
 *  mode		-消息队列权限,创建时必给
 * 返回值:IPC描述符,失败-1
*/
int semget(key_t key, int nsems, int semflg);

/**
 * 功能: 对信号量集中的信号量进行加减操作
 * @semid: IPC描述符
 * @sops: 结构体数组首地址
 * struct sembuf{
 *     unsigned short sem_num;  //	信号量在信号集中的下标
 *     short sem_op;   //  操作数
 *  				   //	如果sem_op大于0,则将其加到下标sem_num号信号量中,相当于资源释放
 * 				       //	如果sem_op小于0,则将其对下标sem_num号信号量中的值相减,相当于获取资源
 *     short sem_flg;  //	操作标记位
 *    			       //	0 如果下标sem_num号信号量不够减,进程会阻塞等待,直到资源数够减为止
 *    			       //	IPC_NOWAIT  不阻塞等待
 * };
 * @nsops: 表示sops结构体数组的指针中指向了多少个结构体,就是要操作的信号量的数量,一般写1
*/
int semop(int semid, struct sembuf *sops, size_t nsops);

/**
 * 功能: 删除信号量、获取信号量属性、设置信号量属性
 * @semnum: 表示对信号量集合中semnum下标的信号量进行操作
 * @cmd:
 *	IPC_SET		-设置信号量的属性  buf输入型参数,只有uid、gid、mode可设置
 *	IPC_STAT	-获取信号量属性	buf输出型参数
 *	IPC_RMID	-删除信号量	buf给NULL即可
 *	GETALL		-获取信号量集合中所有信号量的值,放入semun.array
 *	SETALL		-设置信号量集合中所有信号量的值,通过semun.array修改
 *	SETVAL		-设置信号量集合中某个信号量的值,通过semun.val修改
 *	GETVAL		-获取信号量集合中某个信号量的值,通过返回值获取
 * 根据cmd的选择,使用第四个参数semun的值
 * union semun {
 *     int val;    // Value for SETVAL
 *     struct semid_ds *buf;    // Buffer for IPC_STAT, IPC_SET
 *     unsigned short *array;  /// Array for GETALL, SETALL
 *     struct seminfo *__buf;  // Buffer for IPC_INF (Linux-specific)
 *  };
*/
int semctl(int semid, int semnum, int cmd, ...);
posted @   sleeeeeping  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
  1. 1 吹梦到西洲 恋恋故人难,黄诗扶,妖扬
  2. 2 敢归云间宿 三无Marblue
吹梦到西洲 - 恋恋故人难,黄诗扶,妖扬
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

作词 : 颀鞍

作曲 : 铃木航海

编曲 : 远藤直弥/冯帆

制作人 : 冯帆/铃木航海

(妖扬)

(妖扬)

无何化有 感物知春秋

秋毫濡沫欲绸缪 搦管相留

(黄诗扶)

留骨攒峰 留容映水秀

留观四时曾邂逅 佳人西洲

(妖扬)

(妖扬)

西洲何有 远树平高丘

云闲方外雨不收 稚子牵牛

(黄诗扶)

闹市无声 百态阴晴栩栩侔

藤衣半卷苔衣皱 岁月自无忧

(妖扬)

(妖扬)

驾马驱车 尚几程扶摇入画中 咫尺

(黄诗扶)

径曲桥横 精诚难通

(黄诗扶、妖扬)

(黄诗扶、妖扬)

盼你渡口 待你桥头

松香接地走

挥癯龙绣虎出怀袖

起微石落海连波动

描数曲箜篌线同轴

勒笔烟直大漠 沧浪盘虬

一纸淋漓漫点方圆透

记我 长风万里绕指未相勾

形生意成 此意 逍遥不游

(妖扬)

(妖扬)

日月何寿 江海滴更漏

爱向人间借朝暮 悲喜为酬

(黄诗扶)

种柳春莺 知它风尘不可救

绵绵更在三生后 谁隔世读关鸠

(妖扬)

(妖扬)

诗说红豆 遍南国未见人长久 见多少

(黄诗扶)

来时芳华 去时白头

(黄诗扶、妖扬)

(黄诗扶、妖扬)

忘你不舍 寻你不休

画外人易朽

似浓淡相间色相构

染冰雪先披琉璃胄

蘸朱紫将登金银楼

天命碧城灰土 刀弓褐锈

举手夜古泼断青蓝右

照我 萤灯嫁昼只影归洪流

身魂如寄 此世 逍遥不游

(黄诗扶)

(黄诗扶)

情一物 无木成林无水行舟

情一事 未算藏谋真还谬

情一人 积深不厚积年不旧

情一念 墨尽非空 百代飞白骤 划地为囚

(妖扬)

(妖扬)

蓝田需汲酒 惟琼浆能浇美玉瘦

至高者清难垢 至贵者润因愁

痴竭火 知她不能求

醉逢歌 知他不必候

只约灵犀过隙灵光暗相投

(黄诗扶、妖扬)

(黄诗扶、妖扬)

万籁停吹奏

支颐听秋水问蜉蝣

既玄冥不可量北斗

却何信相思最温柔

顾盼花发鸿蒙 怦然而梦

你与二十八宿皆回眸

系我 彩翼鲸尾红丝天地周

情之所至 此心 逍遥不游

吉他 : ShadOw

钢琴 : ShadOw

和声编写 : 冯帆

和声 : 黄诗扶

人声混音 : 徐志明

混音 : 冯帆

母带 : 冯帆

企划 : 三糙文化

出品公司 : Negia Entertainment Inc.

点击右上角即可分享
微信分享提示