Linux内核面试知识总结
Linux启动过程
1、主机加电自检,加载BIOS硬件信息
2、读取MBR引导文件
3、引导linux内核
4、启动第一个进程init(进程号永远为1)
5、进度相应的运行级别
6、运行终端,输入用户名和密码
linux系统缺省的运行级别
关机、单机用户模式、字符界面的多用户模式(不支持网络)、字符界面的多用户模式、未分配使用、图形界面的多用户模式、重启
fork函数说明:
失败返回-1;
fork方法被调用一次,成功就会有两次返回;
在父进程中返回一次,返回的是子进程的pid(非0)
在子进程中返回一次,返回值为0
fork被调用后的linux内核动作:
1、分配新的内存块和内核数据结构给子进程;
2、将父进程部分数据结构内容拷贝到子进程;
3、将子进程添加到系统进程调度表
4、fork返回开始调度器,调度
fork后,子进程是否可以共享父进程打开的文件?
fork之后,父子进程会共享文件的描述符,主要是对文件读写偏移量的共享。
linux进程间通信方式
1、管道(pipe)、流管道(s_pipe)、有名管道(FIFO)
常说的管道多为匿名管道,匿名管道只能用于具有亲缘关系的进程之间,这是和有名管道之间最大的区别,有名管道可以用mkfifo()创建,匿名管道用pipe()创建
#include<stdio.h>
#include<unistd.h>
int main()
{
int n, fd[2]; // 这里的fd是文件描述符的数组,用于创建管道做准备的
pid_t pid;
char line[100];
if (pipe(fd) < 0) // 创建管道
printf("pipe create error/n");
if ((pid = fork()) < 0) //利用fork()创建新进程
printf("fork error/n");
else if (pid > 0) { //这里是父进程,先关闭管道的读出端,然后在管道的写端写入“hello world"
close(fd[0]);
write(fd[1], "hello word/n", 11);
}
else {
close(fd[1]); //这里是子进程,先关闭管道的写入端,然后在管道的读出端读出数据
n = read(fd[0], line, 100);
write(STDOUT_FILENO, line, n);
}
exit(0);
}
2、信号
信号的处理方式:
忽略此信号(SIGKILL和SIGSTOP除外,原因:他们向内核和超级用户提供了使进程终止或停止的可靠方法)
捕捉信号:通知内核在某种信号发生时,调用一个用户函数
执行默认动作
3、消息队列
消息队列本身是异步操作,消息队列的发送/接收者不需要同时与消息队列交互,消息保存在对列中,直到接收者取回处理。
消息队列的标识符是消息队列在内核系统的身份信息,是唯一的。
消息队列的限制:
1、消息队列个数最多为16个
2、消息队列总容量最多为1024*16=16384字节
3、每个消息队列的内容最大为1024*8=8192字节
System V提供的IPC通信机制需要一个key值,通过key 值就可在系统内获得一个唯一的消息队列。
key值可以是人为指定的,也可以通过ftok函数获得。
1.创建消息队列
int msgget(key_t key, int msgflg);
msgflg: 标识函数的行为及消息队列的权限,其取值如下
'IPC_CREAT':创建消息队列。
'IPC_EXCL': 检测消息队列是否存在。
位或权限位:消息队列位或权限位后可以设置消息队列的访问权限,但可执行权限未使用。一般是0666
返回值: 成功返回消息队列的标识符 ;失败返回
2.写入消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msgflg:函数的控制属性,其取值如下:
'0': msgsnd()调用阻塞直到条件满足为止。
'IPC_NOWAIT': 若消息没有立即发送则调用该函数的进程会立即返回。
返回值:成功返回0,失败返回-1。
3.读取消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
msgtyp:消息的类型:
msgtyp = 0:返回队列中的第一个消息。
msgtyp > 0:返回队列中消息类型为 msgtyp 的消息(常用)。
msgtyp < 0:返回队列中消息类型值小于或等于 msgtyp 绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。在获取某类型消息的时候,若队列中有多条 此类型的消息,则获取最先添加的消息,即先进先出原则。
msgflg:函数的控制属性:
'0': msgrcv() 调用阻塞直到接收消息成功为止。
'MSG_NOERROR': 若返回的消息字节数比 nbytes 字节数多,则消息就会截短到 nbytes 字节,且不通知消息发送进程。
IPC_NOWAIT: 调用进程会立即返回。若没有收到消息则立即返回 -1。
返回值: 成功读取消息的长度; 失败:-1
4.消息队列控制
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
cmd::
'IPC_RMID' :删除消息队列。从系统中删除给消息队列以及仍在该队列上的所有数据,这种删除立即生效。仍在使用这一消息\
队列的其他进程在它们下一次试图对此队列进行操作时,将出错,并返回EIDRM。 此命令只能由如下两种进程执行:
1.其有效用户ID等于msg_perm.cuid或msg_perm.guid的进程。
2.另一种是具有超级用户特权的进程。
'IPC_SET' :设置消息队列的属性。按照buf指向的结构中的值,来设置此队列的msqid_id结构。该命令的执行特权与上一个相同。
'IPC_STAT':读取消息队列的属性。取得此队列的msqid_ds结构,并存放在buf*中。
'IPC_INFO':读取消息队列基本情况。
buf:队列中的内容,一般为NULL。
4、共享内存
共享内存不同于内存映射区,它不属于任何进程,且不受进程生命周期的影响。通过调用linux的系统函数获得该块共享内存,使用之前需要将共享内存和进程进行关联,得到共享内存的起始地址就可以进行
读写操作。进程也可以和该块共享内存取消关联,取消关联后就不能操作该块内存了。在所有的进程间通信方式中,共享内存是效率最高的。
共享内存默认是不阻塞的,如果多个进程同时读写共享内存,可以出现数据错乱,共享内存需要借助其他机制来保证进程间的数据同步,比如信号量
共享内存使用步骤
调用shmget创建一个新的共享内存段或者获取一个既有的共享内存段的标识符。
使用shmat来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分,调用后返回addr值,即指向共享内存起始地址的指针
调用shmdt来分离共享内存段,调用后,该进程就无法再引用该段共享内存
调用shmctl来删除共享内存段,只有当当前所有附件内存段的进程都与之分离后内存段才会销毁,只有一个进程需要执行这一步。
共享内存和内存映射的区别
1、共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
2、共享内存效果更高
3、内存
1、所有的进程操作的是同一块共享内存。
2、内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
4、数据安全
1、进程突然退出
共享内存还存在
内存映射区消失
2、运行进程的电脑死机,宕机了
数据存储在共享内存中,没有了
内存映射区的数据,由于磁盘文件中的数据还在,所以内存映射区的数据还存在
5、生命周期
共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机、如果一个进程退出,会自动和共享内存进行取消关联。
内存映射区: 进程退出,内存映射区销毁
5、信号量
信号量是用于不同进程或者不同线程之间同步手段的原语,分为:
1、Posix有名信号量:可用于进程或线程间的同步;
sem_open:创建或者打开一个信号量;
name:信号量在文件系统上的文件名;
oflag:可以为0,O_CREAT或者O_CREAT|O_EXCL;
mode:信号量的权限位;
value:信号量的初值不能大于SEM_VALUE_MAX;
返回自sem_t是一个该信号量的标识符的指针;
sem_close:关闭一个信号量,但是并不删除信号量,另外进程终止系统会自动关闭当前进程打开的信号量;
sem:信号量的指针,通过sem_open获得;
sem_unlink:删除信号量,需要注意的是信号量同样维护了一个引用计数,只有当引用计数为0时才会显示的删除;
name:信号量的在文件系统上的唯一标示;
sem_post:V操作,将信号量的值+1,并唤醒等待信号量的任意线程;
sem:信号量的指针,通过sem_open获得;
sem_wait:P操作,如果当前信号量小于等于0则阻塞,将信号量的值-1,否则直接将信号量-1;
sem:信号量的指针,通过sem_open获得;
sem_trywait:非阻塞的sem_wait;
sem:信号量的指针,通过sem_open获得;
sem_getvalue:获取当前信号量的值,如果当前信号量上锁则返回0或者负值,其绝对值为信号量的值;
sem:信号量的指针,通过sem_open获得;
sval:信号量的值;
以上的函数除了sem_open失败返回SEM_FAILED,均是成功返回0,失败返回-1并且设置errno。
2、Posix基于内存的信号量(无名信号量):存放于共享内存中,可用于进程或者线程同步
```sem_init``:初始化一个无名信号量;
sem:信号量的指针;
pshared:标志是否共享:
pshared==0:该信号量只能在同一进程不同线程之间共享,当进程终止则消失;
pshared!=0:该信号量驻留与共享内存区,可以在不同进程之间进行共享;
value:信号量的初值;
返回值出错返回-1,成功并不返回0;
sem_destroy:销毁信号量。成功返回0,失败返回-1。
3、System V信号量:在内核中维护,可用于进程或线程间同步
6、套接字
套接字类型:
流式套接字,即TCP套接字,用SOCK_STREAM表示
数据报套接字,即UDP套接字(或称无连接套接字),用SOCK_DGRAM表示
原始套接字,用SOCK_RAM表示
套接字地址结构由网络地址和端口号组成
int Socket_fd = socket(AF_INET, SOCK_DGRAM, 0);//指定协议族AF_INET/PF_INET,SOCK_DGRAM/SOCK_STREAM使用数据包传输还是字节流传输
struct sockaddr_in address;//IPv4的地址结构体,之后给地址结构体赋值
address.sin_family = AF_INET;//指定使用的地址族
address.sin_port = htons(port);//指定感兴趣的端口号,并要转换成网络字节序
inet_pton(AF_INET, ip, &address.sin_addr);//将ip地址转换成网络字节序,赋值给sin_addr
int ret = bind(Socket_fd, (struct sockaddr*)&address, sizeof(address));//将ip地址和感兴趣的port关联到创建的udp文件描述符
ret = listen(Socket_fd, backlog);//监听是否有连接,backlog为连接的最大数
//这里的recvfrom()和sendto是Udp传输专用的API函数。
recvfrom(Socket_fd, buf, sizeof(buf), 0, (struct sockaddr*)&send_addr, &addrlen);//将接收到数据存放在buf中, 并将对方的ip地址和port端口号存储在send_addr结构体中。
sendto(Socket_fd, buf, sizeof(buf), 0, (struct sockaddr*)&send_addr, &addrlen);//将数据buf发送给地址和端口号为send_addr的机器上。
//TCP数据读写函数
ssize_t recv(socketfd, buf, sizeof(buf), 0);
ssize_t send(socketfd, buf, sizeof(buf), 0);
//接收连接与发起连接函数
int accept(socketfd, struct sockaddr* addr, socklen_t *addrlen);//接收连接保存的是对方的ip和port
int connect(socketfd, const struct sockaddr* addr, socklen_t *addrlen);//服务器的ip和端口号
Linux交换空间(swap space)
交换空间是linux使用的一定空间(可以是一个分区也可以是一个文件,或者文件和分区的组合),用于临时保存一些并发运行程序,当RAM没有足够的内存来容纳正在执行的所有程序时,
就会发生这种情况。linux物理内存吃紧时,将内存中不常访问的数据保存到swap上,这样系统就有更多的物理内存为各个进程服务。
swap space好处
1、系统就可以将这部分不这么使用的内存数据保存到 swap 上去,从而释放出更多的物理内存供系统使用。
2、很多发行版本的休眠功能依赖于swap分区,当系统休眠的时候,会将内存中的数据保存到swap上,等下次启动时再将数据加载到内存中,这样可以加速系统启动,所以使用休眠功能必须配置swap分区,
并且大小一定要大于物理内存
3、在某些情况下物理内存是有限的,但是为了能达到运行耗内存程序的目的,就通过配置足够的swap空间来达到目的,虽然慢一些,但是可以达到运行的目的
4、虽然大部分情况下,物理内存都是够用的,但是总有一些意想不到的状况,比如某个进程需要的内存超过了预期,或者有进程存在内存泄漏等,当内存不够的时候,就会触发内核的 OOM killer,
根据 OOM killer 的配置,某些进程会被 kill 掉或者系统直接重启(默认情况是优先 kill 耗内存最多的那个进程),不过有了 swap 后,可以拿 swap 当内存用,虽然速度慢了点,但至少给了我
们一个去 debug、kill 进程或者保存当前工作进度的机会。
5、如果能将不怎么常用的内存数据移动到 swap 上,就会有更多的物理内存用于 cache,从而提高系统整体性能。
交换空间配置:
1、创建swap文件:#dd if=/dev/zero of=/swap1 bs=1M count=1024
dd:用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换。
if=文件名:输入文件名,缺省为标准输入。即指定源文件
of=文件名:输出文件名,缺省为标准输入。即指目的文件。
bs=1M:设置输入/输出的块的大小
count=1024:指拷贝1024个块,即1G。
2、将上一步新建的文件设置为swap分区文件:#mkswap /swap1
3、激活swap,启用分区文件:#swapon /swap1
4、在/etc/fstab文件中添加下面的内容:/swap1 swap swap defaults 0 0
5、 通过free查看交换空间大小,看是否修改成功
LILO
LILO是linux的引导加载程序,主要用于将linux操作系统加载到主内存中,以便它可以开始运行
Linux配置lilo方法:修改/etc/lilo.conf文件,然后运行lilo命令。
inode
每个文件的属性信息,比如大小,时间,类型等成为文件的元数据(metadata),这些数据都存在inode(index node)表中,innode表中是由很多记录组成的,一条记录对应的存放的
一个文件的元数据信息
每一个inode表记录对应的保存了以下的信息
inode number 节点号
文件类型
UID
GID
链接数(指向这个文件名路径名称个数)
该文件的大小和不同的时间戳
有关文件的其他数据
#dumpe2fs /dev/sdc1
Inode count: 65536 inode号数量
Block count: 262144 块数量
Reserved block count: 13107 保留块数量
Block size: 4096 块大小 4K
Reserved blocks uid: 0 (user root) 保留下来的块分配给root用户与root组
Reserved blocks gid: 0 (group root)
superblock 超级块存放元数据信息
Backup superblock 备份超级块
Group 0: (Blocks 0-32767) [ITABLE_ZEROED] 组0
mkfs.ext4 --help
[-i bytes-per-inode] 默认16K
[-N number-of-inodes]
[-I inode-size]
[-J journal-options]
[-G flex-group-size]
[-N number-of-inodes]
软链接和硬链接
硬链接:可以理解为一个指针指向文件索引节点的指针,系统不另外给分配inode,每添加一个硬链接,文件链接数加1。
不足:
1、不可以在不同文件系统的文件间建立连接;2、只有超级用户才可以为目录创建硬链接
软链接:软连接克服了硬链接的不足,没有文件系统的限制,使用更广泛,甚至可以跨越不同机器,不同网络对文件进行链接。
不足:
因为链接文件包含有原文件的路径信息,所以当原文件从一个目录下移动到其他目录中,再访问链接文件,系统就找不到了,而硬链接没这个缺陷。
实际使用场景下,基本使用软链接。
总结:
1、硬链接不可以跨区,软链接可以;
2、硬链接指向一个inode的节点,软链接创建一个新的inode
3、删除硬链接文件,不会删除原文件,删除软链接文件,会把原文件删除
CC攻击和DDOS攻击
CC攻击主要用来攻击页面的,模拟多个用户不停的对页面进行访问,从而使系统资源耗尽
DDOS攻击(分布式拒绝服务攻击),借助服务器技术将多个计算机联合起来攻击平台,来对一个或多个目标发动DDOS攻击
防止攻击:通过硬件防火墙做流量清洗,将攻击流量引入黑洞。
Linux性能调优
1、Disabling daemons(关闭daemons)
2、shutting down the GUI(关闭GUI)
3、changing kernel params(改变内核参数)
4、内核参数
5、处理器子系统调优
6、内存子系统调优
7、文件系统子系统调优
8、网络系统子系统调优
Linux内核锁
自旋锁和信号量
自旋锁:
最多被一个可执行线程持有,如果一个执行线程试图请求一个已被占用的自旋锁,那么这个线程就会一直进行忙循环-旋转-等待锁重新可用。要是锁未被征用,请求它的执行线程便能立刻得到它并
继续进行。自旋锁可以在任何时刻防止多余一个的执行线程同时进入临界区。
信号量:
linux的信号量是一种睡眠锁,如果有一个任务试图获取一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。
Linux怎么申请大块内存
随着系统运行时间的增加,申请大块内存的机会逐渐减少,所以一般建议在系统启动阶段申请大块内存,但是其成功的几率只是比后续申请要高而已,不是100%。如果程序很在意申请成功与否,就
只能退用“启动内存(Boot Memory)”。
通过伙伴系统申请内核内存的函数有哪些
在物理页面管理上实现了基于区的伙伴系统(zone based buddy system),对不同区的内存使用单独的伙伴系统(buddy system)管理,而且独立监控空闲页。相应接口alloc_pages(gfp_mask, order),
__get_free_pages(gfp_mask,order)等。
Linux虚拟文件系统的关键数据结构
struct super_block 超级块代表了整个文件系统,超级块是文件系统的控制块,有整个文件系统信息,一个文件系统所有的inode都要连接到超级块上,可以说,一个超级块就代表了一个文件系统。
struct inode 索引节点对象由inode结构体表示,定义文件在linux/fs.h中
struct file 定义在include/linux/fs.h中定义,文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的 struct file。
struct dentry 目录项是描述文件的逻辑属性。文件系统中某个索引(inode)的链接。这个索引节点可以是文件,也可以是目录
Linux中的文件包括哪些
执行文件、普通文件、目录文件、链接文件、设备文件、管道文件
创建一个进程的系统调用
clone\fork\vfork\
使用schedule()进行进程切换的方式
1、系统调用do_fork()
2、定时中断do_timer()
3、唤醒进程wake_up_process()
4、改变进程的调度策略setscheduler()
5、系统调用礼让sys_sched_yield()
进程调度的核心数据结构
strcut runqueue:实时运行队列结构
加载和卸载一个模块
insmod加载、rmmod卸载
模块和应用程序分别运行的空间
模块运行在内核空间,应用程序运行在用户空间
Linux 中的浮点运算由应用程序实现还是内核实现
应用程序实现,Linux 中的浮点运算是利用数学库函数实现的,库函数能够被应用程序链接后调用,不能被内核链接调用。这些运算是在应用程序中运行的,然后再把结果反馈给系统。 Linux 内核如果
一定要进行浮点运算,需要在建立内核时选上 math-emu,使用软件模拟计算 浮点运算,据说这样做的代价有两个:用户在安装驱动时需要重建内核,可能会影响到其他的应用程序,使得这些应用程序在做
浮点运算的时候也使用 math-emu,大大的降低了效率。
TLB中缓存的内容
TLB 页表缓存,当线性地址被第一次转换成物理地址的时候,将线性地址和物理地址的对应放到TLB中,用于下次访问这个线性地址时,加快转换速度。
设备驱动程序函数
open\read\write\llseek\release
Linux系统调用的方法
靠软件中断实现的,首先,用户程序为系统调用设置参数,其中一个编号是系统调用编号,参数设置完成后,程序执行系统调用指令,X86上软中断是有int产生的,这个指令会导致一个异常,产生于一个
事件,这个事情会导致处理器跳到内核态并跳转到一个新地址。并开始处理那里的异常处理程序,此时的异常处理就是系统调用程序
Linux软中断和工作队列的作用
Linux中的软中断和工作队列是中断处理。
1、软中断一般是“可延迟函数”的总成,它不能睡眠,不能阻塞,处于中断上下文,不能进程切换,软中断不能被自己打断,只能被硬件中断打断,可以并发的运行在多个CPU上。
所以软中断必须设计成可重入的函数,因此也需要自旋锁来保护其数据结构。
2、工作队列中的函数处在进程上下文中,它可以睡眠,也能被阻塞,能够在不同的进程间切换。已完成不同的工作。可延迟函数和工作队列都不能访问用户的进程空间,可延时函数在执行时不可能有任何正在
运行的进程,工作队列的函数有内核进程执行,他不能访问用户空间地址。
Linux线程优先级设置
SCHED_FIFO:先进先出调度在先进先出调度方式下,一个线程直到它被更高优先级的线程抢占或者运行结束,才会交出控制权。相同优先级的任务不能打断该线程。当线程完成后,内核会寻找处于就绪状态相同
优先级的线程,如果不存在,则寻找低优先级线程。FIFO调度本身实现了数据的互斥,在线程运行时间内其他相同优先级线程无法进行资源抢占。
SCHED_RR:时间片轮转调度在时间片轮转调度下,一个线程放弃内核有三种情况:运行结束,被更高优先级抢占或消耗完自己的时间片。时间片是线程运行的最小时间单元,由操作系统预先设定。
当时间片用完时,该线程交出控制权,之后内核会按照和FIFO相同的方式搜索下一个工作线程。轮转调度可以防止某一个任务连续占用太多的资源,而导致其他线程信息得不到及时处理。缺点是轮转调度
会增加由于任务切换而导致的开销。
SCHED_OTHER:分时调度,一般采用CFS算法,CFS算法定义了一个新的调度模型,它给cfs_rq(run queue)中的每一个进程都设置一个虚拟时钟。如果一个进程得以执行,随着执行时间的不断增加,其
虚拟时钟-virtual runtime(vruntime)也将不断增加,没有得到执行的进程虚拟时钟将保持不变。
而调度器将会选择最小的vruntime那个进程来执行。这就是所谓的“完全公平”。不同优先级的进程其vruntime增长速度不同,优先级高的进程vruntime增长得慢,所以它可能得到更多的运行机会。
优先级设置和获取优先级函数
pthread_attr_setschedparam、pthread_attr_getschedparam
系统创建线程时,默认的线程是SCHED_OTHER。所以如果我们要改变线程的调度策略的话,可以通过pthread_attr_setschedpolicy实现
Linux下优先级翻转
优先级翻转
当一个高优先级线程通过信号量机制访问共享资源时,该信号量已被一个低优先级线程占用,而这个低优先级的任务在访问共享资源时可能又被一个中等优先级任务抢占。从以上描述,高优先级被许多
较低优先级的任务阻塞,导致高优先级的实时性得不到保证。
解决方案:
1、设置优先级上限,给临界区一个高优先级,进入临界区的进程都将获得这个高优先级,如果其他试图进入临界区的进程的优先级都低于这个高优先级,那么优先级反转就不会发生。
2、优先级继承,当一个高优先级进程等待一个低优先级进程持有的资源时,低优先级进程将暂时获得高优先级进程的优先级,在释放资源后,低优先级进程回到原来的优先级。嵌入式系统VxWorks就是采用
这个策略。
Linux线程礼让
让当前正在执行的线程暂停,但不阻塞;将线程从运行状态转为就绪状态;让CPU重新调度,礼让不一定成功,看CPU当前情况。
A和B两个线程,当CPU执行B的时候,B进行礼让,那么就离开CPU,这个时候B就变为就绪状态,CPU就重新在A线程和B线程之间进行选择,有可能还是让B线程执行,这个时候就没礼让成功。