通俗解释:内存中的每一个正在执行的文件,都是一个进程。操作系统会给每一个进程分配很多资源,比如说内存空间,文件描述符,IO的端口号等等
通俗解释:一个程序里面不同的执行路径,就叫做一个线程。
区别
联系
1.进程的创建
pid_t fork(void); // 创建一个进程
pid_t getpid(void); // 获取当前进程号(ID)
pid_t getppid(void); // 获取父进程号(ID)
进程之间的通信方式主要有六种,包括管道,信号量,消息队列,信号,共享内存,套接字。
管道:
匿名管道pipe和命名管道除了建立,打开,删除的方式不同外,其余都是一样的。匿名管道只允许有亲缘关系的进程之间通信,也就是父子进程之间的通信,命名管道允许具有非亲缘关系的进程间通信。
信号量:
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。信号量只有等待和发送两种操作。等待(P(sv))就是将其值减一或者挂起进程,发送(V(sv))就是将其值加一或者将进程恢复运行。
信号:
信号是Linux系统中用于进程之间通信或操作的一种机制,信号可以在任何时候发送给某一进程,而无须知道该进程的状态。如果该进程并未处于执行状态,则该信号就由内核保存起来,知道该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。 信号是开销最小的
共享内存:
共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,就像由malloc()分配的内存一样使用。一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信。共享内存的效率最高,缺点是没有提供同步机制,需要使用锁等其他机制进行同步。
消息队列:
消息队列就是一个保存在内核中的消息的链表,用户进程可以向消息队列添加或读取消息。与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,有读权限的进程可以从消息队列中读取消息。
套接字:
套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。
先来先服务 (FCFS):按照作业到达任务队列的顺序调度
短作业优先 (SJF):每次从队列里选择预计时间最短的作业运行。
最短剩余时间优先(SRTF):
时间片轮转
进程的执行需要经过三大步骤:编译,链接和装入。
编译:
将源代码编译成若干模块
链接:
将编译后的模块和所需要的库函数进行链接。链接包括三种形式:静态链接,装入时动态链接(将编译后的模块在链接时一边链接一边装入),运行时动态链接(在执行时才把需要的模块进行链接)
装入:
将模块装入内存运行
操作系统的内存管理包括物理内存管理和虚拟内存管理
(1) 互斥:一个资源每次只能被一个进程使用。
(2) 占有并请求:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不可剥夺:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。
产生死锁的原因主要是:
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
1.重新启动:是最简单、最常用的死锁消除方法,但代价很大,之前所有进程已经完成的工作都将付之东流,不仅包括死锁的全部进程,也包括未参与死锁的全部进程。
2.终止进程:终止参与死锁的进程并回收它们所占资源。
(1) 一次性全部终止;(2) 逐步终止(优先级,代价函数)
3.剥夺资源(resource preemption):
剥夺死锁进程所占有的全部或者部分资源。
(1) 逐步剥夺 (2) 一次剥夺:一次性地剥夺死锁进程所占有的全部资源。
4.进程回退(rollback):
让参与死锁的进程回退到以前没有发生死锁的某个点处,并由此点开始继续执行,希望进程交叉执行时不再发生死锁。但是系统开销很大:
饥饿是由于资源分配策略不公引起的,当进程或线程无法访问它所需要的资源而不能继续执行时,就会发生饥饿现象。
实现mutex最重要的就是实现它的lock()方法和unlock()方法。我们保存一个全局变量flag,flag=1表明该锁已经锁住,flag=0表明锁没有锁住。
实现lock()时,使用一个while循环不断检测flag是否等于1,如果等于1就一直循环。然后将flag设置为1;unlock()方法就将flag置为0;
static int flag=0;
void lock(){
while(TestAndSet(&flag,1)==1);
//flag=1;
}
void unlock(){
flag=0;
}
因为while有可能被重入,所以可以用TestandSet()方法。
int TestAndSet(int *ptr, int new) {
int old = *ptr;
*ptr = new;
return old;
}
线程之间通信:
进程与线程的同步
进程:无名管道、有名管道、信号、共享内存、消息队列、信号量
线程:互斥量、读写锁、线程信号、条件变量
比喻:如果说进程是工厂,那么线程就是工厂里的工人,而工人是要干活的。工厂(进程)给工人(线程)提供了内存空间,让工人(线程)干活。
优先使用多线程:
优先使用多进程:
但是实际中更常见的是进程+线程的结合方式
僵尸进程定义
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或者waitpid获取子进程的状态信息,那么子进程的进程描述符等一系列信息还会保存在系统中。这种进程称之为僵死进程。
危害:
僵尸进程虽然不占有任何内存空间,但如果父进程不调用 wait() / waitpid() 的话,那么保留的信息就不会释放,其进程号就会一直被占用,而系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。
处理方法:
kill父进程:罪魁祸首是产生出大量僵尸进程的那个父进程。通过 kill 发送 SIGKILL 信号把产生大量僵死进程的那个元凶枪毙掉。元凶死了后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被 init 进程接管,init 进程会 wait() 这些孤儿进程,释放它们在系统进程表中占用的资源。
孤儿进程
PCB就是进程控制块,是操作系统中的一种数据结构,用于表示进程状态,操作系统通过PCB对进程进行管理。
PCB中包含有:进程标识符,处理器状态,进程调度信息,进程控制信息
进程地址空间内有:
在Linux中虚拟地址空间范围为0到4G,最高的1G地址(0xC0000000到0xFFFFFFFF)供内核使用,称为内核空间
低的3G空间(0x00000000到0xBFFFFFFF)供各个进程使用,就是用户空间。
内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。
操作系统需要两种CPU的状态:
之所以要进行区分是为了进行权限保护,防止用户的程序乱搞操作系统。如果人人都能随意读写任意地址,那软件的管理就会乱套。
内核态:运行的是操作系统的程序,操作硬件
用户态:运行的是用户的程序
线程的栈空间是自己独有的
中断和异常都是CPU对系统发生的某个事情做出的一种反应。
区别:中断由外因引起,异常由CPU本身原因引起
在Linux下栈空间通常是8M,Windows下是1M
在运行一个进程的时候,它所需要的内存空间可能大于系统的物理内存容量。通常一个进程会有4G的空间,但是物理内存并没有这么大,所以这些空间都是虚拟内存,它的地址都是逻辑地址,每次在访问的时候都需要映射成物理地址。
当进程访问某个逻辑地址的时候,会去查看页表,如果页表中没有相应的物理地址,说明内存中没有这页的数据,发生缺页异常,这时候进程需要把数据从磁盘拷贝到物理内存中。如果物理内存已经满了,就需要覆盖已有的页,如果这个页曾经被修改过,那么还要把它写回磁盘。
五态模型
从操作系统层面上看,malloc是通过两个系统调用来实现的: brk和mmap
brk是将进程数据段(.data)的最高地址指针向高处移动,这一步可以扩大进程在运行时的堆大小
mmap是在进程的虚拟地址空间中寻找一块空闲的虚拟内存,这一步可以获得一块可以操作的堆内存。
通常,分配的内存小于128k时,使用brk调用来获得虚拟内存,大于128k时就使用mmap来获得虚拟内存。
进程先通过这两个系统调用获取或者扩大进程的虚拟内存,获得相应的虚拟地址,在访问这些虚拟地址的时候,通过缺页中断,让内核分配相应的物理内存,这样内存分配才算完成。
字节序是对象在内存中存储的方式,大端即为最高有效位在前面,小端即为最低有效位在前面。
判断大小端的方法:使用一个union数据结构
union{
short s;
char c[2]; // sizeof(short)=2;
}un;
un.s=0x0102;
if(un.c[0]==1 and un.c[1]==2) cout<<"大端";
if(un.c[0]==2 and un.c[1]==1) cout<<"小端";
互斥锁是一种简单的加锁方法控制对共享资源的访问,一次只能一个线程拥有互斥锁,其他线程只有等待。互斥锁只有两个状态:上锁lock,解锁unlock。
这个过程有点类似于,把打印机放在一个房间里,给这个房间安把锁,这个锁默认是打开的。当 A 需要打印时,他先过来检查这把锁有没有锁着,没有的话就进去,同时上锁在房间里打印。而在这时,刚好 B 也需要打印,B 同样先检查锁,发现锁是锁住的,他就在门外等着。而当 A 打印结束后,他会开锁出来,这时候 B 才进去上锁打印。
如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。如果别的线程长时期占有锁,那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。
曾经有个经典的例子来比喻自旋锁:A,B两个人合租一套房子,共用一个厕所,那么这个厕所就是共享资源,且在任一时刻最多只能有一个人在使用。当厕所闲置时,谁来了都可以使用,当A使用时,就会关上厕所门,而B也要使用,但是急啊,就得在门外焦急地等待,急得团团转,是为“自旋”,
操作系统需要两种CPU的状态:内核态,用户态
区分内核态和用户态的目的: 这样做是为了进行权限保护,限定用户的程序不能乱搞操作系统,如果人人都可以随意读写任意的地址空间,那么软件管理将会乱套。
内核态:运行的是操作系统的程序,操作硬件
用户态:运行的是用户的程序。
用户态——内核态:唯一的方式是通过中断或异常
内核态——用户态:通过设置程序状态字PSW
每一个进程都一个PCB,有一个进程a和一个进程b,进程a先去cpu执行,这个运行不是无节制的,当CPU时间片耗尽后,就会通过PCB中的程序计数器找到下一个即将执行的指令b的地址,此时a的执行结果与上下文信息保存在寄存器中。线程切换与其相似
1.最佳置换算法(OPT)
2. 先进先出置换算法(FIFO)
3.最近最久未使用(LRU)算法
4. 时钟(CLOCK)置换算法
1.用户线程 2.守护线程 3.主线程
软链接:
以路径形式存在,类似于快捷方式
可以跨文件系统,对一个不存在的文件名进行链接,可以对目录进行链接
硬链接:
以文件副本形式存在,但不占用实际空间
不允许给目录创建硬链接,只能在同一个文件系统中才能创建
首先它们都是把虚拟内存空间映射到物理地址空间的机制,分页和分段的区别在于粒度,
分页按照固定大小分割,分段按照逻辑功能分割大小不固定,分页地址是一维的,分段地址空间是二维的
缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。
原因:造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。
危害有以下两点:
1、程序崩溃,导致拒绝服务
2、跳转并且执行一段恶意代码
除了进程ID不一样外,子进程获得父进程的数据空间、堆和栈的复制,变量的地址也都一样。