Open lunix555 opened 2 years ago
大端指的是低地址存储数据的高位,小端值的是低地址存储数据的低位
int main() { union { int i; char c; }un; un.i = 1;//低位为1 if (un.c)//取低地址 cout << "小端" << endl; else cout << "大端" << endl; return 0; }
两者的最大区别是权限不同,运行在用户态的程序不能直接访问操作系统内核数据结构和程序,只有运行在内核态的程序才可以访问。两者转换方式主要是。系统调用,异常和中断
生产者和消费者模型通常用于大量数据的产生与处理的场景。其是通过一个缓冲队列来实现的。开启一个或者多个线程来生产任务,生产的任务放到缓冲队列中。再开启一个或多个线程从缓冲队列中取出任务并进行处理。这样子降低了生产者和消费之间的耦合性,并且支持忙闲不均,例如生成任务速度过快,这时候就可以多开几个消费者线程来处理任务
系统调用指运行时用户态的程序向操作系统内核请求需要更高权限的运行服务。系统调用提供了用户程序与操作系统之间的接口。例如对文件的读写操作是调用的open、write、read等还有创建进程时调用的fork和vfork
悲观锁:悲观锁是认为我们在使用共享资源时会被其他线程修改,容易导致线程安全的问题,所以在访问前总是先对共享资源进行加锁,阻塞其他线程。适用于经常进行写操作的场景 乐观锁:乐观锁通常以CAS操作和版本号机制来实现。其悲观锁相反,会直接对共享资源进行修改,但是在提交更新修改结果前会验证这修改期间有没有其他线程对该资源进行修改,如果没有则提交更新,如果有则放弃本次操作。适用于经常进行读操作而很少进行写操作的场景 互斥锁:互斥锁是最底层的一种锁,其原理是当一个线程占据了这个锁后,其他线程想加锁会加锁失败并且会被阻塞。适用于加锁部分代码的执行时间长 自旋锁:自旋锁也是基于CAS操作来实现的,任何尝试获取该锁的线程都将一直进行自旋,占用cpu的资源。直到获得该锁,并且同一时间内只能由一个线程能获得该锁。适用于加锁部分代码的执行时间短 读写锁:读锁是当没有线程持有写锁时,读锁就能被多个线程并发持有,从而提高资源利用率和访问效率,并且不存在线程安全问题;写锁是任何一个线程持有写锁时,其他线程想要获取读锁或者写锁都会被阻塞。适用于并发要求高的场景
乐观锁的实现主要是通过cas机制和版本号机制来实现的。在cas机制中有3个操作数,分别是内存变量值,旧预期值,新预期值。当我们要修改一个变量时,会对内存变量值和旧预期值进行比较,如果相同则将交换新预期值和内存变量值;如果不同则将内存变量值作为旧预期值,继续重复比较。而版本号机制主要是用来解决ABA问题,每当修改一次都会标记为新的版本号,只有内存变量值和新旧预期值的版本号相同,才会进行修改
第一个是阻塞IO模型当调用者进行IO请求调用时,由于请求调用的条件不满足,就会一直阻塞等待,直到条件满足才会进行下一步的操作。第二个是非阻塞IO模型当发起IO请求调用后,会每隔一段时间去检测IO事件是否就绪,没有就绪就可以做其他事情。第三个是信号驱动IO模型,其原理是通过定义信号处理函数,当进程收到SIGIO信号时表示IO就绪,进程就会去处理IO事件。第四个是异步IO模型,通过自定义IO完成信号处理方式,发起异步调用,告诉操作系统要完成指定功能,剩下的IO功能完全由操作系统来完成,完成后通过信号来通知进程。第五个是IO复用模型 ,其可以用select、poll和epoll函数来实现,可以同一时间对大量描述符进行IO就绪事件监控,当知道有数据可读或可写时才会调用IO操作函数
select所能监控的描述符数量是有限的,默认是1024个,poll和epoll对监控的描述符数量没有限制;select和poll采用的是轮旋遍历,会随着监控的描述符的增多,性能也会随之降低,而epoll监控采用的是异步阻塞,不会随着描述符的增多导致性能下降;select的跨平台移植比poll和epoll好;select和poll每次监控调用返回后需要程序员遍历判断哪些描述符是否就绪,而epoll会直接向程序员返回就绪的文件描述符,可以直接对描述符进行操作;epoll支持水平触发和边缘触发
线程池是使用了已经创建好的线程进行循环处理任务,避免了大量线程的频繁创建与销毁的时间成本。要实现线程池,我们可以设置一个生产者消费者队列,作为临界资源,然后初始化多个线程,并让其运行起来,加锁去队列取任务并运行。当任务队列为空时,所有线程都阻塞。当生产者队列来了一个任务后,先对队列加锁,把任务挂载到队列上,然后使用条件变量去通知阻塞中的一个线程,让其获得任务并处理
水平触发是不断查询可用的描述符,当有可用的描述符就会触发事件; 边缘触发是只有当IO的转态改变时才会触发事件,并且会一次性把数据全部处理完
临界区是一段对共享资源的保护代码,该保护代码在任意时刻只允许一个线程对共享资源访问。 进程进入临界区的调度原则是: ①如果有若干进程要求进入空闲的临界区,一次仅允许一个进程进入。②任何时候,处于临界区内的进程不可多于一个。如已有进程进入自己的临界区,则其它所有试图进入临界区的进程必须等待。③进入临界区的进程要在有限时间内退出,以便其它进程能及时进入自己的临界区。④如果进程不能进入自己的临界区,则应让出CPU,避免进程出现“忙等”现象。
互斥是一种用途非常广泛的内核对象。能够保证多个线程对同一共享资源的互斥访问。 事件对象也是属于内核对象,它的主要成员包括:1.使用计数 2.指明该事件是一个自动重置事件还是一个人工重置事件的布尔值3.指明该事件处于已通知状态还是未通知状态的布尔值。
互斥对象、事件对象都是属于内核对象,利用内核对象进行线程同步,速度较慢,但可以在多个进程中的多个线程间可以进行同步。 临界区属于在用户模式下,同步速度较快,但是很容易进入死锁状态,因为在等待进入临界区时无法设定超时值。
预处理,编译,汇编,链接。
源代码.c 文件先经过预处理器,生成一个中间文件.i 文件,这个阶段有两个作用,一是把include的头文件内容进行替换,二是处理宏定义。 .i 文件经过编译生成汇编.s 文件 .s 的汇编文件经过汇编器生成.obj 的目标文件 .obj 经过链接器和 lib(静态链接库) dll(动态链接库)文件生成 exe 可执行程序
头文件并不参加链接和编译。编译器第一步要做的就是简单的把头文件在包含它的源文件中展开。不知你是否能理解这句话。也就是头文件里面有什么内容,通通把它移到包含这个头文件的源文件里。(我觉得这是个很重要的概念,可以帮助我们简化理解编译链接的过程,包括理解头文件中定义静态变量或静态函数是怎么回事)。编译器经过这一步转换后剩下什么呢?就是一堆cpp文件了。而头文件已经不再是编译器需要关心的东西了。编译器接下来就要处理这一堆cpp文件了。 所以第一个阶段是预处理阶段,在正式的编译阶段之前进行。预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容。如#include指令就是一个预处理指令,它把头文件的内容添加到.cpp文件中。 第二个阶段编译、优化阶段。
防止多重定义。
管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
锁机制 互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。 读写锁:允许多个线程同时读共享数据,而对写操作互斥。 条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。 信号量机制:包括无名线程信号量与有名线程信号量 信号机制:类似于进程间的信号处理。 线程间通信的主要目的是用于线程同步,所以线程没有象进程通信中用于数据交换的通信机制
阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu 不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。
指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。就是调用我(函数),我(函数)立即返回,通过 select 通知调用者
同步 IO 进行 IO 操作时会阻塞进程。IO 操作分两个阶段(数据准备和拷贝数据),阻塞 IO 是这两步都阻塞,非阻塞 IO 是数据准备阶段,不会阻塞进程。数据准备完成后,进程主动在此调用 recvfrom 函数将数据从内核拷贝到用户内存
相当于用户进程将 IO 操作整个交给内核去完成,内核会返回事件完成通知。在此阶段,用户进程不需要检查 IO 操作状态,也不需要主动拷贝数据,用户进程完全没有阻塞
LT 模式 当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。 一个 socket 第一次有数据,LT 模式下检测到 socket 处于活跃状态,接下来再有数据过来,接着触发。或者说可以根据需要收取部分数据,只要 socket 上数据不收取完,epoll_wait 会接着返回这个可读 socket。编程上如果需要数据包一部分一部分的处理,可以使用 LT 模式 ET 模式 当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。第一次有数据会触发 socket,第二次再有数据不会触发,必须等第一次的数据完全读完或写完。必须把当前 socket 数据全部收完,才能接受下一次的可读 socket编程上 ET 模式必须是一个循环,收取数据到 recv 返回-1,errno=EAGAIN epoll 的 ET 模式为什么一定要使用非阻塞 IO ET 模式下每次 write 或 read 需要循环 write 或 read 直到返回 EAGAIN 错误。以读操作为例,这是因为 ET 模式只在 socket 描述符状态发生变化时才触发事件,如果不一次把 socket 内核缓冲区的数据读完,会导致 socket 内核缓冲区中即使还有一部分数据,该 socket 的可读事件也不会被触发。根据上面的讨论,若 ET 模式下使用阻塞 IO,则程序一定会阻塞在最后一次 write 或 read 操作,因此说 ET 模式下一定要使用非阻塞 IO
内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制。
检查方法:在 main 函数最后面一行,加上一句_CrtDumpMemoryLeaks()。调试程序,自然关闭程序让其退出,查看输出:
输出这样的格式{453}normal block at 0x02432CA8,868 bytes long 被{}包围的 453 就是我们需要的内存泄漏定位值,868 bytes long 就是说这个地方有 868 比特内存没有释放。
定位代码位置
在 main 函数第一行加上_CrtSetBreakAlloc(453);意思就是在申请 453这块内存的位置中断。然后调试程序,程序中断了,查看调用堆栈。加上头文件#include
1、进程和线程的区别
进程是资源分配的最小单位,线程是CPU调度的最小单位 进程有独立的系统资源,而同一进程内的线程共享进程的大部分系统资源,每个线程只拥有一些在运行中必不可少的私有属性 一个进程崩溃,不会对其他进程产生影响;一个线程崩溃,会让同一进程内的其他线程也死掉 进程在创建、切换和销毁开销比较大,而线程比较小。进程创建的时候需要分配系统资源,销毁时要释放系统资源。 进程间通信比较复杂,而同一进程的线程由于共享代码段和数据段,所以通信比较容易
2、什么是死锁,死锁的条件及如何预防死锁
死锁:死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进 原因:进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。 四个必要条件:1、互斥条件:一个资源同一时间只能被一个进程访问,若其他进程请求该资源,则只能等待2、请求与保持条件:进程已经保持了至少一个资源,但是又提出了新的资源请求,如果新资源已被占有,这只能进行等待,但对自己以占有的资源保持不释放3、不可剥夺条件:进程获得资源在未使用完之前,不能被其他进程强行夺走,只能由获得该资源的进程来释放4、循环等待条件: 若干个进程间形成了收尾相接循环等待资源的情况 预防死锁:破坏产生死锁的4个必要条件,主要是破坏请求与保持条件和破坏循环等待条件。破坏请求与保持条件可以用预先静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不把它投入运行。一旦投入运行后,这些资源就一直归它所有,也不再提出其他资源请求,这样就可以保证系统不会发生死锁。破坏循环等待条件可以采用顺序资源分配法,首先给系统中的资源进行编号,规定进程必须按编号递增的顺序请求资源。例如一个进程只有已占有小编号资源时,才可以申请更大编号的资源
3、虚拟地址空间了解吗
虚拟地址空间是操作系统为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏而引入的。虚拟地址空间是操作系统为一个进程描述的虚拟的、连续的、完整的、线性的地址空间,在linux下是一个mm_struct结构体。好处是保证每个进程在各自虚拟地址空间运行,互相不能干扰对方,采用虚拟地址空间,通过页表映射,可以实现进程中的数据在物理内存上的离散式存储,减少了内存碎片,提高了内存的利用率
4、请你说说操作系统中的缺页中断
缺页中断指的是在请求分页系统中,当通过页表要访问的页面不在内存中时,会产生缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
5、进程间通信方式
操作系统根据不同的场景提供了不同的方式,管道,共享内存,消息队列和信号量。其中管道是内核中的一块缓冲区,分为匿名管道和命名管道。匿名管道只能用于具有亲缘关系的进程间;而命名管道可用于同一主机上任意进程间通信。共享内存的本质是一块物理内存,多个进程将同一块物理内存映射到自己的虚拟地址空间中,再通过页表映射到物理地址达到进程间通信,它是最快的进程间通信方式,相较其他通信方式少了两步数据拷贝操作。消息队列是内核中的一个优先级队列,多个进程通过访问同一个队列,在队列当中添加或者获取节点来实现进程间通信。信号量的本质是内核中的一个计数器,主要实现进程间的同步与互斥,对资源进行计数,有两种操作,分别是在访问资源之前进行的p操作,还有产生资源之后的v操作。
6、僵尸进程和孤儿进程
僵尸进程:子进程先于父进程退出,但是父进程没有进行进程等待,导致无法获取子进程的退出状态信息,使操作系统无法释放子进程的资源,这时候的子进程就是僵尸进程 孤儿进程:父进程先于子进程退出,则子进程称为孤儿进程,此时孤儿进程的父进程成为了1号进程,并且这个孤儿进程运行在后台,并不占据前台终端 如何处理僵尸进程:kill -9命令。子进程退出时向父进程发送SIGCHLD信号,父进程处理SIGCHLD信号,在信号处理函数中调用wait进行处理僵尸进程
7、并发和并行的理解
并发:并发指同一时间段内多个程序的运行 并行:并行指同一时间多个线程或多个程序同时进行
8、有了进程为什么还要线程
进程可以使多个程序并发执行,提高资源利用率和系统的吞吐量,但是进程同一时间只能干一件事,进程在执行的过程中如果阻塞,整个进程就会挂起,即使进程中有些工作不依赖于等待资源,扔不会执行。操作系统引入线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时空开销,提高并发性。
9、你了解IPC吗
1、进程间通信方式是操作系统为用户提供的进程之间实现数据通信的方式,因为进程之间具有独立性,无法直接通信。各进程都使用的是自己的虚拟地址空间,无法通过同一地址访问同一块内存 2、操作系统根据不同的场景提供了不同的方式,管道,共享内存,消息队列和信号量。其中管道是内核中的一块缓冲区,分为匿名管道和命名管道。匿名管道只能用于具有亲缘关系的进程间;而命名管道可用于同一主机上任意进程间通信。共享内存的本质是一块物理内存,多个进程将同一块物理内存映射到自己的虚拟地址空间中,再通过页表映射到物理地址达到进程间通信,它是最快的进程间通信方式,相较其他通信方式少了两步数据拷贝操作。消息队列是内核中的一个优先级队列,多个进程通过访问同一个队列,在队列当中添加或者获取节点来实现进程间通信。信号量的本质是内核中的一个计数器,主要实现进程间的同步与互斥,对资源进行计数,有两种操作,分别是在访问资源之前进行的p操作,还有产生资源之后的v操作。
10、fork和vfork的区别
fork()创建出来的子进程拷贝父进程大部分资源,也有自己的虚拟地址空间;而vfork创建出来的子进程与父进程共享数据段 fork()的父子进程执行顺序是不确定的,而vfork保证子进程先运行,只有子进程退出之后父进程才能运行
11、请问单核机器上写多线程程序,是否需要加锁
需要,在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后去运行另一个线程。如果这两个线程共享某些数据,不使用线程加锁,也是有可能导致线程的不安全的
12、线程同步方式,并说出系统调用
信号量:信号量可以实现线程的同步与互斥,通过自身计数器对资源进行计数,并通过该计数器判断当前线程是否具有访问的条件,如果可以访问就访问,不可以访问就会挂到pcb等待队列中,直到被其他线程唤醒,才会去争抢资源 int sem_wait(sem_t sem)信号量减1,如果此时信号量为0则阻塞 int sem_post(sem_t sem)信号量加1,当信号量大于1时会唤醒阻塞的线程 条件变量和互斥锁:通过条件判断当前线程是否具备访问的权利,如果不满足条件则阻塞,直到等待条件满足才唤醒阻塞线程。因为条件变量本身就是一个临界资源,所以通常搭配互斥锁一起使用 pthread_mutex_init 初始化互斥锁 pthread_mutex_destroy 销毁互斥锁 pthread_mutex_lock(pthread_mutex_t mutex)给互斥锁加锁,如果以加锁则阻塞,直到持有互斥锁者解锁 pthread_mutex_unlock(pthread_mutex_t mutex)给互斥锁解锁 pthread_cond_init 初始化条件变量 pthread_cond_destroy 销毁条件变量 pthread_cond_wait(pthread_cond_t cond, pthread_mutex_t mutex) 等待满足条件进行加锁 pthread_cond_signal(pthread_cond_t *cond)唤醒至少一个线程
13、多线程和多进程的优缺点和各自使用场景
多线程之间共享一个进程的地址空间,线程间通信简单,线程的创建和销毁都比多进程简单,速度快,占用内存少。但是线程间会相互影响,一个线程的崩溃也有可能导致其他线程的终止,可靠性若;多进程都有各自独立的虚拟地址空间,进程间不会相互影响,可靠性强。但是进程间通信复杂,进程的创建和销毁复杂,占用的内存多 多线程适用于I/O密集型程序,多进程适用于CPU密集型程序
14、操作系统的缺页置换及算法
当访问内存中不存在的页,且内存已满时,就会从内存中调出一页送至交换区,再从交换区置换出另一页,这种现象就是缺页置换。 最近最少使用(LRU)算法:将最近一段时间以来最长时间未被访问的页面置换出去。实现:使用一个栈,将新页面或者命中的页面移动到栈底,替换时替换栈顶页面 先进先出(FIFO)算法:将内存中驻留时间最久的页面置换出去。实现:使用一个队列,新加入的页面放入队尾,置换时置换队首页面
15、什么是进程,什么是线程
进程:进程是一个正在运行的程序,在操作系统角度讲,进程就是一个pcb,pcb就是操作系统对一个正在运行的程序的描述,通过这个描述实现对指定程序的调度管理。在linux中是一个task_struct结构体 线程:线程就是进程中的一条执行流,是cpu调度的基本单位,但是这个执行流在linux下是通过pcb实现的,因此实际上linux下的线程就是一个pcb,并且在linux下的pcb共用一个虚拟地址空间(进程的虚拟地址空间),共享了大部分资源。相比进程更加的轻量化,所以线程也被称为轻量级进程
16、什么是重定向
通过改变原本文件的文件描述符对应的文件描述信息,从而导致改变了数据的流向,将数据不再写入原本的文件而写入到新指定的文件
17、软链接和硬链接的区别
1、通过ln创建的文件是硬链接文件,硬链接文件和源文件共用一个inode结点,也就是说硬链接文件只是源文件的一个别名。而通过ln -s创建的软链接文件是拥有自己独立的inode结点号,是一个新的文件,只是这个文件的内容保存了一个文件的路径名 2、删除源文件后,软链接文件会失效,而硬链接文件只是链接数-1 3、由于软链接是保存的是源文件的路径,所以可以跨分区链接。而硬链接是使用了同一个inode结点号,跨分区将无法找到唯一的 那个源文件