操作系统相关学习(1)

面试中对面试者操作系统方面的考察也是非常重要的,因为操作系统基础就是一个程序猿的内功;而语言则是相当与你的外功。由此操作系统方面的知识是非常重要的, 无论在面试中还是在日常工作开发中

操作系统主要重点学习的分支:进程与线程, 内存管理,文件管理, 死锁, 网络

注:在这里插入图片描述

进程与线程

进程,线程与协程的概念,进程与线程的区别,为什么要有线程

**进程**:计算机中所有可运行的软件包括操作系统按若干顺序,即程序正在执行的一个实例
		 此实例拥有PC, 寄存器,变量, 文件资源以及虚拟CPU以及内存。是资源分配和执
		 行的最小单位,实现了系统的并发。
**线程**:进程的子任务。与同一进程内的其他进程共享地址空间(.text段,数据段(.date, 		
		.bss段)),自己独有的包括:TID, PC, 寄存器(线程上下文切换),堆栈。
		是CPU调度和执行的最小单位。
**协程**:用户级线程,比线程更加轻量级,无需陷入内核省去系统开销,由程序觉得其调度

区别:
(1) 进程是资源分配和执行的最小单位, 线程是CPU调度和执行的最小单位;

(2) 进程有自己独立地址空间,而线程间却共享进程的地址空间,但是线程自己也有TID,
PC, 寄存器(线程上下文切换),堆栈

(3)进程由于地址空间的独立性,由此一个进程崩溃不会影响别的进程,但是一个线程崩溃却能导致一整个进程崩溃

(4)系统开销:由于进程在创建,切换,销毁的时候操作系统要为其分配, 保存, 回收资源,例如文件资源, 内存资源等等, 在进程切换时,涉及到当前进程CPU环境的保存和新被调度运行进程的CPU环境的设置。而线程的切换不会引起进程的切换,由此只要保持少量的寄存器即可(PC, SP, EAX)。故进程的系统开销要远远大于线程的系统开销

(5)数据共享与数据同步(通信):进程依赖于IPC,而线程共享地址空间,因此线程数据共享比进程容易。但是由于多线程同时访问同一资源时会造成严重的线程安全问题,因此线程需要同步和互斥操作

(6)编程性:线程难度高于进程

(7)多进程适用于多核多机的场景, 多线程适用与多核分布的场景

为什么有线程?
线程产生的原因:
进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;但是其具有一些缺点:
进程在同一时间只能干一件事
进程在执行的过程中如果阻塞,整个进程就会挂起,即使进程中有些工作不依赖于等待的资源,仍然不会执行。

线程的优点?
除以上优点外,多线程程序作为一种多任务、并发的工作方式,还有如下优点:

1、使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

2、改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序才会利于理解和修改。

多线程与多进程引用的场景?

在这里插入图片描述

进程上下文切换

1)进程上下文切换可以描述为kernel执行下面的操作
	a. 挂起一个进程,并储存该进程当时寄存器和程序计数器的状态
	b. 从内存中恢复下一个要执行的进程,恢复该进程原来的状态到寄存器,返回到其上次暂停的执行代码然后继续执行
2)上下文切换只能发生在内核态,所以还会触发用户态与内核态切换

内核同步,进程同步,线程同步

  •   同步是指用于实现控制多个进程按照一定的规则或顺序访问某些系统资源的机制。
    
  •   互斥是指用于实现控制某些系统资源在任意时刻只能允许一个进程访问的机制。互斥是同步机制中的一种特殊情况。
    
  •   同步机制是linux操作系统可以高效稳定运行的重要机制
    

进程间的通信与线程的同步

进程间的通信(IPC)主要的方式:管道, 系统IPC(消息队列, 共享内存, 信号, 信号量)以及Sokect

管道:分为普通管道(PIPE)以及有名管道(FIFO)
管道主要包括无名管道和命名管道:管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信

1.1 普通管道PIPE:

1)它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端

2)它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)

3)它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

命名管道FIFO:

1)FIFO可以在无关的进程之间交换数据

2)FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

区别:命名管道使用之前需要使用open()打开。这是因为:命名管道是设备文件,它是存储在硬盘上的,而管道是存在内存中的特殊文件。但是需要注意的是,命名管道调用open()打开有可能会阻塞,但是如果以读写方式(O_RDWR)打开则一定不会阻塞;以只读(O_RDONLY)方式打开时,调用open()的函数会被阻塞直到有数据可读;如果以只写方式(O_WRONLY)打开时同样也会被阻塞,知道有以读方式打开该管道。

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标记。 (消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点)具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;

特点:

1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。

2)消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。

3)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

共享内存:通过mmap()等函数将物理内存映射到虚拟内存的共享区,使得多个进程能够访问同一块内存。目前共享内存是IPC中最快的方式,但是访问期间需要同步操作需要用到信号量, 互斥锁等等

特点:

1)共享内存是最快的一种IPC,因为进程是直接对内存进行存取

2)因为多个进程可以同时操作,所以需要进行同步

3)信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

特点:

1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

2)信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

4)支持信号量组。

系统API:
sem_wait(sem_t *sem):以原子操作的方式将信号量减1,如果信号量值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。

sem_post(sem_t *sem):以原子操作将信号量值+1。当信号量大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。

信号:是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生

  1. #include <sys/types.h>

  2. #include <signal.h>

  3. void (*signal(int sig,void (*func)(int)))(int); //用于截取系统信号,第一个参数为信号,第二个参数为对此信号挂接用户自己的处理函数指针。返回值为以前信号处理程序的指针。

  4. eg.int ret = signal(SIGSTOP, sig_handle);

  5. nt kill(pid_t pid,int sig); //kill函数向进程号为pid的进程发送信号,信号值为sig。当pid为0时,向当前系统的所有进程发送信号sig。

  6. int raise(int sig);//向当前进程中自举一个信号sig, 即向当前进程发送信号。

  7. #include <unistd.h>

  8. unsigned int alarm(unsigned int seconds); //alarm()用来设置信号SIGALRM在经过参数seconds指定的秒数后传送给目前的进程。如果参数seconds为0,则之前设置的闹钟会被取消,并将剩下的时间返回。使用alarm函数的时候要注意alarm函数的覆盖性,即在一个进程中采用一次alarm函数则该进程之前的alarm函数将失效。

  9. int pause(void); //使调用进程(或线程)睡眠状态,直到接收到信号,要么终止,或导致它调用一个信号捕获函数。

信号是异步的,一个进程不可能等待信号的到来,也不知道信号会到来,那么,进程是如何发现和接受信号呢?实际上,信号的接收不是由用户进程来完成的,而是由内核代理。当一个进程P2向另一个进程P1发送信号后,内核接受到信号,并将其放在P1的信号队列当中。当P1再次陷入内核态时,会检查信号队列,并根据相应的信号调取相应的信号处理函数。

用户进程提供的信号处理函数是在用户态里的,而我们发现信号,找到信号处理函数的时刻处于内核态中,所以我们需要从内核态跑到用户态去执行信号处理程序,执行完毕后还要返回内核态。返回内核态去检查是否还有信号处理函数需要处理。

线程间的同步
(1)不同进程内的线程通信,实际就是进程间的通信
(2)同一进程内的不同线程间的通信,因此需要同步操作(互斥锁, 原子变量, 自旋锁, 读写锁)

互斥锁:互斥量本身就是一把锁,对访问共享资源加锁,访问过后释放锁,如果其他线程也需要这把锁,那么就必须阻塞直到互斥锁资源被释放。如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变成运行状态的线程可以对互斥量加锁,其他线程就会看到互斥量依然是锁着,只能再次阻塞等待它重新变成可用,这样,一次只有一个线程可以向前执行。

#include <pthread.h>

常用API

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);//互斥初始化
int pthread_mutex_destroy(pthread_mutex_t *mutex);//销毁互斥
int pthread_mutex_lock(pthread_mutex_t *mutex);//锁定互斥
int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁互斥
int pthread_mutex_trylock(pthread_mutex_t *mutex);//销毁互斥
eg.pthread_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
...
pthread_mutex_unlock(&mutex);
pthread_mutex_detroy(&mutex);

读写锁:允许同时有多个线程拥有读锁,但是只有一个线程拥有写锁,而且写锁的优先级高于读锁。故当写模式加锁时所有尝试对这读写锁加锁的线程都会处于阻塞状态,直到写锁被释放,而当读模式加锁时尝试对读模式加锁的线程都能得到读锁;

常用API

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *rwlockattr);//初始化读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//销毁读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//读模式锁定读写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//写模式锁定读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁读写锁
eg.pthread_rwlock_t q_lock;
pthread_rwlock_init(&q_lock, NULL);
pthread_rwlock_rdlock(&q_lock);
...
pthread_rwlock_unlock(&q_lock);
pthread_rwlock_detroy(&q_lock);

条件变量:条件变量是线程可用的另一种同步机制。互斥量用于上锁,条件变量则用于等待,并且条件变量总是需要与互斥量一起使用,运行线程以无竞争的方式等待特定的条件发生。
条件变量本身是由互斥量保护的,线程在改变条件变量之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种变化,因为互斥量必须在锁定之后才能计算条件。

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);//初始化条件变量
int pthread_cond_destroy(pthread_cond_t *cond);//销毁条件变量
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);//无条件等待条件变量变为真
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *tsptr);//在给定时间内,等待条件变量变为真
eg.pthread_mutex_t mutex;
pthread_cond_t cond;
...
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
...
pthread_mutex_unlock(&mutex);

信号量线程的信号和进程的信号量类似,使用线程的信号量可以高效地完成基于线程的资源计数。信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量就增加;公共资源减少的时候,信号量就减少;只有当信号量的值大于0的时候,才能访问信号量所代表的公共资源。
常用头文件:

#include <semaphore.h>

常用API

sem_t sem_event;
int sem_init(sem_t *sem, int pshared, unsigned int value);//初始化一个信号量 
int sem_destroy(sem_t * sem);//销毁信号量
int sem_post(sem_t * sem);//信号量增加1
int sem_wait(sem_t * sem);//信号量减少1
int sem_getvalue(sem_t * sem, int * sval);//获取当前信号量的值

另外还有以下几点需要注意:
1、信号量可以模拟条件变量,因为条件变量和互斥量配合使用,相当于信号量模拟条件变量和互斥量的组合。在生产者消费者线程池中,生产者生产数据后就会发送一个信号 pthread_cond_signal通知消费者线程,消费者线程通过pthread_cond_wait等待到了信号就可以继续执行。这是用条件变量和互斥锁实现生产者消费者线程的同步,用信号量一样可以实现!

2、信号量可以模拟互斥量,因为互斥量只能为加锁或解锁(0 or 1),信号量值可以为非负整数,也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量时,就完成一个资源的互斥访问。前面说了,信号量主要用做多线程多任务之间的同步,而同步能够控制线程访问的流程,当信号量为单值时,必须有线程释放,其他线程才能获得,同一个时刻只有一个线程在运行(注意,这个运行不一定是访问资源,可能是计算)。如果线程是在访问资源,就相当于实现了对这个资源的互斥访问。

3、互斥锁是为上锁而优化的;条件变量是为等待而优化的; 信号量既可用于上锁,也可用于等待,因此会有更多的开销和更高的复杂性。

4、互斥锁,条件变量都只用于同一个进程的各线程间,而信号量(有名信号量)可用于不同进程间的同步。当信号量用于进程间同步时,要求信号量建立在共享内存区。

5、互斥量必须由同一线程获取以及释放,信号量和条件变量则可以由一个线程释放,另一个线程得到。

6、信号量的递增和减少会被系统自动记住,系统内部的计数器实现信号量,不必担心丢失,而唤醒一个条件变量时,如果没有相应的线程在等待该条件变量,此次唤醒会被丢失。

调度

当计算机系统在处理多到程序设计系统时,通常就有多个进程或者线程同时竞争CPU的情况。只要有两个及以上的进程就绪就会发生竞争。因此CPU需要去为如何调度线程或者进程设计调度算法

在这里插入图片描述
(非抢占是调度)
先来先服务算法
就和名字一样,哪个进程先来就先获得处理器时间,,用一个队列暂存等待处理器的进程,优点是实现简单(太简单了吧喂),缺点,遇到那种又臭又长的进程就很不爽了,好比食堂打饭,前面的人不买一直问,后面的人一直排队,那么后面的人就怎么了呢?后面的人就饥饿!同时如果现在有一个马上就要饿死的人急需吃饭,这就很尴尬了(紧急的进程无法处理,优先级高的进程处于饥饿状态),所以有了优先级队列的先来先服务算法,这样也不是很好,因为总有又臭又长的进程,排队是谁都不乐意的吧,而且处理器时间就不公平了。

(非抢占是调度)
短作业优先
因为先来先服务不好,所以有了短作业优先,通过设置执行时间短的进程作业的优先级为高来实现,也很简单粗暴,就是说进程时间越短就越先执行,看着是比较好了,不浪费时间了,但是有没有想过长进程的感受,来了一群短的进程,然后一直来短进程,这是要饿死长进程的节奏,人家长有错么?如果是可抢占的方式(见最短剩余时间版本),就更惨了,只要来了更短的就别想好好执行了。。。

(最短作业优先的抢占版)
最短剩余时间
就是刚才说的短作业优先的抢占版本,说过他的缺点了,当前执行的进程还剩10个时间单位,但是一直来了一群只要2个时间单位就跑完的进程,那当前的进程就会被抢占,然后含恨饿死。。

时间片轮转
既然上面几种算法都有可能出现饥饿进程,那么我就干脆让每个进程都执行那么一会,这样不就比较公平了?每个进程都有机会在处理器上跑,看起来很和谐,但是还是没有解决优先级的问题,优先级不好控制,比如有什么紧急的进程需要立即执行,就不好办了。而且每个进程的具体情况也是不一样的,比如有I/O消耗型进程,和处理器消耗型进程,在同样的事件片里真正占用处理器的时间是不一样的,而我们是真正占用处理器的时间希望能一样的,这样就公平了嘛。这样看来,时间片轮转也是有缺点的。

优先级调度
  优先级调度算法又称优先权调度算法,该算法既可以用于作业调度,也可以用于进程调度,该算法中的优先级用于描述作业运行的紧迫程度
  在作业调度中,优先级调度算法每次从后备作业队列中选择优先级最髙的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列。在进程调度中,优先级调度算法每次从就绪队列中选择优先级最高的进程,将处理机分配给它,使之投入运行

最高响应比优先
什么是响应比?看一下这个公式:R=(w+s)/s,其中R是响应比,w是等待处理器的时间,s是期待的服务时间,简单的来说响应比就是,进程从加入等待队列开始一直到执行完毕经历的时间除以进程使用处理器的时间,这个响应比比较高的就证明该进程等待比较久了,它估计会很饿,先让它吃!

多级反馈队列算法
概念:
设置多个就绪队列,分别赋予不同的优先级,如逐级降低,队列1的优先级最高。每个队列执行时间片的长度也不同,规定优先级越低则时间片越长,如逐级加倍。

新进程进入内存后,先投入队列1的末尾,按FCFS算法调度;若按队列1一个时间片未能执行完,则降低投入到队列2的末尾,同样按FCFS算法调度;如此下去,降低到最后的队列,则按“时间片轮转”算法调度直到完成。

仅当较高优先级的队列为空,才调度较低优先级的队列中的进程执行。如果进程执行时有新进程进入较高优先级的队列,则抢先执行新进程,并把被抢先的进程投入原队列的末尾。

多级反馈队列调度算法又称反馈循环队列或多队列策略,主要思想是将就绪进程分为两级或多级,系统相应建立两个或多个就绪进程队列,较高优先级的队列一般分配给较短的时间片。处理器调度先从高级就绪进程队列中选取可占有处理器的进程,只有在选不到时,才从较低级的就绪进程队列中选取。
优点:
为提高系统吞吐量和缩短平均周转时间而照顾短进程。
为获得较好的I/O设备利用率和缩短响应时间而照顾I/O型进程。
不必估计进程的执行时间,动态调节

消费者生产者问题

概念:一组生产者进程和消费者进程共享一个初始为空,大小为n的缓冲区。只有当缓冲区没满的时候,生产者才能将消息放进去。同理,只有当缓冲区不空的时候,消费者才能从中取消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,也只允许一个消费者拿出消息。这里我再解释一下,意思是,同一个时刻只能是一个生产者或者一个消费者操作缓冲区,禁止一下情况:多个生产者或者多个消费者操作缓冲区,同样,一个生产者和一个消费者同时操作也是禁止的。

分析:生产者之间,消费者之间是互斥的关系,同时生产者消费者之间又是协同的关系,属于进程同步。

Use pthead and crond

void producer(void* ptr)
{
	int i;
	for (I = 1; I <= MAX; I++)
	{
		pthread_mutex_lock(&mutex);
		while(buffer != 0) pthread_cond_wait(&condp)
		buffer = I;
		pthread_cond_signal(&condc);
		pthread_cond_unlock(&mutex);		
	}
	pthread_exit();
}

void consumer(void* ptr)
{
	int i;
	for (I = 1; I <= MAX; I++)
	{
		pthread_mutex_lock(&mutex);
		while(buffer == 0) pthread_cond_wait(&condc)
		buffer = I;
		pthread_cond_signal(&condp	);
		pthread_cond_unlock(&mutex);		
	}
	pthread_exit();
}

Use semaphore
Dijkstra建议设立两种操作:down和up(分别为一般化后的sleep和wakeup)。对一个信号量执行down操作,则是检查其值是否大于0。若该值大于0,则将其减1(即用掉一个保存的唤醒信号)并继续;若该值为0,则进程将睡眠,而且此时down操作并未结束。检查数值、修改变量值以及可能发生的睡眠操作均作为一个单一的、不可分割的原子操作完成。保证一旦一个信号量操作开始,则在该操作完成或阻塞之前,其他进程均不允许访问该信号量。这种原子性对于解决同步问题和避免竞争条件是绝对必要的。所谓原子操作,是指一组相关联的操作要么都不间断地执行,要么不执行。

#define N 100
Semaphore mutex = 1;
Semaphore full = 0;
Semaphore empty  = N;

Void producer()
{	
	int I;
	while (1)
	{

		item = produce_item();
		down(&empty);				//空槽数目减1,相当于P(empty)
		down(&mutex);				//进入临界区,相当于P(mutex)
		insert_item(item);			//将新数据放到缓冲区中
		up(&mutex);				//离开临界区,相当于V(mutex)
		up(&full);				//满槽数目加1,相当于V(full)
	}
}
Void consumer()
{	
	int I;
	while (1)
	{		
		down(&full);				//将满槽数目减1,相当于P(full)
		down(&mutex);				//进入临界区,相当于P(mutex)
		item = remove_item();	   		 //从缓冲区中取出数据
		up(&mutex);				//离开临界区,相当于V(mutex)		
		up(&empty);				//将空槽数目加1 ,相当于V(empty)
		consume_item(item);			//处理取出的数据项
	}
}

其他相关问题

多进程的TCP服务端,能否互换fork()与accept()的位置?

1)对于父进程在fork()之前所建立的连接,子进程都会继承,与父进程共享相同的文件偏移量。系统文件表位于系统空间中,不会被fork()复制,但是系统文件表中的条目会保存指向它的文件描述符表的计数,
fork()时需要对这个计数进行维护,以体现子进程对应的新的文件描述符表也指向它。程序关闭文件时,也是将系统文件表条目内部的计数减一,当计数值减为0时,将其删除。
2)对于父进程在fork()之后建立连接,此时还没有打开文件描述符,所以子进程没有继承到文件描述符,子进程将会自己建立一条连接,不与父进程共享偏移量,而此时父进程也会建立一条连接,并且文件描述符表中的计数器会增加,当子进程结束后,文件计数器减一,而父进程一直执行,但不会为零,所以这个文件描述符会一直存在,占用资源。
所以在accept之前fork()后要在父进程中关闭accept的描述符,并且在fork子进程中关闭listen的描述符;而在accept后调用fork()则只需要在子进程中关闭listen描述符,父进程中不做处理

//因此在fork之后socketFd和connFd的引用计数均为2,父子进程均有一份
// 在父进程中, 需要关闭客户端连接套接字的文件描述符号connfd -= 1
// 在子进程中, 需要关闭服务器的文件socketFd,
// 因为他不需要监听客户端的连接 socketFd -= 1
// 然后处理完毕后需要关闭自己的客户端套接字connFd -= 1
//
// 这样在客户端处理完信息后, connFd == 0将被完全关闭
// socketFd仅在父亲进程中仍被打开

fork()与vfork() 的区别

fork和vfork的区别:

  1. fork( )的子进程拷贝父进程的数据段和代码段;vfork( )的子进程与父进程共享数据段
  2. fork( )的父子进程的执行次序不确定;vfork( )保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。
  3. vfork( )保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
  4. 当需要改变共享数据段中变量的值,则拷贝父进程。

exit() 与_exit()的区别

1._exit()执行后会立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。

2.调用_exit()函数时,其会关闭进程所有的文件描述符,清理内存,以及其他一些内核清理函数,但不会刷新流(stdin 、stdout、stderr……)。exit()函数是在_exit()函数上的一个封装,它会调用_exit,并在调用之前先刷新流。

3.exit()函数与_exit()函数最大的区别就在于,exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件。

孤儿进程,僵尸进程,守护进程

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:子进程结束(调用exit相关函数)后父进程并没有为其回收资源,因此在系统中留下僵尸进程。
查看进程 ps -ef | grep defunct_process_pid。
由于孤儿进程会被init进程给收
(2)改写父进程,在子进程死后要为它收尸。
具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行 waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。

(3)杀父进程不行的话,就尝试用skill -t TTY关闭相应终端,TTY是进程相应的tty号(终 端号)。但是,ps可能会查不到特定进程的tty号,这时就需要自己判断了。
(4)重启系统,这也是最常用到方法之一

守护进程
守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。

一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
守护进程的名称通常以d结尾,比如sshd、xinetd、crond等

内存管理

概念:程序运行的时候计算机内存资源分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源

虚拟内存

虚拟内存作为现代操作系统用来管理进程内存的方法。其核心思想为:让每个程序拥有自己独立的地址空间,这个空间分为多个块,每一块又成为页。每一页映射到实际的物理内存。

进程与内存

在这里插入图片描述
内核虚拟存储器:由内核管理的内存区,当用户空间发生异常,中断和系统调用时即陷入内核区

栈区:由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。

堆区:用于动态(由用户)分配内存,位于BSS和栈中间的地址区域。由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多

BSS段(未初始化数据区):通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。BSS段属于静态分配,程序结束后静态变量资源由系统自动释放。

数据段:存放程序中已初始化的全局变量的一块内存区域。数据段也属于静态内存分配。

代码段:存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量

text段和data段在编译时已经分配了空间,而BSS段并不占用可执行文件的大小,它是由链接器来获取内存的。

bss段(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将它们设置为0。需要存放在程序文件中的只有正文段和初始化数据段。

data段(已经初始化的数据)则为数据分配空间,数据保存到目标文件中。

数据段包含经过初始化的全局变量以及它们的值。BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段的后面。当这个内存进入程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。

静态内存分配与动态内存分配的区别?

(1)时间上的区别:静态内存分配在程序编译和链接的时候,而动态内存分配在程序执行的时候分配
(2)空间的区别:堆都是动态内存分配,而栈分为动态内存分配和静态内存分配,不想堆区一样需要用户手动分配和释放,栈区由操作系统管理。内存区从逻辑上分为:代码区,静态分配区和动态分配区,静态分配区主要是(data段和BSS段)动态分配区主要是堆,栈区

*C++的RAII设计:
RAII是Resource Acquisition Is Initialization的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。

堆栈的区别?

1、管理方式不同;
栈是由编译器自动分配管理,而堆由程序猿分配和回收,容易产生memory leak
2 、空间大小不同;
一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角
度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M
3、能否产生碎片不同;
堆由于频繁的new/delete, malloc/free容易成内存空间的不连续,从而造成大量的碎片,使程序效率降低。而栈是一个先进先出的队列实现
4、生长方向不同;
堆是向着高地址方向生长,栈是向着低地址方向生长
5、分配方式不同;
堆都是动态分配,栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现
6、分配效率不同;
栈是操作系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行。而堆是由程序员malloc/free 库函数,然后通过系统调用去分配,会产生大量的内存分配。因此栈的分配效率远远高于堆

new和malloc的区别?

(1)属性
new/delete是C++的操作符,而malloc()/free()是一组库函数
(2)参数
new根据需要创建的对象自动分配内存,而malloc需要计算内存大小,然后在申请空间
(3)返回类型
new 返回的是指向申请空间类型的指针,而malloc返回的是void*,需要强转成申请对象类型的指针
(4)分配失败
new分配失败跑出bac_alloc的异常,而mallco返回NULL
(5)自定义类型
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

(6)重载
C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
(7)内存区域
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。

页式管理

将内存作为基本单位来管理物理内存,在内核看来,所有的RAM都被划分成了固定长度的页帧

(1)物理寻址和虚拟寻址
在这里插入图片描述

缺页:DRAM缓存不命中
缺页的异常处理操作;内核从磁盘拷贝VP3到存储器的PP3中,更新PTE3
显示
在这里插入图片描述

页表格式:

加速分页,页表必须考虑的两个问题:
(1)虚拟地址到物理地址的映射必须快
:避免映射成为性能的主要瓶颈
(2)如果虚拟地址空间的空间大那么页表也会很大
:随着64位操作系统的普及,页表必然也越来越大

解决方案:转换检测缓冲区(TLB):将虚拟地址直接映射物理地址,而不是再去访问页表,通常在MMU中

页面置换算法

LRU (Least Recent Use)最近最少使用

当发生缺页中断时,置换未使用时间最长的页面

缺点:为了实现LRU需要在内存中维护一个所有的页面的链表,而且每次访问内存时必须要更新链表

算法的实现有很多种,基本都是通过数据的历史访问记录来作文章。但这个算法有一个比较明显的问题,就是在某些波动的情况或者周期性的批量数据访问下,缓存的命中率会大幅下降,也就是判断某一时刻的热数据而将广域的热数据剔除掉了。

目前这个算法有很多变种,比如LRU-K(k就是最近使用过的次数),它将LRU的最近使用一次扩展成了最近使用K次(本质上是LRU与LFU的结合),进过验证k=2的时候,效率可以达到一个比较均衡的高水平

最优页面置换(算法不可能实现)

在发生缺页中断时,有些页面在内存中,其中有一个页面将很快被访问,其他页面则可能要到20, 200条指令后才能被访问。每个页面将需要的执行指令数作为标记,每次置换时将最大标记数页面置换

最近未被使用页面置换算法(NRU)

由于操作系统未每一页面设置的R位(页面被访问)和M位(修改页面),因此每次内存访问时更新这些位
0类:R:0,M : 0/ 1类:R:0,M : 1
2类:R:1,M : 0/ 3类R:1,M : 1
算法:随机的从这些类非空类选一个页面置换

优点:易于理解和能够有效的被实现,虽然它的性能不是最好,但是已经够用

先进先出页面置换(FIFO)

由操作系统维护一个所有当前在内存中页面的链表,将最新进入的放在表尾,最久进入放在表头。发生页面中断时淘汰表头,并且把最新的页面调入表尾

缺点:容易将经常使用的页面置换出去

第二次机会页面置换算法

基于FIFO算法简单修改,检查页面的R位(被访问)如果R位为0则该页面又老又没有被访问故直接置换,如果R位为1则清除R位将次页面放在链尾

缺点:
经常要在链表中移动页面,既降低了效率有不是很又必要

时钟页面置换算法

改善第二次机会算法缺点
算法:将所有页面都保存在一个类似钟表德 环形链表,指针指向最老的页面
当发生缺页中断时,检测R位是否为0。为0时淘汰页面并且将新的页面插入,把指针向前移。为1将R位清0,指针向前移

工作集页面置换算法

工作集:一个进程当前使用的页面的集合
工作模型:在进程要运行之前它的工作集就已经在内存之中,目的是为了减少缺页中断率。
预先调页:在进程运行前预先装入其工作集页面
算法:当发生缺页中断时,淘汰一个不再工作集中的页面

MQ(Mutil Queue)

MQ实际上是LRU的一种扩展,希望做到的是尽量的贴近实际用户最可能访问的逻辑,将访问的热点数据缓存起来。

MQ算法根据访问频率将数据划分为多个队列,不同的队列具有不同的访问优先级,其核心思想是:优先缓存访问次数多的数据。

MQ算法将缓存划分为多个LRU队列,每个队列对应不同的访问优先级。访问优先级是根据访问次数计算出来的

详细的算法结构图如下,Q0,Q1….Qk代表不同的优先级队列,Q-history代表从缓存中淘汰数据,但记录了数据的索引和引用次数的队列:

通过对几个队列的维护,保证:
新的被多次访问的数据优先级可以提升
旧的访问次数减少的数据优先级被降低

具体的做法就是在多个不同优先级序列的队列之间切换。当然这个算法相对于LRU来说复杂了许多,并且在大量数据涌入的时候可能会造成一些由于系统本身置换而产生的效率影响。

除了MQ,还有2Q(Two Queue)等等,都是对LRU经典算法的改进,不过由于本身算法复杂度和空间占比的影响,使用率都不高。

总结:
在这里插入图片描述

其他问题

请你说一说Linux虚拟地址空间?

为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。
虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。

请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。

虚拟内存的好处:
1.扩大地址空间;
2.内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
3.公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
4.当进程通信时,可采用虚存共享的方式实现。
5.当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
6.虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
7.在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片

虚拟内存的代价:
1.虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
2.虚拟地址到物理地址的转换,增加了指令的执行时间。
3.页面的换入换出需要磁盘I/O,这是很耗时的
4.如果一页中只有一部分数据,会浪费内存。

请你说一说操作系统中的页表寻址?

页式内存管理,内存分成固定长度的一个个页片。操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。页表中的每一项都记录了这个页的基地址。通过页表,由逻辑地址的高位部分先找到逻辑地址对应的页基地址,再由页基地址偏移一定长度就得到最后的物理地址,偏移的长度由逻辑地址的低位部分决定。一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。页式内存管理的优点就是比较灵活,内存管理以较小的页为单位,方便内存换入换出和扩充地址空间。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值