进程控制理论基础
进程特点:动态性,并发性,独立性,异步性
进程三态:就绪,执行,阻塞
进程ID:PID 父进程ID:PPID 启动进程的用户ID:UID
进程互斥是指若干进程都要使用某一共享资源时,任何时刻最多允许一个进程使用,其他进程必须等待,知道占用该资源的进程释放资源。
临界资源——一次只允许一个进程访问的资源称为临界资源。
临界区——进程中访问临界资源的那段程序代码称为临界区。
进程同步——一组并发进程按一定的顺序执行的过程称为进程间的同步。
死锁——多个进程因竞争资源而形成的额一种僵局。
进程环境
每个程序都会接收到一张环境表。环境变量存在 环境表中,环境表其实就是一个字符串的数组(字符指针数组)。
extern char** environ; -> 直接拿到环境表的首地址
打印环境变量的值,可以用 echo 命令
echo$PATH
PATH - 系统路径,系统会自动查找到PATH中的路径。因此PATH中配置的路径下的可执行文件可以省略路径直接运行。
CPATH - 头文件的系统路径。系统查找头文件时,会找它。
配置环境变量,在bash下:
export 环境变量名=环境变量值
注意: = 两边不要留空格
exportPATH=.:$PATH
想永久有效,放入系统的启动文件中。系统的启动文件很多,比如: 登录目录下的.bashrc。重启机器以后或者用 source .bashrc 生效。
系统提供了关于环境变量和环境表的函数:
getenv() - 可以通过环境变量的名取得环境变量的值
putenv() setenv() -新增/修改环境变量
区别在于putenv()存在必定修改,setenv()存在可选是否修改
unsetenv() - 单删某个环境变量
clearenv() - 全部删除环境变量
C程序的存储空间布局:
1. 正文段(代码区) - 用于存放代码(函数)的区域,是只读区。函数指针就是指向代码区的地址。
2. 初始化数据段(全局区) - 用于存放全局变量(定义在函数外的变量)和static的局部变量的区域。
3. 未初始化数据段(BSS段) - 用于存放未初始化的全局变量的区域。BSS段在main函数执行之前,会清0。
4. 栈(堆栈区stack) - 用于存放局部变量的区域,包括:函数的参数和非static的局部变量。系统自动管理栈区内存。
5. 堆(heap) - 也叫自由区,程序员唯一可以控制的区域。通常内存分配、回收都是在堆区。malloc()、free()都是在堆区。堆区的内存系统完全不管,程序员全权管理堆区内存。
6. 只读常量区 - 存放字符串字面值(""括起来的字符串)和const修饰的全局变量。这个区域也是只读区,很多书都是把只读常量区并入代码区。
虚拟内存地址空间
Unix/Linux使用虚拟内存地址的方式管理内存,每个进程先天都有0到4G-1的虚拟内存地址空间,本质就是整数(编号)。虚拟内存地址本身不能存储任何的数据,必须映射到物理内存或硬盘上的文件后 才能存储数据,否则引发 段错误。
虚拟内存地址是分为用户层和内核层,用户层0-3G,3G-4G是内核层(可以设置)。用户层不能直接访问内核层,通过系统函数可以。内存的基本单位是字节,内存地址是逐字节的,但内存映射的基本单位不是一个字节,是 4096个字节(4k),叫一个内存页。函数getpagesize()可以查看内存页的大小。
进程的内存空间排列次序:(地址从小到大)
代码区、只读常量区、全局区、BSS段、堆、 ....... 栈
内存中的每个进程,都在 /proc 目录下建立一个目录,目录名就是进程的ID(PID),进程结束 目录就消失。这个目录就是进程的文件方式。cat 命令 查看 /proc/进程ID/maps就可以看到进程的内存分配情况。函数 getpid()可以获取当前进程的ID
段错误的引发原因:
1. 使用没有映射的虚拟内存地址 存储/获取数据。
2. 对内存区域进行没有权限的操作。比如:修改只读区。
C语言的字符串有三种形式:
字面值("abc")、字符指针(char*)、字符数组(char a[])
字符串必须有一个结尾,以'\0'结尾。关于字符串的基本操作写在string.c中。
存储器分配
内存管理的相关函数
STL -> 全自动管理 (C++后期)
|
C++ -> new分配,delete回收内存
|
C语言-> malloc()分配,free()回收内存
|
Unix系统函数 -> sbrk() / brk() 分配和回收内存
|
Unix系统函数 -> mmap()映射物理内存/硬盘文件,munmap()解除映射
(用户层函数到此为止)
------------------------------------------------------------------------------------------------------
Unix内核层函数 ->kmalloc()/vmalloc() (内核层,嵌入式)
malloc() -> 传入分配内存的大小(字节),返回分配内存的首地址。
内存分配的函数干两件事情:
1. 分配虚拟内存地址 - 所有情况下
2. 映射物理内存/硬盘文件 - 第一次映射,后面用完了再映射
malloc()申请小块内存时 一次映射33个内存页(4k*33),用完后继续映射。申请大块内存时(超过31个内存页) 会映射 比申请稍多一点的内存页。malloc()函数除了分配正常的内存空间,还需要额外开辟一些空间,用于存储一些附加信息,比如分配的大小。存在前面4个字节。
free()函数 - 释放内存
free() 一定会释放虚拟内存地址,以便虚拟内存地址可以循环利用,不一定解除 映射。超过33个内存页的部分会释放,最后33个内存页不释放,直到进程结束时释放。
sbrk() 和 brk() - Unix的系统函数
sbrk()和brk() 系统的底层会维护一个位置,通过位置的移动完成内存的分配和回收。映射内存时 以一个内存页作为基本单位。
void* sbrk(intincrement)
参数是增量
增量为正数时,分配内存
增量为负数时,回收内存
增量为0时,取当前的位置
返回 移动之前的位置(可用内存的首地址),这个返回值对于增量为负数的情况没有意义。
sbrk() 在分配内存时很方便,但在回收内存时比较麻烦;
brk()则相反-> 开发中,一般用sbrk()分配内存,用brk()回收内存。
brk()的使用方式就是直接传递一个地址过来,做新的位置。
brk()必须和sbrk()结合使用,获得第一个位置。
mmap() 和 munmap() - Unix的系统函数,更贴近底层
void* mmap(void*addr,size_t size,int prot,int flags,int fd, off_t offset)
addr:可以指定映射的首地址,一般为0 交给内核指定。
size :就是分配内存的大小,映射时以 页 为单位。
prot:是分配内存的权限,一般用PROT_READ|PROT_WRITE
flags:是标识,通常包括以下三个:
MAP_SHARED/MAP_PRIVATE :二选一,指明映射的内存是否共享,MAP_SHARED只对映射文件有效;MAP_ANONYMOUS: 映射物理内存,默认映射文件。
fd:是文件描述符,在映射文件时有用。
offset:是文件的偏移量,指定映射文件时从哪里开始。
映射物理内存时,fd和offset给 0 即可。
成功返回首地址,失败返回 MAP_FAILED==(void*)-1
如果有多个权限、选项的拼接,一般设计的方式就是:
每种权限/选项 用一个二进制表示(一位是1,其他全0),用位或运算连起来。
进程终止
正常退出:
1. 在main()中执行return 语句
2. 执行exit()函数
3. 执行_Exit()或_exit()函数
4. 最后一个线程从其启动例程返回
5. 最后一个线程调用pthread_exit() 退出
非正常退出
1. 调用abort()
2. 接受一个信号并终止
3. 最后一个线程对取消请求做出响应,即被取消。
_exit()和_Exit()是一样的,前者由POSIX说明,后者是标C。
_Exit()是立即退出,不做清理处理;
exit()则先执行一些清理处理(调用各终止处理程序,关闭标准I/O流等)
int atexit( void (*func) (void)
可以使用atexit()函数注册一些终止处理函数,这个函数在exit()之前会被自动调用,return也会调用。
进程控制编程
Linux/Unix用 ps 命令查看进程。
ps 只能显示当前终端启动的进程。
ps -aux :Linux专用选项,Unix不直接支持 (/usr/ucb/ps可以用)
ps -ef : Unix/Linux通用的选项
进程比较多 可以用 管道实现分页,命令:
ps -aux | more
whereis 可以查看 命令在哪个目录下
其中进程的状态主要包括:
S - 休眠状态,进程大多数处于休眠状态
s- 说明该进程有子进程(父进程)
R- 正在运行的进程
Z- 僵尸进程 (已经结束但资源没有回收的进程)
关于父进程和子进程
操作系统中的多进程是有启动的次序的,Unix/Linux系统先启动0进程,0进程再启动进程1和进程2(有些系统只启动进程1),然后0进程就休眠。进程1和进程2 启动其他的进程,其他进程再启动其他的进程,直到所有进程都启动为止。
父子进程的关系:
fork()创建子进程后,父子进程同时运行,如果子进程先结束,子进程会给父进程发信号,父进程回收子进程的资源。
fork()创建子进程后,父子进程同时运行,如果父进程先结束,子进程变成孤儿进程,认进程1(init进程)做新的父进程。
fork()创建子进程后,父子进程同时运行,如果子进程结束时父进程没有收到信号或没及时处理,子进程将变成僵尸进程。
获取进程ID
pid_t getpid(void)获取本进程ID
pid_t getppid(void) 获取父进程ID
创建进程
pid_t fork (void)
fork被调用一次却返回两次,返回子进程的PID或者0,失败返回 -1 。
fork()创建子进程时,会复制除了代码区之外的所有区域,包括缓冲区。
fork()创建子进程时,如果父进程有文件描述符,子进程会复制文件描述符,不复制文件表(父子进程共用一个文件表)。
pid_t vfork(void)
与fork区别:
fork 子进程拷贝父进程的数据段,vfork子进程与父进程共享数据段,在调用exit()或exex()函数之前,在父进程空间中运行
fork父子进程执行顺序不确定,vfork子进程先运行,父进程后运行
注: vfork()如果占用的是父进程的资源,必须用exit()显式退出。
**调用fork两次以避免僵死进程。如果一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死状态直到父进程终止,那就调用fork两次。
创建子进程的方法:
1. fork() 创建子进程,通过复制父进程创建子进程。因此父子进程对应相同的代码区。
2. vfork()+ execl() 创建启动全新的子进程,父进程和子进程的代码区完全不同,父子进程执行的是完全不同的代码。(比采用写时复制的fork 更快)
fork()创建的子进程会复制父进程的虚拟内存地址,但映射到不同的物理内存上,同时把原来的值拷贝过来(即虚拟内存地址是相同的但实际上在不同的物理内存上)复制完成以后父子进程的内存就独立了。
wait()和waitpid()
wait()和waitpid()可以让父进程等待子进程的结束,并取得子进程的退出状态和退出码(return后面的值或exit()中的值)。
wait()和waitpid()的区别在于wait()很固定,而waitpid()更灵活。wait()是等待任意一个子进程的结束后返回,包括僵尸子进程。而waitpid()可以选择等待的子进程,也可以不等待。
pid_t wait(int* status)
参数是一个传出参数,用来取得结束子进程的退出码和退出状态;
返回值,有结束子进程就返回它的PID,没有结束就等待,父进程自己阻塞,如果出了错 返回-1.
宏函数WIFEXITED(status)可以判断是否正常退出,
WEXITSTATUS(status) 可以取到退出码。
waitpid() 有更多的选择
pid_t waitpid(pid_t pid,int* status,int option)
参数 status 和wait()一样,pid可以设定等待哪个哪些子进程,option可以设定是否等待。
pid的值可能是:
==-1 等待任意子进程,与wait()等效
>0 等待指定子进程(指定pid)
==0 等待本进程组的任一 子进程
<-1 等待进程组ID等于pid绝对值的任一子进程
后两种情况了解即可,比较少使用。
option的值:
0 阻塞,父进程等待
WNOHANG 不阻塞,直接返回0
返回值: 有子进程结束时返回 子进程的pid;出错返回 -1.
如果阻塞方式,没有子进程结束继续等待;
如果是 WNOHANG,没有子进程 返回0.
exec函数族
vfork() 和 execl()的合作方式:
vfork()可以创建新的进程,但没有代码和数据,execl()创建不了新进程,但可以为进程提供代码和数据 。
execl()是exec系统函数中的第一个,功能是启动一个全新的进程,替换当前的进程。新的进程会全面覆盖旧进程,但不会新建进程。(会替换各种内存区域,但进程ID不变)
execl("程序的路径","执行命令","选项","参数",NULL);
只有第一个参数是必须正确的,第二个参数必须存在但可以不正确,第三个和第四个参数可以没有,NULL代表参数结束了。
比如运行我们的程序:
execl("./b.out","b.out",NULL)
进程时间
任一进程都可以调用times函数获得它自己及已终止子进程的时间(墙上时钟时间、用户CPU时间和系统CPU时间)
clock_t times(struct tms *buf) times函数返回墙上时钟时间。
tms结构如下:
struct tms {
clock_t tms_utime; //用户CPU时间
clock_t tms _stime; //系统CPU时间
clock_t tms_cutime; //子进程终止的用户时间
clock_t tms_sctime;
}
clock_t的值都用_SC_CLK_TCK变换成秒数,即
clktck =sysconf(_SC_CLK_TCK) 获得每秒时钟滴答数
times(..)/clktck 即秒数