Skip to content
academia.sh

Lesson 04 / 22

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.

Contents

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.

$ 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.
# --- 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.

# 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.

# 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close