linux内核的namespace机制
Linux的namespace机制是一种用于实现进程隔离和资源隔离的核心特性
例如docker容器本质上就是一个linux进程,docker的容器隔离就依赖了linux的namespace机制
有了namespace机制,进程号、主机名、挂载点等信息就不再是唯一的,而是一个namespace下面唯一的,因此在操作系统层面上看,就会出现多个相同pid的进程,且他们并不会出现冲突。每个namespace看上去就像一个单独的linux系统
namespace的六项隔离及对应参数
UTS:系统调用参数CLONE_NEWUTS,用于隔离主机名、域名,通过clone()函数创建新UTS,同时子进程执行sethostname操作,可以发现子进程的shell变成了新配置的域名
IPC:系统调用参数CLONE_NEWIPC,用于隔离信号量、消息队列、共享内存,通过ipcmk -Q创建一个message queue,通过ipcs -q查看已有的message queue,如果使用clone()创建子进程及新IPC,可以发现在主进程可以查到的message queue在子进程查不到了
PID:系统调用参数CLONE_NEWPID,用于隔离进程编号
Network:ClONE_NEWNET,用于隔离网络设备、网络栈、端口等。一个docker container和其宿主机之间的网络就是不同namespace之间的通信,通过创建一个veth pair,一段在container的namespace中(eth0),一段在原先的namesapce中连接物理网络,再通过网桥进行转发
Mount:CLONE_NEWNS,用于隔离挂载点(文件系统)。不同mount namespace中的文件结构发生变化互不影响,可以通过/proc/<pid>/mounts查看到挂载在当前namespace中的文件系统,在/procs/<pid>/mountstats有mount namespace中文件设备的统计信息
User:CLONE_NEWUSER,用于隔离用户和用户组,使得一个在父进程里面可能是普通用户,进入特定的namespace里面可以是一个特权用户
在linux系统中,进入/proc/<pid>/ns
目录下面执行ll
,可以看到里面有很多软连接文件,即上面这六类,其链接的对象即namespace编号,相同的namespace编号代表这个进程的xx属性属于相同的namespace
namespace API
clone()
clone()在创建新进程的同时创建namespace。它也是linux系统调用fork()的一种更同意的实现。
其中flags就是namespace标志位,即CLONE_NEW**
setns()
通过setns()加入一个已存在的namespace
fd是上面看到的指向/proc/<pid>/ns
目录的文件的描述符,nstype是是否校验符合要求
unshare()
在原先的进程上进行namespace隔离,不启动新线程
fork()
比较基础的api,当程序调用fork()
时,系统会创建新进程,为其分配资源,例如存储数据和代码的空间,然后把原来所有的值都复制到新进程中,只有少数值与原来进程不同,相当于复用了自身。
fork()
函数一次调用,返回两次,分别是:
在父进程中返回新创建子进程的进程ID
在子进程中返回0
如果出现异常返回一个负值
使用fork后,父进程负责监控子进程的运行状态,并且在质检处退出后自己才能正常退出。
PID进程命名空间的特殊性
PID namespace隔离是比较实用的,它使两个namespace可以有相同的PID。
内核中所有的PID namespace是一个树形结构,最顶层是系统初始时创建的root namespace,它创建的新PID namespace是孩子节点,依次类推
所属的父节点可以看到子节点中的进程,并且可以通过信号等方式对子节点中的进程产生影响,反过来子节点不能看到父节点中的任何内容
PID namespace一些机制
每个PID namespace中第一个进程
PID 1
的作用与传统linux中的init进程一样拥有特权,起特殊作用一个namesapce中的进程不可能通过kill和ptrace影响父节点或兄弟节点进程
如果在新的PID namespace中重新挂载
/proc
文件系统,会发现旗下只显示同属一个PID namespace中的其他进程,如果不重新挂载,使用ps aux/top
之类的命令可能能看到所有父进程PID,读的可能是全局的/proc
这时才会显示所有namespace的进程在root namespace可以看到所有的进程,并且递归包含所有子节点中的进程
PID namespace中的init进程
在linux中,PID为1的是init进程,它是所有进程的父进程,维护一张进程表,不断检查进程状态,一旦某个进程的父进程异常了,这个进程就变成了孤儿进程,init就会负责收养这个子进程并最终回收资源结束进程
容器启动(例如docker run)时,也要有一个进程实现类似init的功能,即容器启动时的第一个进程,具备资源监控和回收能力,例如bash
信号与init进程
init进程具备特权:信号屏蔽,即init进程没有编写处理某个信号的逻辑,那么与init在同一个PID namespace下的进程发送给它的该信号都会被屏蔽(即使有超级权限)。这是为了防止init进程被误杀
而父节点中的进程发送的信号,如果不是SIGKILL(销毁)和SIGSTOP(暂停),也会被忽略,但是上面这两个信号子节点init会强制执行。也就是说父节点有权终止子节点进程
一旦init进程销毁,同一PID namespace中的其他进程也随之收到SIGKILL信号被销毁,该PID namespace也不存在了,除非/proc/<pid>/ns/pid
处于被挂载或打开状态,namespace就会保留,但是也没法执行setns()
或fork()
了,即变成了一个无用namespace
tini - 常用的init进程
在docker和k8s技术中,tini是非常常用的init进程,可以参考docker高级实践部分
挂载proc文件系统
如果新的进程在新的PID namespace中使用ps
命令查看,看到的还是所有的进程,因为与PID直接相关的/proc
文件系统没有挂载到一个原/proc
不同的位置。如果只想看到PID namespace本身应该看到的,需要重新挂载/proc
:
unshare()和setns()
unshare()
是在原有进程中建立新namespace进行隔离,setns()
是创建新的namespace,这两个在创建其他namespace时,调用者进程都会进入新的namespace,但是创建PID namespace时不会,这时因为linux系统中认为进程的PID是一个常量,如果PID变化,就会导致进程崩溃。
因此docker使调用者进入新namespace实际还是调用了clone()
linux内核的cgroups机制
cgroups是Linux内核提供的一种机制,目的是把一系列任务及子任务划分在等级不同的组内,对每组进行专门的资源管控
cgroups可以限制、记录任务组所使用的的物理资源,包括CPU、内存、IO等。cgroups的作用包括:资源限制(例如linux的oomkill)、优先级分配、资源统计、任务控制
cgroups的子系统
blkio:块设备输入输出限制,比如磁盘、固态、USB等
cpu:CPU使用限制
cpuacct:cpu资源使用情况报告
cpuset:可以为cgroup中的任务分配独立的cpu和内存
devices:可以开启、关闭cgroup中任务对设备的访问
freezer:可以挂起/恢复cgroup中的任务
memory:设定cgroup中任务对内存的使用量限定
perf_event:cgroup组任务统一性能测试
net_cls:网络标记数据
cgroups的运作方式
通过fs进行运作,提供对程序的api访问这些fs中的文件,基础目录:
传统linux:/sys/fs/cgroup/xxx/进程
docker下:/sys/fs/cgroup/xxx/docker容器id
k8s下:/sys/fs/cgroup/xxx/kubernentes/k8s容器id
其中有部分文件需要关注:
tasks:罗列了该cgroup中的任务的TID,即所有进程或线程的ID,该文件并不保证任务的TID有序,如果这个任务所在的任务组与其不在同一个cgroup,那么会在cgroup.procs中记录一个该任务所在的任务组的TGID(线程组ID),但是该任务组其他任务不受此cgroup影响
cgroup.procs:罗列所有在该cgroup中的TGID(线程组id)
notify_on_release:0或1,标识是否在cgroup中最后一个任务退出时通知运行release agent,0是默认不运行
release_agent:自动化卸载无用的cgroup
memory.usage_in_bytes:动态值,内存使用,这里变动的可能只有直接内存,堆内存一旦分配给jvm,就不还了,因此这里很可能都是一个最大值
memory.max….:限制最大值
Memory Cgroup与OOM_Killer的运作机制
概念
OOM Killer就是在Linux系统里如果内存不足时,就需要杀死一个正在运行的进程来释放一些内存
Linux允许进程在申请内存的时候是overcommit的,这是什么意思呢?就是说允许进程申请超过实际物理内存上限的内存,因为malloc()
申请的是内存的虚拟地址,系统只是给了程序一个地址范围,由于没有写入数据,所以程序并没有得到真正的物理内存
物理内存只有程序真的往这个地址写入数据的时候,才会分配给程序
这种overcommit的内存申请模式可以带来一个好处,它可以有效提高系统的内存利用率,同样的道理,遇到内存不够的这种情况,Linux采取的措施就是杀死某个正在运行的进程。这种情况就是OOM_Kill
可以参考上面计算机原理内存部分
OOM_Killer选定标准
Linux内核里有一个 oom_badness()
函数,就是它定义了选择进程的标准。其实这里的判断标准也很简单,函数中涉及两个条件:
进程已经使用的物理内存页面数。
第二,Cgroup中每个进程的OOM校准值oom_score_adj。在/proc文件系统中,每个进程都有一个
/proc/oom_score_adj
的接口文件。我们可以在这个文件中输入-1000 到1000之间的任意一个数值,调整进程被OOM Kill的几率。
函数oom_badness()
里的最终计算方法是这样的:
用系统总的可用页面数,去乘以OOM校准值oom_score_adj,再加上进程已经使用的物理页面数,计算出来的值越大,那么这个进程被OOM Kill的几率也就越大
linux常用系统
ubuntu
ubuntu操作系统的常用指令包括:
安装卸载软件 - apt
常用linux命令和bash脚本语法
expr - 实时计算
expr命令是一个手工命令行计数器,用于在UNIX/LINUX下求表达式变量的值,一般用于整数值,也可用于字符串。
基础语法:
表达式说明:
用空格隔开每个项;
用 / (反斜杠) 放在 shell 特定的字符前面;
对包含空格和其他特殊字符的字符串要用引号括起来
安装软件
还有一个rpm包用的
赋值命令xargs
xargs可以将管道或标准输入转换成命令行参数,并用这些参数来执行指定的命令。默认情况下, xargs 命令会将输入按照空格、制表符、换行符等符号进行分隔,并将它们作为一组参数传递给指定的命令。如果没有输入,则 xargs 命令会读取用户的键盘输入,并将其用作参数
-I 选项的语法是 -I <替代字符串>
,它允许您在命令行中使用替代字符串来代替 xargs 接收到的参数。特别地,{} 符号通常用作替代字符串。当 xargs 命令遇到 {} 符号时,它会将其替换为输入中的值,然后执行指定的命令。
其他命令
bash脚本语法
特殊关键字汇总
引号
``号
$0关键字
$关键字
if+fi语法
跟java的if语句效果一致,fi表示一个if语句的结尾
case语法
本质上类似于java的switch语句,语法格式如下:
while语法
跟java的while语句效果一致,格式如下:
for语法
跟java的for循环语句效果一致
评论区