STRUCT SCHED_EXT_OPS(9)
hiii bestie π welcome to struct sched_ext_ops(9) from the Kernel API Manual β the operation table page for a BPF scheduler implementation. This struct is literally the schedulerβs main-character vibe sheet: every callback and every knob for βwhat do I do when X happens.β Weβre keeping every single detail, fr fr. β¨
πΈ NAME πΈ
struct sched_ext_ops β operation table for BPF scheduler implementation. Thatβs the whole name, no cap: itβs the ops table your BPF scheduler implementation fills out. π
β¨ SYNOPSIS β¨
okay so hereβs the struct in all its glory β code is verbatim, bestie, donβt let the line wraps scare you π«Ά
struct sched_ext_ops {
s32 (*select_cpu)(struct task_struct *p, s32 prev_cpu, u64
wake_flags);
void (*enqueue)(struct task_struct *p, u64 enq_flags);
void (*dequeue)(struct task_struct *p, u64 deq_flags);
void (*dispatch)(s32 cpu, struct task_struct *prev);
void (*tick)(struct task_struct *p);
void (*runnable)(struct task_struct *p, u64 enq_flags);
void (*running)(struct task_struct *p);
void (*stopping)(struct task_struct *p, bool runnable);
void (*quiescent)(struct task_struct *p, u64 deq_flags);
bool (*yield)(struct task_struct *from, struct task_struct *to);
bool (*core_sched_before)(struct task_struct *a, struct task_struct
*b);
void (*set_weight)(struct task_struct *p, u32 weight);
void (*set_cpumask)(struct task_struct *p, const struct cpumask
*cpumask);
void (*update_idle)(s32 cpu, bool idle);
s32 (*init_task)(struct task_struct *p, struct scx_init_task_args
*args);
void (*exit_task)(struct task_struct *p, struct scx_exit_task_args
*args);
void (*enable)(struct task_struct *p);
void (*disable)(struct task_struct *p);
void (*dump)(struct scx_dump_ctx *ctx);
void (*dump_cpu)(struct scx_dump_ctx *ctx, s32 cpu, bool idle);
void (*dump_task)(struct scx_dump_ctx *ctx, struct task_struct *p);
#ifdef CONFIG_EXT_GROUP_SCHED;
s32 (*cgroup_init)(struct cgroup *cgrp, struct scx_cgroup_init_args
*args);
void (*cgroup_exit)(struct cgroup *cgrp);
s32 (*cgroup_prep_move)(struct task_struct *p, struct cgroup *from,
struct cgroup *to);
void (*cgroup_move)(struct task_struct *p, struct cgroup *from,
struct cgroup *to);
void (*cgroup_cancel_move)(struct task_struct *p, struct cgroup
*from, struct cgroup *to);
void (*cgroup_set_weight)(struct cgroup *cgrp, u32 weight);
void (*cgroup_set_bandwidth)(struct cgroup *cgrp, u64 period_us, u64
quota_us, u64 burst_us);
void (*cgroup_set_idle)(struct cgroup *cgrp, bool idle);
#endif ;
s32 (*sub_attach)(struct scx_sub_attach_args *args);
void (*sub_detach)(struct scx_sub_detach_args *args);
void (*sub_caps_updated)(const struct scx_cmask *cmask, u64 caps);
void (*sub_ecaps_updated)(s32 cid, u64 before, u64 after);
void (*cpu_online)(s32 cpu);
void (*cpu_offline)(s32 cpu);
s32 (*init_cids)(void);
s32 (*init)(void);
void (*exit)(struct scx_exit_info *info);
u32 dispatch_max_batch;
u64 flags;
u32 timeout_ms;
u32 exit_dump_len;
u64 hotplug_seq;
u32 cid_shard_size;
u32 rescue_bandwidth_ppt;
u32 rescue_quantum_us;
u64 sub_cgroup_id;
char name[SCX_OPS_NAME_LEN];
void *priv;
void (*cpu_acquire)(s32 cpu, struct scx_cpu_acquire_args *args);
void (*cpu_release)(s32 cpu, struct scx_cpu_release_args *args);
};
π Members π
Member-by-member tour time. Every param, every flag, every βthis callback may not do what you thinkβ moment is here. π
select_cpuβ Pick the target CPU for a task which is being woken up. Params:p: task being woken up;prev_cpu: the cpupwas on before sleeping;wake_flags:SCX_WAKE_*. Decision made here isnβt final.pmay be moved to any CPU while it is getting dispatched for execution later. However, aspis not on the rq at this point, getting the eventual execution CPU right here saves a small bit of overhead down the line. If an idle CPU is returned, the CPU is kicked and will try to dispatch. While an explicit custom mechanism can be added,select_cpuserves as the default way to wake up idle CPUs.pmay be inserted into a DSQ directly by callingscx_bpf_dsq_insert. If so, theops.enqueuewill be skipped. Directly inserting intoSCX_DSQ_LOCALwill putpin the local DSQ of the CPU returned by this operation. Note thatselect_cpuis never called for tasks that can only run on a single CPU or tasks with migration disabled, as they donβt have the option to select a different CPU. Seeselect_task_rqfor details. π₯enqueueβ Enqueue a task on the BPF scheduler. Params:p: task being enqueued;enq_flags:SCX_ENQ_*.pis ready to run. Insert directly into a DSQ by callingscx_bpf_dsq_insertor enqueue on the BPF scheduler. If not directly inserted, the bpf scheduler ownspand if it fails to dispatchp, the task will stall. Ifpwas inserted into a DSQ fromops.select_cpu, this callback is skipped. π¦dequeueβ Remove a task from the BPF scheduler. Params:p: task being dequeued;deq_flags:SCX_DEQ_*. Removepfrom the BPF scheduler. This is usually called to isolate the task while updating its scheduling properties (e.g. priority). The ext core keeps track of whether the BPF side owns a given task or not and can gracefully ignore spurious dispatches from BPF side, which makes it safe to not implement this method. However, depending on the scheduling logic, this can lead to confusing behaviors β e.g. scheduling position not being updated across a priority change. πdispatchβ Dispatch tasks from the BPF scheduler and/or user DSQs. Params:cpu: CPU to dispatch tasks for;prev: previous task being switched out. Called when a CPUβs local dsq is empty. The operation should dispatch one or more tasks from the BPF scheduler into the DSQs usingscx_bpf_dsq_insertand/or move from user DSQs into the local DSQ usingscx_bpf_dsq_move_to_local. The maximum number of timesscx_bpf_dsq_insertcan be called without an interveningscx_bpf_dsq_move_to_localis specified byops.dispatch_max_batch. See the comments on top of the two functions for more details. When notNULL,previs an SCX task with its slice depleted. Ifprevis still runnable as indicated by setSCX_TASK_QUEUEDinprev->scx.flags, it is not enqueued yet and will be enqueued afterops.dispatchreturns. To keep executingprev, return without dispatching or moving any tasks. Also seeSCX_OPS_ENQ_LAST. βtickβ Periodic tick. Param:p: task running currently. This operation is called every1/HZseconds on CPUs which are executing an SCX task. Setting a slice of0forpwithscx_bpf_task_set_slicewill trigger an immediate dispatch cycle on the CPU. πrunnableβ A task is becoming runnable on its associated CPU. Params:p: task becoming runnable;enq_flags:SCX_ENQ_*. This and the following three functions can be used to track a taskβs execution state transitions. A task becomes->runnableon a CPU, and then goes through one or more->runningand->stoppingpairs as it runs on the CPU, and eventually becomes->quiescentwhen itβs done running on the CPU.pis becoming runnable on the CPU because itβs waking up (SCX_ENQ_WAKEUP); being moved from another CPU; being restored after temporarily taken off the queue for an attribute change. This and->enqueueare related but not coupled. This operation notifiespβs state transition and may not be followed by->enqueuee.g. whenpis being dispatched to a remote CPU, or whenpis being enqueued on a CPU experiencing a hotplug event. Likewise, a task may be->enqueueβd without being preceded by this operation e.g. after exhausting its slice. πrunningβ A task is starting to run on its associated CPU. Param:p: task starting to run. Note that this callback may be called from a CPU other than the one the task is going to run on. This can happen when a task property is changed (i.e., affinity), sinceset_next_task_scx, which triggers this callback, may run on a CPU different from the taskβs assigned CPU. Therefore, always usescx_bpf_task_cpu(p)to determine the target CPU the task is going to use. See->runnablefor explanation on the task state notifiers. πΈstoppingβ A task is stopping execution. Params:p: task stopping to run;runnable: is taskpstill runnable? Note that this callback may be called from a CPU other than the one the task was running on. This can happen when a task property is changed (i.e., affinity), sincedequeue_task_scx, which triggers this callback, may run on a CPU different from the taskβs assigned CPU. Therefore, always usescx_bpf_task_cpu(p)to retrieve the CPU the task was running on. See->runnablefor explanation on the task state notifiers. If!runnable,->quiescentwill be invoked after this operation returns. π«Άquiescentβ A task is becoming not runnable on its associated CPU. Params:p: task becoming not runnable;deq_flags:SCX_DEQ_*. See->runnablefor explanation on the task state notifiers.pis becoming quiescent on the CPU because itβs sleeping (SCX_DEQ_SLEEP); being moved to another CPU; being temporarily taken off the queue for an attribute change (SCX_DEQ_SCHED_CHANGE). This and->dequeueare related but not coupled. This operation notifiespβs state transition and may not be preceded by->dequeuee.g. whenpis being dispatched to a remote CPU. π¦yieldβ Yield CPU. Params:from: yielding task;to: optional yield target task. IftoisNULL,fromis yielding the CPU to other runnable tasks. The BPF scheduler should ensure that other available tasks are dispatched before the yielding task. Return value is ignored in this case. Iftois not-NULL,fromwants to yield the CPU toto. If the bpf scheduler can implement the request, returntrue; otherwise,false. π«core_sched_beforeβ Task ordering for core-sched. Params:a: task A;b: task B. Used by core-sched to determine the ordering between two tasks. SeeDocumentation/admin-guide/hw-vuln/core-scheduling.rstfor details on core-sched. Bothaandbare runnable and may or may not currently be queued on the BPF scheduler. Should returntrueifashould run beforeb.falseif thereβs no required ordering orbshould run beforea. In a scheduler hierarchy, a pair spanning two schedulers is ordered by the nearest common ancestor implementing this op, so the op may be called on tasks that the scheduler delegated to its sub-schedulers and is not scheduling anymore. Seescx_prio_less. If not specified, the default is ordering them according to when they became runnable. πset_weightβ Set task weight. Params:p: task to set weight for;weight: new weight[1..10000]. Updatepβs weight toweight. πset_cpumaskβ Set CPU affinity. Params:p: task to set CPU affinity for;cpumask: cpumask of cpus thatpcan run on. Updatepβs CPU affinity tocpumask. π§update_idleβ Update the idle state of a CPU. Params:cpu: CPU to update the idle state for;idle: whether entering or exiting the idle state. π
So yeah β this op fires whenever rq's CPU goes into or leaves the idle state πβ¨. But heads up bestie, by default implementing this one disables the built-in idle CPU tracking, and these helpers straight-up stop existing for you:
scx_bpf_select_cpu_dflscx_bpf_select_cpu_andscx_bpf_test_and_clear_cpu_idlescx_bpf_pick_idle_cpu
You also have to implement ops.select_cpu yourself, because the default implementation leans on scx_bpf_select_cpu_dfl π (it's gone, remember?).
Wanna keep the built-in idle tracking? Slap the SCX_OPS_KEEP_BUILTIN_IDLE flag on it. Periodt π
β οΈ Only actual transitions get reported. A CPU that's claimed with an idle pick and kicked but ends up dispatching no task just... goes back to idle with no transition. So if your scheduler is out here tracking idle CPUs on its own, you gotta restore the idle state from ops.dispatch when it returns without a next task to run. No cap, that's on you π«Ά
init_task β initialize a task to run in a BPF scheduler β¨
- p: task to initialize for BPF scheduling
- args: init arguments, see the struct definition
This is the moment of truth: either we're loading a BPF scheduler, or a brand-new task is being forked π. Init p for BPF scheduling. This operation may block and is totally fine to use for allocations β and it's called exactly once for a task. Iconic, one-and-done, love that for us.
Return 0 for success, -errno for failure. If you error out while loading, it aborts loading of the BPF scheduler. If it happens during a fork, it aborts that specific fork. π«
exit_task β exit a previously-running task from the system
- p: task to exit
- args: exit arguments, see the struct definition
Either p is exiting or the BPF scheduler is being unloaded. Do whatever cleanup p needs. Bye bestie ππ
enable β enable BPF scheduling for a task
- p: task to enable BPF scheduling for
Enables p for BPF scheduling. enable is called on p any time it enters SCX, and is always paired with a matching disable. Matching set, we love a coordinated duo π«Ά
disable β disable BPF scheduling for a task
- p: task to disable BPF scheduling for
p is exiting, leaving SCX, or the BPF scheduler is being unloaded. Disable BPF scheduling for p. A disable call is always matched with a prior enable call. No ghosting, no unmatched calls π
dump β dump BPF scheduler state on error
- ctx: debug dump context
Use scx_bpf_dump to generate your scheduler-specific debug dump. Spill the tea βπ
dump_cpu β dump BPF scheduler state for a CPU on error
- ctx: debug dump context
- cpu: CPU to generate debug dump for
- idle: cpu is currently idle without any runnable tasks
Use scx_bpf_dump to generate a scheduler-specific debug dump for cpu. And here's a cute lil detail: if idle is true and this operation produces no output at all, cpu gets skipped for the dump. Quiet = skipped π
dump_task β dump BPF scheduler state for a runnable task on error
- ctx: debug dump context
- p: runnable task to generate debug dump for
Use scx_bpf_dump to generate a scheduler-specific debug dump for p. π§
cgroup_init β initialize a cgroup πΈ
- cgrp: cgroup being initialized
- args: init arguments, see the struct definition
Initialize cgrp for sched_ext β delivered to cgrp's sched either when the BPF scheduler is being loaded or when cgrp is created. This operation may block.
Cgroup handovers generate these ops too, iykyk: an enabling sub-scheduler receives ops.cgroup_init for every cgroup in its subtree while the previous sched receives ops.cgroup_exit β and disabling reverses the two π.
When the BPF scheduler is being loaded, or cgroups are being handed over, cgrp may already have been removed by userspace β a removed cgroup stays schedulable until its dying tasks finish their final context switches π.
Return 0 for success, -errno for failure. An error return while loading aborts loading of the BPF scheduler. During cgroup creation, it aborts that specific cgroup creation.
cgroup_exit β exit a cgroup
- cgrp: cgroup being exited
Exit cgrp for sched_ext β delivered to the sched whose ops.cgroup_init it pairs with, either when the BPF scheduler is being unloaded or when cgrp is destroyed. This operation may block.
For a destroyed cgrp, delivery follows the last scheduling event on it: a removed cgroup stays schedulable until its dying tasks finish their final context switches. Last one out, ghost the lights π―οΈ
cgroup_prep_move β prepare a task to be moved to a different cgroup
- p: task being moved
- from: cgroup p is being moved from
- to: cgroup p is being moved to
Prepare p for the move from cgroup from to to. This operation may block and can be used for allocations.
The cgroup_move ops get delivered to p's sched, and only for moves that don't re-home p. A re-homing move gets reported through ops.exit_task and ops.init_task instead. Also, from and to can reference cgroups the sched never received ops.cgroup_init for β because the cpu controller can be coarser than the sub-scheduler topology π€―.
Return 0 for success, -errno for failure. An error return aborts the migration. β
cgroup_move β commit cgroup move
- p: task being moved
- from: cgroup p is being moved from
- to: cgroup p is being moved to
Commit the move. p is dequeued during this operation. Sealed, delivered, done π
cgroup_cancel_move β cancel cgroup move
- p: task whose cgroup move is being canceled
- from: cgroup p was being moved from
- to: cgroup p was being moved to
p was cgroup_prep_move'd but failed before reaching cgroup_move, so... undo the preparation. Ctrl+Z energy π
cgroup_set_weight β a cgroup's weight is being changed
- cgrp: cgroup whose weight is being updated
- weight: new weight [1..10000]
Update cgrp's weight to weight.
Big important vibe check: knobs of a cgroup belong to the parent, so the set_* ops get delivered to cgrp's parent's sched π³. That sched may never have seen ops.cgroup_init for cgrp β at a sub-scheduler attach point, the parent sched tracks cgrp through ops.sub_attach instead.
cgroup_set_bandwidth β a cgroup's bandwidth is being changed
- cgrp: cgroup whose bandwidth is being updated
- period_us: bandwidth control period
- quota_us: bandwidth control quota
- burst_us: bandwidth control burst
Update cgrp's bandwidth control parameters. This comes from the cpu.max cgroup interface. This operation may block.
quota_us / period_us determines the CPU bandwidth cgrp is entitled to. For example, if period_us is 1_000_000 and quota_us is 2_500_000, cgrp is entitled to 2.5 CPUs π°. burst_us can be interpreted in the same fashion and specifies how much cgrp can burst temporarily. The specific control mechanism β and thus the interpretation of period_us and burstiness β is up to the BPF scheduler. Your rules, your kingdom π.
Delivery follows the same rule as cgroup_set_weight (parent's sched, remember).
cgroup_set_idle β a cgroup's idle state is being changed
- cgrp: cgroup whose idle state is being updated
- idle: whether the cgroup is entering or exiting idle state
Update cgrp's idle state to idle. This callback fires when a cgroup transitions between idle and non-idle states, letting the BPF scheduler adjust its behavior accordingly. Chill mode: on/off π§ββοΈ
Delivery follows the same rule as cgroup_set_weight.
sub_attach β attach a sub-scheduler
- args: argument container, see the struct definition
Return 0 to accept the sub-scheduler. -errno to reject. Simple as that, bestie β swipe right or swipe left π
sub_detach β detach a sub-scheduler
- args: argument container, see the struct definition
(The un-match. See the struct definition for args.)
sub_caps_updated β caps on this sub-sched's shard changed
- cmask: cids whose caps changed (
cmask->baseidentifies the shard) - caps:
SCX_CAP_*that changed
Invoked after grant or revoke modifies caps on a shard. There can be only one in-flight invocation per shard π¦. cmask and caps coalesce all changes since the last delivery. Direction (set vs cleared) isn't encoded β so query current state with scx_bpf_sub_caps.
β οΈ It's delivered asynchronously after the change is recorded, and may run before it takes effect on any given cpu. Use it to track which caps the sub-sched holds and propagate to its own children β not to decide if a task can run on a cpu right now. sub_ecaps_updated reports that per cpu, once it's actually in effect. Timing is everything, fr fr β°
May call scx_bpf_sub_grant / scx_bpf_sub_revoke on children.
sub_ecaps_updated β this sub-sched's effective caps on a cid changed
- cid: the cid whose effective caps changed
- before: effective caps as of the last delivery
- after: effective caps now
Invoked when this sub-sched's effective caps on cid change β once the change is in effect on the cpu β
. Runs in dispatch context with the rq lock held, and can perform all operations allowed in ops.dispatch, including inserting/moving tasks. Full dispatch privileges, no cap π§’π
cpu_online β a CPU became online
- cpu: CPU which just came up
cpu just came online. Beforehand, cpu will not call ops.enqueue or ops.dispatch, nor run tasks associated with other CPUs. Fresh out the box π±
cpu_offline β a CPU is going offline
- cpu: CPU which is going offline
cpu is going offline. Afterwards, cpu will not call ops.enqueue or ops.dispatch, nor run tasks associated with other CPUs. Going to sleep, do not disturb π΄
init_cids β finalize the cid layout (cid-form only)
Runs after the default cid layout is built, before caps and shards are finalized. A cid-form scheduler may call scx_bpf_cid_override here for a custom layout. Ignored for cpu-form schedulers. π¨
init β initialize the BPF scheduler
(Yes. That's the whole thing. Go be the main character β¨)
exit β clean up after the BPF scheduler
- info: Exit info
Heads up: ops.exit is also called on ops.init failure, which is a bit unusual π« . It's that way to allow rich reporting through info on how ops.init failed. Honestly? Thoughtful. We stan a diagnostic king π.
dispatch_max_batch β max nr of tasks that dispatch can dispatch
That's it. That's the setting. Numbers, bestie π’.
flags β SCX_OPS_* flags
Your flag collection. Slap the SCX_OPS_* ones you want in here π
timeout_ms β the max wait before a task gets scheduled
The maximum amount of time, in milliseconds, that a runnable task should be able to wait before being scheduled. The maximum timeout may not exceed the default timeout of 30 seconds β³.
Defaults to the maximum allowed timeout value of 30 seconds.
exit_dump_len β scx_exit_info.dump buffer length
If 0, the default value of 32768 is used. π¦
hotplug_seq β hotplug event detection sequence number
A sequence number that may be set by the scheduler to detect when a hotplug event has occurred during the loading process. If 0, no detection occurs. Otherwise, the scheduler will fail to load if the sequence number does not match scx_hotplug_seq on the enable path. π
cid_shard_size β target number of CIDs per shard
Shards are contiguous CID ranges used as operation and locking domains for sub-scheduling π§©. Each LLC is divided into ceil(nr_cpus / cid_shard_size) shards, then cores are distributed across them evenly. If one core has more logical CPUs than cid_shard_size, its shard will become larger than cid_shard_size. Values above SCX_CID_SHARD_MAX_CPUS are capped. 0 means use the default (24).
rescue_bandwidth_ppt β rescue execution bandwidth in parts per thousand
That's the description, babe π©Ή
The fraction of each CPU's time that may legitimately be spent running tasks off that CPU's rescue DSQ. Bump it up and rescues get admitted + escalated faster β peep rescue_quantum_us for the deets, bestie π
Only the root scheduler's value counts, fr fr. 0 = default of 20 (that's 2%). May not exceed 250 (25%). And SCX_RESCUE_DISABLE? That one just kills rescue altogether β after that, SCX_ENQ_RESCUE inserts get rejected exactly like any other insert that's missing the caps π
rescue_quantum_usβ rescue execution quantum, in microseconds β±οΈ
How much CPU time each individual rescue gets. Rescues run one at a time per CPU, and admissions are paced so rescue execution stays inside rescue_bandwidth_ppt β with the defaults that's literally one 5ms rescue every 250ms π«Ά If the queue is crowded, it round-robins on the quantum split across the waiters, floored at 1ms. A rescue that gets stuck eventually escalates to forced execution. Bigger quantum = fewer CPU interrupts, but each one drags on longer and pushes rescues further apart.
Only the root scheduler's value is used. 0 means the default (5000). Non-zero values must land within [1000, 100000]. Any value too short for the kernel to meter gets lifted silently β no warning, no drama, it just happens π
sub_cgroup_idβ when it's>1, attach the scheduler as a sub-scheduler on the cgroup you specified πnameβ the BPF scheduler's name, iconic as ever π
Must be a non-zero valid BPF object name including only isalnum, directory, /sys/kernel/sched_ext/root/ops for the root scheduler, while the BPF scheduler is enabled.
cpu_acquireβ a CPU is becoming available to the BPF scheduler.cpu: the CPU being acquired by the BPF scheduler.args: acquire arguments, see the struct definition.
A CPU that had previously been released from the BPF scheduler is now back under its control. Deprecated β use SCX_ENQ_IMMED instead π
cpu_releaseβ a CPU is being taken away from the BPF scheduler.cpu: the CPU being released by the BPF scheduler.args: release arguments, see the struct definition.
That CPU is no longer under the control of the BPF scheduler. Could be because a higher priority sched_class preempted it, though ngl there may be other reasons too. The caller should check args->reason to figure out the actual cause. Deprecated β use SCX_ENQ_IMMED instead π
β¨ Description β¨
A BPF scheduler can cook up a totally arbitrary scheduling policy by implementing and loading operations into this table. And plot twist π β a userland scheduling policy can also be implemented by using the BPF scheduler as a shim layer. Slay.
π SEE ALSO π
Kernel file ./kernel/sched/ext/internal.h, scx_add_event(9), __scx_add_event(9), struct scx_enable_args(9), struct sched_ext_ops_cid(9), scx_arena_to_kaddr(9), scx_kaddr_to_arena(9), DEFINE_SCX_COMPAT_MARKER(9), scx_root_protected_live(9), scx_root_protected(9), scx_bypass_dsp_enabled(9), scx_ops_sanitize_err(9), scx_has_subs(9), scx_task_sched(9), scx_task_sched_rcu(9), scx_task_on_sched(9), scx_prog_sched(9), scx_parent(9)
That's the whole page, bestie β you ate that π This one lives in ./kernel/sched/ext, it's the October 2026 era, and yes it's struct sched_ext_ops(9) in all her main character glory ππ Go forth and schedule, periodt β¨
π¦ where this page comes from
- nixpkgs unstable: linux-manual 7.3.1
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms