(1)系统调用和库函数
系统调用:最底层的调用,面向硬件,#include
-------用户进程->系统调用接口->linux内核子系统->硬件
库函数调用:通过系统调用实现,可移植性,对正常文件的访问
(2)进程、线程、管程
进程是程序的一次活动,操作系统执行、资源分配和调度的基本单位;
线程是CPU分配的基本单位,进程之中的实体,一个进程可以包含多个线程,也可以只有一个线程(此时可以称作单线程);进程可以创建和撤销线程;每个进程下的线程共享进程的各种资源,只有少量的独立资源如栈、寄存器;
管程:管程定义了一个数据结构和能为并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据;
区别:
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率;
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行
进程更为健壮。一个进程之间的某个线程死掉了,整个进程就死掉了;一个进程死掉了,对其他的进程没有影响
关系:进程相当于整条道路;线程相当于该道路上的各条车道;线程不能独立于进程存在,线程之间可以根据各种
同步机制防止死锁
(3)进程间通信的方式:
管道——只能父子进程(半双工)
pipe(int fd[2]);//fd[0]读,fd[1]写
创建管道之后调用fork函数,父进程与子进程都有两个读写口,然后一个关闭读一个关闭写即可进行通信。
协同进程:一个进程的标准输入和标准输出通过两个管道连接到父进程。
还可以用mkfifo创建一个命名管道(FIFO其实是一种文件类型):解决了管道只能在父子进程间通信的限制。
使用FIFO的方式和使用文件的方式类似,也需要open等打开(一端对其进行写,另外一端进行读,)
信号;(信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生)
系统IPC:消息队列、信号量、共享存储
信号量;(其实就是计数器)(与信号不同,信号是信号处理机制,信号量是P,V操作)
互斥量;(信号量的一种)
消息队列;(消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息
少、管道只能承载无格式字节流以及缓冲区大小受限等缺点)
共享内存;(共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都
可以访问。共享内存是最快的 IPC (因为不需要系统调用和内核的切入,少了很多次数据的内核用户态拷贝)方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他
通信机制,如信号量,配合使用,来实现进程间共享内存的同步)
一般可以通过mmap函数进行文件映射实现共享内存
套接字;
(4)关于死锁
四个条件:互斥、请求与保持、不剥夺、循环等待;
互斥条件不可破坏;可以破坏其他三个条件来避免死锁;
(5)静态链接和动态链接(都是共享代码的方式)
分别对应静态链接库和动态链接库(DLL);
静态库本身包含实际执行的代码;动态链接库只是一些基本的地址信息;
和速度无关,静态链接库甚至要更快一些;
静态链接:windows: .liblinux:.a
动态链接:windows: .dlllinux:.so
(6)线程通信的方式
临界区;
互斥量;
信号量;
(7)管道与系统IPC(消息队列、信号量、共享内存)之间的优劣比较
管道: 优点是所有的UNIX实现都支持, 并且在最后一个访问管道的进程终止后,管道就被完全删除;
缺陷是管道只允许单向传输或者用于父子进程之间.
系统IPC: 优点是功能强大,能在毫不相关进程之间进行通讯;
缺陷是关键字KEY_T使用了内核标识,占用了内核资源,而且只能被显式删除,没有引用计数
而且不能使用SOCKET的一些机制,例如select,epoll等.
(8)关于fork和vfork
fork产生子进程,子进程拥有父进程的副本,只共享正文段(其实现在的实现采用了写时复制的技术,先让子进程只读
共享内存,如果有一个提出修改请求之后,才会产生变量的副本)和文件偏移量。
fork的两个用法:
①一个进程创建一个自身的副本,让他们互相执行自己的任务。
②一个进程想要执行另外一个程序,fork之后调用exec进行新程序环境替换。会替换当前进程的正文段、数据段堆
和栈;(此时的写时复制技术就可以避免了复制父进程的进程环境)
在调用exec之后,打开的描述符默认会跨exec打开。(当然,可以设置禁止跨exec,也可以手动关闭)
vfork创建子进程,不完全复制地址空间,保证子进程先运行,子进程调用exec或者exit之后父进程才能被调度。
父子进程只是共享代码段,其他的变量都不会共享,只是复制,也就是说,在子进程中修改一个变量,父进程中的该
变量是不会改变的。文件描述符(涉及到计数的问题)也是复制,相当于执行了dup,文件偏移量共享。
(9)exit和wait以及waitpid
exit终止进程:①调用终止处理进程(在之前会有登记)②关闭所有的I/O流
_exit和_Exit立即进入内核;
wait作用:
如果所有子进程都还在运行,则阻塞;如果一个子进程已经终止正在等待父进程获取状态,则wait获取该终止状态
立即返回
waitpid是指对于指定子进程的wait,可以通过参数进行控制
这两个函数原型:
pid_t wait(int *statloc);//一直阻塞到第一个子进程终止
pid_t waitpid(pid_t pid, int *statloc, int options);
options选项可以控制不阻塞
pid参数:
pid == -1等待任一子进程
pid > 0等待进程号等于pid的子进程
pid == 0等待与调用进程为同组的任一进程
pid < -1组ID等于pid绝对值的任一进程
(10)临界区
每个进程中访问临界资源的那段程序称为临界区,每次只准许一个进程进入临界区,进入后不允许其他进程进入。
(11)signal函数是信号机制接口,能够指定某一信号产生时的处理动作;
参数:信号Id,处理函数指针
(12)kill函数和raise函数
int kill(pid_t pid, int signo);//将signo信号发给pid(大于0则指定进程,==0则进程组)
int raise(int signo);//进程向自身发送信号,等价于int kill(getpid(), int signo);
(13)abort函数-使程序异常终止(向调用进程发送一个SIGABRT信号)
(14)屏障(栅栏)
用户协调多个线程并行工作,允许每个线程等待,直到所有的合作线程都达到某一条件。
高级IO
(1)非阻塞IO
阻塞与非阻塞:发起一个请求,一直等到有结果返回时为阻塞式;如果当前没有目标结果,先返回一个错误,则为非阻塞式;
非阻塞式IO一般伴随着轮询(polling),轮询会消耗大量的cpu资源。
某些文件在打开后默认对文件的操作是阻塞的(读/写的时候,由于数据不存在会导致阻塞...),可以以非阻塞的方式打
开标准输入文件:
fd = open("test.txt",O_NONBLOCK);
也可以通过fcntl函数修改已经打开的文件描述符的属性。
(2)记录锁(字节范围锁)
使用fcntl函数设置:int fcntl(int fd, int cmd, .../*struct flock *flockptr */ );
这个函数是一个变参函数,一般情况下只需设置前两个参数,但是设置记录锁时, fcntl函数需要使用到第三个参数。
进程终止时,该进程加的记录锁全部被释放。
fork产生的子进程不会继承父进程加的锁,子进程必须要重新设置。
执行exec后,新程序可以继承原执行程序的锁。
(3)I/O多路转换
当需要从多个描述符(可能是文件描述符,也可能是socket套接字)中取数据,或者需要向多个描述符传数据的时候,
有以下三个选择:
非阻塞式IO伴随轮询,多进程或者多线程会伴随着切换环境消耗和同步消耗,一般采用的是多路复用(select和poll)
多路io复用的原理:
其基本思想是构造一个文件描述符的表,在这个表里面存放了会阻塞文件描述符(非阻塞的没有意义),然后调用多路复
用函数,该函数每隔一段时间会去检查一次,看表中是否有某个或几个文件描述符有动作,没有就休眠一段时间,再隔
一段再去检查一次,如果其中的一个或多个文件名描述符有了动作,函数返回不在休眠,将分别对其有动作的文件描
述符做相应操作。 实际上多路复用本身也存在轮询检查过程,但是绝大部分时间却是在休眠,所以就避免了一般的轮
询机制,以及多进程实现所带来的相当的资源损耗。
select函数
函数原型:intselect(int nfds, fd_set *readfds, fd_set*writefds, fd_set*exceptfds, struct timeval*timeout);
其中nfds取想要检测的描述符最大值+1(用以表示一个范围,范围中包含了所有检测的描述符)
readfds,writefds和exceptfds分别表示读、写、异常描述符集合,不考察则设置为NULL
timeout表示需要等待的时间,一般设置NULL表示永远等待
返回值:-1调用失败;0无就绪描述符;>0表示就绪描述符个数
当内核检测到集合中有某个或几个文件描述符有响应时,这个集合将会被重新设置, 用于存放那些有动作的文件描
述符,所以一旦函数调用完毕我们必须重新设置这些集合。一般的做法将设置、调用都放在循环体内,如下:
FD_ZERO(&rset);
while(1)
{
FD_SET(fd1,&rset);//每次select之后都需要重新设置
FD_SET(fd2,&rset);
maxfdp = max(fd1,fd2)+1;
Select(maxfdp,&rset,NULL,NULL,NULL);
if(FD_ISSET(fd1,&rset)){
/*从fd1中读*/
}
if(FD_ISSET(fd1,&rset)){
/*从fd2中读*/
}
}
pselect的加强:
函数原型中多了一个const sigset_t *restrict sigmask参数用来屏蔽信号
poll函数
poll机制与select差不多,只是具体调用的实现不一样,与select 不同,poll不是为每个条件构造一个描述符集,而是构
造一个pollfd结构数组,每个数组元素指定一个描述符编号以及对其所关心的条件。
函数原型:int poll(struct pollfd *fds, nfds_t nfds,int timeout);
针对每个描述符,都有一个结构体与之对应:
struct pollfd {
int fd; /* file descriptor:文件描述符 */
short events; /* requested events:设置我们希望发生的事件 */
short revents; /* returned events :实际发生的事件*/
};
nfds设置为fds数组中结构体的数量。
events事件由用户设置,表示关心哪些事件
revents由内核设置,表示实际发生的事件
面试题:说说select的缺点以及epoll的改进。
Select的缺点:
》每次调用时,都需要将fd集合从用户态拷贝到内核态,fd大时开销比较大
》每次调用时,都要在内核态遍历fd集合,开销也很大
》支持的文件描述符的数量太小,1024
poll也有以上的缺点
epoll有三个函数,可以解决以上缺点
》每个fd在注册时后将其拷贝到内核当中,只会拷贝一次;
》使用回调函数避免了大规模的遍历
》文件描述符的个数没有限制
int epoll_create(int size);//创建epoll
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//事件注册
int epoll_wait(int epfd, struct epoll_event* event, int maxevents, int timeout);//类似于select调用,等待
水平触发:如果一次数据没有读完,下次调用epoll_wait时,会通知在没有读完的文件描述符上继续读写
边缘触发:如果一次数据没有读完,下次调用时不会通知;效率要高,不会充斥大量不关心的文件描述符
select和poll是水平触发
epoll支持两种触发,默认水平触发
为什么ET模式要求使用非阻塞套接字?
在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。而如果你的文件描述符如果不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。
(4)异步IO
关于同步异步,阻塞与非阻塞区别:https://www.zhihu.com/question/19732473
可以简单的理解为:阻塞与非阻塞是请求者的进程行为状态;同步与异步是服务者的服务方式
阻塞I/O意味着一直等待设备可访问再访问,非阻塞I/O意味着使用poll()来查询是否可访问,而异步通信则意味着设备通知应用程序自身可访问。
异步IO设置的步骤如下:
(1) 调用signal或sigaction为该信号建立一个信号处理程序。
(2) 以命令F_SETOWN调用fcntl来设置接收信号进程PID和进程组GID。
(3) 以命令F_SETFL调用fcntl设置O_ASYNC状态标志,使在该描述符上可以进行异步I/O。第3步
仅用于指向终端或网络的描述符
(5)存储映射IO
将磁盘文件映射到存储空间的一个缓冲区上,从缓冲区读数据就相当于读文件中的数据,写缓冲相当于写文件。
避免了各级缓存之间的数据复制。
可以使用mmap函数进行映射,函数原型:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数解读:
addr:内存中的映射起始位置,通常设置为NULL,让系统选择
length:映射的长度
prot:对映射区的操作权限
flags:是否同步更新文件内容
fd:需要被映射文件的描述符
offset:从文件起始偏移多少字节处开始映射。
返回值:成功则返回分配的映射地址,失败返回(void*)-1
posix信号
信号即软件中断,异步发生,由一个进程发送给另外一个进程或者由内核发送给某个进程
三种处理方式:
①设置信号处理函数捕获信号,当信号产生的时候就被调用
void handler(int signo)
②信号设置为SIG_IGN进行忽略
③设置为SIG_DFL启用默认处理
用signal函数进行设置:
Sigfunc* signal(int signo, Sigfunc* func)
函数返回值和第二个参数都是指向信号处理函数的指针
信号处理函数实例------解决僵尸进程的方法之一:处理SIGCHLD信号
每当一个子进程状态改变的时候,都将向其父进程发送SIGCHLD信号,如果在父进程中捕捉该信号,并且调用wait即可防止僵尸进程的产生。
fork子进程之前调用:signal(SIGCHLD, sig_chld);//即可捕捉该信号
其中sig_chld处理函数如下:
void sig_chld(int signo)
{
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d terminated\n",pid);
return;
}
其实这个地方仍然还有一个问题,就是同时有多个子进程发送SIGCHLD信号的时候,可能只会处理一部分的子进程,正确的做法是用一个循环,里面不停调用waitpid,至于为什么不能循环调用wait,那是因为wait在最后一次循环的时候会阻塞,而waitpid可以控制不阻塞,详见:http://blog.csdn.net/lxjslk/article/details/14231587
(1)32位系统一个进程最多有多少堆内存?
32位意味着4G的寻址空间,Linux把它分为两部分:最高的1G(虚拟地址从0xC0000000到0xffffffff)用做内核本身,成为“系统空间”,而较低的3G字节(从0x00000000到0xbffffff)用作各进程的“用户空间”。每个进程可以使用的用户空间是3G。虽然各个进程拥有其自己的3G用户空间,系统空间却由所有的进程共享。从具体进程的角度看,则每个进程都拥有4G的虚拟空间,较低的3G为自己的用户空间,最高的1G为所有进程以及内核共享的系统空间。实际上有人做过测试也就2G左右。
基本上都用作堆内存,栈内存只有1-4M
(2)写一个c程序辨别系统是大端or小端字节序
union{ short value; char a[sizeof(short)];}test;
test.value= 0x0102;
if((test.a[0] == 1) && (test.a[1] == 2)) cout << "big"<
(3)进程调度策略:FIFO,优先级,时间片轮换,多级反馈
Linux通过优先级对进程进行调度时,通过nice函数进行设置
(4)当一个进程正在读写某个文件的时候,文件管理员将这个文件删除,发生什么?
读写进程正常执行,文件并没有被删除,只有读写完成之后才会删除。
因为文件有两个计数 i_count (引用计数)和 i_nlink(硬盘计数),只有当这两个计数都为0的时候才会执行删除。rm命令只会将i_nlink减一,此时i_count不为0所以不会删除(但是ls也找不到该文件)
文件系统删除机制:http://blog.csdn.net/chiliaolm/article/details/52353188
变形:如果是执行mv操作呢?
1)如果是在同一个设备相同分区类型,mv的实现是调用rename函数,rename只修改文件名称,不会影响已有的打开这个文件的进程的读写操作;写入的内容会保存在重命名后的文件里。
2)如果不在同一个设备或相同的分区类型,mv操作实际是先将原有文件里的内容copy到新的文件里,然后调用unlink函数;unlink时如果该文件在被使用,不会将文件真正删除,只会将文件名改为空,在所有的对该文件的操作都完成并调用close后该文件才会被真正删除;这种情况下对原有文件的写入操作不会保存在新的文件里。
(5)中断和异常
中断:系统停止当前正在运行的程序(会保留断点)转向其他服务,可能是因为优先级高的请求到达,或者人为安排(正常现象),是cpu具备的一种功能。
硬件中断:外围硬件设备发起的中断信号
软件中断:指令发起的
异常:软件错误引起
(6)Linux并不真正区分线程和进程,底层都是通过task结构体定义。只是不同进程使用的是不同的进程空间,而同一个进程的不同线程 使用的是相同的进程空间
(7)零拷贝技术
Linux中传统的 I/O 操作是一种缓冲 I/O,I/O 过程中产生的数据传输通常需要在缓冲区中进行多次的拷贝操作。
零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。零拷贝技术的目标可以概括如下:
---避免数据拷贝
----------避免操作系统内核缓冲区之间进行数据拷贝操作。
----------避免操作系统内核和用户应用程序地址空间这两者之间进行数据拷贝操作。
----------用户应用程序可以避开操作系统直接访问硬件存储。
----------数据传输尽量让 DMA 来做。
---将多种操作结合在一起
----------避免不必要的系统调用和上下文切换。
----------需要拷贝的数据可以先被缓存起来。
----------对数据进行处理尽量让硬件来做。
一些零拷贝技术:
①直接I/O,如果内核不需要对数据进行处理,则可以让应用程序直接访问硬件存储
②避免数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间进行拷贝。mmap
③优化传输过程,写时复制