When bash
is invoked as pid 1 directly through the kernel option init=/bin/bash --login
, it will issue something like this before prompting:
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
And no keyboard-generated signals (e.g ^Z, ^C, ^\) work.
To solve this problem, I wrote a simple program init1.c
as following:
/* init1.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
int main(int argc, char **argv)
{
char *options[] = {"--login", NULL};
int tty_fd = -1;
printf("\n----- Bash Init 1 -----\n\n");
/* Make bash as session leader. */
if (setsid() == -1)
{
fprintf(stderr, "%s : %d : %s\n", "setsid()", __LINE__, strerror(errno));
exit(EXIT_FAILURE);
}
/* Make /dev/tty1 as controlling terminal of Bash. */
tty_fd = open("/dev/tty1", O_RDWR);
if (tty_fd == -1)
{
fprintf(stderr, "%s : %d : %s\n", "open()", __LINE__, strerror(errno));
exit(EXIT_FAILURE);
}
/* Re-connect stdin, stdout, stderr to the controlling terminal. */
dup2(tty_fd, STDIN_FILENO);
dup2(tty_fd, STDOUT_FILENO);
dup2(tty_fd, STDERR_FILENO);
close(tty_fd);
execv("/bin/bash", options);
}
Compiled it as init1
, then invoked it as pid 1 (i.e Bash running as pid 1), the preceding error messages disappear and some signals (e.g ^C, ^\) work, but job control signals (e.g ^Z) still not (unexpected).
So to make job control signals work, I revised the code above as init2.c
(just fork()
):
/* init2.c */
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
int main(int argc, char **argv)
{
char *options[] = {"--login", NULL};
pid_t pid = -1;
int tty_fd = -1;
printf("\n----- Bash Init 2 -----\n\n");
pid = fork();
if (pid < 0)
{
fprintf(stderr, "%s : %d : %s\n", "fork()", __LINE__, strerror(errno));
exit(EXIT_FAILURE);
}
/* Parent. */
if (pid > 0)
{
/* Wait for its child, otherwise all processes would be killed ! */
while (wait(NULL) > 0)
;
exit(EXIT_SUCCESS);
}
/* Child. */
if (setsid() == -1)
{
fprintf(stderr, "%s : %d : %s\n", "setsid()", __LINE__, strerror(errno));
exit(EXIT_FAILURE);
}
/* Make /dev/tty1 as controlling terminal of Bash. */
tty_fd = open("/dev/tty1", O_RDWR);
if (tty_fd == -1)
{
fprintf(stderr, "%s : %d : %s\n", "open()", __LINE__, strerror(errno));
exit(EXIT_FAILURE);
}
/* Re-connect stdin, stdout, stderr to the controlling terminal. */
dup2(tty_fd, STDIN_FILENO);
dup2(tty_fd, STDOUT_FILENO);
dup2(tty_fd, STDERR_FILENO);
close(tty_fd);
execv("/bin/bash", options);
}
Compiled it as init2
and invoked as pid 1 (i.e. finally Bash running as arbitrary PID other than 1), and this time, the job control signals work!
But I didn't figure out why the job control signals work in init2
(Bash isn't pid 1) but not init1
(Bash is pid 1), why does foreground job ignore job control signals when Bash is running as PID 1? It seems that there is something special with pid 1.
Update 3/21/2022:
Recently, I found a very simple shell mysh in github which also implements job control, only 949 lines! When I ran it with init1
and init2
, this shell also has the same problem! (Thanks to it, I don't have to read the complicated bash
source code for figuring out my question. Orz) And the problem lies in waitpid()
which doesn't return immediately when SIGTSTP(^Z) reaches. So this issue is not only relative to bash
, but also the shells that implement job control. However, I don't understand why does't waitpid()
return if SIGTSTP reaches when shell is running as PID 1... 囧