Operating System
15 Core Concepts — The backbone of every systems interview. Know these, own the room.
Process vs Thread
Easy 3 minWhen you double-click Chrome, the OS creates a process. It gets its own memory space, file handles, security context — completely isolated from other processes. If Chrome crashes, your Word document stays safe. That's isolation.
Now inside Chrome, there are many threads — one for UI, one for network, one for each tab's JavaScript. They all share Chrome's memory, so they can easily talk to each other (just read/write shared variables). But they each have their own stack and registers, so they can run independently.
The key insight: OS allocates resources to processes, but schedules threads. A process with 4 threads can use 4 CPU cores simultaneously. A single-threaded process wastes 3 cores. That's why multi-threading matters for performance.
Creating a process is expensive — OS must copy or allocate entire address space, file descriptors, etc. Creating a thread is cheap — just allocate a new stack and register set. Thread switching within the same process is also faster than process switching (no need to change page tables).
Threads share memory within a process. Processes are isolated from each other.
Key Points for Interview
- Process: Own address space, heavier, isolated, crash-safe
- Thread: Shared address space, lighter, faster communication, shared fate (one crashes = all die)
- Thread creation: ~microseconds. Process creation: ~milliseconds (10-100x slower)
- Each thread has: own stack, own registers, own program counter. Shares: code, data, heap, files
- Inter-process communication (IPC) needs pipes, sockets, shared memory (slow). Inter-thread = just variables (fast, but needs sync)
Common Trap
"Threads share everything" — Wrong! Each thread has its own stack and registers. They share heap, code, and data segments. Getting this wrong shows you haven't thought deeply about it.
Competitive Edge
Mention PCB (Process Control Block) vs TCB (Thread Control Block). PCB stores: process state, program counter, registers, memory info, open files, scheduling info. TCB is smaller: just thread state, stack pointer, registers. Also know that modern Linux treats threads as "lightweight processes" (LWP) — uses same task_struct, just with shared memory mappings.
Follow-up Questions
Why does Chrome use multiple processes instead of threads?
Can one thread access another thread's stack?
What's the difference between user-level and kernel-level threads?
Context Switching
Medium 3 minImagine a chef cooking 5 dishes simultaneously. Each time they switch from biryani to pasta, they must: remember where they left off with biryani, put away those spices, pull out pasta ingredients, recall what step they were on. That mental load is context switching.
When a context switch happens, the CPU must: save program counter (where were we?), save all registers, save stack pointer, save memory mappings pointer, update PCB. Then load all these values for the next process. This takes 1-10 microseconds typically — sounds small, but if you're switching thousands of times per second, it adds up.
Thread context switch is faster than process context switch because threads share the same address space — no need to flush TLB (translation lookaside buffer) or switch page tables. That's why servers often use threads instead of processes.
Context switch: Save A's state to PCB, load B's state from PCB. The switch time is pure overhead.
Key Points for Interview
- Triggers: time slice expired, I/O wait, interrupt, higher-priority process arrived
- Thread switch (same process): ~1μs. Process switch: ~10μs (TLB flush, page table switch)
- Saved to PCB: PC, registers, stack pointer, memory maps, process state, scheduling info
- Context switch is pure overhead — no useful work done. Too many switches = thrashing
- Voluntary switch: process yields (I/O wait). Involuntary: preempted by scheduler
Common Trap
Don't say "context switch is slow because saving registers is slow." Saving a few dozen registers is trivial. The real cost is cache pollution — the new process has different working data, so CPU caches are full of useless stuff. Cache misses are the real killer.
Follow-up Questions
How can we reduce context switch overhead?
What triggers an involuntary context switch?
Multithreading
Medium 3 minSingle-threaded program is like a restaurant with one chef. Multithreaded is like having 4 chefs in the same kitchen. They can cook multiple dishes in parallel, share ingredients, but need to coordinate — or they'll grab the same knife at the same time.
Why use multithreading? (1) Utilize multiple CPU cores — a 4-core CPU with single-threaded code wastes 75% of its power. (2) Keep UI responsive — heavy computation in background thread while UI thread stays smooth. (3) Handle I/O efficiently — while one thread waits for disk, another can do CPU work.
The catch: shared memory makes communication easy but creates problems. Two threads modifying the same variable? Race condition. Two threads waiting for each other's locks? Deadlock. Multithreading is powerful but tricky to get right.
Key Points for Interview
- Concurrency ≠ Parallelism. Concurrency = dealing with multiple tasks (can be on 1 core via time-slicing). Parallelism = truly simultaneous execution (needs multiple cores).
- Benefits: better CPU utilization, responsive UI, efficient I/O handling
- Challenges: race conditions, deadlocks, debugging complexity, thread safety
- Thread-safe code: works correctly when called from multiple threads simultaneously
- Synchronization tools: mutex, semaphore, condition variables, atomics, lock-free structures
| Threading Model | Description | Example |
|---|---|---|
| Many-to-One | Many user threads → one kernel thread. Can't use multiple cores. | Green threads (old Java) |
| One-to-One | Each user thread = one kernel thread. True parallelism. | Linux pthreads, Windows threads |
| Many-to-Many | Many user threads → many kernel threads. Best of both. | Go goroutines, Windows fiber |
Competitive Edge
Know Amdahl's Law: Speedup from parallelization is limited by the sequential portion of code. If 10% of your code must be sequential, max speedup is 10x no matter how many cores you add. This is why pure parallelism doesn't scale infinitely. Mention this and watch the interviewer's eyebrows rise.
Follow-up Questions
When should you use processes instead of threads?
What's the difference between I/O-bound and CPU-bound tasks?
Scheduling Algorithms
Hard 4 minFCFS (First Come First Serve): Simple queue — whoever comes first, runs first. Problem: convoy effect. One long job makes everyone behind wait. Average waiting time can be terrible.
SJF (Shortest Job First): Pick the process with shortest burst time. Mathematically optimal for average waiting time. Problem: starvation of long processes, and how do you even know the burst time in advance? (Answer: predict using exponential averaging of past bursts.)
Round Robin: Each process gets a fixed time slice (quantum), then goes to back of queue. Fairest algorithm — everyone gets a turn. Tuning the quantum is key: too small = too many context switches, too large = behaves like FCFS.
Priority Scheduling: Higher priority runs first. Problem: low-priority processes starve. Solution: aging — increase priority of waiting processes over time. Can be preemptive or non-preemptive.
MLFQ (Multi-Level Feedback Queue): Multiple queues with different priorities and time quanta. New processes start at top (high priority, small quantum). If they use full quantum, they're CPU-bound → demote to lower queue. Interactive processes (short bursts) stay at top. Adapts to process behavior. Most real OS use variants of this.
Round Robin: each process gets 4 time units, then goes to back of queue. Fair but more switches.
Key Points for Interview
- Preemptive = OS can interrupt running process. Non-preemptive = process runs until it yields
- SJF is optimal for avg waiting time but impractical (can't predict burst). SRTF is preemptive SJF.
- Round Robin quantum: 10-100ms typical. Trade-off: responsiveness vs overhead
- Starvation in Priority → fix with Aging
- Linux uses CFS (Completely Fair Scheduler) — red-black tree of processes by virtual runtime, not queues
Common Trap
"FCFS is preemptive" — No! FCFS is always non-preemptive. Also, "SJF causes starvation" is true, but interviewers sometimes ask "what algorithm causes convoy effect?" That's FCFS, not SJF. Know which problem belongs to which algorithm.
Competitive Edge
Mention Belady's Anomaly in context of page replacement (FIFO can have MORE page faults with MORE frames). Also know that real-time systems use different schedulers: Rate Monotonic (static priority based on period) and EDF (Earliest Deadline First). Shows you understand scheduling beyond textbook basics.
Follow-up Questions
How do you choose the right time quantum for Round Robin?
What's the difference between preemptive and non-preemptive SJF?
Deadlock
Hard 4 minTwo cars on a narrow bridge, coming from opposite sides, meeting in the middle. Neither can move forward, neither wants to reverse. They wait for each other... forever. That's deadlock.
In OS terms: Process A holds Resource 1 and wants Resource 2. Process B holds Resource 2 and wants Resource 1. Neither releases what they have. Both wait indefinitely. Classic example: two threads each trying to acquire two locks in different orders.
Four Necessary Conditions (Coffman, 1971): ALL four must exist simultaneously for deadlock to occur:
- Mutual Exclusion: At least one resource is non-shareable (only one process can use it at a time)
- Hold and Wait: Process holds some resources while waiting for others
- No Preemption: Resources can't be forcibly taken away
- Circular Wait: A→B→C→A cycle of waiting
Break ANY one of these conditions and deadlock becomes impossible.
P₁ holds R₁, wants R₂. P₂ holds R₂, wants R₁. Circular wait → Deadlock!
Key Points for Interview
- Prevention: Break one of 4 conditions at design time (e.g., order all lock acquisitions — breaks circular wait)
- Avoidance: Banker's Algorithm — before granting resource, check if system stays in safe state
- Detection: Build resource allocation graph. Cycle detection = deadlock (for single-instance resources)
- Recovery: Kill a process, preempt resources, rollback to checkpoint
- Most real OS use Ostrich Algorithm — ignore deadlocks, let user reboot. Full prevention is too expensive.
Common Trap
"Deadlock and starvation are the same" — No! In deadlock, processes are BLOCKED waiting for each other. In starvation, process is READY but never gets scheduled. Deadlock = stuck. Starvation = unfair scheduling. Don't mix them up.
Competitive Edge
Know the Banker's Algorithm in detail — it's a classic interview question. Also mention that databases actively detect and resolve deadlocks (by rolling back one transaction) while OS typically doesn't. Why? Database transactions are designed to be rollback-safe; arbitrary processes aren't.
Follow-up Questions
How does ordering lock acquisition prevent deadlock?
What's the difference between safe state and deadlock-free?
Starvation
Easy 2 minRestaurant analogy: VIP customers always get seated first. A regular customer arrives, but VIPs keep coming. Hours pass. The regular customer is never served — not because the restaurant is closed, but because they're always deprioritized. That's starvation.
Starvation happens in: (1) Priority Scheduling — low-priority process waits forever if high-priority processes keep arriving. (2) SJF — long jobs starve because short jobs keep coming. (3) Reader-Writer Problem — if readers keep arriving, writer starves.
Solution: Aging. Gradually increase priority of waiting processes. Eventually even the lowest priority becomes high enough to run.
Key Points for Interview
- Starvation: process is READY but not scheduled. Deadlock: process is BLOCKED.
- Starvation is a liveness problem (something that should happen eventually, doesn't)
- Algorithms prone to starvation: Priority, SJF, SSTF (disk scheduling)
- Round Robin is inherently starvation-free — everyone gets a turn
- Fix: Aging, fair scheduling, combining priority with round robin
Follow-up Questions
Can FCFS cause starvation?
Race Condition
Medium 3 minTwo people editing the same Google Doc cell simultaneously without seeing each other's changes. Person A reads "50", adds 10, writes "60". Person B reads "50" (before A's write), adds 20, writes "70". Expected: 80. Got: 70. That's a race condition — the outcome depends on who "wins" the race.
The classic example: incrementing a shared counter.
counter++ looks atomic but it's actually: (1) load
counter value, (2) add 1, (3) store result. If two threads do
this simultaneously, they might both load the same value, both
add 1, both store — result: only incremented once instead of
twice.
The code region where shared resources are accessed is called the critical section. Race conditions are fixed by ensuring mutual exclusion in the critical section — only one thread at a time.
Key Points for Interview
- Race condition = bug caused by non-deterministic thread interleaving
- Occurs when: shared data + concurrent access + at least one write + no synchronization
- Critical section = code that accesses shared resources
- Fix: mutex, semaphore, atomic operations, lock-free data structures
-
Even simple operations like
x++orx = ymay not be atomic!
Common Trap
"Race conditions only happen with global variables" — Wrong! Any shared data causes races: heap objects accessed by multiple threads, static variables, file handles, database connections. Even reading and writing "different" struct fields can race if they share a cache line (false sharing).
Follow-up Questions
How do atomic operations prevent race conditions?
What's a TOCTOU race?
Mutex
Medium 3 minA bathroom with a lock. One person enters, locks the door. Others wait outside. When done, unlocks the door. Next person enters. That's a mutex — only the owner can unlock it.
How it works: Thread calls lock().
If mutex is free, thread acquires it and continues. If mutex is
held by another thread, calling thread
blocks (sleeps) until mutex is released. Thread
calls unlock() when done — wakes up one waiting
thread.
Ownership: Mutex has owner. Only the thread that locked it can unlock it. This is THE key difference from binary semaphore. If thread A locks mutex, thread B cannot unlock it.
In C++, use std::lock_guard or
std::unique_lock — RAII wrappers that automatically
unlock when scope ends. Prevents forgetting to unlock on
exceptions or early returns.
Key Points for Interview
- Mutex = binary lock with ownership. Only owner can unlock.
- Blocking: if mutex is held, caller waits (sleeps, doesn't spin by default)
- Recursive mutex: same thread can lock multiple times (must unlock same number)
- Try-lock: attempt to acquire without blocking — returns immediately if unavailable
- Deadlock risk: thread holds mutex A, waits for mutex B; another holds B, waits for A
- Always use RAII (lock_guard) — never raw lock/unlock calls
Common Trap
"Mutex and binary semaphore are the same" — No! Mutex has ownership. Only the locker can unlock. Binary semaphore has no ownership — ANY thread can signal it. Mutex is for protecting critical sections. Semaphore is for signaling between threads. This distinction is asked in almost every OS interview.
Competitive Edge
Know about spinlocks: instead of sleeping when lock is unavailable, thread spins (busy-waits) checking repeatedly. Good for very short critical sections where sleeping overhead is worse than spinning. Bad for long waits (wastes CPU). Linux uses spinlocks in kernel code where sleeping isn't allowed.
Semaphore
Medium 3 minCounting Semaphore: A parking lot with N spaces. Counter shows available spots. Car enters → wait() → counter decrements. Car leaves → signal() → counter increments. When counter is 0, new cars must wait.
Binary Semaphore: Counter is 0 or 1. Works like a lock but without ownership — any thread can signal it, not just the one that waited. Useful for thread-to-thread signaling.
Classic operations: P() or wait() or down() — decrement and block if would go negative. V() or signal() or up() — increment and wake one blocked thread. These operations are atomic.
Classic problems solved by semaphores: Producer-Consumer (bounded buffer), Readers-Writers (shared data access), Dining Philosophers (resource allocation), and synchronizing execution order.
Key Points for Interview
- Counting semaphore: value can be any non-negative integer. Controls N concurrent accesses.
- Binary semaphore: value is 0 or 1. Similar to mutex but NO ownership.
- wait()/P()/down(): if value > 0, decrement; else block
- signal()/V()/up(): increment value, wake one blocked thread if any
- No ownership — any thread can signal (unlike mutex)
- Use case: resource counting (connection pool), ordering (ensure A runs before B), producer-consumer
| Aspect | Mutex | Semaphore |
|---|---|---|
| Value | Locked or unlocked | Non-negative integer |
| Ownership | Yes — only owner unlocks | No — anyone can signal |
| Primary Use | Protect critical section | Signaling, resource counting |
| Recursive | Can be (recursive mutex) | N/A |
Follow-up Questions
How does semaphore solve producer-consumer?
Can semaphore cause deadlock?
Paging
Hard 4 minImagine a book (your process) with 100 pages. The library (RAM) has shelf slots (frames). Pages can go into ANY available slot — they don't need to be next to each other. A table of contents (page table) tells you which page is in which slot. Page 1 might be in slot 47, page 2 in slot 3, etc.
Address translation: Logical address = page number + offset. CPU generates logical address, MMU (Memory Management Unit) looks up page number in page table to find frame number, combines with offset to get physical address. This happens for every memory access.
Page table overhead: Each process needs its own page table. With 4KB pages and 4GB address space, you need 1 million entries. Each entry is ~4 bytes → 4MB just for page table! Solution: multi-level page tables (only allocate what's needed) or inverted page tables.
TLB (Translation Lookaside Buffer): Page table is in main memory. Accessing it for every address is too slow. TLB is a small, fast cache of recent page→frame mappings. TLB hit = fast. TLB miss = slow (access page table, update TLB).
Page table maps logical page numbers to physical frame numbers. Offset stays the same.
Key Points for Interview
- Page size typically 4KB. Page table stored in main memory (PCB points to it)
- No external fragmentation (fixed-size blocks). Internal fragmentation in last page
- TLB = cache for page table. TLB miss → 2 memory accesses (page table + actual data)
- Page fault: page not in RAM → load from disk. Very expensive (~10ms)
- Multi-level page tables reduce memory overhead for sparse address spaces
- Inverted page table: one entry per frame (not per page). Used by some architectures
Competitive Edge
Know page replacement algorithms: FIFO (simple but Belady's anomaly), LRU (optimal approximation, expensive to implement), Clock/Second-Chance (practical LRU approximation using reference bit). Also know that Linux uses a variant of LRU with active/inactive lists.
Follow-up Questions
What's the effective memory access time with TLB?
Why is page fault so expensive?
Segmentation
Medium 3 minWhile paging blindly chops memory into equal-size pages (like cutting a pizza into identical slices without looking at toppings), segmentation respects the logical structure of your program. Code is one segment, data is another, stack is another. Each has a different size based on need.
Segment table: Each entry has: base (starting physical address) + limit (segment size). Logical address = segment number + offset. If offset > limit → segmentation fault! This is protection built into the hardware.
Modern systems use segmented paging: segments are further divided into pages. You get logical grouping of segments + no external fragmentation of paging. x86 supports both, but modern OS (Linux, Windows) use flat segmentation (all segments have base=0, limit=max) and rely entirely on paging.
Key Points for Interview
- Paging: fixed-size, invisible to programmer, no external fragmentation
- Segmentation: variable-size, programmer-visible, external fragmentation
- Segment table: base + limit for each segment
- Protection: offset must be < limit, else segfault
- Modern x86-64: segmentation mostly disabled (flat model), paging does all the work
| Aspect | Paging | Segmentation |
|---|---|---|
| Division | Fixed-size pages/frames | Variable-size segments |
| Visibility | Transparent to programmer | Programmer-visible |
| Fragmentation | Internal (last page) | External (between segments) |
| Address | Page # + Offset | Segment # + Offset |
Virtual Memory
Hard 4 minEvery process believes it owns the entire memory — say, 4GB on 32-bit system. But your machine might only have 2GB RAM. How? Virtual memory. Each process has a virtual address space. OS maps only the pages actually being used to physical RAM. Rest stays on disk (swap space).
Demand paging: Don't load entire program at startup. Load pages only when accessed. First access to a page → page fault → OS loads it from disk. Subsequent accesses → fast (already in RAM).
Key benefits: (1) Run programs larger than physical memory. (2) Process isolation — process A can't access process B's memory because they have different page tables. (3) Memory protection — pages can be read-only, no-execute, etc. (4) Shared libraries — multiple processes can share the same code pages.
Thrashing: If working set (frequently used pages) exceeds RAM, system spends all time swapping pages in and out. CPU utilization drops, system becomes unusable. Solution: add RAM, reduce multiprogramming degree, or use working set model.
Key Points for Interview
- Virtual address → MMU + Page table → Physical address (or page fault)
- Swap space on disk holds pages evicted from RAM
- Demand paging: load pages lazily, only when accessed
- Copy-on-write: parent and child share pages until one writes → then copy
- Thrashing: too many page faults, system spends all time swapping
- Working set: set of pages process is actively using. Keep these in RAM
Common Trap
"Virtual memory means using disk as RAM" — Incomplete! Virtual memory is primarily about giving each process its own address space and providing isolation. Disk-backed swapping is just ONE feature. Even with infinite RAM, you'd still use virtual memory for isolation and protection.
Competitive Edge
Know about memory-mapped files: map a file directly into virtual address space. Read/write file as if it's memory. OS handles paging automatically. Used for efficient file I/O and shared memory between processes. Also know copy-on-write (COW): fork() doesn't copy memory — both parent and child share pages (marked read-only). Only when one writes, that page is copied. Huge performance win for fork().
Follow-up Questions
How does virtual memory provide process isolation?
What triggers thrashing and how to detect it?
Heap vs Stack (Memory Layout)
Medium 3 minEvery process has a memory layout (from low to high address): Text (code) → Data (initialized globals) → BSS (uninitialized globals) → Heap (grows ↑) → ... free space ... → Stack (grows ↓) → Kernel space.
Stack: LIFO structure. Each function call pushes a frame (local vars, return address, params). Function return pops the frame. Super fast allocation (just move stack pointer). Limited size (1-8MB typically). Stack overflow = too deep recursion or huge local arrays.
Heap: Dynamic memory — you request (malloc/new), you release (free/delete). Managed by allocator, can fragment. Slower than stack (must find free block). No size limit (well, virtual memory limit). Memory leak = forgetting to free.
Process memory layout. Stack grows down, heap grows up. They must not collide!
Key Points for Interview
- Stack: auto-managed (LIFO), fast, limited size, stores local vars and function calls
- Heap: manually managed, slower, large, stores dynamic allocations
- Text segment: read-only, contains executable instructions
- Data/BSS: global and static variables (initialized vs uninitialized)
- Each thread has own stack, all threads share heap
- Stack overflow: too deep recursion. Heap exhaustion: too much allocation without freeing
Follow-up Questions
Why is stack allocation faster than heap?
What happens during a function call (stack frame)?
Memory Allocation Strategies
Medium 3 minFirst Fit: Scan from beginning, pick first hole big enough. Fast (stops early). Problem: fragments the beginning of memory.
Next Fit: Like First Fit, but start from where last allocation ended. Distributes allocation more evenly. Slightly better than First Fit in practice.
Best Fit: Search entire list, pick smallest hole that fits. Minimizes wasted space per allocation. Problem: leaves tiny unusable fragments, slow (full scan).
Worst Fit: Pick largest hole. Theory: leftover will be big enough to be useful. Practice: quickly fragments large blocks, performs worst overall.
Buddy System: Memory divided into power-of-2 blocks. Request for 70KB? Allocate 128KB block. To allocate, split larger block into two "buddies." To free, merge buddies back. Used in Linux kernel. Fast and limits fragmentation.
Key Points for Interview
- First Fit is generally best overall (good balance of speed and fragmentation)
- Best Fit leaves tiny holes (external fragmentation still happens)
- Compaction: move processes to create one large free block (expensive, needs relocation)
- Internal fragmentation: wasted space INSIDE allocated block
- External fragmentation: wasted space BETWEEN allocated blocks
- Modern allocators (jemalloc, tcmalloc): size classes, thread caches, much smarter
Competitive Edge
Know that modern allocators like jemalloc (used by Firefox, Redis) and tcmalloc (Google) use per-thread caches, size classes (buckets for common sizes), and sophisticated techniques to minimize fragmentation and contention. Mentioning these shows you know real-world systems, not just textbook algorithms.
Garbage Collection
Medium 3 minIn C/C++, you allocate memory, you free it. Forget to free → memory leak. Free twice → crash. In garbage-collected languages, you just allocate. The GC periodically finds unreachable objects and frees them. You don't call free/delete.
Mark and Sweep: (1) Start from "roots" (stack, globals, registers). (2) Mark all reachable objects. (3) Sweep: free all unmarked objects. Problem: needs to pause program ("stop the world"). Modern GCs use incremental/concurrent marking to reduce pauses.
Reference Counting: Each object has a counter — how many references point to it. When counter = 0, free immediately. Problem: circular references (A→B→A, both have count 1, neither freed). Solution: combine with mark-sweep or use weak references.
Generational GC: Observation: most objects die young. Divide heap into generations: young (Eden), old (Tenured). Collect young generation frequently (fast, most garbage is here). Collect old generation rarely (slow but infrequent). Objects that survive young collections get promoted to old. Java uses this.
Key Points for Interview
- C/C++ has no GC — uses RAII, smart pointers, manual management
- Mark-and-Sweep: handles cycles, but needs pauses
- Reference counting: immediate collection, but can't handle cycles alone
- Generational: exploits "most objects die young" — very effective
- GC overhead: memory (tracking info), CPU (collection work), pauses (latency spikes)
- Real-time systems avoid GC — unpredictable pauses are unacceptable
Common Trap
"GC prevents all memory leaks" — Wrong! GC only frees unreachable objects. If you keep a reference to something you don't need (like adding to a list and never removing), it's still reachable → still a leak. These "logical leaks" are common in GC languages. Close resources explicitly!
Competitive Edge
Know why C++ doesn't have GC: C++ values deterministic destruction (RAII) and zero-cost abstractions. With GC, you can't predict when destructor runs — problematic for resource management (files, locks, connections). RAII + smart pointers give you GC-like safety WITH deterministic timing. This is why system software (OS kernels, game engines, trading systems) uses C++, not Java.
Follow-up Questions
How does Python handle circular references?
What's a GC pause and why does it matter?
Ready to test OS?
Check process, memory, scheduling, and concurrency recall before moving on.