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()的一种更同意的实现。
int clone(int (*child_func)(void ), void child_stack, int flags, void *arg)
其中flags就是namespace标志位,即CLONE_NEW**
setns()
通过setns()加入一个已存在的namespace
int setns(int fd, int nstype)
fd是上面看到的指向/proc/<pid>/ns
目录的文件的描述符,nstype是是否校验符合要求
unshare()
int unshare(int flags)
在原先的进程上进行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
:
mount -t proc proc /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
# 安装docker
apt install docker
# 保留配置文件删除docker软件
apt remove docker
apt-get remove
# 不保留配置文件删除docker软件
apt purge docker
常用linux命令和bash脚本语法
expr - 实时计算
expr命令是一个手工命令行计数器,用于在UNIX/LINUX下求表达式变量的值,一般用于整数值,也可用于字符串。
基础语法:
expr 表达式
表达式说明:
用空格隔开每个项;
用 / (反斜杠) 放在 shell 特定的字符前面;
对包含空格和其他特殊字符的字符串要用引号括起来
# 计算字符串长度
expr length “this is a test”
# 截取字符串
expr substr “this is a test” 3 5 -> is is
# 抓取第一个字符/数字出现位置
expr index “sarasara” a -> 2
# 运算
expr 30 / 3 / 2 -> 5
expr 30 /* 3 (使用乘号时,必须用反斜线屏蔽其特定含义) -> 90
# 正则匹配两种写法
# 如果expr做正则匹配时,正则部分使用\(xx\)形式,则expr的输出是\(xx\)中的实际内容,否则输出匹配结果长度
expr match 字符串 正则
expr 字符串 : 正则
link=`expr "$ls" : '.*-> \(.*\)$'`
安装软件
yum install nc # 安装,适配RedHat系列linux系统,例如centOS
apt-get install nc # 安装,适配Debian系列的linux系统,例如ubuntu
还有一个rpm包用的
# rpm相关
# 查看是否通过rpm安装了该软件
rpm -qa | grep 软件名
赋值命令xargs
xargs可以将管道或标准输入转换成命令行参数,并用这些参数来执行指定的命令。默认情况下, xargs 命令会将输入按照空格、制表符、换行符等符号进行分隔,并将它们作为一组参数传递给指定的命令。如果没有输入,则 xargs 命令会读取用户的键盘输入,并将其用作参数
ls *.txt | xargs rm # 查找所有txt,通过xargs赋值给rm执行
-I 选项的语法是 -I <替代字符串>
,它允许您在命令行中使用替代字符串来代替 xargs 接收到的参数。特别地,{} 符号通常用作替代字符串。当 xargs 命令遇到 {} 符号时,它会将其替换为输入中的值,然后执行指定的命令。
grep "^user" /etc/passwd | cut -d ":" -f 1 | xargs -I{} sudo userdel {}
# 在etc/passwd中查找以user开头的内容
# 根据分割符:取第一位置
# 基于xargs命令传给sudo userdel后面的{}执行删除
其他命令
# 获取当前服务器操作系统名
uname
# 去除文件名中的非目录部分,仅显示与目录有关的内容。dirname命令读取指定路径名保留最后一个/及其后面的字符
dirname
# &&命令:前面执行成功才执行后面的
[ -z "$CATALINA_HOME" ] && CATALINA_HOME=`cd "$PRGDIR/.." >/dev/null; pwd`
# &:表示程序要在后台执行
# |:管道命令,上一条输出作为下一条输入
# ||:逻辑或,上一条执行成功,后面不执行了
echo “ztj” || wc -l
bash脚本语法
特殊关键字汇总
引号
# bash中引号的说明
str1="test String"
str2='test String'
str3=test String
echo $str1 -> test String
echo $str2 -> test String
echo $str3 ->
双引号:引用的内容,所见非所得。如果内容中有命令、变量等,会先把变量、命令解析出结果,然后在输出最终内容。
var=dablelv
echo “$var” -> dablelv
单引号是全引用,被单引号括起的内容不管是常量还是变量都不会发生替换,单引号中的内容被认为是字符串
var=dablelv
echo ‘$var’ -> $var
无引号:不加引号和单引号一样,但是需要注意的是如果字符串中间有空格,就必须加单引号
``号
# bash中的 ``符号
``中的语句表示一个完整的执行语句,比如
echo `expr 1 + 1` 结果是2
echo expr 1 + 1 结果是expr 1 + 1
$0关键字
# bash中的$0关键字
在 Bash 脚本中,$0 是一个特殊变量,它代表当前脚本的路径和名称。这个变量用于表示脚本自身,它是 Bash 环境中的一个重要组成部分。$0 变量是一个只读变量,无法更改。
var=”$0”
echo “$var” -> 输出当前脚本的路径+名称
$关键字
# bash中的$
$()和``等同,用于命令替换
${}用于变量替换
if+fi语法
跟java的if语句效果一致,fi表示一个if语句的结尾
if [ condition ] 逻辑连接符 [condition]; then
# 代码块
else/elif [condition]; then
# 代码块
fi
case语法
本质上类似于java的switch语句,语法格式如下:
case 变量 in
result1) 执行1;;
result2) 执行2;;
……
esac
while语法
跟java的while语句效果一致,格式如下:
while [ condition ]
do
# 代码块
done
for语法
跟java的for循环语句效果一致
for 条件
do
操作
done
-参数语法
[ -a FILE ] 如果 FILE 存在则为真。
[ -b FILE ] 如果 FILE 存在且是一个块特殊文件则为真。
[ -c FILE ] 如果 FILE 存在且是一个字特殊文件则为真。
[ -d FILE ] 如果 FILE 存在且是一个目录则为真。
[ -e FILE ] 如果 FILE 存在则为真。
[ -f FILE ] 如果 FILE 存在且是一个普通文件则为真。
[ -g FILE ] 如果 FILE 存在且已经设置了SGID则为真。
[ -h FILE ] 如果 FILE 存在且是一个符号连接则为真。
# Catalina.sh中的案例:
PRG=”$0”
while [-h “$PRG”]; do
……
[ -k FILE ] 如果 FILE 存在且已经设置了粘制位则为真。
[ -p FILE ] 如果 FILE 存在且是一个名字管道(F如果O)则为真。
[ -r FILE ] 如果 FILE 存在且是可读的则为真。
[ -s FILE ] 如果 FILE 存在且大小不为0则为真。
[ -t FD ] 如果文件描述符 FD 打开且指向一个终端则为真。
[ -u FILE ] 如果 FILE 存在且设置了SUID (set user ID)则为真。
[ -w FILE ] 如果 FILE 如果 FILE 存在且是可写的则为真。
[ -x FILE ] 如果 FILE 存在且是可执行的则为真。
[ -O FILE ] 如果 FILE 存在且属有效用户ID则为真。
[ -G FILE ] 如果 FILE 存在且属有效用户组则为真。
[ -L FILE ] 如果 FILE 存在且是一个符号连接则为真。
[ -N FILE ] 如果 FILE 存在 and has been mod如果ied since it was last read则为真。
[ -S FILE ] 如果 FILE 存在且是一个套接字则为真。
[ FILE1 -nt FILE2 ] 如果 FILE1 has been changed more recently than FILE2, or 如果 FILE1 exists and FILE2 does not则为真。
[ FILE1 -ot FILE2 ] 如果 FILE1 比 FILE2 要老, 或者 FILE2 存在且 FILE1 不存在则为真。
[ FILE1 -ef FILE2 ] 如果 FILE1 和 FILE2 指向相同的设备和节点号则为真。
[ -o OPTIONNAME ] 如果 shell选项 “OPTIONNAME” 开启则为真。
[ -z STRING ] “STRING” 的长度为零则为真。
# Catalina.sh中的案例:
[ -z "$CATALINA_HOME" ] && CATALINA_HOME=`cd "$PRGDIR/.." >/dev/null; pwd`
[ -n STRING ] or [ STRING ] “STRING” 的长度为非零 non-zero则为真。
[ STRING1 == STRING2 ] 如果2个字符串相同。 “=” may be used instead of “==” for strict POSIX compliance则为真。
[ STRING1 != STRING2 ] 如果字符串不相等则为真。
[ STRING1 < STRING2 ] 如果 “STRING1” sorts before “STRING2” lexicographically in the current locale则为真。
[ STRING1 > STRING2 ] 如果 “STRING1” sorts after “STRING2” lexicographically in the current locale则为真。
[ ARG1 OP ARG2 ] “OP” is one of -eq, -ne, -lt, -le, -gt or -ge. These arithmetic binary operators return true if “ARG1” is equal to, not equal to, less than, less than or equal to, greater than, or greater than or equal to “ARG2”, respectively. “ARG1” and “ARG2” are integers.
评论区