sysvinit: init.c : waitpid
- schorsch_76
- Beiträge: 2597
- Registriert: 06.11.2007 16:00:42
- Lizenz eigener Beiträge: MIT Lizenz
sysvinit: init.c : waitpid
Hallo,
Ich versuch gerade den Code wie sysvinit Kindprozesse spawnt zu verstehen. In der Funktion spawn() wird 2 mal geforkt und dann im "Kind/Vater" waitpid() aufgerufen. In Zeile 1116 unter [1] ist dieser waitpid() Aufruf.
Meine Fragen sind:
a) Warum wird hier double fork() gemacht? Auch um die Rechte abzulegen wie ein normaler daemon? Um das controlling tty zu setzen?
b) Warum muss hier, nachdem per execvp (Zeile 1163) im Kind/Kind im "Kind/Vater" waitpid() aufgerufen werden?
Meine Idee war das hier einfach das Kind geforkt wird und dann per exec() es dann weiter geht.
Fragen über Fragen ...
[1] https://sources.debian.net/src/sysvinit ... rc/init.c/
[2] https://linux.die.net/man/2/waitpid
Ich versuch gerade den Code wie sysvinit Kindprozesse spawnt zu verstehen. In der Funktion spawn() wird 2 mal geforkt und dann im "Kind/Vater" waitpid() aufgerufen. In Zeile 1116 unter [1] ist dieser waitpid() Aufruf.
Meine Fragen sind:
a) Warum wird hier double fork() gemacht? Auch um die Rechte abzulegen wie ein normaler daemon? Um das controlling tty zu setzen?
b) Warum muss hier, nachdem per execvp (Zeile 1163) im Kind/Kind im "Kind/Vater" waitpid() aufgerufen werden?
Meine Idee war das hier einfach das Kind geforkt wird und dann per exec() es dann weiter geht.
Fragen über Fragen ...
[1] https://sources.debian.net/src/sysvinit ... rc/init.c/
[2] https://linux.die.net/man/2/waitpid
Re: sysvinit: init.c : waitpid
Siehe hier: http://stackoverflow.com/questions/8813 ... g-a-daemonschorsch_76 hat geschrieben: a) Warum wird hier double fork() gemacht? Auch um die Rechte abzulegen wie ein normaler daemon? Um das controlling tty zu setzen?
Use ed once in a while!
- schorsch_76
- Beiträge: 2597
- Registriert: 06.11.2007 16:00:42
- Lizenz eigener Beiträge: MIT Lizenz
Re: sysvinit: init.c : waitpid
Ich weis schon, warum man fork()/setsid()/fork() in einem daemon macht. Wir sind hier aber in pid1. sysvinit. Deshalb frag ich. Wenn ich allen Subprozessen die Möglichkeit nehme ein Terminal zu bekommen, kann ein weiterer Subprozess ja auch kein Terminal mehr bekommen. Wie bekommt dann bsp. ein getty ein Controlling terminal?
Hier ist die Funktion spawn() aus sysvinit
Hier ist die Funktion spawn() aus sysvinit
Code: Alles auswählen
/*
* Fork and execute.
*
* This function is too long and indents too deep.
*
*/
static
pid_t spawn(CHILD *ch, int *res)
{
char *args[16]; /* Argv array */
char buf[136]; /* Line buffer */
int f, st; /* Scratch variables */
char *ptr; /* Ditto */
time_t t; /* System time */
int oldAlarm; /* Previous alarm value */
char *proc = ch->process; /* Command line */
pid_t pid, pgrp; /* child, console process group. */
sigset_t nmask, omask; /* For blocking SIGCHLD */
struct sigaction sa;
*res = -1;
buf[sizeof(buf) - 1] = 0;
/* Skip '+' if it's there */
if (proc[0] == '+') proc++;
ch->flags |= XECUTED;
if (ch->action == RESPAWN || ch->action == ONDEMAND) {
/* Is the date stamp from less than 2 minutes ago? */
time(&t);
if (ch->tm + TESTTIME > t) {
ch->count++;
} else {
ch->count = 0;
ch->tm = t;
}
/* Do we try to respawn too fast? */
if (ch->count >= MAXSPAWN) {
initlog(L_VB,
"Id \"%s\" respawning too fast: disabled for %d minutes",
ch->id, SLEEPTIME / 60);
ch->flags &= ~RUNNING;
ch->flags |= FAILING;
/* Remember the time we stopped */
ch->tm = t;
/* Try again in 5 minutes */
oldAlarm = alarm(0);
if (oldAlarm > SLEEPTIME || oldAlarm <= 0) oldAlarm = SLEEPTIME;
alarm(oldAlarm);
return(-1);
}
}
/* See if there is an "initscript" (except in single user mode). */
if (access(INITSCRIPT, R_OK) == 0 && runlevel != 'S') {
/* Build command line using "initscript" */
args[1] = SHELL;
args[2] = INITSCRIPT;
args[3] = ch->id;
args[4] = ch->rlevel;
args[5] = "unknown";
for(f = 0; actions[f].name; f++) {
if (ch->action == actions[f].act) {
args[5] = actions[f].name;
break;
}
}
args[6] = proc;
args[7] = NULL;
} else if (strpbrk(proc, "~`!$^&*()=|\\{}[];\"'<>?")) {
/* See if we need to fire off a shell for this command */
/* Give command line to shell */
args[1] = SHELL;
args[2] = "-c";
strcpy(buf, "exec ");
strncat(buf, proc, sizeof(buf) - strlen(buf) - 1);
args[3] = buf;
args[4] = NULL;
} else {
/* Split up command line arguments */
buf[0] = 0;
strncat(buf, proc, sizeof(buf) - 1);
ptr = buf;
for(f = 1; f < 15; f++) {
/* Skip white space */
while(*ptr == ' ' || *ptr == '\t') ptr++;
args[f] = ptr;
/* May be trailing space.. */
if (*ptr == 0) break;
/* Skip this `word' */
while(*ptr && *ptr != ' ' && *ptr != '\t' && *ptr != '#')
ptr++;
/* If end-of-line, break */
if (*ptr == '#' || *ptr == 0) {
f++;
*ptr = 0;
break;
}
/* End word with \0 and continue */
*ptr++ = 0;
}
args[f] = NULL;
}
args[0] = args[1];
while(1) {
/*
* Block sigchild while forking.
*/
sigemptyset(&nmask);
sigaddset(&nmask, SIGCHLD);
sigprocmask(SIG_BLOCK, &nmask, &omask);
if ((pid = fork()) == 0) {
close(0);
close(1);
close(2);
if (pipe_fd >= 0) close(pipe_fd);
sigprocmask(SIG_SETMASK, &omask, NULL);
/*
* In sysinit, boot, bootwait or single user mode:
* for any wait-type subprocess we _force_ the console
* to be its controlling tty.
*/
if (strchr("*#sS", runlevel) && ch->flags & WAITING) {
/*
* We fork once extra. This is so that we can
* wait and change the process group and session
* of the console after exit of the leader.
*/
setsid();
if ((f = console_open(O_RDWR|O_NOCTTY)) >= 0) {
/* Take over controlling tty by force */
(void)ioctl(f, TIOCSCTTY, 1);
dup(f);
dup(f);
}
/*
* 4 Sep 2001, Andrea Arcangeli:
* Fix a race in spawn() that is used to deadlock init in a
* waitpid() loop: must set the childhandler as default before forking
* off the child or the chld_handler could run before the waitpid loop
* has a chance to find its zombie-child.
*/
SETSIG(sa, SIGCHLD, SIG_DFL, SA_RESTART);
if ((pid = fork()) < 0) {
initlog(L_VB, "cannot fork: %s",
strerror(errno));
exit(1);
}
if (pid > 0) {
pid_t rc;
/*
* Ignore keyboard signals etc.
* Then wait for child to exit.
*/
SETSIG(sa, SIGINT, SIG_IGN, SA_RESTART);
SETSIG(sa, SIGTSTP, SIG_IGN, SA_RESTART);
SETSIG(sa, SIGQUIT, SIG_IGN, SA_RESTART);
while ((rc = waitpid(pid, &st, 0)) != pid) // <<< warum hier ein waitpid?
if (rc < 0 && errno == ECHILD)
break;
/*
* Small optimization. See if stealing
* controlling tty back is needed.
*/
pgrp = tcgetpgrp(f);
if (pgrp != getpid())
exit(0);
/*
* Steal controlling tty away. We do
* this with a temporary process.
*/
if ((pid = fork()) < 0) {
initlog(L_VB, "cannot fork: %s",
strerror(errno));
exit(1);
}
if (pid == 0) {
setsid();
(void)ioctl(f, TIOCSCTTY, 1);
exit(0);
}
while((rc = waitpid(pid, &st, 0)) != pid)
if (rc < 0 && errno == ECHILD)
break;
exit(0);
}
/* Set ioctl settings to default ones */
console_stty();
} else {
setsid();
if ((f = console_open(O_RDWR|O_NOCTTY)) < 0) {
initlog(L_VB, "open(%s): %s", console_dev,
strerror(errno));
f = open("/dev/null", O_RDWR);
}
dup(f);
dup(f);
}
/*
* Update utmp/wtmp file prior to starting
* any child. This MUST be done right here in
* the child process in order to prevent a race
* condition that occurs when the child
* process' time slice executes before the
* parent (can and does happen in a uniprocessor
* environment). If the child is a getty and
* the race condition happens, then init's utmp
* update will happen AFTER the getty runs
* and expects utmp to be updated already!
*
* Do NOT log if process field starts with '+'
* FIXME: that's for compatibility with *very*
* old getties - probably it can be taken out.
*/
if (ch->process[0] != '+')
write_utmp_wtmp("", ch->id, getpid(), INIT_PROCESS, "");
/* Reset all the signals, set up environment */
for(f = 1; f < NSIG; f++) SETSIG(sa, f, SIG_DFL, SA_RESTART);
environ = init_buildenv(1);
/*
* Execute prog. In case of ENOEXEC try again
* as a shell script.
*/
execvp(args[1], args + 1);
if (errno == ENOEXEC) {
args[1] = SHELL;
args[2] = "-c";
strcpy(buf, "exec ");
strncat(buf, proc, sizeof(buf) - strlen(buf) - 1);
args[3] = buf;
args[4] = NULL;
execvp(args[1], args + 1);
}
initlog(L_VB, "cannot execute \"%s\"", args[1]);
if (ch->process[0] != '+')
write_utmp_wtmp("", ch->id, getpid(), DEAD_PROCESS, NULL);
exit(1);
}
*res = pid;
sigprocmask(SIG_SETMASK, &omask, NULL);
INITDBG(L_VB, "Started id %s (pid %d)", ch->id, pid);
if (pid == -1) {
initlog(L_VB, "cannot fork, retry..");
do_sleep(5);
continue;
}
return(pid);
}
}
/*
* Start a child running!
*/
static
void startup(CHILD *ch)
{
/*
* See if it's disabled
*/
if (ch->flags & FAILING) return;
switch(ch->action) {
case SYSINIT:
case BOOTWAIT:
case WAIT:
case POWERWAIT:
case POWERFAILNOW:
case POWEROKWAIT:
case CTRLALTDEL:
if (!(ch->flags & XECUTED)) ch->flags |= WAITING;
case KBREQUEST:
case BOOT:
case POWERFAIL:
case ONCE:
if (ch->flags & XECUTED) break;
case ONDEMAND:
case RESPAWN:
ch->flags |= RUNNING;
(void)spawn(ch, &(ch->pid));
break;
}
}
Re: sysvinit: init.c : waitpid
Ah, hab nicht so genau gelesen.schorsch_76 hat geschrieben:Ich weis schon, warum man fork()/setsid()/fork() in einem daemon macht. Wir sind hier aber in pid1. sysvinit.
Ich kann's dir nicht sagen.Deshalb frag ich. Wenn ich allen Subprozessen die Möglichkeit nehme ein Terminal zu bekommen, kann ein weiterer Subprozess ja auch kein Terminal mehr bekommen. Wie bekommt dann bsp. ein getty ein Controlling terminal?
Auf den ersten Blick sind mir aber folgende Kommentare aufgefallen:
Code: Alles auswählen
/*
* In sysinit, boot, bootwait or single user mode:
* for any wait-type subprocess we _force_ the console
* to be its controlling tty.
*/
if (strchr("*#sS", runlevel) && ch->flags & WAITING) {
/*
* We fork once extra. This is so that we can
* wait and change the process group and session
* of the console after exit of the leader.
*/
Und die beiden von die angefragten Stellen betreffen ja nur die hier gezeigte Bedingung, also den Single-User-Mode und boot/bootwait (was auch immer das ist).
Vielleicht hilft dir das ja weiter. Mehr habe ich nicht zu bieten ... bin aber an der Antwort am Ende interessiert.
Use ed once in a while!
- schorsch_76
- Beiträge: 2597
- Registriert: 06.11.2007 16:00:42
- Lizenz eigener Beiträge: MIT Lizenz
Re: sysvinit: init.c : waitpid
Ich werde dazu mal mein "Linux Programming Interface" Buch in Holzversion befragen müssen ....
- schorsch_76
- Beiträge: 2597
- Registriert: 06.11.2007 16:00:42
- Lizenz eigener Beiträge: MIT Lizenz
Re: sysvinit: init.c : waitpid
Ich habs jetzt kapiert:
das macht genau das was da steht und zwar nur im Singleuser mode. Ansonsten gibt's 'nen normalen fork() und exec().
/*
* Steal controlling tty away. We do
* this with a temporary process.
*/
Die waitpid() schleife
wartet hier auf pid "4". Pid 4 hat hier mit setsid() die Session übernommen und beendet sich gleich.
Pid "3" beendet sich auch.
Pid 2 macht dann den execcve()
Getty macht in der main() ein setsid() und öffnet die Console. Da hier keine Rechte abgelegt werden und user geändert wird, kann das auch gemacht werden.
// fgetty
[1] https://sources.debian.net/src/fgetty/0.7-2/fgetty.c/
das macht genau das was da steht und zwar nur im Singleuser mode. Ansonsten gibt's 'nen normalen fork() und exec().
/*
* Steal controlling tty away. We do
* this with a temporary process.
*/
Die waitpid() schleife
Code: Alles auswählen
while ((rc = waitpid(pid, &st, 0)) != pid)
if (rc < 0 && errno == ECHILD)
break;
Pid "3" beendet sich auch.
Pid 2 macht dann den execcve()
Code: Alles auswählen
pid_t spawn(CHILD *ch, int *res)
{
....
// wenn ein spawn noetig ist ...
if (ch->action == RESPAWN || ch->action == ONDEMAND) { baue commando parameter fuer exec }
args[0] = args[1];
while(1) {
/*
* Block sigchild while forking.
*/
...
if ((pid = fork()) == 0) {
// der gewollte pid "2". wird exec'ed
close(0);
close(1);
close(2);
if (pipe_fd >= 0) close(pipe_fd);
sigprocmask(SIG_SETMASK, &omask, NULL);
/*
* In sysinit, boot, bootwait or single user mode:
* for any wait-type subprocess we _force_ the console
* to be its controlling tty.
*/
if (strchr("*#sS", runlevel) && ch->flags & WAITING) {
/*
* We fork once extra. This is so that we can
* wait and change the process group and session
* of the console after exit of the leader.
*/
setsid();
if ((f = console_open(O_RDWR|O_NOCTTY)) >= 0) {
/* Take over controlling tty by force */
(void)ioctl(f, TIOCSCTTY, 1);
dup(f);
dup(f);
}
/*
* 4 Sep 2001, Andrea Arcangeli:
* Fix a race in spawn() that is used to deadlock init in a
* waitpid() loop: must set the childhandler as default before forking
* off the child or the chld_handler could run before the waitpid loop
* has a chance to find its zombie-child.
*/
SETSIG(sa, SIGCHLD, SIG_DFL, SA_RESTART);
if ((pid = fork()) < 0) {
initlog(L_VB, "cannot fork: %s",
strerror(errno));
exit(1);
}
if (pid > 0) {
// pid "3"
pid_t rc;
/*
* Ignore keyboard signals etc.
* Then wait for child to exit.
*/
SETSIG(sa, SIGINT, SIG_IGN, SA_RESTART);
SETSIG(sa, SIGTSTP, SIG_IGN, SA_RESTART);
SETSIG(sa, SIGQUIT, SIG_IGN, SA_RESTART);
while ((rc = waitpid(pid, &st, 0)) != pid)
if (rc < 0 && errno == ECHILD)
break;
/*
* Small optimization. See if stealing
* controlling tty back is needed.
*/
pgrp = tcgetpgrp(f);
if (pgrp != getpid())
exit(0);
/*
* Steal controlling tty away. We do
* this with a temporary process.
*/
if ((pid = fork()) < 0) {
initlog(L_VB, "cannot fork: %s",
strerror(errno));
exit(1);
}
if (pid == 0) {
// pid "4"
setsid();
(void)ioctl(f, TIOCSCTTY, 1);
exit(0);
}
while((rc = waitpid(pid, &st, 0)) != pid)
if (rc < 0 && errno == ECHILD)
break;
exit(0);
}
/* Set ioctl settings to default ones */
console_stty();
} else {
// pid "2"
setsid();
if ((f = console_open(O_RDWR|O_NOCTTY)) < 0) {
initlog(L_VB, "open(%s): %s", console_dev,
strerror(errno));
f = open("/dev/null", O_RDWR);
}
dup(f);
dup(f);
}
/*
* Update utmp/wtmp file prior to starting
* any child. This MUST be done right here in
* the child process in order to prevent a race
* condition that occurs when the child
* process' time slice executes before the
* parent (can and does happen in a uniprocessor
* environment). If the child is a getty and
* the race condition happens, then init's utmp
* update will happen AFTER the getty runs
* and expects utmp to be updated already!
*
* Do NOT log if process field starts with '+'
* FIXME: that's for compatibility with *very*
* old getties - probably it can be taken out.
*/
if (ch->process[0] != '+')
write_utmp_wtmp("", ch->id, getpid(), INIT_PROCESS, "");
/* Reset all the signals, set up environment */
for(f = 1; f < NSIG; f++) SETSIG(sa, f, SIG_DFL, SA_RESTART);
environ = init_buildenv(1);
/*
* Execute prog. In case of ENOEXEC try again
* as a shell script.
*/
execvp(args[1], args + 1);
if (errno == ENOEXEC) {
args[1] = SHELL;
args[2] = "-c";
strcpy(buf, "exec ");
strncat(buf, proc, sizeof(buf) - strlen(buf) - 1);
args[3] = buf;
args[4] = NULL;
execvp(args[1], args + 1);
}
initlog(L_VB, "cannot execute \"%s\"", args[1]);
if (ch->process[0] != '+')
write_utmp_wtmp("", ch->id, getpid(), DEAD_PROCESS, NULL);
exit(1);
}
*res = pid;
sigprocmask(SIG_SETMASK, &omask, NULL);
INITDBG(L_VB, "Started id %s (pid %d)", ch->id, pid);
if (pid == -1) {
initlog(L_VB, "cannot fork, retry..");
do_sleep(5);
continue;
}
return(pid);
}
}
// fgetty
Code: Alles auswählen
void open_tty() {
...
setsid();
...
Re: sysvinit: init.c : waitpid
... wenn's nur immer so einfach waere!schorsch_76 hat geschrieben:Ich habs jetzt kapiert:
das macht genau das was da steht
Use ed once in a while!
Re: sysvinit: init.c : waitpid
he, was macht ihr da? Sieht aus wie Vorbereitungen für den Katastrophenfall, wenn es bei Debian kein init mehr gibt
Beware of programmers who carry screwdrivers.
Re: sysvinit: init.c : waitpid
Ihr? Ich mache gar nichts ... haenge bloss hier ein bisschen rum und gucke neugierig was andere so machen.cosmac hat geschrieben:he, was macht ihr da?
(Und ganz im Ernst: Ich finde es super, wenn sich jemand tatsaechlich mal in den Code von so zentralen Komponenten eines Unix-Systems einliest. Das bringt soviel Verstaendnis, und trotzdem wir machen das viel zu selten. Ist halt anstrengend, wie man anhand dieses Threads sehen kann, ... aber ich find's grossartig! *Darum* haenge ich hier rum.)
Use ed once in a while!
- schorsch_76
- Beiträge: 2597
- Registriert: 06.11.2007 16:00:42
- Lizenz eigener Beiträge: MIT Lizenz
Re: sysvinit: init.c : waitpid
Mir ging es zu verstehen was in pid1 bisher (pre systemd) geschah/geschieht.
Den systemd Code hab ich mir auch schon angesehen. Nur, das ist ein echtes Ungetüm. execute.c. [1] 122,629 Bytes. 3800+ SLOC.
Frühere Versionen (wheezy) [2] 69,245 Bytes. 2112 SLOC.
sysvinit. Init.c 62,497 Bytes 2800+ SLOC. Aber das ist fast das ganze init.
Ich will verstehen warum bsp. udev, in systemd integriert wurde und warum ein standalone betrieb wohl offiziell nicht mehr unterstützt wird. Warum logind so tief in systemd verankert ist, was die "geheime Magie" ist und warum es kein Standalone logind geben soll.
Deshalb studiere ich zur Zeit den Code der beiden Debian Init Systeme
Das Problem das ich bei systemd im Code habe, es gibt unglaublich viele Indirektionen und praktisch keine Kommentare im Code.
Bis man die Kette von main(), Manager.c zu execute.c findet, hat etliche Stunden gedauert.
Jetzt möchte ich mal einen Parser für systemd unit files Schreiben und diesen dann über /lib/systemd/System und den /etc/systemd/System drüber laufen lassen. Diese Informationen will ich dann in einen BGL Tree einbasteln [4] und dabei hoffentlich ein paar Erkenntnisse gewinnen. Aber das ist nur ein Hobbyprojekt das mein Verständnis steigern soll
Die Konkurenten zu sysd runit, s6 und minit sind dazu Zwerge (verglichen am SLOC). Es trainiert aber auch das Verständnis für C/C++ und allgemein Programmierfähigkeit wenn man fremden Code, den man noch sie gesehen hat, versucht zu verstehen. Viele OSS Leute sagen ja, dass viele den Code ansehen, aber bsp. hier im Forum sieht man wenig Threads wie diesen ... Es kann auch einfach sein, das hier mehr Nutzer als Programmierer da sind
Auch dieser Artikel find ich bei solch gewaltigen Source Trees interesannt. Bugs per SLOC [5]
[1] https://sources.debian.net/src/systemd/ ... execute.c/
[2] https://sources.debian.net/src/systemd/ ... execute.c/
[3] https://sources.debian.net/src/sysvinit ... rc/init.c/
[4] http://www.boost.org/doc/libs/1_64_0/li ... index.html
[5] https://www.mayerdan.com/ruby/2012/11/1 ... code-ratio
Den systemd Code hab ich mir auch schon angesehen. Nur, das ist ein echtes Ungetüm. execute.c. [1] 122,629 Bytes. 3800+ SLOC.
Frühere Versionen (wheezy) [2] 69,245 Bytes. 2112 SLOC.
sysvinit. Init.c 62,497 Bytes 2800+ SLOC. Aber das ist fast das ganze init.
Ich will verstehen warum bsp. udev, in systemd integriert wurde und warum ein standalone betrieb wohl offiziell nicht mehr unterstützt wird. Warum logind so tief in systemd verankert ist, was die "geheime Magie" ist und warum es kein Standalone logind geben soll.
Deshalb studiere ich zur Zeit den Code der beiden Debian Init Systeme
Das Problem das ich bei systemd im Code habe, es gibt unglaublich viele Indirektionen und praktisch keine Kommentare im Code.
Bis man die Kette von main(), Manager.c zu execute.c findet, hat etliche Stunden gedauert.
Jetzt möchte ich mal einen Parser für systemd unit files Schreiben und diesen dann über /lib/systemd/System und den /etc/systemd/System drüber laufen lassen. Diese Informationen will ich dann in einen BGL Tree einbasteln [4] und dabei hoffentlich ein paar Erkenntnisse gewinnen. Aber das ist nur ein Hobbyprojekt das mein Verständnis steigern soll
Die Konkurenten zu sysd runit, s6 und minit sind dazu Zwerge (verglichen am SLOC). Es trainiert aber auch das Verständnis für C/C++ und allgemein Programmierfähigkeit wenn man fremden Code, den man noch sie gesehen hat, versucht zu verstehen. Viele OSS Leute sagen ja, dass viele den Code ansehen, aber bsp. hier im Forum sieht man wenig Threads wie diesen ... Es kann auch einfach sein, das hier mehr Nutzer als Programmierer da sind
Auch dieser Artikel find ich bei solch gewaltigen Source Trees interesannt. Bugs per SLOC [5]
[1] https://sources.debian.net/src/systemd/ ... execute.c/
[2] https://sources.debian.net/src/systemd/ ... execute.c/
[3] https://sources.debian.net/src/sysvinit ... rc/init.c/
[4] http://www.boost.org/doc/libs/1_64_0/li ... index.html
[5] https://www.mayerdan.com/ruby/2012/11/1 ... code-ratio