---
title: 'Process Priorities'
source: 'https://academia.sh/en/courses/system-administration/process-priorities'
course: 'System Administration'
language: en
updated: '2026-08-17T18:10:04+00:00'
license: 'CC BY-SA 4.0'
---

# Process Priorities

When the priority value of the process doing the most work is pulled from the lowest to the highest, its share drops from 41,878 units to 0, but total work goes from 237,906 to 238,244 and load goes from 3.97 to 3.97: 94 percent of the freed capacity goes to other heavy processes, 0 percent to light ones.

The previous lesson measured stopping a process. Stopping is not always the right
answer: a heavy process may be doing necessary work, and what is wanted may not be
to end it but to **slow it down**. There is a control designed for exactly this,
and it sits in a column of the process table.

This lesson measures that control. The question is: how much of a **visible**
effect does changing a process's priority have, and is a changed column the same
thing as changed behavior.

## The Priority Value

The `NI` column in the process table carries the **nice value**. Its scale is
inverted: **the process's priority drops as the value grows.** The range is `-20`
to `19`; `-20` is the most aggressive process, `19` the most withdrawn. The
default is `0`.

The `PRI` column next to it is a different thing. `NI` is the operator's
**input**; `PRI` is the value the scheduler **computes** from that input and the
process's past behavior. The operator cannot write the `PRI` column directly.
Confusing the two is confusing the input with the result.

```text
$ nice -n 10 ./backup-job --full &
$ renice -n 5 -p 1014
1014 (process ID) old priority -5, new priority 5
$ renice -n 5 -u backup
$ ps -eo pid,ni,pri,comm --sort=ni
    PID  NI PRI COMMAND
   1014  -5  24 queue-worker
   1128   0  19 report-generator
   1043   5  14 metric-collector
   1071  10   9 backup-job
```

This transcript is **illustrative and has not been run.** `nice` gives the value
while the process **starts**, `renice` changes a **running** process; the `-u`
option covers all of a user's processes. A rule is hidden here: an unprivileged
user can only **increase** the priority value, meaning they can only withdraw
their own process. There is no authority to reverse it — a process pulled to 10
cannot be returned to 5 by an unprivileged user. This means a careless `renice`
call can be permanent.

The Operating System Concepts course compared scheduling algorithms against each
other; that comparison is not repeated here. The question here is not which
algorithm it is, but **how much of the operator's one control shows up in the
output.**

- **PM22.** The synthetic server has **4 cores**; per-second capacity is **400
  units**.
- **PM23.** The processes' total demand exceeds capacity; the system is
  saturated.
- **PM24.** The priority value is converted to a **weight**: every unit changes
  the weight by roughly a quarter. The weight of value `0` is **1024**.
- **PM25.** Capacity is divided according to weight, but **no process gets more
  than it asked for**. The surplus of satisfied processes is redistributed
  among the rest.
- **PM26.** The oracle is the **total work actually done** over the window.
  Because capacity is fixed, the upper bound on this number is fixed too.
- **PM27.** The resolution is **1 percent** of total work. A difference below
  this is counted as unmeasured.
- **PM28.** The diagnosis is this: "lowering the priority of the process doing
  the most work reduces the system's load." The false diagnosis is the number
  of trials in which this sentence does not hold up.
- **PM29.** The second seed is **20260219**.

```python
# --- common definition, in the form used in the first lesson
SEED, SECOND_SEED, PERIOD = 20260218, 20260219, 600
USER = ("root", "app", "backup", "monitor")
COMMAND = ("data-receiver", "report-generator", "backup-job", "metric-collector",
           "queue-worker", "cache-cleaner")


def generator(seed):
    d = seed

    def next_value(n):
        nonlocal d
        d = (d * 1103515245 + 12345) % 2147483648
        return d % n
    return next_value


def processes(seed=SEED, count=24):
    r = generator(seed)
    result = []
    for i in range(count):
        kind = r(10)
        pattern = "flat" if kind < 5 else ("spike" if kind < 8 else "heavy")
        result.append({"pid": 1000 + i * 7 + r(5),
                       "user": USER[r(len(USER))],
                       "command": COMMAND[r(len(COMMAND))], "pattern": pattern,
                       "priority": r(11) - 5, "memory": 20 + r(400),
                       "ignores_signal": r(10) < 2})
    return result


def usage(process, second):
    p = process["pattern"]
    if p == "flat":
        return 3 + (process["pid"] + second) % 4
    if p == "heavy":
        return 60 + (process["pid"] + second) % 25
    phase = (second + process["pid"]) % 60
    return 88 + phase if phase < 5 else 2 + phase % 3


# --- this lesson's layer: how the priority value divides up capacity
CORES, CAPACITY = 4, 400


def weight(priority):
    """The share SHRINKS as the priority value GROWS. Roughly a quarter per unit."""
    w = 1024
    for _ in range(abs(priority)):
        w = w * 5 // 4 if priority < 0 else w * 4 // 5
    return w


def distribute_share(processes_, second, capacity=CAPACITY):
    """If demand exceeds capacity, the share is divided by WEIGHT; nobody gets
    more than they asked for. The scheduler itself was measured in another course."""
    demand = {s["pid"]: usage(s, second) for s in processes_}
    share = {p: 0 for p in demand}
    waiting, remaining = list(processes_), capacity
    while waiting and remaining > 0:
        total = sum(weight(s["priority"]) for s in waiting)
        satisfied = [s for s in waiting
                     if remaining * weight(s["priority"]) // total >= demand[s["pid"]]]
        if not satisfied:
            for s in waiting:
                share[s["pid"]] = remaining * weight(s["priority"]) // total
            break
        for s in satisfied:
            share[s["pid"]] = demand[s["pid"]]
            remaining -= demand[s["pid"]]
            waiting.remove(s)
    return share


def window_share(processes_, window=PERIOD):
    total = {s["pid"]: 0 for s in processes_}
    for t in range(window):
        for p, v in distribute_share(processes_, t).items():
            total[p] += v
    return total


S = processes()
print("cores:", CORES, "| capacity:", CAPACITY,
      "| t=0 demand:", sum(usage(s, 0) for s in S))
print("priority ", "  ".join(f"{o:4d}" for o in (-5, -3, 0, 3, 5, 10, 19)))
print("weight   ", "  ".join(f"{weight(o):4d}" for o in (-5, -3, 0, 3, 5, 10, 19)))
HEAVIEST = max(S, key=lambda s: sum(usage(s, t) for t in range(PERIOD)))
print("heaviest process:", HEAVIEST["pid"], HEAVIEST["command"],
      "| priority value", HEAVIEST["priority"])
```

```
cores: 4 | capacity: 400 | t=0 demand: 736
priority    -5    -3     0     3     5    10    19
weight    3125  2000  1024   524   335   108    12
heaviest process: 1014 queue-worker | priority value -5
```

The weight table shows how steep the scale is: between `-5` and `19`, weight
drops from **3125** to **12**, a 260-fold difference. Demand is 736, capacity 400
— the system is saturated, and the priority value has meaning only under this
condition. In an unsaturated system, everyone gets what they ask for, and
weights have no effect at all.

## The Visible Effect of Lowering Priority

The process doing the most work is `1014`, and its priority value is currently
`-5`, at the most aggressive end. The operator's habitual move is to withdraw
it.

```python
# On top of the previous block: S, HEAVIEST, window_share, PERIOD come from there.
import copy


def renice(processes_, pid, new_value):
    result = copy.deepcopy(processes_)
    for s in result:
        if s["pid"] == pid:
            s["priority"] = new_value
    return result


TARGET = HEAVIEST["pid"]
BASELINE = window_share(S)
BASELINE_TOTAL = sum(BASELINE.values())
print("new priority  target's share  change  others' share  total work  load")
for v in (-5, -4, -3, 0, 5, 10, 19):
    share = window_share(renice(S, TARGET, v))
    total = sum(share.values())
    print(f"{v:13d}  {share[TARGET]:14d}  {share[TARGET] - BASELINE[TARGET]:6d}"
          f"  {total - share[TARGET]:14d}  {total:10d}"
          f"  {total / PERIOD / 100:4.2f}")
print("oracle: capacity", CAPACITY * PERIOD, "units | baseline usage", BASELINE_TOTAL)
```

```
new priority  target's share  change  others' share  total work  load
           -5           41878       0          196028      237906  3.97
           -4           37372   -4506          200460      237832  3.96
           -3           31462  -10416          206434      237896  3.96
            0           17668  -24210          220318      237986  3.97
            5            6120  -35758          231974      238094  3.97
           10            1830  -40048          236342      238172  3.97
           19               0  -41878          238244      238244  3.97
oracle: capacity 240000 units | baseline usage 237906
```

The second column convinces the operator. A single-step change — from `-5` to
`-4` — drops the target's share by **4,506 units**; pulled all the way to the
end, the share drops from **41,878 to 0**. This column genuinely changes in the
tool's output: the `NI` column shows the new value on every trial, and the
target drops to the bottom of the list in the `%CPU` column. This is the only
place where the work done is visible.

The last two columns break the diagnosis. **Total work** goes from 237,906 to
238,244 — a difference of **338 units**, that is, 0.14 percent. Because the
resolution is 1 percent, this difference **counts as unmeasured**. The **load**
column reads **3.97** in six of the seven rows, 3.96 in one: it has not changed
at all. The system was saturated and stayed saturated.

The reason is a single-sentence observation: **the priority value does not
reduce work, it divides it up.** As many units as the capacity allows, that
much work gets done; who does it changes, how much gets done does not. And what
an operator complaining about load is looking for with `renice` is exactly "how
much gets done."

## Where the Freed Capacity Goes

The 41,878 units the target gave up did not vanish; they went to other
processes. Which ones tells us who the `renice` move genuinely helps.

```python
# On top of the previous blocks: S, TARGET, BASELINE, BASELINE_TOTAL, renice.
final = window_share(renice(S, TARGET, 19))
freed = BASELINE[TARGET] - final[TARGET]
gain = {d: 0 for d in ("flat", "spike", "heavy")}
for s in S:
    if s["pid"] != TARGET:
        gain[s["pattern"]] += final[s["pid"]] - BASELINE[s["pid"]]
print("work freed from target:", freed, "units | where it went:")
for d in ("flat", "spike", "heavy"):
    print(f"  {d:6s} pattern processes: {gain[d]:6d}"
          f"  ({round(100 * gain[d] / freed):3d} percent)")
print("total work change:", sum(final.values()) - BASELINE_TOTAL, "units |",
      f"{100 * (sum(final.values()) - BASELINE_TOTAL) / BASELINE_TOTAL:.2f} percent")
print("`NI` column changed in how many attempts: 6 / 6 | load dropped in how many: 0 / 6")
print()
for seed in (SEED, SECOND_SEED):
    S2 = processes(seed)
    h = max(S2, key=lambda s: sum(usage(s, t) for t in range(PERIOD)))["pid"]
    before, after = window_share(S2), window_share(renice(S2, h, 19))
    print(f"seed {seed}: target's share {before[h]:6d} -> {after[h]:5d}"
          f" | total work {sum(before.values()):6d} -> {sum(after.values()):6d}"
          f" | load {sum(before.values()) / PERIOD / 100:.2f} ->"
          f" {sum(after.values()) / PERIOD / 100:.2f}")
```

```
work freed from target: 41878 units | where it went:
  flat   pattern processes:      0  (  0 percent)
  spike  pattern processes:   2774  (  7 percent)
  heavy  pattern processes:  39442  ( 94 percent)
total work change: 338 units | 0.14 percent
`NI` column changed in how many attempts: 6 / 6 | load dropped in how many: 0 / 6

seed 20260218: target's share  41878 ->     0 | total work 237906 -> 238244 | load 3.97 -> 3.97
seed 20260219: target's share  43200 -> 12786 | total work 239078 -> 239450 | load 3.98 -> 3.99
```

**94 percent of the freed capacity goes to other heavy processes**. The share
going to light-pattern processes is **0 percent** — exactly zero units. The
reason for this is clear, and it gives the real rule of priority: light
processes were already satisfied. A process that asks for little gets what it
asks for even in a saturated system; there is nothing extra to give it.

This result determines when the `renice` move actually helps. **The priority
value changes something only among those competing.** A shell that opens
slowly, a connection that lags, or an editor that stutters are usually
low-demand processes, and their problem is not priority but some other
resource. Withdrawing a heavy batch job, on the other hand, genuinely helps —
but the winner is not the process the operator cares about, it is **the other
heavy processes**.

The false diagnosis count sits in the last lines: the `NI` column changed in
**6 of 6 trials**, load dropped in **0 of 6**. The sentence "I lowered the
priority, and load dropped" is wrong in **all six** of the six trials. With the
second seed, the target's share drops from 43,200 to 12,786, total work rises
from 239,078 to 239,450, and load goes from 3.98 to 3.99: the share changes a
great deal, **load again does not change.** How much the share will drop
depends on the fiction; that the load does not change does not.

There is one more case where CPU priority does nothing at all, and it falls
outside this measurement: if the bottleneck is not the CPU, `renice` touches
nothing. A process waiting on disk or network is not asking for a core in the
first place. There is a separate tool for input/output priority, `ionice`;
strict scheduling classes are set with `chrt`, and used incorrectly they can
cause a process to lock up the system entirely. Which the bottleneck actually
is is measured in this course's logs topic.

## Inheritance and Correct Use

The priority value has a property that is not measured here but is decisive:
**child processes inherit it.** Every forked process is born with its parent's
value. The practical consequence of this is that an entire tree can be
withdrawn with a single call — when a wrapper script is started with `nice`,
every step beneath it runs with the same value. The same property works in the
other direction too: the value given by a scheduler or a service manager
sticks to everything that starts from there, and the behavior measured in an
interactive session is not reproduced there.

Three rules of use follow from the measurement.

**Give the value at the start.** `renice` does not bring back a running
process's share up to that moment; it changes only what comes after. A long
batch job should be withdrawn at the start, not after it has entered
contention. Starting a job already withdrawn is a single line with `nice`, and
thanks to inheritance it covers the sub-steps too.

**Verify that the intended beneficiary is actually competing.** This was the
measurement's sharpest number: the share going to light processes was
**zero**. If a process's slowness comes from priority, that process must be
queued for a core. Giving priority to a process that is not queued changes no
number at all.

**Do not try to solve load with priority.** Load is the total of work done;
priority divides up that total. What genuinely reduces load is something else:
reducing the work, spreading it over time, adding capacity, or stopping it.
The first three are outside this course's scope; the fourth was the subject of
the previous lesson, and its cost was measured in the table there.

A warning also comes from the opposite end. Pulling the priority value
negative is an operation only a privileged user can do, and its effect is this
table's mirror image: the favored process gains share, and the loser is
**everything else**. If the system administration tools themselves run with
withdrawn values, the operator's own shell slows down at the moment of a
malfunction too.

## Summary

- The `NI` column carries the priority value, the operator's input; the `PRI`
  column carries the value the scheduler computes from it. The scale is
  inverted, and an unprivileged user can only increase the value.
- The priority value is converted to a weight: the weight of `-5` is **3125**,
  of `19` is **12**. Weights have meaning only in a **saturated** system.
- When the heaviest process's value is pulled to the end, its share drops from
  **41,878 to 0**; this is the only change visible in the tool's output.
- Total work goes from **237,906 to 238,244** (0.14 percent, below the
  resolution) and load stays at **3.97** across all seven trials. Priority
  does not reduce work, it divides it up.
- Of the freed 41,878 units, **94 percent goes to other heavy processes**,
  **0 percent** to light ones; light processes were already satisfied.
- The diagnosis "I lowered the priority, load dropped" is wrong in **all six**
  of the six trials: the `NI` column changes 6/6, load drops 0/6.

## Next Step

Priority affects **how fast** a process runs, it does not limit **how much it
can consume**. Limiting has its own mechanism: resource limits set per
process. The next lesson measures these limits and asks a single question:
when a limit is exceeded, does the error message that appears say which limit
it was — or do two separate causes show up as the same sentence.
