sysvinit: init.c : waitpid

Vom einfachen Programm zum fertigen Debian-Paket, Fragen rund um Programmiersprachen, Scripting und Lizenzierung.
Antworten
Benutzeravatar
schorsch_76
Beiträge: 2535
Registriert: 06.11.2007 16:00:42
Lizenz eigener Beiträge: MIT Lizenz

sysvinit: init.c : waitpid

Beitrag von schorsch_76 » 10.05.2017 11:49:01

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 ... :oops:

[1] https://sources.debian.net/src/sysvinit ... rc/init.c/
[2] https://linux.die.net/man/2/waitpid

Benutzeravatar
Meillo
Moderator
Beiträge: 8782
Registriert: 21.06.2005 14:55:06
Wohnort: Balmora
Kontaktdaten:

Re: sysvinit: init.c : waitpid

Beitrag von Meillo » 10.05.2017 11:55:54

schorsch_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?
Siehe hier: http://stackoverflow.com/questions/8813 ... g-a-daemon
Use ed once in a while!

Benutzeravatar
schorsch_76
Beiträge: 2535
Registriert: 06.11.2007 16:00:42
Lizenz eigener Beiträge: MIT Lizenz

Re: sysvinit: init.c : waitpid

Beitrag von schorsch_76 » 10.05.2017 12:56:09

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

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;
	}
}



Benutzeravatar
Meillo
Moderator
Beiträge: 8782
Registriert: 21.06.2005 14:55:06
Wohnort: Balmora
Kontaktdaten:

Re: sysvinit: init.c : waitpid

Beitrag von Meillo » 10.05.2017 13:12:39

schorsch_76 hat geschrieben:Ich weis schon, warum man fork()/setsid()/fork() in einem daemon macht. Wir sind hier aber in pid1. sysvinit.
Ah, hab nicht so genau gelesen.
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?
Ich kann's dir nicht sagen.

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.
			 */
Erklaeren die was?

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!

Benutzeravatar
schorsch_76
Beiträge: 2535
Registriert: 06.11.2007 16:00:42
Lizenz eigener Beiträge: MIT Lizenz

Re: sysvinit: init.c : waitpid

Beitrag von schorsch_76 » 10.05.2017 15:59:03

Ich werde dazu mal mein "Linux Programming Interface" Buch in Holzversion befragen müssen ....

Benutzeravatar
schorsch_76
Beiträge: 2535
Registriert: 06.11.2007 16:00:42
Lizenz eigener Beiträge: MIT Lizenz

Re: sysvinit: init.c : waitpid

Beitrag von schorsch_76 » 11.05.2017 08:55:04

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

Code: Alles auswählen

while ((rc = waitpid(pid, &st, 0)) != pid)
		if (rc < 0 && errno == ECHILD)
			break;
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()

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);
  }
}

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

Code: Alles auswählen

void open_tty() {
  ...
  setsid();
...
[1] https://sources.debian.net/src/fgetty/0.7-2/fgetty.c/

Benutzeravatar
Meillo
Moderator
Beiträge: 8782
Registriert: 21.06.2005 14:55:06
Wohnort: Balmora
Kontaktdaten:

Re: sysvinit: init.c : waitpid

Beitrag von Meillo » 11.05.2017 09:33:00

schorsch_76 hat geschrieben:Ich habs jetzt kapiert:
das macht genau das was da steht ;)
;-) ... wenn's nur immer so einfach waere!
Use ed once in a while!

cosmac
Beiträge: 4573
Registriert: 28.03.2005 22:24:30

Re: sysvinit: init.c : waitpid

Beitrag von cosmac » 11.05.2017 10:46:49

he, was macht ihr da? Sieht aus wie Vorbereitungen für den Katastrophenfall, wenn es bei Debian kein init mehr gibt 8)
Beware of programmers who carry screwdrivers.

Benutzeravatar
Meillo
Moderator
Beiträge: 8782
Registriert: 21.06.2005 14:55:06
Wohnort: Balmora
Kontaktdaten:

Re: sysvinit: init.c : waitpid

Beitrag von Meillo » 11.05.2017 11:04:41

cosmac hat geschrieben:he, was macht ihr da?
Ihr? Ich mache gar nichts ... haenge bloss hier ein bisschen rum und gucke neugierig was andere so machen. :P


(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!

Benutzeravatar
schorsch_76
Beiträge: 2535
Registriert: 06.11.2007 16:00:42
Lizenz eigener Beiträge: MIT Lizenz

Re: sysvinit: init.c : waitpid

Beitrag von schorsch_76 » 11.05.2017 15:48:10

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. :oops:

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 ... :cry: 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

Antworten