文件打开与创建
文件打开创建函数open原型和头文件
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); int creat(const char *pathname, mode_t mode); 返回值 文件描述符 pathname (含路径,缺省为当前路径) flags 权限 O_RDONLY:只读打开 O_WRONLY:只写打开 O_RDWR:可读可写打开 mode 权限模式 1.可读: r 4 2.可写: w 2 3.可执行 x 1 0600:6代表4+2(可读可写)
open函数打开一个文件:
-
利用open函数打开file1文件,如果文件存在则返回3,否则返回-1
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main() { int fd; fd = open("./file1",O_RDWR); printf("fd = %d\n",fd); return 0; }
open函数打开文件失败创建一个文件:
-
使用open函数打开file1文件失败后,就创建一个file1文件
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main() { int fd; fd = open("./file1",O_RDWR); //使用open函数打开file1文件 if(fd == -1){ //如果打开失败 printf("file1 no failed\n"); //输出文件1号失败 fd = open("./file1",O_RDWR|O_CREAT,0600); //文件不存在则创建它,需要同时说明第三个参数mode,权限为可读可写 if(fd > 0){ printf("create file1 success!\n"); //若文件描述符>0则代表创建文件成功! } } return 0; }
//我们创建的file1文件权限为:可读可写 查看文件权限指令:ls -l r 代表“可读” w 代表“可写” x 代表“可执行” mode 权限模式 1.可读: r 4 2.可写: w 2 3.可执行 x 1 0600:6代表4+2(可读可写)
/*O_EXCL如果同时指定了O_CREAT,而文件已经存在,那么打开文件失败*/ int main() { int fd; fd = open("./file1",O_RDWR|O_CREAT|O_EXCL,0600); if(fd == -1){ //如果O_EXCL和O_CREAT同时使用时,文件已经存在就返回-1代表文件存在 printf("file cunZai\n"); } return 0; }
文件写入操作编程
文件写入函数write和文件关闭函数close原型和头文件:
/* Linux下:man 2 write可以查看对应手册*/ #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); int fd //写入到刚打开的文件中 const void *buf //无符号类型的字符缓冲区 size_t count //写入的字节大小 ssize_t //若成功写入文件,函数返回值为写入字节个数,若失败,返回-1。 /*Linux下:man 2 close可以查看对应手册*/ #include <unistd.h> int close(int fd); int fd // 关闭刚写入的文件,一般在写入文件后执行文件的关闭
文件读取操作编程
文件读取函数read函数原型和头文件:
/*Linux下:man 2 read可以查看对应手册*/ #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); fd 文件描述符 void *buf 待读入缓冲区 size_t count 读出字节个数 ssize_t 若成功读取文件,函数返回值为读出字节个数,若失败,返回-1 read函数作用:从fd指向的文件读取count个字节的数据放到buf缓冲区里面
/*main函数参数功能*/ int main(int argc,char **argv) int argc argc 表示运行C文件参数的个数 char **argv argv是字符数组指针,每个指针都是一个数组,在这里表示每个参数的内容
fopen与open的区别
1、来源不同 open是unix系统调用函数(包括Linux),返回的是文件描述符,它是文件描述符表里的索引。 fopen是ANSIC标准中的C语言库函数,在不同的系统中应该调不同的内核api,返回的是一个指向文件结构的指针。
2、移植性 从来源看,fopen是C标准函数,因此拥有良好的移植性,而open是unix系统调用,移植性有限,如windows下相似的功能使用api函数CreatFile。
3、使用范围 open返回文件描述符,而文件描述符是unnix系统下的重要概念,unix下的一切设备都是文件的形式操作,如网络套接字、硬件设备等、当然包括操作普通正规文件(Regular File) Fopen是从来操纵普通正规文件(Regular File)的
4、 文件IO层次 如果从文件IO的角度来看,open属于低级IO函数,fopen属于高级IO函数,低级和高级的简单区分标准是:谁离系统内核更近,低级文件IO运行在内核态、高级文件IO运行在用户态。
5、 缓冲区 open没缓冲区(多次在用户态和内核态切换,执行速度慢,效率低),fopen有缓冲区(在缓冲区读写后一次性写入文件,执行速度快,效率高)
进程
什么是程序?什么是进程?有什么区别?
-
磁盘中生成的a.out文件,就叫做:程序
-
进程是程序的一次运行活动,通俗点意思是程序跑起来了,系统中就多了一个进程
-
程序是静态的概念,进程是动态的概念
在早期面向进程设计的计算机结构中,进程是程序的基本执行实体; 在当代面向线程设计的计算机结构中,进程是线程的容器。 进程是程序真正运行的实例,若干进程可能与同一个程序相关,且每个进程皆可以同步或异步的方式独立运行。
狭义定义:进程是正在运行的程序的实例。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。 第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
进程与程序的主要区别:
程序是永存的;进程是暂时的,是程序在数据集上的一次执行,有创建有撤销,存在是暂时的; 程序是静态的观念,进程是动态的观念; 进程具有并发性,而程序没有; 进程是竞争计算机资源的基本单位,程序不是。 进程和程序不是一一对应的: 一个程序可对应多个进程即多个进程可执行同一程序; 一个进程可以执行一个或几个程序
并发与并行的区别
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。
描述进程—–PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct 。 在Linux中描述进程的结构体叫做task_struct。 task_struct是Linux内核的⼀一种数据结构,它会被装载到RAM(内存)⾥里并且包含着进程的信息。
什么是进程标识符?
进程标识符(process identifier,又略称为进程ID,或者PID)是大多数操作系统的内核用于唯一标识进程的一个数值。这一数值可以作为许多函数调用的参数,以使调整进程优先级、杀死进程之类的进程控制行为成为可能。 在各 PID 中,较为特别的是 0 号 PID 和 1 号 PID。PID 为 0 者为交换进程(swapper),属于内核进程,负责分页任务;PID 为 1 者则常为 init 进程,主要负责启动与关闭系统。值得一提的是,1 号 PID 本来并非是特意为 init 进程预留的,而 init 进程之所以拥有这一 PID,则是因为 init 即是内核创建的第一个进程。不过,现今的许多 UNIX/类 UNIX 系统内核也有以进程形式存在的其他组成部分,而在这种情况下,1 号 PID 则仍为 init 进程保有,以与之前系统保持一致。 每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证
getpid函数
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); 功能: 获取本进程号(PID) pid_t getppid(void); 获取调用此函数的进程的父进程号(PPID)
进程的特征
进程是由多程序的并发执行而引出的,它和程序是两个截然不同的概念。进程的基本特征是对比单个程序的顺序执行提出的,也是对进程管理提出的基本要求。
动态性:进程是程序的一次执行,它有着创建、活动、暂停、终止等过程,具有一定的生命周期,是动态地产生、变化和消亡的。动态性是进程最基本的特征。 并发性:指多个进程实体,同存于内存中,能在一段时间内同时运行,并发性是进程的重要特征,同时也是操作系统的重要特征。引入进程的目的就是为了使程序能与其他进程的程序并发执行,以提高资源利用率。 独立性:指进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位。凡未建立PCB的程序都不能作为一个独立的单位参与运行。 异步性:由于进程的相互制约,使进程具有执行的间断性,即进程按各自独立的、 不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此,在操作系统中必须配置相应的进程同步机制。 结构性:每个进程都配置一个PCB对其进行描述。从结构上看,进程实体是由程序段、数据段和进程控制段三部分组成的。
进程的状态和转换 五种状态
进程在其生命周期内,由于系统中各进程之间的相互制约关系及系统的运行环境的变化,使得进程的状态也在不断地发生变化(一个进程会经历若干种不同状态)。通常进程有以下五种状态,前三种是进程的基本状态。
运行状态:进程正在处理机上运行。在单处理机环境下,每一时刻最多只有一个进程处于运行状态。 就绪状态:进程已处于准备运行的状态,即进程获得了除处理机之外的一切所需资源,一旦得到处理机即可运行。 阻塞状态:又称等待状态:进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理机)或等待输入/输出完成。即使处理机空闲,该进程也不能运行。 创建状态:进程正在被创建,尚未转到就绪状态。创建进程通常需要多个步骤:首先申请一个空白的PCB,并向PCB中填写一些控制和管理进程的信息;然后由系统为该进程分 配运行时所必需的资源;最后把该进程转入到就绪状态。 结束状态:进程正从系统中消失,这可能是进程正常结束或其他原因中断退出运行。当进程需要结束运行时,系统首先必须置该进程为结束状态,然后再进一步处理资源释放和 回收等工作。
注意区别就绪状态和等待状态:就绪状态是指进程仅缺少处理机资源,只要获得处理机资源就立即执行;而等待状态是指进程需要其他资源(除了处理机)或等待某一事件。之所以把处理机和其他资源划分开,是因为在分时系统的时间片轮转机制中,每个进程分到的时间片是若干毫秒。也就是说,进程得到处理机的时间很短且非常频繁,进程在运行过程中实际上是频繁地转换到就绪状态的;而其他资源(如外设)的使用和分配或者某一事件的发生(如I/O操作的完成)对应的时间相对来说很长,进程转换到等待状态的次数也相对较少。这样来看,就绪状态和等待状态是进程生命周期中两个完全不同的状态,很显然需要加以区分。
状态转换
就绪状态 -> 运行状态:处于就绪状态的进程被调度后,获得处理机资源(分派处理机时间片),于是进程由就绪状态转换为运行状态。
运行状态 -> 就绪状态:处于运行状态的进程在时间片用完后,不得不让出处理机,从而进程由运行状态转换为就绪状态。此外,在可剥夺的操作系统中,当有更高优先级的进程就 、 绪时,调度程度将正执行的进程转换为就绪状态,让更高优先级的进程执行。
运行状态 -> 阻塞状态:当进程请求某一资源(如外设)的使用和分配或等待某一事件的发生(如I/O操作的完成)时,它就从运行状态转换为阻塞状态。进程以系统调用的形式请求操作系统提供服务,这是一种特殊的、由运行用户态程序调用操作系统内核过程的形式。
阻塞状态 -> 就绪状态:当进程等待的事件到来时 ,如I/O操作结束或中断结束时,中断处理程序必须把相应进程的状态由阻塞状态转换为就绪状态。
什么是父进程?什么是子进程?
-
进程A创建了进程B,那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系
C程序的存储空间是如何分配的
以下是C程序中存储空间分配的一般规则: 代码段(Code Segment): 代码段包含程序的机器指令,即程序的实际执行代码。 在大多数情况下,代码段被放在内存中的某个固定位置,并且通常对其他部分是不可见的。 代码段通常被加载到内存中的某个固定的起始地址,这个地址称为基地址(Base Address)。 数据段(Data Segment): 数据段包含程序的静态变量、全局变量和常量。 全局变量和静态变量在程序启动时分配内存,并且在程序运行期间保持不变。 常量也被存储在数据段中,它们通常不会被修改。 栈(Stack): 栈是一个后进先出(LIFO)的数据结构,用于存储函数调用、局部变量和返回地址等信息。 栈通常由操作系统自动管理,其大小和位置在程序启动时确定。 函数调用时,栈会为新的函数调用分配内存空间,包括局部变量和返回地址。 函数返回时,栈会释放这些内存空间。 堆(Heap): 堆是动态内存分配的区域,用于存储程序运行期间动态分配的内存。 堆的大小通常在程序运行期间是可变的,并且可以扩展或收缩。 动态分配的内存可以在函数内部使用 malloc()、calloc()、realloc() 和 free() 函数进行管理。
创建进程函数fork的使用
创建进程fork()
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型. #include <sys/types.h> #include <unistd.h> pid_t fork(void);
功能: 用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。 参数: 无 返回值: 成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为整型。 失败:返回-1。 失败的两个主要原因是: 1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。 2)系统内存不足,这时 errno 的值被设置为 ENOMEM
一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。 子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。 UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。所以在移植代码的时候我们不应该对此作出任何的假设。
由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
调用fork之后,数据、堆、栈有两份,代码仍然为一份但是这个代码段成为两个进程的共享代码段都从fork函数中返回,箭头表示各自的执行处。当父子进程有一个想要修改数据或者堆栈时,两个进程真正分裂。
引用一位网友的话来解释Pid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的Pid指向子进程的进程id,因为子进程没有子进程,所以其Pid为0。” 2.3 编程实现创建子进程 并且分别获取子进程和父进程的PID号:
根据父进程和子进程的pid不同的特点,我们可以在创建进程之前获取一次进程pid,这是父进程的pid,创建进程之后再一次获取进程pid,并通过判断两次pid是否相同判断哪个是父进程pid,哪个是子进程pid
#include <sys/types.h> #include <unistd.h> #include <stdio.h> int main() { pid_t pid; pid_t pid2; pid = getpid(); //获取fork之前进程PID printf("fork之前PID = %d\n",pid); fork(); //创建一个子进程 pid2 = getpid(); //获取fork之后进程PID printf("fork之后PID = %d\n",pid2); if(pid == pid2){ //如果pid == pid2代表是父进程 printf("父进程PID\n"); }else{ //如果pid != pid2代表是子进程 printf("子进程PID,子进程PID = %d\n",getpid()); } return 0; }
父子进程交替执行
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <string.h> void test01() { pid_t pid = fork(); if (pid < 0) { perror("fork"); exit(1); } else if (pid == 0) { int i = 0; while (1) { printf("%d this is child process \n",i++); sleep(1); } } else { int j = 0; while (1) { printf("%d this is parent process \n",j++); sleep(1); } } } int main() { test01(); return 0; }
fork创建的子进程 会复制 父进程资源
根据fork函数的返回值也可以判断父子进程:
#include <sys/types.h> #include <unistd.h> #include <stdio.h> int main() { pid_t pid; pid_t pid2; pid_t retpid; pid = getpid(); //获取fork之前进程PID printf("fork之前PID = %d\n",pid); retpid = fork(); //创建一个子进程 pid2 = getpid(); //获取fork之后进程PID printf("fork之后PID = %d\n",pid2); if(pid == pid2){ //如果pid == pid2代表是父进程 printf("父进程PID,retpid = %d,父进程PID = %d\n",retpid,getpid()); }else{ //如果pid != pid2代表是子进程 printf("子进程PID,retpid = %d,子进程PID = %d\n",retpid,getpid()); } }
fork之前只有一个父进程在运行,父进程的PID是54994,然后调用fork函数创建了一个子进程,fork调用成功后返回两次,两次返回唯一的区别是:子进程返回0值,父进程返回子进程PID是54995
fork创建子进程的目的:
-
一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达
创建 子进程的过程
使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。 地址空间: 包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。 子进程所独有的只有它的进程号,计时器等。因此,使用fork函数的代价是很大的
fork创建的父子进程 谁先运行?
在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。 注意:读共享 写独立 那么 子进程 复制父进程的资源 父子进程拥有独立的空间**
创建 子进程的过程
使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。 地址空间: 包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。 子进程所独有的只有它的进程号,计时器等。因此,使用fork函数的代价是很大的
fork创建的父子进程 谁先运行?
在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。 注意:读共享 写独立
vfork函数创建进程
3.1 进程创建函数vfork函数原型和头文件:
#include <sys/types.h> #include <unistd.h> pid_t vfork(void); 无参数 pid_t 是一个宏定义,其实质是int 被定义在<sys/types.h>中 fork函数调用成功,返回两次 返回值为0 代表当前进程是子进程 返回值非负数 代表当前进程为父进程 调用失败,返回-1 vfork - 创建子进程并阻塞父进程
vfork函数与fork函数的关键区别一:
-
vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行
#include <sys/types.h> #include <unistd.h> #include <stdio.h> int main() { pid_t pid; printf("父进程PID = %d\n",getpid()); //获取父进程PID pid = fork(); //创建一个子进程 if(pid == 0){ //返回值如果是0代表是子进程 while(1){ printf("PID = 0代表是子进程,子进程PID = %d\n",getpid()); sleep(1); } }else if(pid > 0){ //返回值如果是非负数整代表是父进程 while(1){ printf("PID > 0代表是父进程,父进程PID = %d\n",getpid()); sleep(1); } } return 0; }
vfork函数与fork函数的关键区别二:
-
vfork直接使用父进程存储空间,与父进程共享数据段,不拷贝。
、进程退出
进程退出的三种情况:
-
代码运行完毕,结果正确
-
代码运行完毕,结果不正确
-
代码异常终止,进程崩溃
进程退出码:
main函数是间接性被操作系统所调用的。当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。 当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码,可以使用echo $?命令查看最近一次进程退出的退出码信息。
代码正常运行结束后可以用echo $?命令查看退出码是0
进程正常退出:
从man函数返回,即调用return函数 调用exit,标准C语言库 调用_exit或者 _Exit,属于系统调用 进程最后一个线程返回 最后一个线程调用pthread_exit 最后一个线程对取消(cancellation)请求做出响应
return退出:
在main函数中使用return退出是我们常用的方法
exit函数退出
exit函数说明:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构。exit(0)表示正常退出,exit(x)(x不为0)表示异常退出,这个x是返回给操作系统(包括UNIX,Linux,和MS DOS)的,以供其他程序使用。
_exit和 _Exit函数退出:
_exit函数会立即终止调用过程。属于该进程的任何打开的文件描述符都被关闭;进程的任何子进程都由init进程(初始化进程,进程ID:1)继承,进程的父进程将被发送一个SIGCHLD信号。值状态作为进程的退出状态返回给父进程,并且可以使用wait(2)系列调用之一收集。
Exit函数等效于 _exit函数。
exit函数和_exit函数的区别:
exit()函数定义在stdlib.h中,而_exit()定义在unistd.h中 最大的区别就在于exit()函数在调用 exit 系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是"清理I/O缓冲"
exit()在结束调用它的进程之前,要进行如下步骤:
调用atexit()注册的函数(出口函数),按ATEXIT注册时相反的顺序调用所有由它注册的函数,这使得我们可以指定在程序终止时执行自己的清理动作。例如,保存程序状态信息于某个文件,解开对共享数据库上的锁等。 cleanup()关闭所有打开的流,这将导致写所有被缓冲的输出,删除用TMPFILE函数建立的所有临时文件。 最后调用_exit()函数终止进程。
_exit在结束调用它的进程之前,要进行如下步骤:
关闭属于该进程的所有打开的文件描述符。 进程的任何子进程都由init进程继承。 向进程的父进程发送SIGCHLD信号。
进程异常退出:
调用abort 由信号终止,如ctrl+c
五、父进程等待子进程退出 为什么父进程要等待子进程退出:
父进程等待子进程退出并收集子进程退出状态,如果子进程退出状态不被收集,那么子进程会变成僵尸进程。 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。
在之前我们使用vfork创建子进程的时候,子进程退出时没有被父进程收集其退出的状态,因此子进程最终会变成“僵尸进程”。
进程等待相关函数wait原型和头文件:
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能: 阻塞等待子进程退出 回收子进程残留资源 获取子进程结束状态(退出原因) wait一旦被调用,就会一直阻塞在这里,直到有一个子进程退出出现为止。 调用成功,则清理掉的子进程ID,失败则返回-1,表示没有子进程。 使用wait函数传出参数status来保存进程的退出状态(正常终止→退出值;异常终止→终止信号)。
借助宏函数来进一步判断进程终止的具体原因。 5.3 检查wait和waitpid所返回的终止状态的宏:
1.WIFEXITED(status) 为非0 → 进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)
2.WIFSIGNALED(status) 为非0 → 进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
3.WIFSTOPPED(status) 为非0 → 进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
进程间通信
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放。那么释放的资源可能是其他进程需要的,然而进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
无名管道通信
-
无名管道,是 UNIX 系统IPC最古老的形式
它是半双工的(即数据只能在一个方向上流动),具有固定读端和写端。
它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
无名管道应用:
单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0]
)与子进程的写端(fd[1]
);反之,则可以使数据流从子进程流向父进程。
有名管道FIFO
-
FIFO( First Input First Output)简单说就是指先进先出,也称为命名管道,它是一种文件类型
FIFO的特点:
-
FIFO可以在无关的进程之间交换数据,与无名管道不同。
-
FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中
一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。
消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
消息队列的特点:
消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
消息队列 创建/打开函数msgget()原型和头文件:
/* Linux下 man 2 msgget查看手册 */ #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget(key_t key, int msgflg); // int 函数返回值,成功:返回消息队列的ID 出错:-1,错误原因存于error中 key_t key 函数ftok的返回值(ID号)或IPC_PRIVATE int msgflg 1. IPC_CREAT:创建新的消息队列。 2. IPC_EXCL:与IPC_CREAT一同使用,表示如果要创建的消息队列已经存在,则返回错误。 3. IPC_NOWAIT:读写消息队列要求无法满足时,不阻塞。返回值: 调用成功返回队列标识符,否则返回-1. /* 函数说明:用于创建一个新的或打开一个已经存在的消息队列,此消息队列与key相对应 */
消息队列 发送消息函数msgsnd()原型和头文件:
/* Linux下 man 2 msgsnd查看手册*/ #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); int 函数返回值,成功返回0,失败返回-1 错误原因存于error中 int msqid 由msgget函数返回的消息队列标识码,表示往哪个消息队列发数据 void *msgp 发送给队列的消息。是⼀个指针,指针指向准备发送的消息(即准备发送的消息的内容)msgp定义的参照格式如下: struct msgbuf { long mtype; /* message type, must be > 0 */ char mtext[128]; /* message data */ }; long mtype:它必须以⼀个long int⻓整数开始,接收者函数将利⽤这个⻓整数确定消息的类型 char mtext[128] :保存消息内容的数组或指针,它必须小于系统规定的上限值 size_t msgsz 要发送消息的大小,不含消息类型占用的4个字节,即mtext的长度 int msgflg 1. 0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列 2. IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回 3. IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。 /* 函数说明:将msgp消息写入到标识符为msqid的消息队列 */
共享内存
共享内存实现进程间通信,是操作系统在实际物理内存开辟一块空间,一个进程在自己的页表中,将该空间和进程地址空间上的共享区的一块地址空间形成映射关系。另外一进程在页表上,将同一块物理空间和该进程地址空间上的共享区的一块地址空间形成映射关系。当一个进程往该空间写入内容时,另外一进程访问该空间,会得到写入的值,即实现了进程间的通信。
共享内存的特点:
共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。 因为多个进程可以同时操作,所以需要进行同步。 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问(共享内存实现的进程间通信底层不提供任何同步与互斥机制。如果想让两进程很好的合作起来,在IPC里要有信号量来支撑。)
用指令来查看和释放已经存在的共享内存:
ipcs -m //查看系统中的共享内存段
共享内存创建/获取函数shmget()原型和头文件:
/* Linux下 man 2 shmget查看手册 */ #include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg); int 函数返回值,成功返回共享内存的标识符ID,失败则返回-1 key_t key 通常要求此值来源于ftok返回的IPC键值 1. 0(IPC_PRIVATE):会建立新共享内存对象 2. 大于0的32位整数:视参数shmilg来确定操作。 size_t size 共享内存的大小 1. 大于0的整数:新建的共享内存大小,以字节为单位 2. 0:只获取共享内存时指定为0 int shmflg 权限标志,常用两个IPC_CREAT和IPC_EXCL,一般后面还加一个权限,相当于文件的权限 1. IPC_CREAT:创建一个共享内存返回,已存在打开返回 2. IPC_EXCL:配合着IPC_CREAT使用,共享内存已存在出错返回。 一般使用:IPC_CREAT | IPC_EXCL | 0666 /*函数说明:得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符*/
使用共享内存实现两个进程之间的通信:
/*shmwrite.c*/ #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> int main() { key_t key; int shmId; char *shmaddr; //key_t ftok(const char *pathname, int proj_id); key = ftok(".",1); //获取IPC键值 if(key == -1){ printf("获取IPC键值失败\n"); } //int shmget(key_t key, size_t size, int shmflg); shmId = shmget(key,1024*4,IPC_CREAT|0666); //创建一个共享内存 if(shmId == -1){ printf("创建共享内存失败\n"); exit(-1); } //void *shmat(int shmid, const void *shmaddr, int shmflg); shmaddr = shmat(shmId,NULL,0); //把共享内存区对象映射到调用进程的地址空间 if(*shmaddr == -1){ printf("映射共享内存失败\n"); } strcpy(shmaddr,"chenlichen handsome"); //往共享内存里面发送数据 sleep(5); //避免写程序运行太快,避免读程序读不到 //int shmdt(const void *shmaddr); int dt = shmdt(shmaddr); //断开与共享内存的连接 if(dt == -1){ printf("共享内存断开连接失败\n"); } //int shmctl(int shmid, int cmd, struct shmid_ds *buf); //shmctl(shmId,IPC_RMID,NULL); //删除共享内存 printf("退出\n"); return 0; }
信号
对于Linux来说,实际信号是软中断,许多重要的程序都需要处理信号,信号为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序。
5.1 信号的名称和编号:
每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。 信号定义在signal.h头文件中,信号名都定义为正整数。 具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号,kill对于信号0又特殊的应用。
信号的处理:
信号的处理有三种方法,分别是:忽略、捕捉和默认动作 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL和SIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景。 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。 具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。
信号的应用之杀死进程:
其实对于常用的 kill 命令就是一个发送信号的工具,kill 9 PID来杀死进程。比如,在后台运行了一个进程,通过 ps 命令可以查看这个进程的 PID,通过 kill 9 来发送了一个终止进程的信号来结束了该进程。如果查看信号编号和名称,可以发现9对应的是 9) SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程。
信号量相关定义:
临界资源:能被多个进程共享,但一次只能允许一个进程使用的资源称为临界资源。 临界区:涉及到临界资源的部分代码,称为临界区。 互斥:亦称间接制约关系,在一个进程的访问周期内,另一个进程就不能进行访问,必须进行等待。当占用临界资源的进程退出临界区后,另一个进程才允许去访问此临界资源。 例如,在仅有一台打印机的系统中,有两个进程A和进程B,如果进程A需要打印时, 系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将进程A唤醒,并将其由阻塞状态变为就绪状态。 同步:亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的同步就是源于它们之间的相互合作。所谓同步其实就是两个进程间的制约关系。 例如,输入进程A通过单缓冲向进程B提供数据。当该缓冲区空时,进程B不能获得所需数据而阻塞,一旦进程A将数据送入缓冲区,进程B被唤醒。反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲数据时,才唤醒进程A。 原子性:对于进程的访问,只有两种状态,要么访问完了,要么不访问。当一个进程在访问某种资源的时候,即便该进程切出去,另一个进程也不能进行访问。