استمرارًا للمقال السابق على KVM ، ننشر ترجمة جديدة ونفهم كيفية عمل الحاويات باستخدام مثال تشغيل صورة busybox Docker.
هذه المقالة عن الحاويات هي استمرار للمقال السابق على KVM. أود أن أوضح لك بالضبط كيف تعمل الحاويات عن طريق تشغيل صورة Busbox Docker في حاويتنا الصغيرة.
على عكس الآلة الافتراضية ، فإن الحاوية غامضة وغامضة للغاية. ما نسميه عادة حاوية هو حزمة مستقلة من التعليمات البرمجية مع جميع التبعيات المطلوبة التي يمكن شحنها معًا وتشغيلها في بيئة معزولة داخل نظام التشغيل المضيف. إذا كنت تعتقد أن هذا وصف لآلة افتراضية ، فدعنا نتعمق في الموضوع ونرى كيف يتم تنفيذ الحاويات.
BusyBox Docker
سيكون هدفنا الرئيسي هو تشغيل صورة busybox العادية لـ Docker ، ولكن بدون Docker. يستخدم Docker btrfs كنظام ملفات لصوره . دعنا نحاول تنزيل الصورة وفك ضغطها في دليل:
mkdir rootfs
docker export $(docker create busybox) | tar -C rootfs -xvf -
لدينا الآن نظام ملفات صورة busybox مفككًا في مجلد rootfs . بالطبع ، يمكنك تشغيل ./rootfs/bin/sh والحصول على قشرة صالحة للعمل ، ولكن إذا نظرنا إلى قائمة العمليات أو الملفات أو واجهات الشبكة ، يمكننا أن نرى أنه يمكننا الوصول إلى نظام التشغيل بأكمله.
لذلك دعونا نحاول خلق بيئة معزولة.
استنساخ
نظرًا لأننا نريد التحكم في ما يمكن للعملية الفرعية الوصول إليه ، فسنستخدم استنساخ (2) بدلاً من fork (2) . يقوم Clone بنفس الشيء تقريبًا ، ولكنه يسمح بتمرير العلامات ، مما يشير إلى الموارد التي تريد مشاركتها (مع المضيف).
يُسمح باستخدام العلامات التالية:
- CLONE_NEWNET - أجهزة الشبكة المعزولة
- CLONE_NEWUTS - اسم المضيف والمجال (نظام مشاركة الوقت UNIX)
- CLONE_NEWIPC - كائنات IPC
- CLONE_NEWPID - معرّفات العملية (PID)
- CLONE_NEWNS - نقاط التحميل (أنظمة الملفات)
- CLONE_NEWUSER - المستخدمون والمجموعات.
في تجربتنا ، سنحاول عزل العمليات ، IPC ، الشبكات وأنظمة الملفات. دعنا نبدأ:
static char child_stack[1024 * 1024];
int child_main(void *arg) {
printf("Hello from child! PID=%d\n", getpid());
return 0;
}
int main(int argc, char *argv[]) {
int flags =
CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNET;
int pid = clone(child_main, child_stack + sizeof(child_stack),
flags | SIGCHLD, argv + 1);
if (pid < 0) {
fprintf(stderr, "clone failed: %d\n", errno);
return 1;
}
waitpid(pid, NULL, 0);
return 0;
}
يجب تشغيل الكود بامتيازات المستخدم المتميز ، وإلا سيفشل الاستنساخ.
تعطي التجربة نتيجة مثيرة للاهتمام: الطفل PID هو 1. نحن نعلم جيدًا أن عملية البادئة عادة ما تحتوي على PID 1. ولكن في هذه الحالة ، تحصل العملية الفرعية على قائمة العمليات المنعزلة الخاصة بها ، حيث أصبحت العملية الأولى.
قذيفة العمل
لتسهيل تعلم بيئة جديدة ، دعنا نبدأ الصدفة في العملية التابعة. لنقم بتشغيل أوامر عشوائية مثل Docker run :
int child_main(void *arg) {
char **argv = (char **)arg;
execvp(argv[0], argv);
return 0;
}
يؤدي تشغيل تطبيقنا الآن باستخدام الوسيطة / bin / sh إلى فتح غلاف حقيقي يمكننا من خلاله إدخال الأوامر. تثبت هذه النتيجة مدى خطأنا عندما تحدثنا عن العزلة:
# echo $$
1
# ps
PID TTY TIME CMD
5998 pts/31 00:00:00 sudo
5999 pts/31 00:00:00 main
6001 pts/31 00:00:00 sh
6004 pts/31 00:00:00 ps
كما نرى ، فإن عملية shell نفسها لها معرف PID بقيمة 1 ، ولكن في الواقع ، يمكنها رؤية جميع العمليات الأخرى لنظام التشغيل الرئيسي والوصول إليها. والسبب هو أن قائمة العمليات تُقرأ من procfs ، والتي لا تزال موروثة.
لذلك ، قم بإلغاء تحميل procfs :
umount2("/proc", MNT_DETACH);
الآن تنكسر أوامر ps و mount والأوامر الأخرى عند بدء تشغيل الغلاف لأن procfs غير مثبت. ومع ذلك ، هذا لا يزال أفضل من تسرب procfs الوالدين.
كروت
عادةً ما يتم استخدام chroot لإنشاء الدليل الجذر ، لكننا سنستخدم البديل pivot_root . يقوم استدعاء النظام هذا بنقل جذر النظام الحالي إلى دليل فرعي وتعيين دليل مختلف للجذر:
int child_main(void *arg) {
/* Unmount procfs */
umount2("/proc", MNT_DETACH);
/* Pivot root */
mount("./rootfs", "./rootfs", "bind", MS_BIND | MS_REC, "");
mkdir("./rootfs/oldrootfs", 0755);
syscall(SYS_pivot_root, "./rootfs", "./rootfs/oldrootfs");
chdir("/");
umount2("/oldrootfs", MNT_DETACH);
rmdir("/oldrootfs");
/* Re-mount procfs */
mount("proc", "/proc", "proc", 0, NULL);
/* Run the process */
char **argv = (char **)arg;
execvp(argv[0], argv);
return 0;
}
من المنطقي تحميل tmpfs إلى / tmp و sysfs إلى / sys وإنشاء نظام ملفات صالح / dev ، لكنني سأتخطى هذه الخطوة للإيجاز.
الآن نرى فقط الملفات من صورة busybox ، كما لو كنا نستخدم chroot :
/ # ls
bin dev etc home proc root sys tmp usr var
/ # mount
/dev/sda2 on / type ext4 (rw,relatime,data=ordered)
proc on /proc type proc (rw,relatime)
/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh
4 root 0:00 ps
/ # ps ax
PID USER TIME COMMAND
1 root 0:00 /bin/sh
5 root 0:00 ps ax
في الوقت الحالي ، تبدو الحاوية معزولة تمامًا ، وربما أكثر من اللازم. لا يمكننا اختبار اتصال أي شيء ويبدو أن الشبكة لا تعمل على الإطلاق.
شبكة الاتصال
كان إنشاء مساحة اسم شبكة جديدة مجرد البداية! تحتاج إلى تعيين واجهات الشبكة وتكوينها لإعادة توجيه الحزم بشكل صحيح.
إذا لم يكن لديك واجهة br0 ، فستحتاج إلى إنشائها يدويًا (brctl جزء من حزمة bridge-utils في Ubuntu):
brctl addbr br0
ip addr add dev br0 172.16.0.100/24
ip link set br0 up
sudo iptables -A FORWARD -i wlp3s0 -o br0 -j ACCEPT
sudo iptables -A FORWARD -o wlp3s0 -i br0 -j ACCEPT
sudo iptables -t nat -A POSTROUTING -s 172.16.0.0/16 -j MASQUERADE
في حالتي ، كانت wlp3s0 هي الواجهة الرئيسية لشبكة WiFi وكانت 172.16.xx هي شبكة الحاوية.
يحتاج مشغل الحاوية الخاص بنا إلى إنشاء زوج من الواجهات ، veth0 و veth1 ، وربطهما بـ br0 ، وإعداد التوجيه داخل الحاوية.
في الدالة main () ، سنقوم بتشغيل هذه الأوامر قبل الاستنساخ:
system("ip link add veth0 type veth peer name veth1");
system("ip link set veth0 up");
system("brctl addif br0 veth0");
عند اكتمال استدعاء استنساخ () ، سنضيف veth1 إلى مساحة الاسم الفرعية الجديدة:
char ip_link_set[4096];
snprintf(ip_link_set, sizeof(ip_link_set) - 1, "ip link set veth1 netns %d",
pid);
system(ip_link_set);
الآن إذا قمنا بتشغيل رابط ip في حاوية حاوية ، فسنرى واجهة loopback وبعض واجهة veth1 @ xxxx. لكن الشبكة ما زالت لا تعمل. لنقم بتعيين اسم مضيف فريد في الحاوية وتهيئة المسارات:
int child_main(void *arg) {
....
sethostname("example", 7);
system("ip link set veth1 up");
char ip_addr_add[4096];
snprintf(ip_addr_add, sizeof(ip_addr_add),
"ip addr add 172.16.0.101/24 dev veth1");
system(ip_addr_add);
system("route add default gw 172.16.0.100 veth1");
char **argv = (char **)arg;
execvp(argv[0], argv);
return 0;
}
دعونا نرى كيف يبدو:
/ # ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
47: veth1@if48: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
link/ether 72:0a:f0:91:d5:11 brd ff:ff:ff:ff:ff:ff
/ # hostname
example
/ # ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: seq=0 ttl=57 time=27.161 ms
64 bytes from 1.1.1.1: seq=1 ttl=57 time=26.048 ms
64 bytes from 1.1.1.1: seq=2 ttl=57 time=26.980 ms
...
يعمل!
خاتمة
كود المصدر الكامل متاح هنا . إذا وجدت خطأ أو كان لديك أي اقتراح ، فالرجاء ترك تعليق!
بالطبع ، يمكن لـ Docker فعل المزيد! لكن من المدهش عدد واجهات برمجة التطبيقات المناسبة التي تمتلكها نواة Linux ومدى سهولة استخدامها لتحقيق المحاكاة الافتراضية على مستوى نظام التشغيل.
ارجو ان تكون قد استمتعت بالمقال. يمكنك العثور على مشاريع المؤلف على Github ومتابعة Twitter لمتابعة الأخبار ، وكذلك عبر RSS .