目录
1.进程相关概念
程序和进程
程序储存在磁盘上,不会占用系统资源,运行程序会产生进程,死的(剧本)
进程是运行起来的程序,会占用内存,cpu,等系统资源,活的(戏)
并发
一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态,但在任一时刻点上仍只有一个进程在运行(分时复用)
单道程序设计和多道程序设计
单道程序设计:每个进程排队执行,串行运行
多道程序设计:同时存放几道相互独立的程序,相互穿插运行,需要有硬件作为基础
时钟中断作为一种强制手段让进程让出cpu资源,对进程来说不可抗拒。由于cpu的计算速度极快,并发实际上是宏观并行,微观串行。
CPU和MMU
MMU:虚拟内存映射单元
进程控制块PCB
每个进程在内核当中都有一个进程控制块,本质是一个结构体,重点包含以下几个成员:
1.进程id
2.进程状态:初始态,就绪态,运行态,挂起态与终止态五种
3.进程切换的一些cpu寄存器
4.描述虚拟内存空间的信息
5.描述控制终端信息
6.当前工作目录
7.umask掩码
8.文件描述符
9.和信号相关的信息
10.用户id和组id
11.会话和进程组
12.进程可以使用的资源上限
2.进程控制
fork函数
用于创建子进程,成功在父进程中返回子进程的pid,在子进程中返回0,失败在父进程中返回-1.子进程未被创建,返回错误值。
getpid函数
获取自身进程id函数
getppid
获取自身的父进程id函数
进程共享
父子进程相同:刚fork后,data段,text段,堆,栈,环境变量,全局变量,宿主目录位置,进程工作目录位置,信号处理方式
父子进程不同:进程id,fork函数返回值,各自的父进程,进程创建时间,闹钟,未决信号集
父子进程共享:读时共享,写时复制----------全局变量 1.文件描述符 2.mmap映射区。
特别注意,fork之后,父进程还是子进程先执行不确定,这取决于操作系统的进程调度算法
3.exec函数族
进程调用exec函数,该进程的用户空间代码和数据完全被新程序替换,从新程序启动例程开始执行,调用exec函数并不创建新的进程,所以调用前后进程的pid并未改变,(换核不换壳)。
execlp函数
int execlp(const *file,const char *arg,...);
加载一个进程,借助PATH环境变量,当PATH中所有目录没有参数1则出错返回-1.注意参数二作为命令行参数argv[0]传入,该函数通常用来调用系统程序,如ls,data,cat等命令。
execl函数
int execl(const char *path,const char *arg,...);
加载一个自己指定路径的程序
回收子进程
4.孤儿进程
父进程先于子进程结束,子进程成为孤儿进程,其父进程变为init进程,成为init进程领养孤儿进程。
5.僵尸进程
子进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核当中,变成僵尸进程,注意:僵尸进程无法使用kill命令消除,因为僵尸进程已经终止执行。
wait函数
回收子进程退出资源,
pid_t wait(int *status)函数有以下功能:
1.阻塞等待子进程退出
2.清理子进程残留在内核的pcb资源
3.通过传出参数status得到子进程结束状态
waitpid函数
指定某一个进程进行回收
pid_t waitpid(pid_t pid, int *status,int options);
参数pid表示指定回收的子进程pid:>0表示pid,-1表示任意子进程,0表示同组的子进程;参数options:WNOHANG指定回收方式位非阻塞
返回值:>0表示成功回收,返回回收进程的pid,0表示函数调用时,参数3指定了WNOHANG,并且没有子进程结束,-1表示回收失败。
注意:wait和waitpid一次只能回收一个子进程,要想回收多个子进程需要用循环。
6.进程间通信
进程地址空间相互独立,每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,进程之间想要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,这种机制称为进程间通信(IPC)
常用的通信方式有:
管道
是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递,调用pipe系统函数即可创建一个管道,有如下特质:
1.其本质是一个伪文件(实为内核缓冲区)
2.由两个文件描述符引用,一个表示读端,一个表示写端
3.规定数据从管道的写端流入,从读端流出
内核使用环形队列机制,借助内核缓冲区实现
局限性:
1.数据不能进程自己写,自己读
2.管道数据不可反复读,一旦读走,管道中不再存在
3.采用半双工通信
4.只能作用域有血缘关系的进程间
pipe函数
创建并打开管道
int pipe(int fd[2])
参数:fd[0]表示读端,fd[1]表示写端
返回值:成功返回0,失败返回-1
管道的读写行为
读管道:
1.管道有数据:read返回实际读到的字节数
2.管道无数据:1)无写端,read返回0;2)有写端,read阻塞等待
写管道:
1.无读端:异常终止。(SIGPIPE信号导致)
2.有读端:1)管道已满,阻塞等待;2)管道未满,返回写出的字节个数
FIFO
命名管道,用于无血缘关系的进程间通信。mkfiifo创建一个命名管道,读端用读的方式打开文件,写端用写的方式打开文件
信号
信号共性
简单、不能携带大量信息,满足条件才发送
信号的特质
信号是软件层面上的“中断”,一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束再执行后续命令。
所有信号的产生以及处理全部都是由内核完成
与信号相关的事件状态
产生信号
1.按键产生
2.系统调用产生
3.软件条件产生
4.硬件异常产生
5.命令产生
递达:递送并且到达的过程
未决:产生和递达之间的状态,主要由于阻塞导致该状态
信号的处理方式
1.执行默认动作
2.忽略(丢弃)
3.捕捉(调用户处理函数)
阻塞信号集
本质是位图,用来记录信号的屏蔽状态,一旦被屏蔽的信号,在接触屏蔽前,一直处于未决态
未决信号集
用来记录信号的处理状态,该信号集中的信号,表示已经产生,但尚未被处理
信号四要素
信号使用之前,应先确定其四要素,而后再用
信号编号、信号名称、信号对应事件、信号默认处理动作
kill命令和kill函数
int kill(pid_t pid ,int signum)
pid: >0:发送信号给指定进程
=0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程
<1:取绝对值,发送信号给该绝对值所对应的进程组的所有成员
= -1:发送信号给有权限发送的所有进程
alarm函数
使用自然计时法,定时发送STGALRM给当前进程
unsighed int alarm(unsigned int seconds):
seconds:定时秒数
返回值:上次定时剩余时间,无错误现象
注:time命令查看程序执行时间,实际时间=用户时间+内核时间+等待时间 ----》优化程序瓶颈:IO
setitimer函数
int setitimer(int which,const struct itimerval *new_value, struct itimerval *old_value):
new_value:定时秒数
其中it_interval表示上次任务间隔时间;it_value表示定时时间
old_value:传出参数,上次定时剩余时间
返回值成功:0;失败:-1 error
信号集操作函数
sigset_t set: 自定义信号集
sigemptyset(sigset_t *set):清空信号集
sigfillset(sigset_t *set):全部置1
sigaddset(sigset_t *set, int signum):将一个信号添加到集合中
sigdelset(sigset_t *set, int signum):将一个信号从集合中移除
sigismember(const sigset_t *set,int signum)“判断一个信号是否在集合中,在返回1,不在返回0
设置信号屏蔽字和接触屏蔽
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset):
how: SIG_BLOCK设置阻塞;SIG_UNBLOCK:取消阻塞SIG_SETMASK用自定义集合set替换mask
set:用户自定义集合
oldset:旧有的mask。成功返回0,事变返回-1
查看未决信号集
int sigpending(sigset_t *set):
set:传出的未决信号集
信号捕捉
signal();
sigaction();
信号捕捉特性:
1.捕捉函数执行期间,信号屏蔽字由mask-》sa_mask,捕捉函数执行结束,恢复回mask
2.捕捉函数执行期间,本信号自动被屏蔽(sa_flags = 0)
3.捕捉函数执行期间,被屏蔽信号多次发送,接触屏蔽后只处理一次
内核实现信号捕捉简析
SIGCHLD信号
产生条件:子进程状态发生改变
借助SIGCHLD信号回收子进程:子进程结束运行时,其父进程会收到SIGCHLD信号,该信号的默认动作是忽略,可以捕捉该信号,在捕捉函数中完成子进程状态的回收。’
共享映射区
文件进程间通信
两个无血缘关系的进程分别以读的方式和写的方式打开一个文件,一个向文件中写入内容,一个向文件中读出内容,用法类似于管道,但是没有管道的属性,不能阻塞等待。
存储映射I/O
mmap
1.用于创建映射区的文件大小为0,实际指定非0大小的映射区,出总线错误
2.用于创建映射区的文件大小为0,实际指定0大小的映射区,出无效参数
3.用于创建映射区的文件读写属性为只读,映射区属性为读、写,出无效参数
4.创建映射区需要read权限,默认有一次读操作,mmap的读写权限应该,<=文件的open权限,文件为只写不行。
5.文件描述符fd在mmap创建映射区完成后即可关闭,后续访问文件用地址访问
6.offset必须是4K的整数倍(4096),(MMU映射的最小单位是4K)。
7.对申请的映射区内存,不能越界访问
8.mmap用于释放的地址,必须是mmap申请返回的地址
9.映射区访问权限为“私有”MAP_PRIVATE,对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上
10.映射区访问权限为“私有”MAP_PRIVATE,只需要open文件时,有读权限,用于创建映射区即可
mmap保险的调用方式:
1.open(O_RDWR)
2.mmap(NULL,有效文件大小,PROT_READ|PORT_WRITE,MAP_SHARED,fd,0)
父子进程之间使用mamap通信
父进程先mmap创建映射区,指定share权限,fork子进程,一个进程读,一个进程写。
无血缘关系进程间通信
两个进程打开同一个文件,创建映射区,此时两个进程的映射区指向相同内存空间。指定flags为MAP_SHARED,一个进程写入,另外一个进程读出。
注意:mmap数据可以重复读取,而fifo数据只能读取一次,读取之后就消失。
匿名映射:只能用于血缘关系进程间通信
映射区大小可以随意指定,在MAP_SHARED参数后加|MAP_ANONYMOUS,文件描述符使用-1
本地套接字
7.守护进程
定义
daemon进程,通常运行于操作系统后台,脱离控制终端。一般不与用户直接交互。周期性的等待某个事件发生或周期性执行某一动作,不受用户登录注销影响。通常采用以d结尾的命名方式
创建方式
1.fork子进程,让父进程终止
2.子进程调用setsid()创建会话
3.通常根据需要改变工作目录位置chdir()
4.通常根据需要重设umask文件权限掩码
5.通常根据需要关闭文件描述符(或重定向)
6.守护进程业务逻辑,while()
实例:
#include <iostream>
#include <sstream>
#include <ctime>
#include <stdio.h>
#include <cstdlib>
#include <sys/stat.h>
#include <time.h>
#include <assert.h>
#include <unistd.h>
int main()
{
// 1.在父进程中执行fork并exit退出;
pid_t m_pid;
m_pid = fork();//创建子进程
if (m_pid < 0)
{
exit(1);
}
else if (m_pid > 0)
{
exit(0);//父进程退出
}
// 2.在子进程中调用setsid函数创建新的会话;
setsid();//若当前进程不是进程组长,创建一个新会话;若当前进程已经是进程组长,返回错误
// 3.调用getcwd()函数获取当前目录路径,并在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;
char m_path[1024];
if (getcwd(m_path, sizeof(m_path)) == NULL)
{
perror("get path error!");
exit(1);
}
printf("get path success !");
chdir("/");
// 4.在子进程中调用umask函数,设置进程的umask为0
umask(0);//设置进程的权限掩码,参数0表示:可以设置任何权限(读、写、执行)
// 5.在子进程中关闭任何不需要的文件描述符
for (int i = 0; i < getdtablesize(); i++) {
close(i);
printf("文件描述符关闭成功!");
}
//也可以利用重定向的方式使文件描述符重定向到文件空洞中
while(1){
//守护进程实现代码
}
return 0;
}
8.线程
概念
线程就是轻量级的进程,有独立的pcb,但是和进程相比没有独立的进程地址空间,是cpu执行的最小单元,进程是分配资源的最小单位
ps -Lf 进程id ----> 线程号(非线程id)
线程共享资源
1.文件描述符表
2.每种信号的处理方式
3.当前工作目录
4.用户ID和组ID
5.内存地址空间(./text/data/bss/heap/共享库)
线程非共享资源
1.线程id
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户空间栈)
4.error变量
5.信号屏蔽字
6.调度优先级
线程优缺点
优点:1.提高程序并发性 2.开销小 3.数据通信,共享数据方便
缺点:1.库函数,不稳定 2.调试编写困难,gdb不支持 3.对信号支持不好
线程控制原语
1.pthread _t pthread_self(void) 获取线程id,线程id是在进程地址空间内部,用来标识线程身份的id号
返回本线程id
2.int pthread_create(pthread_t *tid,const pthread_attr_t *attr, void *(*start_rountn)(void *),void *arg) 参数1:传出参数,表示新创建的子线程id。参数2:线程属性,传NULL表示使用默认属性。参数3:子线程回调函数,线程会执行这个回调函数。
循环创建n个子线程
for循环,将int类型i,强转成void *,传参
3.void pthread_exit(void *retval):退出当前线程;retval:退出值。无退出值时,NULL
4.int pthread_join(pthread_t thread, void **retval); thread:待回收的线程id,retval;传出参数,回收的那个线程退出值,成功返回0;
5.pthread_detach(pthread_t thread):设置线程分离,thread:待分离的线程id
6.int pthread_cancel(pthread_t thread)杀死一个线程,需要到达取消点(保存点), thread:待杀死的线程id,返回值:成功返回0。如果线程没有到达取消点,那么pthread_cancel无效,可以使用pthread_testcancel();成功被其杀死的线程返回-1,使用pthread_join回收
线程同步(锁机制)
协同步调,对公共区域数据按序访问,防止数据混乱,产生与时间有关的错误
锁的使用
建议锁,对公共数据进行保护,所有线程(应该)在访问公共数据前先拿锁再访问,但锁本身不具备强制性
使用互斥锁mutex的步骤
1.pthread_mutex_t lock 创建锁
2.pthread_mutex_init 初始化
3.pthread_mutex_lock 加锁
4.访问共享数据(stdout)
5.pthread_mutex_unlock 解锁
6.pthread_mutex-destroy 销毁锁
注意:尽量保证锁的粒度越小越好(访问共享数据前加锁,访问结束后立即解锁);互斥锁本质是一个结构体,但我们可以将其看作成一个整数,经过pthread_mutex_init函数初始化为1,加锁对其--,解锁对其++
restrict
用来限定指针变量,被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。
死锁
是使用锁不恰当导致的现象,有以下两种可能:
1.对同一个锁反复lock
2.两个线程各自拥有一把锁,互相请求另一把
读写锁
读写锁只有一把,读模式下加锁叫读锁,写模式下加锁叫写锁,遵循读时共享,写时独占原则
1.读写锁是写模式加锁时,解锁前,所有对该锁加锁的线程都会被阻塞。
2.读写锁是读模式加锁时,解锁前,如果线程以读模式对其加锁,会成功,如果线程以写模式加锁会被阻塞
3.读写锁是读模式加锁时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程,那么读写锁会阻塞随后的读模式锁请求,优先满足写模式锁,读锁,注意:写锁并行阻塞,写锁优先级高
读写锁非常适合于对数据结构读的次数远大于写的情况
条件变量
条件变量本身不是锁,但是通常结合锁来使用(mutex)
pthread_cond_t cond初始化条件变量:
1.pthread_cond_init(&cond,NULL);动态初始化
2.pthread_cond_t cond = PTHREAD_COND_INITIALIZER;静态初始化
pthread_cond_signal();唤醒阻塞在条件变量上的(至少)一个线程
pthread_cond_broadcast(),唤醒阻塞在条件变量上的所有线程
信号量
相当于初始化值为N的互斥量
函数:
sem_t sem:定义类型
int sem_init(se_t *sem,int pthread,unsigned int value):
参数:sem:信号量;pshared:0用于线程同步,1用于进程同步吧;value:N值(指定同时访问的线程数)
sem_destroy()
sem_wait(),一次调用,做一次--操作,当信号量为0时,再--就会阻塞(对比pthread_mutex_lock)
sem_post(),一次调用,做一次++操作,当信号量为N时,再次++就会阻塞(对比pthread_mutex_unlock)