aboutsummaryrefslogtreecommitdiff
path: root/posts/2021-12-25-running-golang-application-as-pid1.md
diff options
context:
space:
mode:
Diffstat (limited to 'posts/2021-12-25-running-golang-application-as-pid1.md')
-rw-r--r--posts/2021-12-25-running-golang-application-as-pid1.md229
1 files changed, 229 insertions, 0 deletions
diff --git a/posts/2021-12-25-running-golang-application-as-pid1.md b/posts/2021-12-25-running-golang-application-as-pid1.md
new file mode 100644
index 0000000..1eef97b
--- /dev/null
+++ b/posts/2021-12-25-running-golang-application-as-pid1.md
@@ -0,0 +1,229 @@
1---
2Title: Running Golang application as PID 1 with Linux kernel
3Description: Running Golang application as PID 1 with Linux kernel
4Slug: running-golang-application-as-pid1
5Listing: true
6Created: 2021-12-25
7Tags: []
8---
9
10
11
12I have been reading a lot about [unikernernels](https://en.wikipedia.org/wiki/Unikernel) lately and found them very intriguing. When you push away all the marketing speak and look at the idea, it makes a lot of sense.
13
14> A unikernel is a specialized, single address space machine image constructed by using library operating systems. ([Wikipedia](https://en.wikipedia.org/wiki/Unikernel))
15
16I really like the explanation from the article [Unikernels: Rise of the Virtual Library Operating System](https://queue.acm.org/detail.cfm?id=2566628). Really worth a read.
17
18If we compare a normal operating system to a unikernel side by side, they would look something like this.
19
20![Virtual machines vs Containers vs Unikernels](/assets/pid1/unikernels.png)
21
22From this image, we can see how the complexity significantly decreases with the use of Unikernels. This comes with a price, of course. Unikernels are hard to get running and require a lot of work since you don't have an actual proper kernel running in the background providing network access and drivers etc.
23
24So as a half step to make the stack simpler, I started looking into using Linux kernel as a base and going from there. I came across this [Youtube video talking about Building the Simplest Possible Linux System](https://www.youtube.com/watch?v=Sk9TatW9ino) by [Rob Landley](https://landley.net) and apart from statically compiling the application to be run as PID1 there was really no other obstacles.
25
26## What is PID 1?
27
28PID 1 is the first process that Linux kernel starts after the boot process. It also has a couple of unique properties that are unique to it.
29
30- When the process with PID 1 dies for any reason, all other processes are killed with KILL signal.
31- When any process having children dies for any reason, its children are re-parented to process with PID 1.
32- Many signals which have default action of Term do not have one for PID 1.
33- When the process with PID 1 dies for any reason, kernel panics, which result in system crash.
34
35PID 1 is considered as an Init application which takes care of running other and handling services like:
36
37- sshd,
38- nginx,
39- pulseaudio,
40- etc.
41
42If you are on a Linux machine, you can check what your process is with PID 1 by running the following.
43
44```sh
45$ cat /proc/1/status
46Name: systemd
47Umask: 0000
48State: S (sleeping)
49Tgid: 1
50Ngid: 0
51Pid: 1
52PPid: 0
53...
54```
55
56As we can see on my machine the process with id of 1 is [systemd](https://systemd.io/) which is a software suite that provides an array of system components for Linux operating systems. If you look closely you can also see that the `PPid` (process id of the parent process) is `0` which additionally confirms that this process doesn't have a parent.
57
58## So why even run application as PID 1 instead of just using a container?
59
60Containers are wonderful, but they come with a lot of baggage. And because they are in their nature layered, the images require quite a lot of space and also a lot of additional software to handle them. They are not as lightweight as they seem, and many popular images require 500 MB plus disk space.
61
62The idea of running this as PID 1 would result in a significantly smaller footprint, as we will see later in the post.
63
64> You could run a simple init system inside Docker container described more 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/).
65
66## The plan
67
681. Compile Linux kernel with the default definitions.
692. Prepare a Hello World application in Golang that is statically compiled.
703. Run it with [QEMU](https://www.qemu.org/) and providing Golang application as init application / PID 1.
71
72For the sake of simplicity we will not be cross-compiling any of it and just use the 64bit version.
73
74## Compiling Linux kernel
75
76```sh
77wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.7.tar.xz
78tar xf linux-5.15.7.tar.xz
79
80cd linux-5.15.7
81
82make clean
83
84# read more about this https://stackoverflow.com/a/41886394
85make defconfig
86
87time make -j `nproc`
88
89cd ..
90```
91
92At this point we have kernel image that is located in `arch/x86_64/boot/bzImage`. We will use this this in QEMU later.
93
94To make our lives a bit easier lets move the kernel image to another place. Lets create a folder `bin/` in the root of our project with `mkdir -p bin`.
95
96
97At this point we can copy `bzImage` to `bin/` folder with `cp linux-5.15.7/arch/x86_64/boot/bzImage bin/bzImage`.
98
99The folder structure of this experiment should look like this.
100
101```
102pid1/
103 bin/
104 bzImage
105 linux-5.15.7/
106 linux-5.15.7.tar.xz
107```
108
109## Preparing PID 1 application in Golang
110
111This step is relatively easy. The only thing we must have in mind that we will need to compile the binary as a static one.
112
113Let's create `init.go` file in the root of the project.
114
115```go
116package main
117
118import (
119 "fmt"
120 "time"
121)
122
123func main() {
124 for {
125 fmt.Println("Hello from Golang")
126 time.Sleep(1 * time.Second)
127 }
128}
129```
130
131If you notice, we have a forever loop in the main, with a simple sleep of 1 second to not overwhelm the CPU.
132
133There are two ways of compiling Golang application. Statically and dynamically.
134
135To statically compile the binary, use the following command.
136
137```sh
138go build -ldflags="-extldflags=-static" init.go
139```
140
141We can also check if the binary is statically compiled with:
142
143```sh
144$ file init
145init: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=Ypu8Zw_4NBxm1Yxg2OYO/H5x721rQ9uTPiDVh-VqP/vZN7kXfGG1zhX_qdHMgH/9vBfmK81tFrygfOXDEOo, not stripped
146
147$ ldd init
148not a dynamic executable
149```
150
151At this point, we need to create [initramfs](https://www.linuxfromscratch.org/blfs/view/svn/postlfs/initramfs.html) (abbreviated from "initial RAM file system", is the successor of initrd. It is a cpio archive of the initial file system that gets loaded into memory during the Linux startup process).
152
153```sh
154echo init | cpio -o --format=newc > initramfs
155mv initramfs bin/initramfs
156```
157
158The projects at this stage should look like this.
159
160```
161pid1/
162 bin/
163 bzImage
164 initramfs
165 linux-5.15.7/
166 linux-5.15.7.tar.xz
167 init.go
168```
169
170## Running all of it with QEMU
171
172[QEMU](https://www.qemu.org/) is a free and open-source hypervisor. It emulates the machine's processor through dynamic binary translation and provides a set of different hardware and device models for the machine, enabling it to run a variety of guest operating systems.
173
174```sh
175qemu-system-x86_64 -serial stdio -kernel bin/bzImage -initrd bin/initramfs -append "console=ttyS0" -m 128
176```
177
178```sh
179$ qemu-system-x86_64 -serial stdio -kernel bin/bzImage -initrd bin/initramfs -append "console=ttyS0" -m 128
180[ 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
181[ 0.000000] Command line: console=ttyS0
182[ 0.000000] x86/fpu: x87 FPU will use FXSAVE
183[ 0.000000] signal: max sigframe size: 1440
184[ 0.000000] BIOS-provided physical RAM map:
185[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
186[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
187[ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
188[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x0000000007fdffff] usable
189[ 0.000000] BIOS-e820: [mem 0x0000000007fe0000-0x0000000007ffffff] reserved
190[ 0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
191[ 0.000000] NX (Execute Disable) protection: active
192[ 0.000000] SMBIOS 2.8 present.
193[ 0.000000] DMI: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-6.fc35 04/01/2014
194[ 0.000000] tsc: Fast TSC calibration failed
195...
196[ 2.016106] ALSA device list:
197[ 2.016329] No soundcards found.
198[ 2.053176] Freeing unused kernel image (initmem) memory: 1368K
199[ 2.056095] Write protecting the kernel read-only data: 20480k
200[ 2.058248] Freeing unused kernel image (text/rodata gap) memory: 2032K
201[ 2.058811] Freeing unused kernel image (rodata/data gap) memory: 500K
202[ 2.059164] Run /init as init process
203Hello from Golang
204[ 2.386879] tsc: Refined TSC clocksource calibration: 3192.032 MHz
205[ 2.387114] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x2e02e31fa14, max_idle_ns: 440795264947 ns
206[ 2.387380] clocksource: Switched to clocksource tsc
207[ 2.587895] input: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input3
208Hello from Golang
209Hello from Golang
210Hello from Golang
211```
212
213The whole [log file here](/assets/pid1/qemu.log).
214
215## Size comparison
216
217The cool thing about this approach is that the Linux kernel and the application together only take around 12 MB, which is impressive as hell. And we need to also know that the size of bzImage (Linux kernel) could be greatly decreased by going into `make menuconfig` and removing a ton of features from the kernel, making the size even smaller. I managed to get kernel size down to 2 MB and still working properly.
218
219```sh
220total 12M
221-rw-r--r--. 1 m m 9.3M Dec 13 10:24 bzImage
222-rw-r--r--. 1 m m 1.9M Dec 27 01:19 initramfs
223```
224
225## Is running applications as PID 1 even worth it?
226
227Well, the answer to this is not as simple as one would think. Sometimes it is and sometimes it's not. For embedded systems and very specialized applications it is worth for sure. But in normal uses, I don't think so. It was an interesting exercise in compiling kernels and looking at the guts of the Linux kernel, but sticking to containers for most of the things is a better option in my opinion.
228
229An interesting experiment would be creating an image that supports networking and could be deployed to AWS as an EC2 instance and observing how it fares. But in that case, we would need to write some sort of supervisor that would run on a separate EC2 that would check if other EC2 instances are running properly. Remember that if your application fails, kernel panics and the whole machine is inoperable in this case.