首页 > 系统相关 >操作系统-进程

操作系统-进程

时间:2024-08-22 19:17:09浏览次数:11  
标签:IPC 操作系统 int 创建 信号量 进程 共享内存

一、进程

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:
#include <sys/ipc.h>
#include <sys/shm.h>

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

void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:加载共享内存(把进程中的虚拟地址与内核中的共享内存建立映射关系)
shmid:IPC描述符,要映射哪条共享内存
shmaddr:想要映射的虚拟地址,可以给NULL由操作系统自动分配
shmflg:
	0				- 以读写方式映射共享内存
	SHM_RDONLY		- 以只读方式映射共享内存
返回值:映射成功后的虚拟内存首地址,失败返回-1
    
int shmdt(const void *shmaddr);
功能:取消虚拟内存与共享内存的映射,也叫卸载共享内存    
shmaddr:要取消映射的虚拟内存地址
    
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:销毁共享内存、获取共享内存属性、设置共享内存的属性
shmid:IPC描述符
cmd:
	IPC_SET		-设置共享内存的属性  buf输入型参数,只有uid、gid、mode可设置
	IPC_STAT	-获取共享内存属性	buf输出型参数
	IPC_RMID	-删除共享内存	buf给NULL即可,
    			-删除并非真正直接删除,而是对共享内存的使用计数-1,如果该计数被-1为0后,意味着系统中没有任何的进程映射这块共享内存,才会从内核中真正销毁它
    			
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
  创建共享内存		获取共享内存
  映射共享内存		映射共享内存
  写数据并通知对方	  接收到通知后读取数据
  接收到通知后读取数据 写数据并通知对方
  取消映射		     取消映射
  删除共享内存
//	shmA.c

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

static int shmid;
static char* shm;

//  接收到对方发送信号 才读取数据
void sigread(int num)
{
    printf("\nread:%s\n",shm);
    if(0 == strncmp(shm,"quit",4))
    {   
        printf("对方不想鸟你了\n");

        //  取消映射
        if(shmdt(shm))
        {
            perror("shmdt");
            exit(EXIT_FAILURE);
        }

        //  删除共享内存
        if(shmctl(shmid,IPC_RMID,NULL))
        {
            perror("shmctl");
            exit(EXIT_FAILURE);
        }      
        //  必须结束进程 return没有用
        exit(EXIT_SUCCESS);
    }   
}
int main(int argc,const char* argv[])
{
    signal(SIGRTMIN,sigread);
    printf("我是进程%u\n",getpid());

    //  创建共享内存
    shmid = shmget(ftok(".",110),4096,IPC_CREAT|0644);
    if(0 > shmid)
    {
        perror("shmget");
        return EXIT_FAILURE;
    }

    //  映射共享内存
    shm = shmat(shmid,NULL,0);
    if((void*)-1 == shm)
    {
        perror("shmat");
        return EXIT_FAILURE;
    }

    //  获取对方的进程id
    pid_t pid = 0;
    printf("请输入与我通信的进程ID:");
    scanf("%u",&pid);

    //  写入数据并通知对方
    for(;;)
    {
        printf(">>>");
        scanf("%s",shm);

        //  发送信号
        kill(pid,SIGRTMIN);
        if(0 == strncmp(shm,"quit",4))
        {
            printf("你终止通信了\n");
            break;
        }
    }

    //  取消映射
    if(shmdt(shm))
    {
        perror("shmdt");
        return EXIT_FAILURE;
    }
    //  删除共享内存
    if(shmctl(shmid,IPC_RMID,NULL))
    {
        perror("shmctl");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}



3、消息队列:

基本原理:
  • 由系统内核维护的一个链式队列,每个节点称为一条消息,每条消息由消息类型、数据、长度信息组成
  • 和管道类似,可以双向通信,并且从中读取一个消息后,会自动出队,而且不是按照顺序读取,是按照消息类型读取
相关API:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

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

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,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字节
返回值:成功接收到的消息的字节数

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

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:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
功能:创建或获取信号量集合
key:IPC键值
nsems:表示信号量集合中信号量的数量,一般写1即可
semflg:
	0			-获取,不存在则失败
	IPC_CREAT	-创建,不存在则创建,存在会获取,除非
    IPC_EXCL	-排斥,已存在则创建失败
    mode		-消息队列权限,创建时必给
返回值:IPC描述符,失败-1
    
int semop(int semid, struct sembuf *sops, size_t nsops);
功能:对信号量集中的信号量进行加减操作
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 semctl(int semid, int semnum, int cmd, ...);
功能:删除信号量、获取信号量属性、设置信号量属性
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_INFO
                                   (Linux-specific) */
   };

标签:IPC,操作系统,int,创建,信号量,进程,共享内存
From: https://www.cnblogs.com/sleeeeeping/p/18369834

相关文章

  • 不同平台下对进程资源进行限制(CPU与内存)
    不同平台下对进程资源进行限制(CPU与内存)因实际工作中发现,如果不对某些进程硬件资源进行限制,可能某个进程会把操作系统资源耗尽,导致操作系统死机等问题出现。于是就想,是否有什么方法可以限制指定进程内存使用上限,避免其无上限申请内存。WindowsWindows平台可通过作业对......
  • Node.js获取操作系统指标和参数
    constos=require('os');console.log("操作系统临时文件夹os.tmpdir():"+os.tmpdir());console.log("CPU的字节序os.endianness():"+os.endianness());console.log("操作系统主机名os.hostname():"+os.hostname());console.log("操作......
  • 电脑三大操作系统
    电脑需要运行,那就必须要有一个操作系统,一般情况下,电脑所装的系统是windows系统,除此之外,电脑的操作系统有很多的,windows是使用最多的一种,是微软公司的产品。下面就介绍下三大电脑操作系统,供大家参考。1、Windows使用最多MicrosoftWindows是美国微软公司研发的一套操作系统......
  • 操作系统之面试常考
    【转载】:https://www.cnblogs.com/zyf-zhaoyafei/p/4714598.html最近这段时间正在积极准备面试,复习到操作系统部分,本篇文章就介绍操作系统基础内容,参考第四版《计算机操作系统》这本文章总结了面试中常考、常用到的基本知识点,希望对准备面试的同学和想回顾操作系统知识点的程序......
  • 进程间通信方式详解
    正文每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。Linux内核提供了不少进程间通信的机制,我们来一起瞧瞧有哪些?管道如果你学过Linux命令,那你肯定很熟悉「|」这个竖线。$psauxf|grep......
  • 【OS系列】程序、进程与线程之区别大揭秘,一图读懂胜千言
    1.程序(Program)程序是一组指令的集合,它存储在磁盘上,是一个静态的实体。程序本身并不执行任何操作,它只是提供了一个执行的蓝图。例如,一个编译好的可执行文件(如Windows的.exe文件)就是一个程序。2.进程(Process)进程是程序的一次执行实例,是操作系统进行资源分配和调度的基本......
  • 进程(2) wait、exec函数族
    目录1. fork() 函数功能使用时注意事项2. exit() 函数功能使用时注意事项3. wait() 函数功能使用时注意事项总结wait()异常信号结束waitpidexec函数族execl()execlp()execvexecvpfork()、exit() 和 wait() 函数在进程管理中扮演着重要的角色,它们......
  • 【读书笔记-《30天自制操作系统》-6】Day7
    本篇向着移动鼠标的目标继续前进。先对中断处理进行一些补充说明,然后建立完善缓冲区来实现键盘数据接收。最后是在此基础上的初始化鼠标控制电路与鼠标的数据接收。1.中断处理程序补充说明前面的处理中,接收到键盘中断后只是显示一行信息,现在把按键的信息也一并显示出来......
  • 计算机四个方面:计算、存储、通信与程序;操作系统
    一、计算、存储、通信与程序计算机系统的四个基本方面是计算、存储、通信与程序。下面我将详细介绍这四个方面的特点、区别,以及在Linux系统中与之对应的自带命令。1.计算特点:计算是计算机最基本的功能,涉及数据的处理、运算和逻辑判断。计算能力决定了计算机解决问题的......
  • Python被远程主机强制关闭后怎么自动重新运行进程
    要实现Python程序在被远程主机强制关闭后能够自动重新运行,我们可以采用几种方法,但最直接且常用的方法之一是结合操作系统级的工具或脚本。在Linux系统中,我们可以使用cron作业或者systemd服务来实现这一功能;在Windows系统中,可以使用任务计划程序。但在这里,为了提供一个跨平台的、更......