aboutsummaryrefslogtreecommitdiff
path: root/content/posts/2021-12-25-running-golang-application-as-pid1.md
diff options
context:
space:
mode:
authorMitja Felicijan <m@mitjafelicijan.com>2023-07-08 23:25:41 +0200
committerMitja Felicijan <m@mitjafelicijan.com>2023-07-08 23:25:41 +0200
commitcd6644ea4ddc78597934ab0ef5ba50e3c3daa927 (patch)
tree03de331a8db6386dfd6fa75155bfbcea6b4feaf3 /content/posts/2021-12-25-running-golang-application-as-pid1.md
parent84ed124529ffeee1590295b8de3a8faf51848680 (diff)
downloadmitjafelicijan.com-cd6644ea4ddc78597934ab0ef5ba50e3c3daa927.tar.gz
Moved to a simpler SSG
Diffstat (limited to 'content/posts/2021-12-25-running-golang-application-as-pid1.md')
-rw-r--r--content/posts/2021-12-25-running-golang-application-as-pid1.md347
1 files changed, 0 insertions, 347 deletions
diff --git a/content/posts/2021-12-25-running-golang-application-as-pid1.md b/content/posts/2021-12-25-running-golang-application-as-pid1.md
deleted file mode 100644
index 60d0400..0000000
--- a/content/posts/2021-12-25-running-golang-application-as-pid1.md
+++ /dev/null
@@ -1,347 +0,0 @@
1---
2title: Running Golang application as PID 1 with Linux kernel
3url: running-golang-application-as-pid1.html
4date: 2021-12-25T12:00:00+02:00
5draft: false
6---
7
8## Unikernels, kernels, and alike
9
10I have been reading a lot about
11[unikernernels](https://en.wikipedia.org/wiki/Unikernel) lately and found them
12very intriguing. When you push away all the marketing speak and look at the
13idea, it makes a lot of sense.
14
15> A unikernel is a specialized, single address space machine image constructed
16> by using library operating systems. ([Wikipedia](https://en.wikipedia.org/wiki/Unikernel))
17
18I really like the explanation from the article
19[Unikernels: Rise of the Virtual Library Operating System](https://queue.acm.org/detail.cfm?id=2566628).
20Really worth a read.
21
22If we compare a normal operating system to a unikernel side by side, they would
23look something like this.
24
25![Virtual machines vs Containers vs Unikernels](/assets/pid1/unikernels.png)
26
27From this image, we can see how the complexity significantly decreases with
28the use of Unikernels. This comes with a price, of course. Unikernels are hard
29to get running and require a lot of work since you don't have an actual proper
30kernel running in the background providing network access and drivers etc.
31
32So as a half step to make the stack simpler, I started looking into using
33Linux kernel as a base and going from there. I came across this
34[Youtube video talking about Building the Simplest Possible Linux System](https://www.youtube.com/watch?v=Sk9TatW9ino)
35by [Rob Landley](https://landley.net) and apart from statically compiling the
36application to be run as PID1 there was really no other obstacles.
37
38## What is PID 1?
39
40PID 1 is the first process that Linux kernel starts after the boot process.
41It also has a couple of unique properties that are unique to it.
42
43- When the process with PID 1 dies for any reason, all other processes are
44 killed with KILL signal.
45- When any process having children dies for any reason, its children are
46 re-parented to process with PID 1.
47- Many signals which have default action of Term do not have one for PID 1.
48- When the process with PID 1 dies for any reason, kernel panics, which
49 result in system crash.
50
51PID 1 is considered as an Init application which takes care of running other
52and handling services like:
53
54- sshd,
55- nginx,
56- pulseaudio,
57- etc.
58
59If you are on a Linux machine, you can check what your process is with PID 1
60by running the following.
61
62```sh
63$ cat /proc/1/status
64Name: systemd
65Umask: 0000
66State: S (sleeping)
67Tgid: 1
68Ngid: 0
69Pid: 1
70PPid: 0
71...
72```
73
74As we can see on my machine the process with id of 1 is [systemd](https://systemd.io/)
75which is a software suite that provides an array of system components for Linux
76operating systems. If you look closely you can also see that the `PPid`
77(process id of the parent process) is `0` which additionally confirms that
78this process doesn't have a parent.
79
80## So why even run application as PID 1 instead of just using a container?
81
82Containers are wonderful, but they come with a lot of baggage. And because they
83are in their nature layered, the images require quite a lot of space and also a
84lot of additional software to handle them. They are not as lightweight as they
85seem, and many popular images require 500 MB plus disk space.
86
87The idea of running this as PID 1 would result in a significantly smaller footprint,
88as we will see later in the post.
89
90> You could run a simple init system inside Docker container described more
91> in this article [Docker and the PID 1 zombie reaping problem](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/).
92
93## The master plan
94
951. Compile Linux kernel with the default definitions.
962. Prepare a Hello World application in Golang that is statically compiled.
973. Run it with [QEMU](https://www.qemu.org/) and providing Golang application
98 as init application / PID 1.
99
100For the sake of simplicity we will not be cross-compiling any of it and just
101use the 64bit version.
102
103## Compiling Linux kernel
104
105```sh
106$ wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.7.tar.xz
107$ tar xf linux-5.15.7.tar.xz
108
109$ cd linux-5.15.7
110
111$ make clean
112
113# read more about this https://stackoverflow.com/a/41886394
114$ make defconfig
115
116$ time make -j `nproc`
117
118$ cd ..
119```
120
121At this point we have kernel image that is located in `arch/x86_64/boot/bzImage`.
122We will use this in QEMU later.
123
124To make our lives a bit easier lets move the kernel image to another place.
125Lets create a folder `bin/` in the root of our project with `mkdir -p bin`.
126
127
128At this point we can copy `bzImage` to `bin/` folder with
129`cp linux-5.15.7/arch/x86_64/boot/bzImage bin/bzImage`.
130
131The folder structure of this experiment should look like this.
132
133```
134pid1/
135 bin/
136 bzImage
137 linux-5.15.7/
138 linux-5.15.7.tar.xz
139```
140
141## Preparing PID 1 application in Golang
142
143This step is relatively easy. The only thing we must have in mind that we will
144need to compile the binary as a static one.
145
146Let's create `init.go` file in the root of the project.
147
148```go
149package main
150
151import (
152 "fmt"
153 "time"
154)
155
156func main() {
157 for {
158 fmt.Println("Hello from Golang")
159 time.Sleep(1 * time.Second)
160 }
161}
162```
163
164If you notice, we have a forever loop in the main, with a simple sleep of 1
165second to not overwhelm the CPU. This is because PID 1 should never complete
166and/or exit. That would result in a kernel panic. Which is BAD!
167
168There are two ways of compiling Golang application. Statically and dynamically.
169
170To statically compile the binary, use the following command.
171
172```sh
173$ go build -ldflags="-extldflags=-static" init.go
174```
175
176We can also check if the binary is statically compiled with:
177
178```sh
179$ file init
180init: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=Ypu8Zw_4NBxm1Yxg2OYO/H5x721rQ9uTPiDVh-VqP/vZN7kXfGG1zhX_qdHMgH/9vBfmK81tFrygfOXDEOo, not stripped
181
182$ ldd init
183not a dynamic executable
184```
185
186At this point, we need to create [initramfs](https://www.linuxfromscratch.org/blfs/view/svn/postlfs/initramfs.html)
187(abbreviated from "initial RAM file system", is the successor of initrd. It
188is a cpio archive of the initial file system that gets loaded into memory
189during the Linux startup process).
190
191```sh
192$ echo init | cpio -o --format=newc > initramfs
193$ mv initramfs bin/initramfs
194```
195
196The projects at this stage should look like this.
197
198```
199pid1/
200 bin/
201 bzImage
202 initramfs
203 linux-5.15.7/
204 linux-5.15.7.tar.xz
205 init.go
206```
207
208## Running all of it with QEMU
209
210[QEMU](https://www.qemu.org/) is a free and open-source hypervisor. It emulates
211the machine's processor through dynamic binary translation and provides a set
212of different hardware and device models for the machine, enabling it to run a
213variety of guest operating systems.
214
215```sh
216$ qemu-system-x86_64 -serial stdio -kernel bin/bzImage -initrd bin/initramfs -append "console=ttyS0" -m 128
217```
218
219```sh
220$ qemu-system-x86_64 -serial stdio -kernel bin/bzImage -initrd bin/initramfs -append "console=ttyS0" -m 128
221[ 0.000000] Linux version 5.15.7 (m@khan) (gcc (GCC) 11.2.1 20211203 (Red Hat 11.2.1-7), GNU ld version 2.37-10.fc35) #7 SMP Mon Dec 13 10:23:25 CET 2021
222[ 0.000000] Command line: console=ttyS0
223[ 0.000000] x86/fpu: x87 FPU will use FXSAVE
224[ 0.000000] signal: max sigframe size: 1440
225[ 0.000000] BIOS-provided physical RAM map:
226[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
227[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
228[ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
229[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x0000000007fdffff] usable
230[ 0.000000] BIOS-e820: [mem 0x0000000007fe0000-0x0000000007ffffff] reserved
231[ 0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
232[ 0.000000] NX (Execute Disable) protection: active
233[ 0.000000] SMBIOS 2.8 present.
234[ 0.000000] DMI: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-6.fc35 04/01/2014
235[ 0.000000] tsc: Fast TSC calibration failed
236...
237[ 2.016106] ALSA device list:
238[ 2.016329] No soundcards found.
239[ 2.053176] Freeing unused kernel image (initmem) memory: 1368K
240[ 2.056095] Write protecting the kernel read-only data: 20480k
241[ 2.058248] Freeing unused kernel image (text/rodata gap) memory: 2032K
242[ 2.058811] Freeing unused kernel image (rodata/data gap) memory: 500K
243[ 2.059164] Run /init as init process
244Hello from Golang
245[ 2.386879] tsc: Refined TSC clocksource calibration: 3192.032 MHz
246[ 2.387114] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x2e02e31fa14, max_idle_ns: 440795264947 ns
247[ 2.387380] clocksource: Switched to clocksource tsc
248[ 2.587895] input: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input3
249Hello from Golang
250Hello from Golang
251Hello from Golang
252```
253
254The whole [log file here](/assets/pid1/qemu.log).
255
256## Size comparison
257
258The cool thing about this approach is that the Linux kernel and the application
259together only take around 12 MB, which is impressive as hell. And we need to
260also know that the size of bzImage (Linux kernel) could be greatly decreased
261by going into `make menuconfig` and removing a ton of features from the kernel,
262making the size even smaller. I managed to get kernel size down to 2 MB and
263still working properly.
264
265```sh
266total 12M
267-rw-r--r--. 1 m m 9.3M Dec 13 10:24 bzImage
268-rw-r--r--. 1 m m 1.9M Dec 27 01:19 initramfs
269```
270
271## Creating ISO image and running it with Gnome Boxes
272
273First we need to create proper folder structure with `mkdir -p iso/boot/grub`.
274
275Then we need to download the [grub binary](https://github.com/littleosbook/littleosbook/raw/master/files/stage2_eltorito).
276You can read more about this program on https://github.com/littleosbook/littleosbook.
277
278```sh
279$ wget -O iso/boot/grub/stage2_eltorito https://github.com/littleosbook/littleosbook/raw/master/files/stage2_eltorito
280```
281
282```sh
283$ tree iso/boot/
284iso/boot/
285├── bzImage
286├── grub
287│   ├── menu.lst
288│   └── stage2_eltorito
289└── initramfs
290```
291
292Let's copy files into proper folders.
293
294
295```sh
296$ cp stage2_eltorito iso/boot/grub/
297$ cp bin/bzImage iso/boot/
298$ cp bin/initramfs iso/boot/
299```
300
301Lets create a GRUB config file at `nano iso/boot/grub/menu.lst` with contents.
302
303```ini
304default=0
305timeout=5
306
307title GoAsPID1
308kernel /boot/bzImage
309initrd /boot/initramfs
310```
311
312Let's create iso file by using genisoimage:
313
314```sh
315genisoimage -R \
316 -b boot/grub/stage2_eltorito \
317 -no-emul-boot \
318 -boot-load-size 4 \
319 -A os \
320 -input-charset utf8 \
321 -quiet \
322 -boot-info-table \
323 -o GoAsPID1.iso \
324 iso
325```
326
327This will produce `GoAsPID1.iso` which you can use with [Virtualbox](https://www.virtualbox.org/)
328or [Gnome Boxes](https://apps.gnome.org/app/org.gnome.Boxes/).
329
330<video src="/assets/pid1/boxes.mp4" controls></video>
331
332## Is running applications as PID 1 even worth it?
333
334Well, the answer to this is not as simple as one would think. Sometimes it is
335and sometimes it's not. For embedded systems and very specialized applications
336it is worth for sure. But in normal uses, I don't think so. It was an interesting
337exercise in compiling kernels and looking at the guts of the Linux kernel,
338but sticking to containers for most of the things is a better option in my
339opinion.
340
341An interesting experiment would be creating an image that supports networking
342and could be deployed to AWS as an EC2 instance and observing how it fares.
343But in that case, we would need to write some sort of supervisor that would
344run on a separate EC2 that would check if other EC2 instances are running
345properly. Remember that if your application fails, kernel panics and the
346whole machine is inoperable in this case.
347