最近偶然听了几堂极客时间的云原生免费公开课程,首次接触到了Linux namespace技术,并了解到这正是现在风头正劲的容器技术基石,引起了自己探究一二的兴趣,结合课程+网络搜索+实践操作,也算有了一些初步的了解,这里记录、总结一些学习过程。
namespace技术网上的介绍已经很多了,这里不做过多赘述,简单总结namespace是Linux 内核提供的支持内核资源隔离的关键技术,目前包含以下7类namespace:
Namespace 变量 隔离资源
Cgroup CLONE_NEWCGROUP Cgroup 根目录
IPC CLONE_NEWIPC System V IPC, POSIX 消息队列等
Network CLONE_NEWNET 网络设备,协议栈、端口等
Mount CLONE_NEWNS 挂载点
PID CLONE_NEWPID 进程ID
User CLONE_NEWUSER 用户和group ID
UTS CLONE_NEWUTS Hostname和NIS域名
本文中主要涉及到的是Network+PID+Mount三个namespace。
下载使用docker官方提供的基础操作系统镜像-本例中为deiban--时会发现很多命令都默认没有安装--比如网络抓包tcpdump、甚至进程信息查看ps/top等,直觉上的办法只能进入容器内部逐个安装。然而如果每次运行新容器都需要安装一遍相关工具包的话未免有些繁琐,另外如果只是启动初期临时使用一下这些工具调试,之后便不再需要,额外安装这些工具其实也不必要的增大了容器本身的复杂度。
针对这一问题,其实linux提供了nsenter、unshare命令用于进入容器进程所属Network、PID、Mount 等namespace执行宿主机命令,从而达到无需在容器中安装命令,直接使用宿主机相应命令的目的,以下以tcpdump/ps/top三个命令的执行为例进行进行介绍。
利用nsenter命令可以指定目标namespace,并在其中执行对应命令。
以下命令先运行一个debian基础镜像的容器,而后在其中执行ip addr命令查看网络配置,并尝试执行tcpdump命令抓包
~# docker run -it --name ns_test_net -d debian:stretch
d221b13a5fbcbf23a981a3067847b743081fff20ae05e6892b8744546cb1b148
~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d221b13a5fbc debian:stretch "bash" 9 seconds ago Up 6 seconds ns_test_net
~# docker exec -it ns_test_net bash
root@d221b13a5fbc:/# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1
link/ipip 0.0.0.0 brd 0.0.0.0
23: eth0@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
root@d221b13a5fbc:/# tcpdump dump -i any -nvv
bash: tcpdump: command not found
可以看到报错command not found,此时可以简单通过nsenter使用宿主机命令进入容器所属namespace执行相关命令:
通过ip addr 查看容器网络配置,通过tcpdump 尝试抓包
~# docker inspect -f {{.State.Pid}} ns_test_net # 获取容器进程在宿主机上的pid
9164
nsenter -t 9164 -n ip addr # -t指定容器进程pid,-n指定使用对应pid的Network namespace, 执行ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1
link/ipip 0.0.0.0 brd 0.0.0.0
23: eth0@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
~# nsenter -t 9164 -n tcpdump -nvv -i any # 使用宿主机tcpdump命令对容器所属Network namespace抓包,注意需要同时在宿主机上执行:ping 172.17.0.3
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
17:07:50.290707 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 172.17.0.3 tell 172.17.0.1, length 28
17:07:50.290743 ARP, Ethernet (len 6), IPv4 (len 4), Reply 172.17.0.3 is-at 02:42:ac:11:00:03, length 28
17:07:50.290761 IP (tos 0x0, ttl 64, id 22629, offset 0, flags [none], proto ICMP (1), length 84)
172.17.0.1 > 172.17.0.3: ICMP echo request, id 10895, seq 1, length 64
17:07:50.290777 IP (tos 0x0, ttl 64, id 9364, offset 0, flags [none], proto ICMP (1), length 84)
172.17.0.3 > 172.17.0.1: ICMP echo reply, id 10895, seq 1, length 64
17:07:51.307360 IP (tos 0x0, ttl 64, id 22696, offset 0, flags [none], proto ICMP (1), length 84)
172.17.0.1 > 172.17.0.3: ICMP echo request, id 10895, seq 2, length 64
17:07:51.307397 IP (tos 0x0, ttl 64, id 9365, offset 0, flags [none], proto ICMP (1), length 84)
172.17.0.3 > 172.17.0.1: ICMP echo reply, id 10895, seq 2, length 64
17:07:52.331317 IP (tos 0x0, ttl 64, id 22867, offset 0, flags [none], proto ICMP (1), length 84)
172.17.0.1 > 172.17.0.3: ICMP echo request, id 10895, seq 3, length 64
17:07:52.331352 IP (tos 0x0, ttl 64, id 9564, offset 0, flags [none], proto ICMP (1), length 84)
172.17.0.3 > 172.17.0.1: ICMP echo reply, id 10895, seq 3, length 64
17:07:53.355311 IP (tos 0x0, ttl 64, id 22927, offset 0, flags [none], proto ICMP (1), length 84)
根据前面使用nsenter进入Network namespace执行网络相关命令的经验,很容易得出使用nsenter进入PID namespace空间执行ps/top等命令即可获取容器内进程状态的想法,然而实际执行后会发现:
~# nsenter -t 9164 -p ps -elf
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
4 S root 1 0 0 80 0 - 34783 SyS_ep 2021 ? 00:20:18 /sbin/init
1 S root 2 0 0 80 0 - 0 - 2021 ? 00:00:02 [kthreadd]
1 S root 3 2 0 80 0 - 0 - 2021 ? 00:04:39 [ksoftirqd/0]
1 S root 5 2 0 60 -20 - 0 - 2021 ? 00:00:00 [kworker/0:0H]
1 S root 7 2 0 80 0 - 0 rcu_gp 2021 ? 01:46:22 [rcu_sched]
1 S root 8 2 0 80 0 - 0 - 2021 ? 00:00:00 [rcu_bh]
1 S root 9 2 0 -40 - - 0 - 2021 ? 00:03:31 [migration/0]
1 S root 10 2 0 60 -20 - 0 - 2021 ? 00:00:00 [lru-add-drain]
ps 实际显示的还是宿主机当前的进程信息(1号进程为/sbin/init),而非容器内部的进程信息,top也是一样的效果,这是为什么呢?
https://github.com/util-linux/util-linux/issues/660 解释到:
The command nsenter just enters the namespace(s), and nothing else. The behaviour of the utils like ps(1) depend on environment in the namespace. It's out of nsenter business to setup the environment (for example mount /proc). Maybe docker also uses mount namespace in the container, in this care you also need to enter --mount namespace etc.
大意是nsenter实际做的只是进入对应的namespace,而ps这些进程监控工具实际上依赖namespace中的环境设置--如/proc文件系统,nsenter并不会负责这些环境设置工作,所以需要使用者自己负责--比如mount /proc系统等。
在 man ps中实际也可以找到:
This ps works by reading the virtual files in /proc. This ps does not need to be setuid kmem or have any privileges to run. Do not give this ps any special permissions.
明确说明了ps依赖于/proc文件系统执行实际工作。
通过man proc 简单看一下什么是proc filesystem:
The proc filesystem is a pseudo-filesystem which provides an interface to kernel data structures. It is commonly mounted at /proc. Most of it is read-only, but some files allow kernel variables to be changed.
这是一个提供内核数据结构访问接口的伪文件系统,一般挂载在/proc路径下,那么之前使用nsenter -t 9164 -p 实际只是进入了PID namespace,但是使用的Mount namespace依然属于宿主机,所以ps/top这些工具依然读取的是宿主机的/proc文件,所以其输出的内容自然也就是宿主机进程执行信息了,为了解决这个问题我们需要让ps能够读取到容器拥有的proc文件系统。
第一个直觉反应是直接使用nsenter同时进入PID+Mount namespace,想当然既然都已经进入了容器的Mount namespace, 那ps命令自然读取的就是容器的/proc路径了,执行以下命令:
~# nsenter -t 9164 -p -m ps -elf
nsenter: failed to execute ps: No such file or directory
发现报 No such file or directory,思考后得出结论,既然都是用容器的 Mount namespace了,那ps命令的执行路径也就变成了容器内的文件系统了,而容器本身并没有安装ps命令,自然也就会报找不到文件了,所以同时挂载Mount namespace执行命令成功的前提是容器内部本来也已经安装了对应命令--这很明显无法满足我们的需求。
退一步考虑,只重新挂载proc文件系统可否呢,尝试通过 nsenter进入容器namespace 后先mount proc而后执行ps:
:~# nsenter -t 9164 -p
~# ps -lf # 输出结果为宿主机上进程状态
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
4 S root 8742 8719 0 80 0 - 5306 - 16:50 pts/2 00:00:00 -bash
4 S root 18449 8742 0 80 0 - 3694 - 18:10 pts/2 00:00:00 nsenter -t 9164 -p
0 S root 18450 18449 0 80 0 - 5305 - 18:10 pts/2 00:00:00 -bash
0 R root 18462 18450 0 80 0 - 9576 - 18:10 pts/2 00:00:00 ps -lf
~# mount -t proc proc /proc
~# ps -lf # 重新mount proc后执行结果为容器内进程状态
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
0 S root 40 0 0 80 0 - 5305 - 18:10 pts/2 00:00:00 -bash
0 R root 50 40 0 80 0 - 9576 - 18:10 pts/2 00:00:00 ps -lf
~# top # top执行结果类似
top - 18:10:21 up 279 days, 3:47, 7 users, load average: 0.00, 0.00, 0.00
Tasks: 3 total, 1 running, 2 sleeping, 0 stopped, 0 zombie
%Cpu(s): 1.1 us, 0.0 sy, 0.0 ni, 98.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 12355128 total, 1187052 free, 5869104 used, 5298972 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 6130300 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 18120 3100 2672 S 0.0 0.0 0:00.03 bash
40 root 20 0 21220 5044 3140 S 0.0 0.0 0:00.02 bash
51 root 20 0 44824 3540 3028 R 0.0 0.0 0:00.00 top
咋一看,通过nsenter进入PID namespace且重新mount proc文件系统后,ps与top输出的都已经是容器内的进程信息了,一切问题好像都解决了?
但是此时如果回到宿主机中执行ps与top会发现出问题了:
~# ps
Error, do this: mount -t proc proc /proc
~# top
Error, do this: mount -t proc proc /proc
这是因为nsenter中用的依然是宿主机的Mount namespace,这种情况下重新mount proc改变的是宿主机Mount namespace的状态,于是在nsenter内部使用容器的PID namespace+容器的proc系统工作正常,但是在宿主机上使用宿主机的PID namespace+容器的proc系统就会出错了。
通过 findmnt -o+PROPAGATION 命令可以查看当前mount状态,正常的proc状态如下:
~# findmnt -o+PROPAGATION

而nsenter 重新mount proc系统,宿主机再次mount proc后会变成这样
~# findmnt -o+PROPAGATION

于是直接通过nsenter 进入容器PID namespace 并重新mount proc的方法能够正常执行容器内的ps/top等命令,但是却有影响宿主机正常行为的副作用,不可取。
看起来需要探究一个既能在容器中正确执行宿主机的ps、top命令而又不能有影响宿主机状态副作用的方法,在网上查了不少资料还真没找到一个明确的方案,忍不住思考、摸索了数日,终于自己想出了一个目前看来能正确work的方案--引入unshare。
通过 unshare 命令可以在原进程上进行 namespace 隔离,也就是创建并加入新的 namespace,我们考虑先通过unshare命令将宿主机Mount namespace进行隔离,而后在隔离后进程中再次执行nsenter 进入容器的PID namespace,并重新挂载proc文件系统,这样新挂载的proc系统只会影响unshare的子进程,而不会穿透到宿主机之上。
具体在宿主机上执行以下命令:
~# unshare -fm
~# nsenter -t 9164 -p
~# mount -t proc proc /proc
~# ps -elf
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
4 S root 1 0 0 80 0 - 4530 core_s 16:53 pts/0 00:00:00 bash
0 S root 72 0 0 80 0 - 5305 - 18:48 pts/2 00:00:00 -bash
0 R root 81 72 0 80 0 - 9576 - 18:48 pts/2 00:00:00 ps -elf
~# top
top - 18:49:17 up 279 days, 4:26, 7 users, load average: 0.00, 0.04, 0.06
Tasks: 3 total, 1 running, 2 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.5 us, 0.2 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 12355128 total, 1036408 free, 5898532 used, 5420188 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 6100836 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 18120 3100 2672 S 0.0 0.0 0:00.03 bash
72 root 20 0 21220 5064 3160 S 0.0 0.0 0:00.02 -bash
82 root 20 0 44824 3496 2992 R 0.0 0.0 0:00.00 top
~# findmnt -o+PROPAGATION

可以看到ps/top输出的正是容器内的进程信息,同时findmnt结果中可以看到proc系统的PROPAGATION已经变成了private,表明mount变更不会影响其他namespace的状态。
回过来在宿主机上执行以下命令验证其proc系统不受影响:
~# ps -elf | head
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
4 S root 1 0 0 80 0 - 34783 SyS_ep 2021 ? 00:20:18 /sbin/init
1 S root 2 0 0 80 0 - 0 - 2021 ? 00:00:02 [kthreadd]
1 S root 3 2 0 80 0 - 0 - 2021 ? 00:04:39 [ksoftirqd/0]
1 S root 5 2 0 60 -20 - 0 - 2021 ? 00:00:00 [kworker/0:0H]
1 S root 7 2 0 80 0 - 0 rcu_gp 2021 ? 01:46:24 [rcu_sched]
1 S root 8 2 0 80 0 - 0 - 2021 ? 00:00:00 [rcu_bh]
1 S root 9 2 0 -40 - - 0 - 2021 ? 00:03:31 [migration/0]
1 S root 10 2 0 60 -20 - 0 - 2021 ? 00:00:00 [lru-add-drain]
5 S root 11 2 0 -40 - - 0 - 2021 ? 00:00:36 [watchdog/0]
~# findmnt -o+PROPAGATION

转载请注明出处,原文地址:https://www.cnblogs.com/AcAc-t/p/host_command_execution_in_linux_container.html
https://zhuanlan.zhihu.com/p/73248894
https://unix.stackexchange.com/questions/124162/reliable-way-to-jail-child-processes-using-nsenter/124194#124194
https://www.cnblogs.com/mrhelloworld/p/docker11.html
https://www.zhihu.com/question/24964878
https://github.com/util-linux/util-linux/issues/660
我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0
为了将Cucumber用于命令行脚本,我按照提供的说明安装了arubagem。它在我的Gemfile中,我可以验证是否安装了正确的版本并且我已经包含了require'aruba/cucumber'在'features/env.rb'中为了确保它能正常工作,我写了以下场景:@announceScenario:Testingcucumber/arubaGivenablankslateThentheoutputfrom"ls-la"shouldcontain"drw"假设事情应该失败。它确实失败了,但失败的原因是错误的:@announceScenario:Testingcucumber/ar
在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',
这个问题在这里已经有了答案:Checktoseeifanarrayisalreadysorted?(8个答案)关闭9年前。我只是想知道是否有办法检查数组是否在增加?这是我的解决方案,但我正在寻找更漂亮的方法:n=-1@arr.flatten.each{|e|returnfalseife
我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是
我不确定传递给方法的对象的类型是否正确。我可能会将一个字符串传递给一个只能处理整数的函数。某种运行时保证怎么样?我看不到比以下更好的选择:defsomeFixNumMangler(input)raise"wrongtype:integerrequired"unlessinput.class==FixNumother_stuffend有更好的选择吗? 最佳答案 使用Kernel#Integer在使用之前转换输入的方法。当无法以任何合理的方式将输入转换为整数时,它将引发ArgumentError。defmy_method(number)
当我的预订模型通过rake任务在状态机上转换时,我试图找出如何跳过对ActiveRecord对象的特定实例的验证。我想在reservation.close时跳过所有验证!叫做。希望调用reservation.close!(:validate=>false)之类的东西。仅供引用,我们正在使用https://github.com/pluginaweek/state_machine用于状态机。这是我的预订模型的示例。classReservation["requested","negotiating","approved"])}state_machine:initial=>'requested
我有一个包含多个键的散列和一个字符串,该字符串不包含散列中的任何键或包含一个键。h={"k1"=>"v1","k2"=>"v2","k3"=>"v3"}s="thisisanexamplestringthatmightoccurwithakeysomewhereinthestringk1(withspecialcharacterslike(^&*$#@!^&&*))"检查s是否包含h中的任何键的最佳方法是什么,如果包含,则返回它包含的键的值?例如,对于上面的h和s的例子,输出应该是v1。编辑:只有字符串是用户定义的。哈希将始终相同。 最佳答案
我需要检查DateTime是否采用有效的ISO8601格式。喜欢:#iso8601?我检查了ruby是否有特定方法,但没有找到。目前我正在使用date.iso8601==date来检查这个。有什么好的方法吗?编辑解释我的环境,并改变问题的范围。因此,我的项目将使用jsapiFullCalendar,这就是我需要iso8601字符串格式的原因。我想知道更好或正确的方法是什么,以正确的格式将日期保存在数据库中,或者让ActiveRecord完成它们的工作并在我需要时间信息时对其进行操作。 最佳答案 我不太明白你的问题。我假设您想检查
我正在尝试编写一个将文件上传到AWS并公开该文件的Ruby脚本。我做了以下事情:s3=Aws::S3::Resource.new(credentials:Aws::Credentials.new(KEY,SECRET),region:'us-west-2')obj=s3.bucket('stg-db').object('key')obj.upload_file(filename)这似乎工作正常,除了该文件不是公开可用的,而且我无法获得它的公共(public)URL。但是当我登录到S3时,我可以正常查看我的文件。为了使其公开可用,我将最后一行更改为obj.upload_file(file