To you, an app is one thing: an icon you click, a window you use, something you install and delete. To the computer, it's three separate things that only loosely belong together:
- files on disk: the program, the libraries it uses, its images, translations and icons;
- processes in memory: one or more running instances of programs, each with its own address space;
- data files somewhere else: your settings, your documents, caches, saved state.
Deleting the app's icon removes some of the first; quitting it ends the second; the third usually stays behind. This chapter looks at each in turn, on the Mac this chapter was written on.
An app on disk: the bundle
On macOS, an app is a bundle: a directory whose name ends in .app, which Finder shows as a single icon. Apple's Calculator, opened up with ls:
/System/Applications/Calculator.app/Contents
├── Info.plist ← the app's identity card
├── MacOS/Calculator ← the executable
├── PlugIns/CalculatorWidget.appex ← a widget: a second, separate program
├── Resources/ ← icons, compiled images, translations
│ ├── AppIcon.icns
│ ├── Assets.car
│ ├── Localizable.loctable
│ └── ar.lproj, ca.lproj, … fr.lproj, … (46 entries in all)
├── _CodeSignature/CodeResources ← hashes of every file, for the signature
└── version.plist
The whole bundle is 3.4 MB, and nearly all of it is the executable. Info.plist is a small structured file that tells the system how to treat the app:
$ plutil -p Calculator.app/Contents/Info.plist
"CFBundleExecutable" => "Calculator"
"CFBundleIdentifier" => "com.apple.calculator"
"CFBundleShortVersionString" => "12.0"
"LSMinimumSystemVersion" => "26.6"
…
The bundle identifier, com.apple.calculator, is the app's real name for the system. It's what settings, permissions and sandbox containers are keyed on: rename Calculator.app and it's still the same app.
The executable is a Mach-O file, the format described in the executable files chapter, and it's a universal binary: two complete programs in one file, for Intel (x86_64) and for Apple silicon (arm64e). codesign shows that the bundle is signed and that the signature covers both:
$ codesign -dv /System/Applications/Calculator.app
Identifier=com.apple.calculator
Format=app bundle with Mach-O universal (x86_64 arm64e)
To the operating system, a file is just a sequence of bytes, with any further structure left to applications, unlike the record-structured files of traditional mainframes. The byte stream won everywhere outside mainframes, and a bundle shows how structure gets added on top: it's an ordinary directory of ordinary files, and "this directory is an app" is a convention that Finder and the rest of the system agree on.
The code an app doesn't ship
Most of what Calculator runs isn't in its bundle. otool -L lists 44 libraries and frameworks it depends on: AppKit for windows and menus, SwiftUI for its interface, Foundation, CoreGraphics, the Swift runtime and more. None of them is in the bundle; they're part of the operating system, prelinked together in the dyld shared cache, as the app launch chapter showed.
Other systems organize the same pieces differently:
| Executable | Libraries | Resources | |
|---|---|---|---|
| macOS | inside the .app bundle | system frameworks, or embedded in the bundle's Frameworks/ | in the bundle's Resources/ |
| Windows | C:\Program Files\<App>\<app>.exe | system DLLs, plus the app's own DLLs next to the .exe | next to the .exe, or embedded in it as PE resources |
| Linux (packages) | /usr/bin | /usr/lib, each library in its own package | /usr/share/<app>, /usr/share/doc, /usr/share/man |
Linux distributions spread an application's files by kind instead of grouping them by app. Debian's curl package, listed with dpkg -L curl, puts its program in /usr/bin, its manual page in /usr/share/man/man1, its documentation in /usr/share/doc/curl and its shell completions in /usr/share/zsh; the actual networking code is in a separate library package, libcurl, shared with every other program that uses it. The package manager keeps track of which file belongs to which package. Formats like Flatpak and Snap, and container images, bring back the bundle idea on Linux: one self-contained directory per app.
An app running: processes
A process is a program running, with its own address space, its own saved registers and its own open files, as the processes chapter explains. The distinction matters because the mapping between apps and processes is not one-to-one:
- The same program can run as many processes at once: two terminal windows running
zshare two processes executing the same file. - One app is often several processes.
pson this Mac showed one Chrome instance running as eight: the main browser process, a GPU process, a network service, a storage service and four renderer processes, one per group of pages.
PID PPID role
7470 1 browser (main)
7488 7470 --type=gpu-process
7491 7470 --type=utility --utility-sub-type=network.mojom.NetworkService
7494 7470 --type=utility --utility-sub-type=storage.mojom.StorageService
84402 7470 --type=renderer
84403 7470 --type=renderer
84404 7470 --type=renderer
84405 7470 --type=renderer
Why split an app into processes? Because the process is the operating system's unit of isolation. A crash in a renderer kills one tab, not the browser. A renderer that processes untrusted web pages can be sandboxed: denied access to files, the network and most system calls, so that a bug exploited by a malicious page gains very little. The GPU driver, which crashes more often than anyone would like, is kept out of the main process. The price is memory (each process has its own copies of some data) and communication, which has to go through the kernel.
macOS apps do the same on a smaller scale. Calculator's widget is a separate program in its own process, and many apps delegate work to small XPC services in their bundle. Across the whole system, ps -A counted 1,246 processes while this chapter was written: about 950 belonged to the logged-in user, 188 to root, and about 110 to system accounts such as _windowserver, the owner of the window server process. Most users have no idea they're running a thousand programs.
Files as a process sees them
A running process doesn't see file names most of the time. It asks the kernel to open a file by name; the kernel checks permissions, finds the file, and returns a small integer, a file descriptor, that the process uses for every later read and write. Three are already open when every UNIX process starts: 0 (standard input), 1 (standard output) and 2 (standard error). The classic description of file I/O (open, then read with a file indication, a buffer and a byte count, then close) is exactly this interface, unchanged.
Try it: open a few files, close one, and open another. Each open gets the lowest free number, so a closed descriptor is reused right away, which is why, in a program that has opened nothing yet, open returns 3.
Try it: Click a file on the left to open it: it gets the lowest free number. Close one with ×, then open another and see which number it gets.
- 0keyboard
- 1screen
- 2screen (errors)
The table lives in the kernel, not in the process: the process only ever holds the numbers, which is how the kernel can check every read and write. How the kernel turns a name into blocks on a disk is the subject of the files and I/O chapter.
Where settings and data live
An app's bundle is read-only. On macOS, Apple's apps sit on a sealed system volume that can't be modified at all. So everything the app writes goes somewhere else, in places set by convention:
| Settings | App data | Caches | |
|---|---|---|---|
| macOS | ~/Library/Preferences/<bundle id>.plist | ~/Library/Application Support/<App>/ | ~/Library/Caches/<bundle id>/ |
| macOS, sandboxed apps | the same paths inside ~/Library/Containers/<bundle id>/Data/ | ||
| Windows | the registry (HKEY_CURRENT_USER\Software\…) and %APPDATA% | %APPDATA% and %LOCALAPPDATA% | %LOCALAPPDATA% |
| Linux | ~/.config/<app>/ | ~/.local/share/<app>/ | ~/.cache/<app>/ |
Calculator is sandboxed, so the system gave it a private container: a whole fake home directory, ~/Library/Containers/com.apple.calculator/Data/, with links to the real Desktop, Downloads and so on that it may use only with permission. Its settings file is inside:
$ plutil -p ~/Library/Containers/com.apple.calculator/Data/Library/Preferences/com.apple.calculator.plist
{
"LastResultValue" => …
"NSWindow Frame main" => …
"RestoreInputValue" => …
"TrigonometricModeKey" => true
}
That's why Calculator reopens where you left it, with the last result and in the same mode: it wrote its window position and state into a property list when you quit, and read it back at launch. On this Mac there are 712 such containers, one per sandboxed app, helper and extension.
This separation explains a familiar annoyance. Dragging an app to the Trash deletes the bundle (the files of the first kind) but leaves its settings, caches and containers behind; reinstall it, and your old settings come back. Package managers on Linux have the same split: removing a package deletes its files under /usr, while your own ~/.config stays untouched.
Takeaways
- An app is files (the program, its resources and the libraries it uses), one or more processes (the program running), and data files (settings, documents, caches) stored elsewhere.
- On macOS, an app is a bundle directory:
Info.plist, the executable (here a universal binary), resources and a code signature. The bundle identifier is its real name. - Most of an app's code is shared system libraries: Calculator names 44. Linux packages spread files by kind; macOS bundles group them by app.
- One app can be many processes (one Chrome instance was 8) because the process is the unit of isolation and sandboxing. This Mac ran 1,246 processes.
- Processes use files through file descriptors: 0, 1 and 2 are open from the start, and
openreturns the next free one. - Settings live in per-user places (
~/Libraryon macOS, the registry and%APPDATA%on Windows,~/.configon Linux), which is why deleting an app usually leaves its data behind.