程序: 是存储在存储设备(磁盘)上的数据,包含了可执行机器指令(二进制代码)和数据的静态实体。我们说程序不占用系统资源,这里的系统资源指CPU、内存等,但是不包括磁盘
进程: 运行的程序会变成进程,是已经被 OS 从磁盘加载到内存上的、动态的、可运行的指令与数据的集合
存在于CPU中,他有两个功能:
① 完成虚拟内存地址到物理内存地址的映射
② 设置 / 修改内存的访问级别
每个进程都有独立的虚拟地址空间,进程访问的虚拟地址并不是真正的物理地址。虚拟内存可以看作是由软件产生的,并不真实存在,他和物理内存相反,物理内存就是内存条等真实存在的存储空间。虚拟地址可通过每个进程的页表与物理地址进行映射,获得真正的物理地址。在32位系统上,虚拟内存的大小是4G,它被分为用户存储空间和内核存储空间两大块。
虚拟内存中保存了进程运行的所有数据,如:代码,变量,栈(里面有函数及函数里的变量等),堆(malloc分配的存储空间等),PCB等。程序中的变量等使用的都是虚拟内存,但是,虚拟内存实际上并不存在,他们的虚拟内存地址实际上是被MMU映射到了真实的物理内存中。
进程在实际的物理内存中也远远没有4G那么大,进程运行时需要多大内存,才把相应的数据映射到物理内存中。
由低地址到高低值分别为:
1、只读段: 该部分空间只能读,不可写;(包括:代码段、rodata 段(C常量字符串和#define定义的常量) )
2、数据段: 保存全局变量、静态变量的空间;
3、堆 : 就是平时所说的动态内存, malloc/new 大部分都来源于此。其中堆顶的位置可通过函数 brk 和 sbrk 进行动态调整。
4、文件映射区域: 如动态库、共享内存等映射物理空间的内存,一般是 mmap 函数所分配的虚拟地址空间。
5、栈: 用于维护函数调用的上下文空间,一般为 8M ,可通过 ulimit –s 查看。
6、内核虚拟空间: 用户代码不可见的内存区域,由内核管理(页表就存放在内核虚拟空间)。
PCB本质是一个结构体(task_struct
),每个进程的PCB都存储在内核虚拟空间中中,PCB结构体中包含如下信息:
pid_t
,本质是个unsigned int
umask
掩码,保护文件创建或修改的默认权限用于指定操作系统运行环境的一些参数。环境变量有很多。
PATH
就是一个典型的环境变量,他用于存储可执行文件的路径。当我们在shell上输入一个命令时,shell就会在自己的PATH
环境变量中寻找该命令对应的文件在哪条路径中。进行如下实验:
输入命令①,shell在自己的PATH
环境变量中寻找哪条路径含有date
文件
输入命令②,shell直接去到/bin
路径运行date
文件
命令③则是查看shell的PATH
环境变量。(echo
用于输出shell的变量的值)
fork
#include
#include
pid_t fork(void);
创建一个子进程。父进程中返回子进程的进程ID,子进程中返回0,失败返回-1。
创建子进程后,父进程中打开的文件描述符在子进程中也是打开的,且文件描述符的引用计数+1,次啊外,父进程的用户根目录、当前工作目录等变量的引用计数都会+1。
fork
后父子进程的异同父子相同处: 全局变量、数据段、代码段、栈、堆、环境变量、用户ID、宿主目录、进程工作目录
父子不同处: 进程ID、fork
的返回值、父进程ID、进程运行时间、定时器、未决信号集、
可以认为:子进程0-3G用户区的内容和父进程相同,内核区的内容(主要是PCB)有所不同。
虽然父子进程0-3G的内容一样,但子进程并不是将0-3G地址空间完全拷贝一份,而是遵循 读时共享写时复制 的原则。共享和复制都是指共享或复制物理内存,与虚拟内存无关。若fork
调用的后续只有对数据的读操作,那么子进程与父进程共享同一块物理内存,若存在写操作,那么子进程则会复制一份自己的数据。这样的设计,能够节省系统内存开销。
Linux操作系统底层借助MMU实现读时共享写时复制。
mmap
建立的映射区 注意:对于全局变量这种父子相同的内容,如果后面只有读操作,则父子共享该全局变量,若后面有写操作,则父子将不再共享。
getpid
和 getppid
#include
#include
pid_t getpid(void); 获取 当前 进程的ID。
pid_t getppid(void); 获取 父 进程的ID。
getuid
和 geteuid
#include
#include
pid_t getuid(void); 获取当前进程 实际 用户ID
pid_t geteuid(void); 获取当前进程 有效 用户ID
getgid
和 getegid
#include
#include
pid_t getgid(void); 获取当前进程 实际 用户组ID
pid_t getegid(void); 获取当前进程 有效 用户组ID
exec
族函数fork
创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec
函数以执行另一个程序。当进程调用一种exec
函数时,该进程的用户空间(0-3G的部分)代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec
并不创建新进程,所以调用exec
前后进程的ID并未改变。
一共有很多种exec
函数,但是它们的功能都是执行另一个程序,具体的方式有略微变化。
注意: exec
族函数只有失败才返回值-1并设置errno
。因为一旦这类函数调用成功,则后续的代码永远不会被执行,而是执行exec
加载的新程序并结束。原来程序的代码段整个被替换成了新程序的代码段。所以exec
族函数的调用成功返回值没有意义。原来进程中打开的文件会通过隐式回收关闭,不用担心这个问题。
exec
族函数不会关闭源程序打开的文件描述符,除非该文件描述符被设置了类似SOCK_CLOEXEC
的属性。
execlp
函数 函数名中的l
是 ‘list’ ,代表命令行参数列表 ; p
是PATH
,代表PATH
环境变量 。所以该函数的功能是加载一个进程,可以借助PATH
环境变量。
#include
int execlp(const char* file, const char* arg, ...);
file
参数:要执行的可执行程序的文件名
arg
参数:可执行程序的argv[0]
参数,其实还是可执行程序的文件名。
...
可变参数:命令行参数列表,以NULL
结尾。
execlp("ls", "ls", "-l", "-a", NULL); 等价于在 shell 中执行 "ls -l -a" 命令
execlp("ls", "djsdkjash", "-l", "-a", NULL); 等价于上一行调用
为什么第二个参数可以乱传呢?
第二个参数相当于argv[0]
参数,在ls
的内部实现中,该参数并没有被使用,所以可以乱传,所以第三个、第四个以及往后的参数都不允许乱传。
execl
函数没有了PATH
环境变量,需要使用路径 + 程序名的方式加载程序。
#include
int execl(const char* pathname, const char* arg, ...);
除了第一个参数的含义有改变外,其他参数含义与execlp
相同。
pathname
必须是含根目录的完整路径名 或 当前目录的相对路径。
execl("/bin/ls", "ls", "-l", "-a"); 与上面的 execlp 调用示例等价
这个函数使我们能够调用自己编写的可执行程序,只要把相对路径说清楚即可。
execle
函数末尾的e
是 environment。该函数令加载的新程序使用调用者提供的环境变量表,不使用进程原有的环境变量表。即,设置新加载程序运行的环境变量表。
#include
int execle(const char* pathname, const char* arg, ...);
execv
函数末尾的v
是 vector 。使用命令行参数数组。
#include
int execv(const char* pathname, char* const argv[]);
使用该函数前,首先要构建命令行参数数组,即,第二个参数argv
。如下面的例子所示:
char* argv[] = {"ls", "-l", "-a", NULL};
execv("/bin/ls", argv); 其实和我们在 shell 中敲命令差不多
execvp
函数和execv
函数类似,只不过在使用时可以借助PATH
环境变量。
execve
函数和execv
函数类似,只不过在使用时可以借助我们提供的环境变量表。
一个进程在终止时会关闭所有文件描述符,并释放在用户空间分配的内存,但他的PCB还保留着,内核在其中保存了一些信息:如果正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait
或 waitpid
获取这些信息,并彻底清除掉这个进程(及其PCB)。
在shell中,可以使用echo $?
命令查看上一个进程的退出状态,因为shell是他的父进程,当他终止时shell调用wait
或 waitpid
得到他的退出状态同时彻底清除掉这个进程。
父进程先于子进程结束,则子进程称为孤儿进程。此时,子进程的父进程变成了 init
进程,由init
进程回收该子进程。
子进程先于父进程终止,但是父进程没有回收子进程,此时,子进程的PCB残留于内核中,占有部分资源,变成僵尸进程。这样的僵尸进程越来越多会对系统运行造成危害。
kill
命令不能回收僵尸进程。 因为kill
命令是用来终止进程的,而僵尸进程已经终止了。
wait
函数父进程调用 wait
函数能够阻塞的回收一个子进程,并获得其结束状态:
#include
#include
pid_t wait(int* status);
status
是一个传出参数,用于获取子进程退出状态,我们需要定义一个整型参数,并传其地址进去。
函数成功返回回收的进程ID,调用失败或没有可回收的子进程返回-1并设置errno
。
wait
会阻塞的等待子进程退出,只要子进程不退出,父进程也干不了别的。
如何回收僵尸进程?只能杀死他的父进程,从而迫使子进程的父进程变更为init
进程,init
进程发现他是个僵尸进程,会调用wait
函数回收它。
status
获取子进程退出状态首先明确子进程终止的两种情况:
———正常终止 → 退出值
———异常终止 → 终止信号(linux所有程序的异常终止都是因为收到了某个信号)
我们的目的是获取子进程的退出值 或 终止信号。需要借助status
和宏函数进一步判断进程终止的具体原因。分为三种情况。
WIFEXITED(status); 返回非0 ---→ 进程正常结束,要获取退出值继续调用下面的宏
WEXITSTATUS(status); 若上宏为真,调用此宏 ---→ 获取退出值(exit函数的参数,或最后return的值)
第一个宏只能判断是否正常结束,要获取具体的退出值需第一个宏为真后调用第二个宏。
WIFSIGNALED(status); 为非0 ---→ 进程异常结束,要获取退出值继续调用下面的宏
WTERMSIG(status); 若上宏为真,调用此宏 ---→ 获取使进程终止的信号的编号
WIFSTOPPED(status); 为非0 ---→进程暂停
WSTOPSIG(status); 若上宏为真,调用此宏 ---→ 获取使进程暂停的信号的编号
WIFCONTINUED(status); 为真 ---→ 进程暂停后以继续运行
waitpid
函数waitpid
相较于wait
函数更加灵活,他能回收指定ID的子进程,而且可以工作在非阻塞模式:
#include
#include
pid_t waitpid(pid_t pid, int* status, int options);
pid
指定要回收的进程ID,其值有四种情况:
① >0: 要回收的子进程ID
② - 1: 回收任意一个子进程
③ =0: 回收和当前调用waitpid
一个组的任意一个子进程
④ < -1: 回收指定进程组内的任意一个子进程,如-10023
表示回收10023
进程组内的任意一个子进程。
status
参数的含义与wait
函数相同。
options
参数用于指定回收是阻塞还是非阻塞。若其值为0,则为阻塞,若其值为 WNOHANG
,则为非阻塞。
若第三个参数的值为WNOHANG
,且调用waitpid
时子进程还没结束,那么此次调用返回0。若子进程被回收,则返回子进程ID,调用失败或没有可回收的子进程返回-1。
在调用fork
创建子进程之前,创建一个管道,然后后面调用fork
后,父子进程就都有了管道的读端和写端,这样父子进程就能通过管道通信了。
mmap
函数mmap
函数就是把磁盘中的文件映射到内存中的一片地址空间(这个工作是由MMU完成的),并返回指向该地址空间的指针。
#inlcude<sys/mman.h>
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
addr参数: 我们可以用该参数指定建立映射区的首地址,但是一般都传入NULL
,表示由Linux内核指定首地址。该参数类型void*
是个泛型指针,它可以隐式转换为任何类型的指针,因为可能还不知道映射区中存放什么类型的数据。
length参数: 指定映射区的大小,一般由被映射的文件大小决定。毕竟你是吧文件映射到这里。
prot参数: 设置用户对该映射区的访问权限(注意:这只是对内存的限制)
flags参数: 设置 映射区内数据被修改后产生的结果。(如:映射区数据修改后,映射的文件中的数据也被修改还是不被修改)
fd参数: 映射的文件
offset参数: 映射文件的偏移(必须是4Kb的整数倍),从开头开始映射,还是从4Kb的位置开始映射,还是从8Kb的位置开始映射…
函数调用成功返回映射区的首地址,失败返回MAP_FAILED
并设置errno
。
munmap
函数:#include
int munmap(void* addr, size_t length);
该函数用来关闭mmap
打开的映射区。addr
为映射区首地址的指针,length
为映射区的大小。成功返回0,失败返回-1。
mmap
函数的注意事项: ftruncate
的函数指定文件大小,因为默认创建的文件大小为0,而映射区的大小不能为0,所以必须指定文件大小。<=
文件打开的权限,创建映射区的过程中隐含着一次对文件的读操作。mmap
父子进程通信有血缘关系的父子进程通过mmap
建立的映射区通信时,要注意flags
参数应设置为MAP_SHARED
:
MAP_PRIVATE 父子进程各自独占映射区
MAP_SHARED 父子进程共享映射区
目前为止,每次创建映射区一定要依赖一个临时文件才能实现,我们需要对该临时文件进行open
、unlink
、close
等操作。如果我们创建映射区只是为了父子进程通信,其实根本不需要文件以及里面的内容,这些文件操作就显得很多余。Linux提供了创建匿名映射区的方法,无需依赖文件即可创建映射区。该操作主要通过flags
参数指定。
MAP_ANONYMOUS
该 flags
的值表示:这段映射区不是从文件映射而来的,其内容全部被初始化为0,这时, mmap
的最后两个参数将被忽略。而第二个参数length
(文件的大小),可以随意指定。
无血缘关系的进程间通信的关键是:每个进程中都使用同一个文件去创建映射区。并设置好读方和写方,这样就能实现进程通信了,当然喽,MAP_SHARED
是必须的。
文件名可以使用命令行参数argv[]
传入。