Optimizing an SPH Simulation, Part 1: Speed From the Compiler
Before touching a single line of my fluid simulation, I let the compiler do the work. Just changing flags got me from 1x to roughly 40x.
If you have been working with UNIX (Or at least SUS-compliant systems), you most probably already know about UID/GID.
Of course, it’s one of the most easy to grasp concepts there is: You have a User Identifier, and a Group Identifier, assigned to your user
But as most things UNIX, the inner workings are a bit more complicated than it seems
This article is based on FreeBSD stable/15 @ afaf984ae0dc
Let’s have a look at the kernel’s representation of a process: sys/sys/proc.h. Of course, I’ve elipsed a fair amount of unrelated fields.
Among the hundred of fields in the structure, we can find:
/*
* Process structure.
*/
struct proc {
TAILQ_HEAD(, thread) p_threads; /* (c) all threads. */
struct ucred *p_ucred; /* (c) Process owner's identity. */
struct pwddesc *p_pd; /* (b) Cwd, chroot, jail, umask */
pid_t p_pid; /* (b) Process identifier. */
struct proc *p_pptr; /* (c + e) Pointer to parent process. */
LIST_HEAD(, proc) p_children; /* (e) Pointer to list of children. */
};Each process has a list of threads:
/*
* Kernel runnable context (thread).
* This is what is put to sleep and reactivated.
* Thread context. Processes may have multiple threads.
*/
struct thread {
lwpid_t td_tid; /* (b) Thread ID. */
struct ucred *td_realucred; /* (k) Reference to credentials. */
struct ucred *td_ucred; /* (k) Used credentials, temporarily switchable. */
};(This struct is also elipsed for brevity)
What’s interesting is the ucred structure, because it defines the credentials for a thread / proc
/*
* Credentials.
*
* Please do not inspect cr_uid directly to determine superuserness. The
* priv(9) interface should be used to check for privilege.
*
* Lock reference:
* c - cr_mtx
*
* Unmarked fields are constant after creation.
*
* See "Credential management" comment in kern_prot.c for more information.
*/
struct ucred {
struct mtx cr_mtx;
long cr_ref; /* (c) reference count */
u_int cr_users; /* (c) proc + thread using this cred */
u_int cr_flags; /* credential flags */
struct auditinfo_addr cr_audit; /* Audit properties. */
int cr_ngroups; /* number of supplementary groups */
#define cr_startcopy cr_uid
uid_t cr_uid; /* effective user id */
uid_t cr_ruid; /* real user id */
uid_t cr_svuid; /* saved user id */
gid_t cr_gid; /* effective group id */
gid_t cr_rgid; /* real group id */
gid_t cr_svgid; /* saved group id */
struct uidinfo *cr_uidinfo; /* per euid resource consumption */
struct uidinfo *cr_ruidinfo; /* per ruid resource consumption */
struct prison *cr_prison; /* jail(2) */
struct loginclass *cr_loginclass; /* login class */
void *cr_pspare2[2]; /* general use 2 */
#define cr_endcopy cr_label
struct label *cr_label; /* MAC label */
gid_t *cr_groups; /* groups */
int cr_agroups; /* Available groups */
/* storage for small groups */
gid_t cr_smallgroups[CRED_SMALLGROUPS_NB];
};We have:
cr_uid : Effective User ID ((E)UID)cr_ruid: Real User ID (RUID)cr_svuid: Saved User ID (SVUID)And their equivalent for groups
Now, we need to understand how each of these is used.
The effective user ID, also known as the UID seems to be the simplest
When a execve syscall is triggered, the kernel first checks the permissions of the file and opens the file (exec_check_permissions) and then, maps the first page (exec_map_first_page)
It creates an image_params structure that will be the basis for the creation of the process & thread
It then checks for the presence of SUID/SGID bits on the file. If so, it will switch to credential_changing mode.
credential_changing mode (aka SUID)
#the euid and egid of the newly created image_param will be set to those read from the file (Read during exec_check_permissions !), depending on the SUID/SGID bits respectively.
!credential_changing mode
#The euid and egid are set from the parent’s process credentials.
In both case, svuid & svgid will then be copied from the euid and egid
In the end, it will always be:
uid == svuidgid == svgidYou can read the source in sys/kern/kern_exec.c in the do_execve function
There is one way to change the credentials of a process. Through the kern_setcred function (in sys/kern/kern_prot.c)), we can update a proc’s credentials.
Please note that while the function takes a thread* as it’s first argument, it’s indeed the proc that will be updated.
struct proc *const p = td->td_proc;
// ...
cred_set = proc_set_cred_enforce_proc_lim(p, new_cred);Also, every credential can be changed through this calls.
/*
* Change user IDs.
*/
if (flags & SETCREDF_UID)
change_euid(new_cred, uip);
if (flags & SETCREDF_RUID)
change_ruid(new_cred, ruip);
if (flags & SETCREDF_SVUID)
change_svuid(new_cred, wcred->sc_svuid);
/*
* Change groups.
*/
if (flags & SETCREDF_SUPP_GROUPS)
crsetgroups_internal(new_cred, wcred->sc_supp_groups_nb,
wcred->sc_supp_groups);
if (flags & SETCREDF_GID)
change_egid(new_cred, wcred->sc_gid);
if (flags & SETCREDF_RGID)
change_rgid(new_cred, wcred->sc_rgid);
if (flags & SETCREDF_SVGID)
change_svgid(new_cred, wcred->sc_svgid);The changes are however verified in two ways:
priv_check_cred, to make sure that the process has the rights to update it’s credentialsAnyway, MAC will probably be the subject of another post
Once everything is checked and is in order, then the new privileges are copied in place of the old ones.
These 3 functions all update the euid, via change_euid.
The difference is simple:
sys_setuid : Update the euidsys_setreuid : Updates euid and reuid. If the Real User ID (ruid) doesn’t match the new UID, then update the svuidsys_setresuid : Updates euid, reuid and svuidFrom a farther standpoint, cr_uid appears \(403\) times in FreeBSD across \(114\) files. Which in itself doesn’t mean much.
But, it’s much more than cr_ruid with \(147\) matches, or cr_svuid with \(43\) matches.
The RUID is copied from the creating process.
/*
* In-kernel implementation of execve(). All arguments are assumed to be
* userspace pointers from the passed thread.
*/
static int
do_execve(struct thread *td, struct image_args *args, struct mac *mac_p,
struct vmspace *oldvmspace)
{
struct proc *p = td->td_proc;
struct ucred *oldcred;
// ...
oldcred = p->p_ucred;
// ...
if (credential_changing &&
#ifdef CAPABILITY_MODE
((oldcred->cr_flags & CRED_FLAG_CAPMODE) == 0) &&
#endif
(imgp->vp->v_mount->mnt_flag & MNT_NOSUID) == 0 &&
(p->p_flag & P_TRACED) == 0) {
imgp->newcred = crdup(oldcred);
// ...
} else {
if (oldcred->cr_svuid != oldcred->cr_uid ||
oldcred->cr_svgid != oldcred->cr_gid) {
// ...
imgp->newcred = crdup(oldcred);
// ...
}
}Whether RUID == EUID depends on the SUID/SGID bits.
When a SUID binary starts, the origin EUID is saved here before the EUID is changed.
The svuid is saved on process creation.
When you call setuid to change the UID of a process, the EUID is updated and the cr_svuid remains unchanged. When you call again setuid(uid_t uid), if uid == cr_svuid, you are allowed to return to your original uid
afaf984ae0dcBefore touching a single line of my fluid simulation, I let the compiler do the work. Just changing flags got me from 1x to roughly 40x.
Multiboot2 drops you in 32-bit protected mode; Limine hands you a 64-bit environment. A single entry trampoline that handles both without forking the kernel.
I’m lazy. Not lazy enough to not exercise, but lazy enough to want my gym programs to make themselves. Coincidentally, I’ve started reading “Linear Algebra and its applications”, and one section caught my attention: How to apply linear algebra to real world problems. As it turns out, there are lots of ways to optimize processes around you if you know how to convert your real-life problem, into a mathematical model.