Loading content...
Every process begins its life as a collection of static instructions on disk—a program waiting to be executed. The moment a user double-clicks an application icon, types a command in the terminal, or another process spawns a child, a remarkable transformation begins. The operating system must convert that dormant executable into a living, schedulable entity.
This transformation is the Admit transition: the passage from the New state to the Ready state. It represents the moment when the kernel acknowledges a process's existence and declares it eligible for CPU time. Understanding this transition is fundamental to grasping how operating systems manage process creation, resource allocation, and system admission policies.
By the end of this page, you will understand the complete lifecycle of the Admit transition—from the initial fork() or CreateProcess() call through PCB initialization, memory allocation, resource assignment, admission control decisions, and final placement in the Ready queue. You'll see how the kernel orchestrates this transition and why admission control policies matter for system stability.
Before examining the transition itself, we must clearly understand what the New state represents. A process in the New state is a partially constructed entity—it exists in the kernel's consciousness but isn't yet ready for execution.
What characterizes the New state:
When a process enters the New state, the kernel has begun creating necessary data structures but hasn't completed all initialization tasks. The process exists as a kernel object, but it's not yet a candidate for CPU scheduling. Think of it as a construction site where the foundation is being laid—the structure exists, but it's not habitable.
| Aspect | Status in New State | Completion Required |
|---|---|---|
| Process ID (PID) | Assigned | No |
| Process Control Block (PCB) | Allocated but partially initialized | Yes - fields must be populated |
| Address Space | Being constructed | Yes - segments must be mapped |
| Memory Pages | Allocation in progress | Yes - initial pages must be loaded |
| File Descriptors | Inheritance pending | Yes - must be copied from parent |
| Credentials | Being established | Yes - UID/GID must be set |
| CPU Context | Uninitialized | Yes - initial register values needed |
| Ready Queue Membership | Not present | Yes - must be inserted for scheduling |
The New state exists because process creation is not atomic:
Creating a process involves numerous steps that cannot all complete instantaneously. The New state provides a conceptual holding area during this initialization phase. Some operating systems treat this state as very transient (lasting microseconds), while others may keep processes in New longer due to admission control policies.
Visibility of New state processes:
In most Unix-like systems, you rarely observe processes in the New state because the creation is fast. However, tools like ps may occasionally show a process created but not yet running if you're quick enough. On systems with strict admission control (like batch processing systems), processes may reside in New state waiting for approval.
In early batch systems, the New state was called the 'Submitted' or 'Spooled' state. Jobs would wait in this state while the operating system decided when to admit them based on resource availability. Modern interactive systems compress this waiting period significantly, but the conceptual state remains important for understanding the complete process lifecycle.
The journey from New to Ready begins when a process creation request enters the kernel. This request can originate from multiple sources, each with different semantics and implications.
The fork() system call in detail:
On Unix-like systems, the fork() system call is the primary mechanism for process creation. When a process calls fork(), the kernel must:
1234567891011121314151617181920212223242526272829303132333435
#include <stdio.h>#include <unistd.h>#include <sys/wait.h> int main() { printf("Parent process (PID: %d) about to fork\n", getpid()); // At this point, the parent is in Running state pid_t pid = fork(); if (pid < 0) { // fork() failed - system resource limits or permissions issue perror("fork failed"); return 1; } if (pid == 0) { // Child process - just transitioned from New to Ready, // now in Running (since we're executing) printf("Child process (PID: %d) now running\n", getpid()); printf(" Parent PID from child's view: %d\n", getppid()); // Child inherits file descriptors, environment, etc. } else { // Parent process - continues in Running printf("Parent: created child with PID %d\n", pid); // Parent waits for child to complete int status; waitpid(pid, &status, 0); printf("Parent: child exited with status %d\n", WEXITSTATUS(status)); } return 0;}While fork() appears simple from the programmer's perspective, the kernel performs extensive work behind the scenes. Modern implementations use copy-on-write (COW) to defer the expensive memory copying, making the New → Ready transition faster. But the conceptual steps—PCB creation, resource allocation, queue insertion—still occur.
The Admit transition is orchestrated entirely by the kernel. When transitioning a process from New to Ready, the kernel executes a precise sequence of operations. Understanding this sequence reveals the complexity hidden behind seemingly simple process creation.
Detailed breakdown of each step:
The Admit transition must be carefully synchronized. If the kernel inserted a process into the Ready queue before completing initialization, the scheduler might select it for execution, causing undefined behavior. The kernel uses internal locks and state checks to ensure a process is fully initialized before becoming schedulable.
The Process Control Block is the nucleus of process management. During the Admit transition, the kernel populates this structure with everything needed for scheduling, memory management, and resource tracking. Let's examine the key components initialized during this phase.
| Field Category | Specific Fields | Initialization Action |
|---|---|---|
| Identity | pid, tgid, parent pid, process group, session | Assigned unique values; parent info copied from caller |
| State | state, exit_state, flags | Set to TASK_NEW initially, flags indicate creation mode |
| Scheduling | priority, static_prio, policy, sched_class | Set from parent or defaults; policy inherits or uses default |
| Memory | mm_struct pointer, address space limits | New mm_struct created; limits inherited from parent |
| CPU Context | thread_struct (registers, stack pointer) | Initialized for first execution in user space |
| Timing | start_time, utime, stime | Creation time recorded; CPU times zeroed |
| File System | fs_struct, files_struct | Current directory inherited; file table duplicated |
| Signals | signal_struct, sighand_struct, blocked mask | Handler table initialized; inherited blocked signals |
| Credentials | cred, real_cred | UID/GID from parent or executable's setuid/setgid |
| Resource Limits | signal->rlim[] | Limits (max files, stack size, etc.) inherited from parent |
The Linux task_struct in context:
In Linux, the PCB is implemented as task_struct, one of the largest and most complex structures in the kernel (over 700 bytes, containing pointers to numerous sub-structures). During the Admit transition (in copy_process() called by fork()), the kernel performs a careful dance:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// Simplified view of process creation in Linux kernel// Actual implementation in kernel/fork.c is ~2000 lines struct task_struct* copy_process(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs) { struct task_struct *p; // Step 1: Allocate new task_struct from slab cache p = dup_task_struct(current); if (!p) goto fail_allocation; // Step 2: Initialize unique identity p->pid = alloc_pid(); // Unique PID assignment p->tgid = p->pid; // Thread group leader is self p->parent = current; // Parent is calling process p->real_parent = current; // Step 3: Initialize state as NEW (not schedulable yet) p->state = TASK_NEW; // Process in New state // Step 4: Copy or share resources based on flags if (!(clone_flags & CLONE_FILES)) copy_files(p); // Independent file descriptors if (!(clone_flags & CLONE_VM)) copy_mm(p); // Copy-on-write address space copy_signal_handlers(p); // Signal disposition copy_credentials(p); // UID/GID/capabilities // Step 5: Initialize scheduling parameters p->sched_class = fair_sched_class; p->prio = current->prio; // Inherit priority p->static_prio = current->static_prio; // Step 6: Initialize timekeeping p->start_time = ktime_get_ns(); p->utime = p->stime = 0; // No CPU time yet // Step 7: Set up CPU context for first switch copy_thread(p, regs); // Set up initial register state // Step 8: Transition New → Ready p->state = TASK_RUNNABLE; // Now in Ready state wake_up_new_task(p); // Add to run queue return p; fail_allocation: return ERR_PTR(-ENOMEM);}Linux's clone() system call (which underlies fork()) uses flags to control what's shared vs copied. CLONE_VM shares address space (for threads), CLONE_FILES shares file descriptors, CLONE_SIGHAND shares signal handlers. This flexibility allows fork(), vfork(), and pthread_create() to all use the same underlying mechanism with different flag combinations.
Memory management during the Admit transition is a critical and complex operation. The kernel must establish a complete virtual address space while being efficient about physical memory usage. Modern systems use sophisticated techniques to minimize the overhead of process creation.
Copy-on-Write: The Key to Fast Fork:
Copy-on-write is the optimization that makes fork() viable in modern systems. Without COW, forking a 1GB process would require copying 1GB of memory—taking hundreds of milliseconds and consuming substantial physical memory. With COW:
For processes that fork() and immediately exec() (the common case), COW means almost no memory is ever copied—the child's address space is completely replaced before any COW faults occur.
| Scenario | Without COW | With COW | Improvement |
|---|---|---|---|
| Fork 100MB process (immediate exec) | 100MB copied, 100ms | ~0MB copied, <1ms | 100x faster |
| Fork 1GB process (child reads only) | 1GB copied, 1000ms | ~0MB copied, <1ms | 1000x faster |
| Fork 1GB process (child modifies 10MB) | 1GB copied, 1000ms | 10MB copied, 10ms | 100x faster |
| Fork 1GB process (child modifies 1GB) | 1GB copied, 1000ms | 1GB copied on demand | Same total, better distribution |
COW is why you can have hundreds of shell processes (each forked from bash) on a system with limited RAM. They share most of their memory pages. Only when a process modifies data does it get its own private copy of those specific pages. This sharing is transparent to applications—each believes it has its own private memory.
Admission control determines whether and when a process in the New state should be promoted to Ready. While desktop systems typically admit processes immediately, many production environments implement sophisticated admission policies to prevent resource exhaustion and ensure quality of service.
Linux resource limits (rlimits):
Linux implements admission control through resource limits. When fork() is called, the kernel checks several limits:
pid_max (maximum PIDs), threads-max (maximum kernel threads).These checks happen before significant resources are allocated, failing fast if limits would be exceeded.
12345678910111213141516171819202122232425262728
# View current process limitsulimit -a # Check user's maximum process limitulimit -u# Example output: 63704 # Set maximum processes for current shell and childrenulimit -u 100 # View system-wide limitscat /proc/sys/kernel/pid_max # Maximum PID value# Example: 4194304 cat /proc/sys/kernel/threads-max # Maximum total threads# Example: 126407 # View cgroup process limits (if using systemd)cat /sys/fs/cgroup/user.slice/user-1000.slice/pids.max# Example: max (unlimited) or specific number # Demonstrate fork failure due to limitulimit -u 5 # Set very low limit# Now try to run many processesfor i in {1..10}; do sleep 100 &done# Output: bash: fork: retry: Resource temporarily unavailableA fork bomb is a process that continuously forks children, exponentially consuming process slots until the system becomes unresponsive. Admission control prevents this: setting RLIMIT_NPROC to a reasonable value (e.g., 1000 per user) ensures that even a runaway fork bomb only affects that user and eventually fails when hitting the limit.
The final step of the Admit transition is inserting the process into the Ready queue. This seemingly simple operation involves careful coordination with the scheduler and has implications for when the new process will first receive CPU time.
Ready queue structures:
Modern operating systems don't use a single Ready queue. Instead, they maintain sophisticated data structures optimized for their scheduling algorithms.
Linux's Completely Fair Scheduler (CFS) uses:
Windows uses:
Scheduling considerations for new processes:
When a new process is admitted, the scheduler must decide several things:
Which CPU's queue? On SMP systems, the kernel chooses a CPU to minimize load imbalance. It may consider:
Queue position? The new process shouldn't unfairly jump ahead of processes that have been waiting. CFS gives new processes a slight bonus but not excessive advantage.
Immediate preemption? If the new process has higher priority than the currently running process, the scheduler sets a flag to trigger preemption at the next safe point.
Wake-up latency? How quickly should the new process get CPU time? Interactive processes benefit from quick wake-up.
Linux typically gives the child process priority to run before the parent after fork(). This optimization helps when the child immediately calls exec(): it can begin loading the new program while the parent continues, maximizing parallelism. If the parent ran first and touched shared pages, those pages would need to be copied for the child.
| Scheduling Class | Queue Structure | New Process Placement | Priority Boost |
|---|---|---|---|
| Real-Time FIFO | Per-priority linked list | End of priority queue | None - runs until blocked |
| Real-Time RR | Per-priority linked list | End of priority queue | None - gets full time slice |
| CFS (Normal) | Red-black tree by vruntime | Vruntime = min + slice/2 | Small boost for interactivity |
| Idle | Simple queue | End of queue | Only runs when nothing else needs CPU |
The Admit transition is a universal concept, but its implementation varies significantly across operating systems. Let's compare how different platforms handle process admission.
Linux Process Creation:
fork(), vfork(), clone() - all implemented via kernel_clone()copy_process() in kernel/fork.cwake_up_new_task() adds to CFS run queueUnique characteristics:
1234567891011121314151617
// Simplified call sequence for fork() in Linux// User spacefork() // libc wrapper // Kernel space (simplified)sys_fork() → kernel_clone(SIGCHLD, 0, ...) → copy_process(...) // Create PCB, copy resources → dup_task_struct() // Allocate task_struct → alloc_pid() // Get PID → copy_mm() // Set up address space (COW) → copy_files() // Duplicate file descriptors → copy_thread() // Initialize CPU context → wake_up_new_task(p) // Admit: add to run queue → activate_task() → enqueue_task() // Insert in scheduler queue → check_preempt_curr() // Maybe preempt currentThe fundamental difference is that Unix systems use fork()+exec() to create new processes (copying then replacing), while Windows uses CreateProcess() (creating fresh). Fork is elegant and enables powerful patterns like process supervisors and shell pipelines, but Windows' approach can be more efficient for simply launching new programs since it avoids the copy phase entirely.
We've traced the complete journey of a process from creation to schedulability. Let's consolidate the key concepts of the Admit transition.
Looking ahead:
With processes successfully admitted to the Ready queue, the next question becomes: how does a process go from waiting in the Ready queue to actually executing on a CPU? This is the Dispatch transition—the handoff from Ready to Running state—which we'll explore in the next page.
You now understand the Admit transition (New → Ready) in comprehensive detail—from the initial creation request through kernel initialization, memory allocation, admission control, and finally Ready queue insertion. Next, we'll examine how the scheduler dispatches processes from Ready to Running.