diff --git a/arch/x86/include/asm/signal.h b/arch/x86/include/asm/signal.h
index d8e2efe6cd46..0a01877c11ab 100644
--- a/arch/x86/include/asm/signal.h
+++ b/arch/x86/include/asm/signal.h
@@ -8,7 +8,7 @@
 /* Most things should be clean enough to redefine this at will, if care
    is taken to make libc match.  */
 
-#define _NSIG		64
+#define _NSIG		65
 
 #ifdef __i386__
 # define _NSIG_BPW	32
diff --git a/arch/x86/include/uapi/asm/signal.h b/arch/x86/include/uapi/asm/signal.h
index 164a22a72984..60dca62d3dcf 100644
--- a/arch/x86/include/uapi/asm/signal.h
+++ b/arch/x86/include/uapi/asm/signal.h
@@ -60,7 +60,9 @@ typedef unsigned long sigset_t;
 
 /* These should not be considered constants from userland.  */
 #define SIGRTMIN	32
-#define SIGRTMAX	_NSIG
+#define SIGRTMAX	64
+
+#define SIGINFO		65
 
 #define SA_RESTORER	0x04000000
 
diff --git a/drivers/tty/Makefile b/drivers/tty/Makefile
index a2bd75fbaaa4..d50ba690bb87 100644
--- a/drivers/tty/Makefile
+++ b/drivers/tty/Makefile
@@ -2,7 +2,7 @@
 obj-$(CONFIG_TTY)		+= tty_io.o n_tty.o tty_ioctl.o tty_ldisc.o \
 				   tty_buffer.o tty_port.o tty_mutex.o \
 				   tty_ldsem.o tty_baudrate.o tty_jobctrl.o \
-				   n_null.o
+				   n_null.o tty_status.o
 obj-$(CONFIG_LEGACY_PTYS)	+= pty.o
 obj-$(CONFIG_UNIX98_PTYS)	+= pty.o
 obj-$(CONFIG_AUDIT)		+= tty_audit.o
diff --git a/drivers/tty/n_tty.c b/drivers/tty/n_tty.c
index 0ec93f1a61f5..b510e01289fd 100644
--- a/drivers/tty/n_tty.c
+++ b/drivers/tty/n_tty.c
@@ -1334,6 +1334,24 @@ static void n_tty_receive_char_special(struct tty_struct *tty, unsigned char c)
 			commit_echoes(tty);
 			return;
 		}
+#ifdef VSTATUS
+		if (c == STATUS_CHAR(tty)) {
+			/* Do the status message first and then send
+			 * the signal, otherwise signal delivery can
+			 * change the process state making the status
+			 * message misleading.  Also, use __isig() and
+			 * not sig(), as if we flush the tty we can
+			 * lose parts of the message.
+			 */
+
+			if (!L_NOKERNINFO(tty))
+				tty_status(tty);
+# if defined(SIGINFO) && SIGINFO != SIGPWR
+			__isig(SIGINFO, tty);
+# endif
+			return;
+		}
+#endif	/* VSTATUS */
 		if (c == '\n') {
 			if (L_ECHO(tty) || L_ECHONL(tty)) {
 				echo_char_raw('\n', ldata);
@@ -1763,6 +1781,9 @@ static void n_tty_set_termios(struct tty_struct *tty, struct ktermios *old)
 			set_bit(EOF_CHAR(tty), ldata->char_map);
 			set_bit('\n', ldata->char_map);
 			set_bit(EOL_CHAR(tty), ldata->char_map);
+#ifdef VSTATUS
+			set_bit(STATUS_CHAR(tty), ldata->char_map);
+#endif
 			if (L_IEXTEN(tty)) {
 				set_bit(WERASE_CHAR(tty), ldata->char_map);
 				set_bit(LNEXT_CHAR(tty), ldata->char_map);
diff --git a/drivers/tty/tty_io.c b/drivers/tty/tty_io.c
index 6616d4a0d41d..8e488ecba330 100644
--- a/drivers/tty/tty_io.c
+++ b/drivers/tty/tty_io.c
@@ -120,18 +120,26 @@
 #define TTY_PARANOIA_CHECK 1
 #define CHECK_TTY_COUNT 1
 
+/* Less ugly than an ifdef in the middle of the initalizer below, maybe? */
+#ifdef NOKERNINFO
+# define __NOKERNINFO NOKERNINFO
+#else
+# define __NOKERNINFO 0
+#endif
+
 struct ktermios tty_std_termios = {	/* for the benefit of tty drivers  */
 	.c_iflag = ICRNL | IXON,
 	.c_oflag = OPOST | ONLCR,
 	.c_cflag = B38400 | CS8 | CREAD | HUPCL,
 	.c_lflag = ISIG | ICANON | ECHO | ECHOE | ECHOK |
-		   ECHOCTL | ECHOKE | IEXTEN,
+		   ECHOCTL | ECHOKE | IEXTEN | __NOKERNINFO,
 	.c_cc = INIT_C_CC,
 	.c_ispeed = 38400,
 	.c_ospeed = 38400,
 	/* .c_line = N_TTY, */
 };
 EXPORT_SYMBOL(tty_std_termios);
+#undef __NOKERNINFO
 
 /* This list gets poked at by procfs and various bits of boot up code. This
  * could do with some rationalisation such as pulling the tty proc function
diff --git a/drivers/tty/tty_ioctl.c b/drivers/tty/tty_ioctl.c
index 507a25d692bb..b250eabca1ba 100644
--- a/drivers/tty/tty_ioctl.c
+++ b/drivers/tty/tty_ioctl.c
@@ -809,6 +809,10 @@ int tty_mode_ioctl(struct tty_struct *tty, struct file *file,
 		if (get_user(arg, (unsigned int __user *) arg))
 			return -EFAULT;
 		return tty_change_softcar(real_tty, arg);
+#ifdef TIOCSTAT
+	case TIOCSTAT:
+		return tty_status(real_tty);
+#endif
 	default:
 		return -ENOIOCTLCMD;
 	}
diff --git a/drivers/tty/tty_status.c b/drivers/tty/tty_status.c
new file mode 100644
index 000000000000..a9600f5bd48c
--- /dev/null
+++ b/drivers/tty/tty_status.c
@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: GPL-1.0+
+/*
+ * tty_status.c --- implements VSTATUS and TIOCSTAT from BSD4.3/4.4
+ *
+ */
+
+#include <linux/sched.h>
+#include <linux/mm.h>
+#include <linux/tty.h>
+#include <linux/sched/cputime.h>
+#include <linux/sched/loadavg.h>
+#include <linux/pid.h>
+#include <linux/slab.h>
+#include <linux/math64.h>
+
+#define MSGLEN (160 + TASK_COMM_LEN)
+
+inline unsigned long getRSSk(struct mm_struct *mm)
+{
+	if (mm == NULL)
+		return 0;
+	return get_mm_rss(mm) * PAGE_SIZE / 1024;
+}
+
+inline long nstoms(long l)
+{
+	l /= NSEC_PER_MSEC * 10;
+	if (l < 10)
+		l *= 10;
+	return l;
+}
+
+inline struct task_struct *compare(struct task_struct *new,
+				   struct task_struct *old)
+{
+	unsigned int ostate, nstate;
+
+	if (old == NULL)
+		return new;
+
+	ostate = task_state_index(old);
+	nstate = task_state_index(new);
+
+	if (ostate == nstate) {
+		if (old->start_time > new->start_time)
+			return old;
+		return new;
+	}
+
+	if (ostate < nstate)
+		return old;
+
+	return new;
+}
+
+struct task_struct *pick_process(struct pid *pgrp)
+{
+	struct task_struct *p, *winner = NULL;
+
+	read_lock(&tasklist_lock);
+	do_each_pid_task(pgrp, PIDTYPE_PGID, p) {
+		winner = compare(p, winner);
+	} while_each_pid_task(pgrp, PIDTYPE_PGID, p);
+	read_unlock(&tasklist_lock);
+
+	return winner;
+}
+
+int tty_status(struct tty_struct *tty)
+{
+	char tname[TASK_COMM_LEN];
+	unsigned long loadavg[3];
+	uint64_t pcpu, cputime, wallclock;
+	struct task_struct *p;
+	struct rusage rusage;
+	struct timespec64 utime, stime, rtime;
+	char msg[MSGLEN] = {0};
+	int len = 0;
+
+	if (tty == NULL)
+		return -ENOTTY;
+
+	get_avenrun(loadavg, FIXED_1/200, 0);
+	len += scnprintf((char *)&msg[len], MSGLEN - len, "load: %lu.%02lu  ",
+		       LOAD_INT(loadavg[0]), LOAD_FRAC(loadavg[0]));
+
+	if (tty->ctrl.session == NULL) {
+		len += scnprintf((char *)&msg[len], MSGLEN - len,
+				 "not a controlling terminal");
+		goto print;
+	}
+
+	if (tty->ctrl.pgrp == NULL) {
+		len += scnprintf((char *)&msg[len], MSGLEN - len,
+				 "no foreground process group");
+		goto print;
+	}
+
+	p = pick_process(tty->ctrl.pgrp);
+	if (p == NULL) {
+		len += scnprintf((char *)&msg[len], MSGLEN - len,
+				 "empty foreground process group");
+		goto print;
+	}
+
+	get_task_comm(tname, p);
+	getrusage(p, RUSAGE_BOTH, &rusage);
+	wallclock = ktime_get_ns() - p->start_time;
+
+	utime.tv_sec = rusage.ru_utime.tv_sec;
+	utime.tv_nsec = rusage.ru_utime.tv_usec * NSEC_PER_USEC;
+	stime.tv_sec = rusage.ru_stime.tv_sec;
+	stime.tv_nsec = rusage.ru_stime.tv_usec * NSEC_PER_USEC;
+	rtime = ns_to_timespec64(wallclock);
+
+	cputime = timespec64_to_ns(&utime) + timespec64_to_ns(&stime);
+	pcpu = div64_u64(cputime * 100, wallclock);
+
+	len += scnprintf((char *)&msg[len], MSGLEN - len,
+			 /* task, PID, task state */
+			 "cmd: %s %d [%s] "
+			 /* rtime,    utime,      stime,      %cpu,  rss */
+			 "%llu.%02lur %llu.%02luu %llu.%02lus %llu%% %luk",
+			 tname,	task_pid_vnr(p), (char *)get_task_state_name(p),
+			 rtime.tv_sec, nstoms(rtime.tv_nsec),
+			 utime.tv_sec, nstoms(utime.tv_nsec),
+			 stime.tv_sec, nstoms(stime.tv_nsec),
+			 pcpu, getRSSk(p->mm));
+
+print:
+	len += scnprintf((char *)&msg[len], MSGLEN - len, "\r\n");
+	tty_write_message(tty, msg);
+
+	return 0;
+}
diff --git a/fs/proc/array.c b/fs/proc/array.c
index f37c03077b58..eb14306cdde2 100644
--- a/fs/proc/array.c
+++ b/fs/proc/array.c
@@ -62,6 +62,7 @@
 #include <linux/tty.h>
 #include <linux/string.h>
 #include <linux/mman.h>
+#include <linux/sched.h>
 #include <linux/sched/mm.h>
 #include <linux/sched/numa_balancing.h>
 #include <linux/sched/task_stack.h>
@@ -111,34 +112,6 @@ void proc_task_name(struct seq_file *m, struct task_struct *p, bool escape)
 		seq_printf(m, "%.64s", tcomm);
 }
 
-/*
- * The task state array is a strange "bitmap" of
- * reasons to sleep. Thus "running" is zero, and
- * you can test for combinations of others with
- * simple bit tests.
- */
-static const char * const task_state_array[] = {
-
-	/* states in TASK_REPORT: */
-	"R (running)",		/* 0x00 */
-	"S (sleeping)",		/* 0x01 */
-	"D (disk sleep)",	/* 0x02 */
-	"T (stopped)",		/* 0x04 */
-	"t (tracing stop)",	/* 0x08 */
-	"X (dead)",		/* 0x10 */
-	"Z (zombie)",		/* 0x20 */
-	"P (parked)",		/* 0x40 */
-
-	/* states beyond TASK_REPORT: */
-	"I (idle)",		/* 0x80 */
-};
-
-static inline const char *get_task_state(struct task_struct *tsk)
-{
-	BUILD_BUG_ON(1 + ilog2(TASK_REPORT_MAX) != ARRAY_SIZE(task_state_array));
-	return task_state_array[task_state_index(tsk)];
-}
-
 static inline void task_state(struct seq_file *m, struct pid_namespace *ns,
 				struct pid *pid, struct task_struct *p)
 {
diff --git a/include/asm-generic/termios.h b/include/asm-generic/termios.h
index b1398d0d4a1d..9b080e1a82d4 100644
--- a/include/asm-generic/termios.h
+++ b/include/asm-generic/termios.h
@@ -10,9 +10,9 @@
 	eof=^D		vtime=\0	vmin=\1		sxtc=\0
 	start=^Q	stop=^S		susp=^Z		eol=\0
 	reprint=^R	discard=^U	werase=^W	lnext=^V
-	eol2=\0
+	eol2=\0         status=^T
 */
-#define INIT_C_CC "\003\034\177\025\004\0\1\0\021\023\032\0\022\017\027\026\0"
+#define INIT_C_CC "\003\034\177\025\004\0\1\0\021\023\032\0\022\017\027\026\0\024"
 
 /*
  * Translate a "termio" structure into a "termios". Ugh.
diff --git a/include/linux/sched.h b/include/linux/sched.h
index c1a927ddec64..2171074ec8f5 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -70,7 +70,7 @@ struct task_group;
 
 /*
  * Task state bitmask. NOTE! These bits are also
- * encoded in fs/proc/array.c: get_task_state().
+ * encoded in get_task_state().
  *
  * We have two separate sets of flags: task->state
  * is about runnability, while task->exit_state are
@@ -1643,6 +1643,56 @@ static inline char task_state_to_char(struct task_struct *tsk)
 	return task_index_to_char(task_state_index(tsk));
 }
 
+static inline const char *get_task_state_name(struct task_struct *tsk)
+{
+	static const char * const task_state_array[] = {
+
+		/* states in TASK_REPORT: */
+		"running",		/* 0x00 */
+		"sleeping",		/* 0x01 */
+		"disk sleep",		/* 0x02 */
+		"stopped",		/* 0x04 */
+		"tracing stop",		/* 0x08 */
+		"dead",			/* 0x10 */
+		"zombie",		/* 0x20 */
+		"parked",		/* 0x40 */
+
+		/* states beyond TASK_REPORT: */
+		"idle",			/* 0x80 */
+	};
+
+	BUILD_BUG_ON(1 + ilog2(TASK_REPORT_MAX) != ARRAY_SIZE(task_state_array));
+	return task_state_array[task_state_index(tsk)];
+}
+
+static inline const char *get_task_state(struct task_struct *tsk)
+{
+	/*
+	 * The task state array is a strange "bitmap" of
+	 * reasons to sleep. Thus "running" is zero, and
+	 * you can test for combinations of others with
+	 * simple bit tests.
+	 */
+	static const char * const task_state_array[] = {
+
+		/* states in TASK_REPORT: */
+		"R (running)",		/* 0x00 */
+		"S (sleeping)",		/* 0x01 */
+		"D (disk sleep)",	/* 0x02 */
+		"T (stopped)",		/* 0x04 */
+		"t (tracing stop)",	/* 0x08 */
+		"X (dead)",		/* 0x10 */
+		"Z (zombie)",		/* 0x20 */
+		"P (parked)",		/* 0x40 */
+
+		/* states beyond TASK_REPORT: */
+		"I (idle)",		/* 0x80 */
+	};
+
+	BUILD_BUG_ON(1 + ilog2(TASK_REPORT_MAX) != ARRAY_SIZE(task_state_array));
+	return task_state_array[task_state_index(tsk)];
+}
+
 /**
  * is_global_init - check if a task structure is init. Since init
  * is free to have sub-threads we need to check tgid.
diff --git a/include/linux/signal.h b/include/linux/signal.h
index b77f9472a37c..76bda1a20578 100644
--- a/include/linux/signal.h
+++ b/include/linux/signal.h
@@ -541,6 +541,7 @@ extern bool unhandled_signal(struct task_struct *tsk, int sig);
  *	|  non-POSIX signal  |  default action  |
  *	+--------------------+------------------+
  *	|  SIGEMT            |  coredump	|
+ *	|  SIGINFO	     |	ignore		|
  *	+--------------------+------------------+
  *
  * (+) For SIGKILL and SIGSTOP the action is "always", not just "default".
@@ -567,6 +568,9 @@ static inline int sig_kernel_ignore(unsigned long sig)
 	return	sig == SIGCONT	||
 		sig == SIGCHLD	||
 		sig == SIGWINCH ||
+#if defined(SIGINFO) && SIGINFO != SIGPWR
+		sig == SIGINFO  ||
+#endif
 		sig == SIGURG;
 }
 
diff --git a/include/linux/tty.h b/include/linux/tty.h
index 168e57e40bbb..943d85aa471c 100644
--- a/include/linux/tty.h
+++ b/include/linux/tty.h
@@ -49,6 +49,9 @@
 #define WERASE_CHAR(tty) ((tty)->termios.c_cc[VWERASE])
 #define LNEXT_CHAR(tty)	((tty)->termios.c_cc[VLNEXT])
 #define EOL2_CHAR(tty) ((tty)->termios.c_cc[VEOL2])
+#ifdef VSTATUS
+#define STATUS_CHAR(tty) ((tty)->termios.c_cc[VSTATUS])
+#endif
 
 #define _I_FLAG(tty, f)	((tty)->termios.c_iflag & (f))
 #define _O_FLAG(tty, f)	((tty)->termios.c_oflag & (f))
@@ -114,6 +117,9 @@
 #define L_PENDIN(tty)	_L_FLAG((tty), PENDIN)
 #define L_IEXTEN(tty)	_L_FLAG((tty), IEXTEN)
 #define L_EXTPROC(tty)	_L_FLAG((tty), EXTPROC)
+#ifdef NOKERNINFO
+#define L_NOKERNINFO(tty) _L_FLAG((tty), NOKERNINFO)
+#endif
 
 struct device;
 struct signal_struct;
@@ -428,4 +434,6 @@ extern void tty_lock_slave(struct tty_struct *tty);
 extern void tty_unlock_slave(struct tty_struct *tty);
 extern void tty_set_lock_subclass(struct tty_struct *tty);
 
+extern int tty_status(struct tty_struct *tty);
+
 #endif
diff --git a/include/uapi/asm-generic/ioctls.h b/include/uapi/asm-generic/ioctls.h
index cdc9f4ca8c27..baa2b8d42679 100644
--- a/include/uapi/asm-generic/ioctls.h
+++ b/include/uapi/asm-generic/ioctls.h
@@ -97,6 +97,8 @@
 
 #define TIOCMIWAIT	0x545C	/* wait for a change on serial input line(s) */
 #define TIOCGICOUNT	0x545D	/* read serial port inline interrupt counts */
+/* Some architectures use 0x545E for FIOQSIZE */
+#define TIOCSTAT        0x545F	/* display process group stats on tty */
 
 /*
  * Some arches already define FIOQSIZE due to a historical
diff --git a/include/uapi/asm-generic/signal.h b/include/uapi/asm-generic/signal.h
index 3c4cc9b8378e..0b771eb1db94 100644
--- a/include/uapi/asm-generic/signal.h
+++ b/include/uapi/asm-generic/signal.h
@@ -4,7 +4,7 @@
 
 #include <linux/types.h>
 
-#define _NSIG		64
+#define _NSIG		65
 #define _NSIG_BPW	__BITS_PER_LONG
 #define _NSIG_WORDS	((_NSIG + _NSIG_BPW - 1) / _NSIG_BPW)
 
@@ -49,9 +49,11 @@
 /* These should not be considered constants from userland.  */
 #define SIGRTMIN	32
 #ifndef SIGRTMAX
-#define SIGRTMAX	_NSIG
+#define SIGRTMAX	64
 #endif
 
+#define SIGINFO		65
+
 #if !defined MINSIGSTKSZ || !defined SIGSTKSZ
 #define MINSIGSTKSZ	2048
 #define SIGSTKSZ	8192
diff --git a/include/uapi/asm-generic/termbits.h b/include/uapi/asm-generic/termbits.h
index 2fbaf9ae89dd..cb4e9c6d629f 100644
--- a/include/uapi/asm-generic/termbits.h
+++ b/include/uapi/asm-generic/termbits.h
@@ -58,6 +58,7 @@ struct ktermios {
 #define VWERASE 14
 #define VLNEXT 15
 #define VEOL2 16
+#define VSTATUS 17
 
 /* c_iflag bits */
 #define IGNBRK	0000001
@@ -164,22 +165,23 @@ struct ktermios {
 #define IBSHIFT	  16		/* Shift from CBAUD to CIBAUD */
 
 /* c_lflag bits */
-#define ISIG	0000001
-#define ICANON	0000002
-#define XCASE	0000004
-#define ECHO	0000010
-#define ECHOE	0000020
-#define ECHOK	0000040
-#define ECHONL	0000100
-#define NOFLSH	0000200
-#define TOSTOP	0000400
-#define ECHOCTL	0001000
-#define ECHOPRT	0002000
-#define ECHOKE	0004000
-#define FLUSHO	0010000
-#define PENDIN	0040000
-#define IEXTEN	0100000
-#define EXTPROC	0200000
+#define ISIG	   0000001
+#define ICANON	   0000002
+#define XCASE	   0000004
+#define ECHO	   0000010
+#define ECHOE	   0000020
+#define ECHOK	   0000040
+#define ECHONL	   0000100
+#define NOFLSH	   0000200
+#define TOSTOP	   0000400
+#define ECHOCTL	   0001000
+#define ECHOPRT	   0002000
+#define ECHOKE	   0004000
+#define FLUSHO	   0010000
+#define PENDIN	   0040000
+#define IEXTEN	   0100000
+#define EXTPROC	   0200000
+#define NOKERNINFO 0400000
 
 /* tcflow() and TCXONC use these */
 #define	TCOOFF		0
