Linux, Windows, and macOS solve the same fundamental problems.
They isolate applications, schedule execution, manage memory, organize files, control devices, and provide networking. Most application developers use similar high-level operations on all three systems: start a program, open a file, create a thread, or connect to a server.
The important differences are below that surface.
Each system places kernel components differently, represents processes and resources through different abstractions, organizes storage into a different namespace, and exposes a different stable interface to applications.
| Platform | API used for that same request |
|---|---|
| Linux | POSIX API |
| Windows | Win32 API |
| macOS | POSIX and Apple APIs |
Below those APIs sit different kernel interfaces and filesystem models.
This chapter focuses on the differences that affect application behavior, deployment, debugging, and portability.
All three operating systems separate user applications from privileged kernel code.
Each application normally runs in a process with its own virtual address space. The kernel tracks its identity, threads, open resources, permissions, and execution state.
Each system also uses hardware privilege levels and controlled kernel-entry mechanisms:
The concepts from the previous chapters therefore apply to all three systems. The differences lie in the specific contracts and internal organization.
The textbook labels are:
| Operating system | Common classification | Kernel name |
|---|---|---|
| Linux | Monolithic and modular | Linux kernel |
| Windows | Hybrid | Windows NT kernel and executive |
| macOS | Hybrid | XNU |
These labels are useful starting points, not complete descriptions.
Linux places process management, memory management, filesystems, networking, and most device drivers in one privileged kernel address space.
Its subsystems can call one another directly. Loadable kernel modules allow drivers and features to be added at runtime, but module code still executes with kernel privilege.
This arrangement supports efficient communication between kernel components. A severe bug in a kernel driver can also corrupt shared kernel state or crash the whole system.
Windows is based on the Windows NT architecture and is commonly described as hybrid.
Documentation sometimes uses the word kernel narrowly for low-level scheduling, interrupt, and synchronization code. The privileged Windows executive contains higher-level managers for processes, memory, I/O, security, and kernel objects. Together with device drivers and the hardware abstraction layer, these components form what developers often call the Windows kernel.
Windows uses modular layers and user-space services, but substantial operating-system functionality and many drivers run in kernel mode. Calling it hybrid does not mean that its major services receive microkernel-style isolation from one another.
macOS uses the XNU kernel.
XNU combines components from several traditions:
| XNU component | Broad responsibility |
|---|---|
| Mach-derived core | Scheduling, virtual memory, low-level IPC |
| BSD layer | Processes, POSIX interfaces, filesystems, networking |
| I/O Kit | Driver framework |
These components are tightly integrated, and much of their implementation executes in one privileged kernel environment.
The Mach heritage influences XNU's internal abstractions, while the BSD layer provides the Unix process, file, socket, and permission model visible to many applications.
Linux and the two hybrid kernels all keep a significant amount of trusted code in kernel space.
Their practical differences cannot be predicted from the labels monolithic and hybrid alone. A developer needs to know where a specific driver or service runs, which interface it uses, and what happens if it fails.
At a broad level:
| Question | Linux | Windows | macOS |
|---|---|---|---|
| Are major filesystems and networking in kernel space? | Yes | Yes | Yes |
| Do many drivers run in kernel space? | Yes | Yes | Yes |
| Can user-space services provide OS functionality? | Yes | Yes | Yes |
| Can a bad kernel-mode driver crash the system? | Yes | Yes | Yes |
| Can kernel functionality be modular? | Yes | Yes | Yes |
The architectural labels describe lineage and organization. They do not create an absolute reliability or performance ranking.
All three systems distinguish a process from the threads that execute within it.
A process owns or references resources such as an address space, credentials, and open objects. Threads are the units that the scheduler runs on CPU cores.
The most visible difference is how a new program starts.
fork and execThe traditional Unix model separates process creation from program loading.
fork() creates a child process based on the calling process. An exec operation then replaces the current program image with a new executable.
Creation and program loading are two separate steps here, and the gap between them is where a shell adjusts the child before the new program takes over.
Linux also supports posix_spawn() and lower-level creation mechanisms. The later processes module explains how fork, exec, and Linux's clone mechanism relate.
The fork/exec model influences shell pipelines, server designs, inherited file descriptors, and parent-child relationships throughout Unix software.
CreateProcessWindows does not provide general Win32 applications with a direct equivalent of Unix fork.
The primary Win32 operation, CreateProcess, creates a new process and its initial thread while selecting the executable in one operation.
One call produces all four results at once, with no intermediate state in which the child exists but has not yet been given its program.
The caller receives handles for the new process and thread. It can use those handles to wait, inspect status, adjust allowed properties, or terminate the process when permitted.
Handle inheritance is configured explicitly. This differs from the Unix pattern in which a child created by fork initially inherits a copy of the parent's file-descriptor table.
Windows process creation also commonly begins with a command-line string. The target program's runtime parses that string into arguments. Unix exec receives an already separated array of argument strings.
This difference is why cross-platform command quoting can fail even when the visible command appears identical.
macOS presents the familiar Unix process model through its BSD layer.
Applications can use fork, the exec family, signals, process IDs, and POSIX file descriptors. posix_spawn is also heavily used to create and load a new program through one higher-level operation.
Underneath that Unix-facing model, XNU represents execution using Mach concepts such as tasks and threads. A Mach task is the resource container associated with an address space, while threads execute within it.
Most portable command-line programs can work through the POSIX process interface without using Mach APIs directly.
Linux and macOS applications commonly use small integers called file descriptors for files, pipes, sockets, and other stream-like resources.
The integer indexes an entry in the process's descriptor table. The kernel entry refers to the actual open object and its access state.
Windows uses handles for many kernel-managed objects:
A handle is also a process-local reference, but the Windows object model applies it to a broad range of object types, including files, processes, threads, events, and synchronization objects. Handles carry access rights and are closed with APIs such as CloseHandle.
| Resource example | Linux and macOS | Windows |
|---|---|---|
| File | File descriptor | File handle |
| Process | PID, plus other OS interfaces | PID and process handle |
| Thread | Thread identifier and library handle | Thread ID and thread handle |
| Socket | File descriptor | Winsock SOCKET value |
A numeric PID identifies a process, but it is not equivalent to an open reference. On Windows, a process handle keeps a reference to the process object and records the access granted to the caller. Unix systems provide other descriptor- or handle-like mechanisms when stronger lifetime tracking is required.
Linux and macOS expose a single hierarchical file tree rooted at /.
Windows commonly exposes volumes through drive-letter paths and network shares.
| System | Example path | Typical namespace model |
|---|---|---|
| Linux | /var/log/app/server.log | One tree rooted at /; filesystems mounted into it |
| Windows | C:\ProgramData\App\server.log | Drive letters, volume paths, and UNC network paths |
| macOS | /Users/alex/app/server.log | One tree rooted at /; external volumes commonly under /Volumes |
Linux can mount a storage device at any directory:
macOS follows the same Unix-style root model, with its own mount conventions and APFS filesystem features.
Windows supports drive letters such as C: and UNC paths such as:
Internally, Windows has a richer object and volume namespace than drive letters suggest. Drive letters are familiar aliases used by Win32 applications rather than the complete kernel-level model.
Cross-platform code should use its language's path library rather than manually joining path components with / or \.
Path comparison behavior is a common source of deployment bugs.
| System and common default | Case behavior |
|---|---|
| Linux with filesystems such as ext4 | Case-sensitive |
| Windows with NTFS | Case-preserving but usually case-insensitive |
| macOS with default APFS configurations | Case-preserving but usually case-insensitive |
On a typical Linux system, these can be two different files:
On common Windows and macOS installations, those names usually refer to the same directory entry.
These are defaults rather than universal rules. Windows supports case-sensitive directory behavior in some configurations. macOS volumes can be formatted case-sensitive. Linux filesystems can also provide different name-handling features.
A project developed on a case-insensitive laptop can therefore fail after deployment to a case-sensitive Linux server if an import or path uses the wrong capitalization.
Windows also reserves some file names and has path rules that do not exist in the same form on Unix systems. Portable programs should avoid deriving unrestricted file names directly from user input.
Linux and macOS expose Unix identities based on user IDs and group IDs, together with permission bits and optional access-control lists.
Windows represents identities using security identifiers, commonly called SIDs. A process carries an access token describing its identity, groups, and privileges. Windows objects use security descriptors and access-control entries to decide which operations are allowed.
| Platform | Identity carried by the process | What is checked against it |
|---|---|---|
| Linux and macOS | UID, GID, and group membership | Mode bits and ACLs |
| Windows | An access token holding SIDs and privileges | A security descriptor |
The models can express many similar policies, but they are not byte-for-byte equivalents. Copying files between platforms or translating container volume permissions can lose or reinterpret security information.
Unix systems separate a directory name from the underlying open file object.
On Linux and macOS, a process can keep using a file descriptor after another process renames or unlinks that file. Removing the final directory name prevents new opens by that name, but the underlying object remains until its last open reference is closed.
Two separate references point at the same file object. Removing the name does not remove the object, because the descriptor is still holding it.
This behavior supports log rotation and safe replacement patterns.
Windows file opens include sharing modes that declare whether other opens may read, write, or delete the same file. Deletion or renaming can fail when an existing handle was opened without compatible sharing.
Windows can support delete-sharing behavior similar to Unix when applications request the necessary flags. The important difference is that sharing policy is part of opening the handle, so code cannot assume Unix deletion semantics.
Backend software that rotates logs, replaces binaries, or atomically updates configuration must account for this difference.
All three systems have low-level kernel entry mechanisms, but applications do not treat the same layer as the supported public contract.
Linux exposes a stable, architecture-specific system-call ABI.
Applications normally call glibc, musl, or another library, which invokes operations such as openat, read, write, mmap, and clone.
The path runs from the application, through libc or a language runtime, across the Linux system-call ABI, and into the Linux kernel.
System-call numbers differ across processor architectures, but Linux preserves compatibility within an ABI so existing binaries can continue running on newer kernels.
The supported application-facing contract for most Windows software is the Win32 API.
An application may call CreateFileW, CreateProcessW, or VirtualAlloc. User-mode Windows libraries translate those calls through lower-level native interfaces, often involving ntdll.dll, before entering the NT kernel.
The path runs from the application, through the Win32 API, into the system libraries and ntdll, across a native NT system call, and into the Windows kernel.
The low-level syscall numbers are an implementation detail and can differ between Windows releases. Ordinary applications should not hard-code or invoke them directly. The system libraries provide the compatibility layer.
The suffix W identifies the Unicode form of many Win32 functions. Modern Windows code should generally use these Unicode interfaces rather than older narrow-character variants.
macOS applications commonly use POSIX functions from libSystem, Objective-C or Swift system frameworks, and other documented Apple APIs.
XNU supports BSD-style system calls as well as Mach traps for Mach services.
The path runs from the application, through libSystem or an Apple framework, across a BSD syscall or Mach interface, and into XNU.
Directly hard-coding Darwin syscall numbers is not the normal supported interface. Libraries and frameworks isolate applications from many kernel-level implementation details.
This resembles Windows in one important respect: the documented user-space API is a stronger application contract than a private numeric kernel entry.
The interface style affects how programs detect failure.
Linux and macOS POSIX functions commonly return -1 or another documented sentinel and set errno.
Win32 functions use operation-specific failure values. Many then provide detailed error information through GetLastError().
Native NT interfaces use status values, while Winsock has its own error-access function. Cross-platform libraries translate these conventions into exceptions, error objects, or language-specific result types.
Code must follow the contract of the API it actually calls rather than assuming that every operating system reports errors through errno.
The same high-level action takes a different route on each platform:
| Platform | What one file-open request passes through |
|---|---|
| Linux | open(), libc, the openat syscall, the Linux VFS and filesystem |
| Windows | CreateFileW(), Win32 libraries, a native NT request, the I/O manager |
| macOS | open(), libSystem, a BSD syscall, the XNU filesystem layer |
All three kernels eventually validate a path, check access, find a filesystem object, and create a process-visible reference.
The differences are in path syntax, object namespace, permission model, sharing behavior, error convention, and the stable API layer.
This is why a portability wrapper does more than rename functions. It must translate semantics.
The previous chapter used Linux ELF files. Windows and macOS use different binary formats and loaders.
| System | Native executable format | Dynamic-loading environment |
|---|---|---|
| Linux | ELF | ELF interpreter and libraries such as glibc or musl |
| Windows | PE/COFF | Windows loader and DLLs |
| macOS | Mach-O | dyld and dynamic libraries/frameworks |
macOS can package multiple architecture-specific Mach-O images in a universal binary. The loader selects the image matching the current processor.
A binary is therefore not portable merely because all three machines use 64-bit processors. Its instruction set, executable format, calling convention, library ABI, and kernel-facing interface must all match the target platform.
Several differences regularly appear in development and production:
| Situation | Why the operating system matters |
|---|---|
| Deploying mixed-case paths | Linux commonly distinguishes names that Windows and macOS treat as equal |
| Rotating an open log file | Unix unlink behavior differs from Windows sharing-mode behavior |
| Launching child processes | Unix uses argument arrays and often fork/exec; Windows commonly uses CreateProcess and a command-line string |
| Shipping native dependencies | ELF, PE, and Mach-O binaries are not interchangeable |
| Running Linux containers on macOS or Windows | Linux containers require a Linux kernel, normally supplied by a virtual machine |
| Implementing high-performance I/O | Linux, Windows, and macOS expose different native mechanisms such as epoll, I/O completion ports, and kqueue |
| Managing long-running services | Linux commonly uses systemd, Windows uses the Service Control Manager, and macOS uses launchd |
High-level runtimes hide many of these differences, but they cannot erase them.
A Java, Go, Python, or Node.js program eventually reaches platform-specific libraries and kernel interfaces. Native extensions, process control, file semantics, permissions, and performance-sensitive I/O often reveal the underlying operating system.
Linux containers are a particularly useful example. A container packages user-space files and processes, but it shares a kernel. Docker Desktop normally uses a Linux virtual machine on Windows or macOS to provide the Linux syscall interface expected by Linux container images.
Loading simulation...
Avoid treating one operating system as the standard and the others as unusual variations.
Instead, separate the application need from the platform contract:
For portable application code, use mature language or platform libraries that deliberately handle the differences.
For systems debugging, identify the layer that failed. A path problem, dynamic-loader error, API misuse, permission denial, and rejected syscall belong to different layers even if each appears as “the program would not start.”
Linux uses a monolithic, modular kernel. Windows NT and macOS XNU are commonly described as hybrid kernels, although both retain substantial functionality and many drivers in kernel space.
Linux and macOS expose Unix-style processes, file descriptors, and a single filesystem tree. Windows centers its public process model on CreateProcess, uses handles for many kernel objects, and commonly exposes storage through drive letters and UNC paths.
Linux treats its architecture-specific syscall ABI as a stable user-space boundary. Windows applications normally target Win32, while macOS applications target POSIX functions and documented Apple frameworks; their libraries hide lower-level native kernel entries.
The differences that matter most are semantic: process creation, argument passing, case sensitivity, open-file lifetime, permissions, error reporting, binary formats, and the supported API boundary.
The essential mental model is:
The three systems provide similar capabilities, but portability requires translating contracts and behavior, not merely translating function names.
5 quizzes