diff mbox series

[v5,13/13] tty: gunyah: Add tty console driver for RM Console Services

Message ID 20221011000840.289033-14-quic_eberman@quicinc.com (mailing list archive)
State New, archived
Headers show
Series Drivers for gunyah hypervisor | expand

Commit Message

Elliot Berman Oct. 11, 2022, 12:08 a.m. UTC
Gunyah provides a console for each VM using the VM console resource
manager APIs. This driver allows console data from other
VMs to be accessed via a TTY device and exports a console device to dump
Linux's own logs to our console.

Signed-off-by: Elliot Berman <quic_eberman@quicinc.com>
---
 MAINTAINERS                  |   1 +
 drivers/tty/Kconfig          |   8 +
 drivers/tty/Makefile         |   1 +
 drivers/tty/gunyah_console.c | 439 +++++++++++++++++++++++++++++++++++
 4 files changed, 449 insertions(+)
 create mode 100644 drivers/tty/gunyah_console.c

Comments

Jiri Slaby Oct. 11, 2022, 6:02 a.m. UTC | #1
On 11. 10. 22, 2:08, Elliot Berman wrote:
> Gunyah provides a console for each VM using the VM console resource
> manager APIs. This driver allows console data from other
> VMs to be accessed via a TTY device and exports a console device to dump
> Linux's own logs to our console.
...
> +struct rm_cons_drv_data {
> +	struct tty_driver *tty_driver;
> +	struct device *dev;
> +
> +	spinlock_t ports_lock;
> +	struct rm_cons_port *ports[RM_CONS_TTY_ADAPATERS];
> +
> +	struct notifier_block rm_cons_notif;
> +	struct console console;
> +
> +	/* below are for printk console.
> +	 * gh_rm_console_* calls will sleep and console_write can be called from
> +	 * atomic ctx. Two xmit buffers are used. The active buffer is tracked with
> +	 * co_xmit_idx. Writes go into the co_xmit_buf[co_xmit_idx] buffer.
> +	 * A work is scheduled to flush the bytes. The work will swap the active buffer
> +	 * and write out the other buffer.
> +	 */

Ugh, why? This is too ugly and unnecessary. What about passing the kfifo 
to gh_rm_console_write() instead? You do memcpy() there anyway.

> +	char *co_xmit_buf[2];
> +	int co_xmit_idx;
> +	unsigned int co_xmit_count;
> +	spinlock_t co_xmit_lock;
> +	struct work_struct co_flush_work;
> +};
> +
> +static int rm_cons_notif_handler(struct notifier_block *nb, unsigned long cmd, void *data)
> +{
> +	int count, i;
> +	struct rm_cons_port *rm_port = NULL;
> +	struct tty_port *tty_port = NULL;
> +	struct rm_cons_drv_data *cons_data =
> +		container_of(nb, struct rm_cons_drv_data, rm_cons_notif);
> +	const struct gh_rm_notification *notif = data;
> +	struct gh_rm_notif_vm_console_chars const * const msg = notif->buff;

So you did not comment on/address all my notes?
Arnd Bergmann Oct. 11, 2022, 11:09 a.m. UTC | #2
On Tue, Oct 11, 2022, at 8:02 AM, Jiri Slaby wrote:
> On 11. 10. 22, 2:08, Elliot Berman wrote:
>> +
>> +	/* below are for printk console.
>> +	 * gh_rm_console_* calls will sleep and console_write can be called from
>> +	 * atomic ctx. Two xmit buffers are used. The active buffer is tracked with
>> +	 * co_xmit_idx. Writes go into the co_xmit_buf[co_xmit_idx] buffer.
>> +	 * A work is scheduled to flush the bytes. The work will swap the active buffer
>> +	 * and write out the other buffer.
>> +	 */
>
> Ugh, why? This is too ugly and unnecessary. What about passing the kfifo 
> to gh_rm_console_write() instead? You do memcpy() there anyway.

Another problem here is that you really want the console output to be
printed from atomic context, otherwise one would never see e.g. the
output of a panic() call. Having a deferred write is probably fine for
normal tty operations, but you probably want a different device for the
console here, e.g. the hvc_dcc driver.

     Arnd
Elliot Berman Oct. 11, 2022, 6:22 p.m. UTC | #3
On 10/10/2022 11:02 PM, Jiri Slaby wrote:
>> +    char *co_xmit_buf[2];
>> +    int co_xmit_idx;
>> +    unsigned int co_xmit_count;
>> +    spinlock_t co_xmit_lock;
>> +    struct work_struct co_flush_work;
>> +};
>> +
>> +static int rm_cons_notif_handler(struct notifier_block *nb, unsigned 
>> long cmd, void *data)
>> +{
>> +    int count, i;
>> +    struct rm_cons_port *rm_port = NULL;
>> +    struct tty_port *tty_port = NULL;
>> +    struct rm_cons_drv_data *cons_data =
>> +        container_of(nb, struct rm_cons_drv_data, rm_cons_notif);
>> +    const struct gh_rm_notification *notif = data;
>> +    struct gh_rm_notif_vm_console_chars const * const msg = notif->buff;
> 
> So you did not comment on/address all my notes?
> 

Apologies Jiri, I wanted to get some consensus on direction to take the 
RM console driver and missed out the sorting here.
Elliot Berman Oct. 11, 2022, 10:04 p.m. UTC | #4
On 10/10/2022 11:02 PM, Jiri Slaby wrote:
> On 11. 10. 22, 2:08, Elliot Berman wrote:
>> Gunyah provides a console for each VM using the VM console resource
>> manager APIs. This driver allows console data from other
>> VMs to be accessed via a TTY device and exports a console device to dump
>> Linux's own logs to our console.
> ...
>> +struct rm_cons_drv_data {
>> +    struct tty_driver *tty_driver;
>> +    struct device *dev;
>> +
>> +    spinlock_t ports_lock;
>> +    struct rm_cons_port *ports[RM_CONS_TTY_ADAPATERS];
>> +
>> +    struct notifier_block rm_cons_notif;
>> +    struct console console;
>> +
>> +    /* below are for printk console.
>> +     * gh_rm_console_* calls will sleep and console_write can be 
>> called from
>> +     * atomic ctx. Two xmit buffers are used. The active buffer is 
>> tracked with
>> +     * co_xmit_idx. Writes go into the co_xmit_buf[co_xmit_idx] buffer.
>> +     * A work is scheduled to flush the bytes. The work will swap the 
>> active buffer
>> +     * and write out the other buffer.
>> +     */
> 
> Ugh, why? This is too ugly and unnecessary. What about passing the kfifo 
> to gh_rm_console_write() instead? You do memcpy() there anyway.
> 

Sure, I can do this instead.
Elliot Berman Oct. 11, 2022, 10:04 p.m. UTC | #5
On 10/11/2022 4:09 AM, Arnd Bergmann wrote:
> On Tue, Oct 11, 2022, at 8:02 AM, Jiri Slaby wrote:
>> On 11. 10. 22, 2:08, Elliot Berman wrote:
>>> +
>>> +	/* below are for printk console.
>>> +	 * gh_rm_console_* calls will sleep and console_write can be called from
>>> +	 * atomic ctx. Two xmit buffers are used. The active buffer is tracked with
>>> +	 * co_xmit_idx. Writes go into the co_xmit_buf[co_xmit_idx] buffer.
>>> +	 * A work is scheduled to flush the bytes. The work will swap the active buffer
>>> +	 * and write out the other buffer.
>>> +	 */
>>
>> Ugh, why? This is too ugly and unnecessary. What about passing the kfifo
>> to gh_rm_console_write() instead? You do memcpy() there anyway.
> 
> Another problem here is that you really want the console output to be
> printed from atomic context, otherwise one would never see e.g. the
> output of a panic() call. Having a deferred write is probably fine for
> normal tty operations, but you probably want a different device for the
> console here, e.g. the hvc_dcc driver.
> 

Yes, that is our perspective on the RM console driver as well. I'll make 
this more explicit in the Kconfig/commit text. We expect most VMs 
(especially Linux) to use some other console mechanism provided by their 
VMM. I'm submitting here because we are presently using RM console on 
some of our VMs where we have other ways to collects logs on panic. It 
also makes it easier to implement a simple virtual machine manager that 
does not want to virtualize a serial device or have a virtio stack.

Thanks,
Elliot
Greg Kroah-Hartman Oct. 12, 2022, 6:55 a.m. UTC | #6
On Tue, Oct 11, 2022 at 03:04:47PM -0700, Elliot Berman wrote:
> 
> 
> On 10/11/2022 4:09 AM, Arnd Bergmann wrote:
> > On Tue, Oct 11, 2022, at 8:02 AM, Jiri Slaby wrote:
> > > On 11. 10. 22, 2:08, Elliot Berman wrote:
> > > > +
> > > > +	/* below are for printk console.
> > > > +	 * gh_rm_console_* calls will sleep and console_write can be called from
> > > > +	 * atomic ctx. Two xmit buffers are used. The active buffer is tracked with
> > > > +	 * co_xmit_idx. Writes go into the co_xmit_buf[co_xmit_idx] buffer.
> > > > +	 * A work is scheduled to flush the bytes. The work will swap the active buffer
> > > > +	 * and write out the other buffer.
> > > > +	 */
> > > 
> > > Ugh, why? This is too ugly and unnecessary. What about passing the kfifo
> > > to gh_rm_console_write() instead? You do memcpy() there anyway.
> > 
> > Another problem here is that you really want the console output to be
> > printed from atomic context, otherwise one would never see e.g. the
> > output of a panic() call. Having a deferred write is probably fine for
> > normal tty operations, but you probably want a different device for the
> > console here, e.g. the hvc_dcc driver.
> > 
> 
> Yes, that is our perspective on the RM console driver as well. I'll make
> this more explicit in the Kconfig/commit text. We expect most VMs
> (especially Linux) to use some other console mechanism provided by their
> VMM. I'm submitting here because we are presently using RM console on some
> of our VMs where we have other ways to collects logs on panic. It also makes
> it easier to implement a simple virtual machine manager that does not want
> to virtualize a serial device or have a virtio stack.

The whole goal of virtio was so that we would not have all of these
random custom drivers for new hypervisors all over the place, requiring
custom userspace interaction with them.

Please use virtio, that's what it is there for, don't create a new
console device if you do not have to.

greg k-h
Elliot Berman Oct. 13, 2022, 8:54 p.m. UTC | #7
On 10/11/2022 11:55 PM, Greg Kroah-Hartman wrote:
> On Tue, Oct 11, 2022 at 03:04:47PM -0700, Elliot Berman wrote:
>>
>>
>> On 10/11/2022 4:09 AM, Arnd Bergmann wrote:
>>> On Tue, Oct 11, 2022, at 8:02 AM, Jiri Slaby wrote:
>>>> On 11. 10. 22, 2:08, Elliot Berman wrote:
>>>>> +
>>>>> +	/* below are for printk console.
>>>>> +	 * gh_rm_console_* calls will sleep and console_write can be called from
>>>>> +	 * atomic ctx. Two xmit buffers are used. The active buffer is tracked with
>>>>> +	 * co_xmit_idx. Writes go into the co_xmit_buf[co_xmit_idx] buffer.
>>>>> +	 * A work is scheduled to flush the bytes. The work will swap the active buffer
>>>>> +	 * and write out the other buffer.
>>>>> +	 */
>>>>
>>>> Ugh, why? This is too ugly and unnecessary. What about passing the kfifo
>>>> to gh_rm_console_write() instead? You do memcpy() there anyway.
>>>
>>> Another problem here is that you really want the console output to be
>>> printed from atomic context, otherwise one would never see e.g. the
>>> output of a panic() call. Having a deferred write is probably fine for
>>> normal tty operations, but you probably want a different device for the
>>> console here, e.g. the hvc_dcc driver.
>>>
>>
>> Yes, that is our perspective on the RM console driver as well. I'll make
>> this more explicit in the Kconfig/commit text. We expect most VMs
>> (especially Linux) to use some other console mechanism provided by their
>> VMM. I'm submitting here because we are presently using RM console on some
>> of our VMs where we have other ways to collects logs on panic. It also makes
>> it easier to implement a simple virtual machine manager that does not want
>> to virtualize a serial device or have a virtio stack.
> 
> The whole goal of virtio was so that we would not have all of these
> random custom drivers for new hypervisors all over the place, requiring
> custom userspace interaction with them.
> 
> Please use virtio, that's what it is there for, don't create a new
> console device if you do not have to.

We have a lightweight VM product use case today that doesn't want to 
support an entire virtio stack just for a console. This VM already has a 
Gunyah stack present, and to facilitate their console needs, we want to 
give them the Gunyah console.

There are a few other hypervisors that also provide a console facility 
in Linux: Xen, ePAPR hypervisor and z/VM.

Thanks,
Elliot
Greg Kroah-Hartman Oct. 14, 2022, 7:38 a.m. UTC | #8
On Thu, Oct 13, 2022 at 01:54:36PM -0700, Elliot Berman wrote:
> 
> 
> On 10/11/2022 11:55 PM, Greg Kroah-Hartman wrote:
> > On Tue, Oct 11, 2022 at 03:04:47PM -0700, Elliot Berman wrote:
> > > 
> > > 
> > > On 10/11/2022 4:09 AM, Arnd Bergmann wrote:
> > > > On Tue, Oct 11, 2022, at 8:02 AM, Jiri Slaby wrote:
> > > > > On 11. 10. 22, 2:08, Elliot Berman wrote:
> > > > > > +
> > > > > > +	/* below are for printk console.
> > > > > > +	 * gh_rm_console_* calls will sleep and console_write can be called from
> > > > > > +	 * atomic ctx. Two xmit buffers are used. The active buffer is tracked with
> > > > > > +	 * co_xmit_idx. Writes go into the co_xmit_buf[co_xmit_idx] buffer.
> > > > > > +	 * A work is scheduled to flush the bytes. The work will swap the active buffer
> > > > > > +	 * and write out the other buffer.
> > > > > > +	 */
> > > > > 
> > > > > Ugh, why? This is too ugly and unnecessary. What about passing the kfifo
> > > > > to gh_rm_console_write() instead? You do memcpy() there anyway.
> > > > 
> > > > Another problem here is that you really want the console output to be
> > > > printed from atomic context, otherwise one would never see e.g. the
> > > > output of a panic() call. Having a deferred write is probably fine for
> > > > normal tty operations, but you probably want a different device for the
> > > > console here, e.g. the hvc_dcc driver.
> > > > 
> > > 
> > > Yes, that is our perspective on the RM console driver as well. I'll make
> > > this more explicit in the Kconfig/commit text. We expect most VMs
> > > (especially Linux) to use some other console mechanism provided by their
> > > VMM. I'm submitting here because we are presently using RM console on some
> > > of our VMs where we have other ways to collects logs on panic. It also makes
> > > it easier to implement a simple virtual machine manager that does not want
> > > to virtualize a serial device or have a virtio stack.
> > 
> > The whole goal of virtio was so that we would not have all of these
> > random custom drivers for new hypervisors all over the place, requiring
> > custom userspace interaction with them.
> > 
> > Please use virtio, that's what it is there for, don't create a new
> > console device if you do not have to.
> 
> We have a lightweight VM product use case today that doesn't want to support
> an entire virtio stack just for a console. This VM already has a Gunyah
> stack present, and to facilitate their console needs, we want to give them
> the Gunyah console.
> 
> There are a few other hypervisors that also provide a console facility in
> Linux: Xen, ePAPR hypervisor and z/VM.

Those all pre-dated virtio.  Please do not reinvent the wheel, again,
this is explicitly what virtio was designed for, so that we would not
have per-device/hypervisor drivers constantly being forced to be added.

Learn from the past mistakes and just use the interfaces and apis we
already have.  You don't have to have a "heavy" VM to support just a
virtio console, and in fact, all the code is already written for you!

thanks,

greg k-h
diff mbox series

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index 8a3d79b56698..e85140fa0dfe 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8889,6 +8889,7 @@  F:	Documentation/devicetree/bindings/firmware/gunyah-hypervisor.yaml
 F:	Documentation/virt/gunyah/
 F:	arch/arm64/gunyah/
 F:	drivers/mailbox/gunyah-msgq.c
+F:	drivers/tty/gunyah_tty.c
 F:	drivers/virt/gunyah/
 F:	include/asm-generic/gunyah.h
 F:	include/linux/gunyah*.h
diff --git a/drivers/tty/Kconfig b/drivers/tty/Kconfig
index cc30ff93e2e4..ff86e977f9ac 100644
--- a/drivers/tty/Kconfig
+++ b/drivers/tty/Kconfig
@@ -380,6 +380,14 @@  config RPMSG_TTY
 	  To compile this driver as a module, choose M here: the module will be
 	  called rpmsg_tty.
 
+config GUNYAH_CONSOLE
+	tristate "Gunyah Consoles"
+	depends on GUNYAH
+	help
+	  This enables support for console output using Gunyah's Resource Manager RPC.
+	  This is normally used when a secondary VM which does not have exclusive access
+	  to a real or virtualized serial device and virtio-console is unavailable.
+
 endif # TTY
 
 source "drivers/tty/serdev/Kconfig"
diff --git a/drivers/tty/Makefile b/drivers/tty/Makefile
index 07aca5184a55..134b7321c630 100644
--- a/drivers/tty/Makefile
+++ b/drivers/tty/Makefile
@@ -27,5 +27,6 @@  obj-$(CONFIG_GOLDFISH_TTY)	+= goldfish.o
 obj-$(CONFIG_MIPS_EJTAG_FDC_TTY) += mips_ejtag_fdc.o
 obj-$(CONFIG_VCC)		+= vcc.o
 obj-$(CONFIG_RPMSG_TTY)		+= rpmsg_tty.o
+obj-$(CONFIG_GUNYAH_CONSOLE)	+= gunyah_console.o
 
 obj-y += ipwireless/
diff --git a/drivers/tty/gunyah_console.c b/drivers/tty/gunyah_console.c
new file mode 100644
index 000000000000..71d9f22c2a43
--- /dev/null
+++ b/drivers/tty/gunyah_console.c
@@ -0,0 +1,439 @@ 
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2022 Qualcomm Innovation Center, Inc. All rights reserved.
+ */
+
+#include <linux/console.h>
+#include <linux/kfifo.h>
+#include <linux/kref.h>
+#include <linux/gunyah_rsc_mgr.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/tty.h>
+#include <linux/tty_flip.h>
+#include <linux/workqueue.h>
+
+/*
+ * The Linux TTY code requires us to know ahead of time how many ports we might
+ * need. Each port here corresponds to a VM. 16 seems like a reasonable number
+ * of ports for systems that are running Gunyah and using the console interface.
+ */
+#define RM_CONS_TTY_ADAPATERS		(16)
+
+struct rm_cons_port {
+	struct tty_port port;
+	u16 vmid;
+	unsigned int index;
+};
+
+struct rm_cons_drv_data {
+	struct tty_driver *tty_driver;
+	struct device *dev;
+
+	spinlock_t ports_lock;
+	struct rm_cons_port *ports[RM_CONS_TTY_ADAPATERS];
+
+	struct notifier_block rm_cons_notif;
+	struct console console;
+
+	/* below are for printk console.
+	 * gh_rm_console_* calls will sleep and console_write can be called from
+	 * atomic ctx. Two xmit buffers are used. The active buffer is tracked with
+	 * co_xmit_idx. Writes go into the co_xmit_buf[co_xmit_idx] buffer.
+	 * A work is scheduled to flush the bytes. The work will swap the active buffer
+	 * and write out the other buffer.
+	 */
+	char *co_xmit_buf[2];
+	int co_xmit_idx;
+	unsigned int co_xmit_count;
+	spinlock_t co_xmit_lock;
+	struct work_struct co_flush_work;
+};
+
+static int rm_cons_notif_handler(struct notifier_block *nb, unsigned long cmd, void *data)
+{
+	int count, i;
+	struct rm_cons_port *rm_port = NULL;
+	struct tty_port *tty_port = NULL;
+	struct rm_cons_drv_data *cons_data =
+		container_of(nb, struct rm_cons_drv_data, rm_cons_notif);
+	const struct gh_rm_notification *notif = data;
+	struct gh_rm_notif_vm_console_chars const * const msg = notif->buff;
+
+	if (cmd != GH_RM_NOTIF_VM_CONSOLE_CHARS ||
+		notif->size < sizeof(*msg))
+		return NOTIFY_DONE;
+
+	spin_lock(&cons_data->ports_lock);
+	for (i = 0; i < RM_CONS_TTY_ADAPATERS; i++) {
+		if (!cons_data->ports[i])
+			continue;
+		if (cons_data->ports[i]->vmid == msg->vmid) {
+			rm_port = cons_data->ports[i];
+			break;
+		}
+	}
+	if (rm_port)
+		tty_port = tty_port_get(&rm_port->port);
+	spin_unlock(&cons_data->ports_lock);
+
+	if (!rm_port)
+		dev_warn(cons_data->dev, "Received unexpected console characters for VMID %u\n",
+			msg->vmid);
+	if (!tty_port)
+		return NOTIFY_DONE;
+
+	count = tty_buffer_request_room(tty_port, msg->num_bytes);
+	tty_insert_flip_string(tty_port, msg->bytes, count);
+	tty_flip_buffer_push(tty_port);
+
+	tty_port_put(tty_port);
+	return NOTIFY_OK;
+}
+
+static int rm_cons_tty_open(struct tty_struct *tty, struct file *filp)
+{
+	struct rm_cons_port *rm_port = dev_get_drvdata(tty->dev);
+
+	return tty_port_open(&rm_port->port, tty, filp);
+}
+
+static void rm_cons_tty_close(struct tty_struct *tty, struct file *filp)
+{
+	struct rm_cons_port *rm_port = dev_get_drvdata(tty->dev);
+
+	tty_port_close(&rm_port->port, tty, filp);
+}
+
+static int rm_cons_tty_write(struct tty_struct *tty, const unsigned char *buf, int count)
+{
+	struct rm_cons_port *rm_port = dev_get_drvdata(tty->dev);
+	int ret;
+
+	if (!count)
+		return 0;
+
+	ret = gh_rm_console_write(rm_port->vmid, buf, count);
+	if (ret)
+		return -EAGAIN;
+	return count;
+}
+
+static void rm_cons_tty_flush_chars(struct tty_struct *tty)
+{
+	struct rm_cons_port *rm_port = dev_get_drvdata(tty->dev);
+
+	gh_rm_console_flush(rm_port->vmid);
+}
+
+static unsigned int rm_cons_mgr_tty_write_room(struct tty_struct *tty)
+{
+	return GH_RM_CONSOLE_WRITE_CHARS;
+}
+
+static const struct tty_operations rm_cons_tty_ops = {
+	.open = rm_cons_tty_open,
+	.close = rm_cons_tty_close,
+	.write = rm_cons_tty_write,
+	.flush_chars = rm_cons_tty_flush_chars,
+	.write_room = rm_cons_mgr_tty_write_room,
+};
+
+static int rm_cons_port_activate(struct tty_port *port, struct tty_struct *tty)
+{
+	struct rm_cons_port *rm_port = container_of(port, struct rm_cons_port, port);
+
+	return gh_rm_console_open(rm_port->vmid);
+}
+
+static void rm_cons_port_shutdown(struct tty_port *port)
+{
+	struct rm_cons_port *rm_port = container_of(port, struct rm_cons_port, port);
+	int ret;
+
+	ret = gh_rm_console_close(rm_port->vmid);
+	if (ret)
+		dev_err(port->tty->dev, "Failed to close ttyGH%d: %d\n", rm_port->index, ret);
+}
+
+static void rm_cons_port_destruct(struct tty_port *port)
+{
+	struct rm_cons_port *rm_port = container_of(port, struct rm_cons_port, port);
+	struct rm_cons_drv_data *cons_data = dev_get_drvdata(port->tty->dev);
+
+	spin_lock(&cons_data->ports_lock);
+	cons_data->ports[rm_port->index] = NULL;
+	spin_unlock(&cons_data->ports_lock);
+	kfree(rm_port);
+}
+
+static const struct tty_port_operations rm_cons_port_ops = {
+	.activate = rm_cons_port_activate,
+	.shutdown = rm_cons_port_shutdown,
+	.destruct = rm_cons_port_destruct,
+};
+
+static void rm_cons_console_flush_work(struct work_struct *ws)
+{
+	struct rm_cons_drv_data *cons_data =
+		container_of(ws, struct rm_cons_drv_data, co_flush_work);
+	struct console *co = &cons_data->console;
+	struct rm_cons_port *rm_port = co->data;
+	unsigned long flags;
+	size_t size;
+	int ret, old_buf = cons_data->co_xmit_idx;
+
+	spin_lock_irqsave(&cons_data->co_xmit_lock, flags);
+	cons_data->co_xmit_idx = 1 - old_buf;
+	size = cons_data->co_xmit_count;
+	cons_data->co_xmit_count = 0;
+	spin_unlock_irqrestore(&cons_data->co_xmit_lock, flags);
+
+	do {
+		ret = gh_rm_console_write(rm_port->vmid, cons_data->co_xmit_buf[old_buf], size);
+	} while (ret && ret != -ENOMEM);
+
+	gh_rm_console_flush(rm_port->vmid);
+}
+
+static void rm_cons_console_write(struct console *co, const char *buf, unsigned count)
+{
+	struct rm_cons_drv_data *cons_data = container_of(co, struct rm_cons_drv_data, console);
+	unsigned long flags;
+
+	spin_lock_irqsave(&cons_data->co_xmit_lock, flags);
+	count = min(count, (unsigned)(PAGE_SIZE - cons_data->co_xmit_count));
+	memcpy(&cons_data->co_xmit_buf[cons_data->co_xmit_idx][cons_data->co_xmit_count], buf,
+		count);
+	cons_data->co_xmit_count += count;
+	spin_unlock_irqrestore(&cons_data->co_xmit_lock, flags);
+
+	schedule_work(&cons_data->co_flush_work);
+}
+
+static struct tty_driver *rm_cons_console_device(struct console *co, int *index)
+{
+	struct rm_cons_drv_data *cons_data = container_of(co, struct rm_cons_drv_data, console);
+	struct rm_cons_port *rm_port = co->data;
+
+	*index = rm_port->index;
+	return cons_data->tty_driver;
+}
+
+static int rm_cons_console_setup(struct console *co, char *unused)
+{
+	struct rm_cons_drv_data *cons_data = container_of(co, struct rm_cons_drv_data, console);
+	struct rm_cons_port *rm_port = co->data;
+	int ret;
+
+	if (!tty_port_get(&rm_port->port))
+		return -ENODEV;
+
+	cons_data->co_xmit_buf[0] = (unsigned char *)get_zeroed_page(GFP_KERNEL);
+	if (!cons_data->co_xmit_buf[0])
+		goto err;
+	cons_data->co_xmit_buf[1] = (unsigned char *)get_zeroed_page(GFP_KERNEL);
+	if (!cons_data->co_xmit_buf[1])
+		goto err;
+
+	mutex_lock(&rm_port->port.mutex);
+	if (!tty_port_initialized(&rm_port->port)) {
+		ret = gh_rm_console_open(rm_port->vmid);
+		if (ret) {
+			dev_err(rm_port->port.tty->dev, "Failed to open %s%d: %d\n",
+				co->name, rm_port->index, ret);
+			goto err;
+		}
+		tty_port_set_initialized(&rm_port->port, true);
+	}
+	rm_port->port.console = true;
+	mutex_unlock(&rm_port->port.mutex);
+
+	return 0;
+err:
+	if (cons_data->co_xmit_buf[1])
+		free_page((unsigned long)cons_data->co_xmit_buf[1]);
+	if (cons_data->co_xmit_buf[0])
+		free_page((unsigned long)cons_data->co_xmit_buf[0]);
+	mutex_unlock(&rm_port->port.mutex);
+	tty_port_put(&rm_port->port);
+	return ret;
+}
+
+static int rm_cons_console_exit(struct console *co)
+{
+	struct rm_cons_port *rm_port = co->data;
+	int ret;
+
+	mutex_lock(&rm_port->port.mutex);
+	rm_port->port.console = false;
+
+	if (!tty_port_active(&rm_port->port)) {
+		ret = gh_rm_console_close(rm_port->vmid);
+		if (ret)
+			dev_err(rm_port->port.tty->dev, "Failed to close %s%d: %d\n",
+				co->name, rm_port->index, ret);
+		tty_port_set_initialized(&rm_port->port, false);
+	}
+
+	mutex_unlock(&rm_port->port.mutex);
+	tty_port_put(&rm_port->port);
+
+	return 0;
+}
+
+static struct rm_cons_port *rm_cons_port_create(struct rm_cons_drv_data *cons_data, u16 vmid)
+{
+	struct rm_cons_port *rm_port;
+	struct device *ttydev;
+	unsigned int index;
+	int ret;
+
+	rm_port = kzalloc(sizeof(*rm_port), GFP_KERNEL);
+	if (!rm_port)
+		return ERR_PTR(-ENOMEM);
+	rm_port->vmid = vmid;
+	tty_port_init(&rm_port->port);
+	rm_port->port.ops = &rm_cons_port_ops;
+
+	spin_lock(&cons_data->ports_lock);
+	for (index = 0; index < RM_CONS_TTY_ADAPATERS; index++) {
+		if (!cons_data->ports[index]) {
+			cons_data->ports[index] = rm_port;
+			rm_port->index = index;
+			break;
+		}
+	}
+	spin_unlock(&cons_data->ports_lock);
+	if (index >= RM_CONS_TTY_ADAPATERS) {
+		ret = -ENOSPC;
+		goto err_put_port;
+	}
+
+	ttydev = tty_port_register_device_attr(&rm_port->port, cons_data->tty_driver, index,
+					      cons_data->dev, rm_port, NULL);
+	if (IS_ERR(ttydev)) {
+		ret = PTR_ERR(ttydev);
+		goto err_put_port;
+	}
+
+	return rm_port;
+err_put_port:
+	tty_port_put(&rm_port->port);
+	return ERR_PTR(ret);
+}
+
+static int rm_cons_console_probe(struct device *dev)
+{
+	struct rm_cons_drv_data *cons_data;
+	struct rm_cons_port *rm_port;
+	int ret;
+	u16 vmid;
+
+	cons_data = devm_kzalloc(dev, sizeof(*cons_data), GFP_KERNEL);
+	if (!cons_data)
+		return -ENOMEM;
+	dev_set_drvdata(dev, cons_data);
+	cons_data->dev = dev;
+
+	cons_data->tty_driver = tty_alloc_driver(RM_CONS_TTY_ADAPATERS,
+						 TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV |
+						 TTY_DRIVER_RESET_TERMIOS);
+	if (IS_ERR(cons_data->tty_driver))
+		return PTR_ERR(cons_data->tty_driver);
+
+	cons_data->tty_driver->driver_name = KBUILD_MODNAME;
+	cons_data->tty_driver->name = "ttyGH";
+	cons_data->tty_driver->type = TTY_DRIVER_TYPE_SYSTEM;
+	cons_data->tty_driver->subtype = SYSTEM_TYPE_TTY;
+	cons_data->tty_driver->init_termios = tty_std_termios;
+	tty_set_operations(cons_data->tty_driver, &rm_cons_tty_ops);
+
+	ret = tty_register_driver(cons_data->tty_driver);
+	if (ret) {
+		dev_err(dev, "Could not register tty driver: %d\n", ret);
+		goto err_put_tty;
+	}
+
+	spin_lock_init(&cons_data->ports_lock);
+
+	cons_data->rm_cons_notif.notifier_call = rm_cons_notif_handler;
+	ret = gh_rm_register_notifier(&cons_data->rm_cons_notif);
+	if (ret) {
+		dev_err(dev, "Could not register for resource manager notifications: %d\n", ret);
+		goto err_put_tty;
+	}
+
+	spin_lock_init(&cons_data->co_xmit_lock);
+	INIT_WORK(&cons_data->co_flush_work, rm_cons_console_flush_work);
+
+	rm_port = rm_cons_port_create(cons_data, GH_VMID_SELF);
+	if (IS_ERR(rm_port)) {
+		ret = PTR_ERR(rm_port);
+		dev_err(dev, "Could not create own console: %d\n", ret);
+		goto err_unreg_notif;
+	}
+
+	rm_port->port.console = 1;
+	strncpy(cons_data->console.name, "ttyGH", sizeof(cons_data->console.name));
+	cons_data->console.write = rm_cons_console_write;
+	cons_data->console.device = rm_cons_console_device;
+	cons_data->console.setup = rm_cons_console_setup;
+	cons_data->console.exit = rm_cons_console_exit;
+	cons_data->console.index = rm_port->index;
+	cons_data->console.data = rm_port;
+	register_console(&cons_data->console);
+
+	ret = gh_rm_get_vmid(&vmid);
+	if (!ret) {
+		rm_port = rm_cons_port_create(cons_data, vmid);
+		if (IS_ERR(rm_port))
+			dev_warn(dev, "Could not create loopback console: %ld\n", PTR_ERR(rm_port));
+	} else {
+		dev_warn(dev, "Failed to get this VM's VMID: %d. Not creating loop-back console\n",
+			 ret);
+	}
+
+	return 0;
+err_unreg_notif:
+	gh_rm_unregister_notifier(&cons_data->rm_cons_notif);
+err_put_tty:
+	tty_driver_kref_put(cons_data->tty_driver);
+	return ret;
+}
+
+static int rm_cons_console_remove(struct device *dev)
+{
+	struct rm_cons_drv_data *cons_data = dev_get_drvdata(dev);
+
+	unregister_console(&cons_data->console);
+	if (cons_data->co_xmit_buf[1])
+		free_page((unsigned long)cons_data->co_xmit_buf[1]);
+	if (cons_data->co_xmit_buf[0])
+		free_page((unsigned long)cons_data->co_xmit_buf[0]);
+	gh_rm_unregister_notifier(&cons_data->rm_cons_notif);
+	tty_unregister_driver(cons_data->tty_driver);
+	tty_driver_kref_put(cons_data->tty_driver);
+
+	return 0;
+}
+
+static struct gunyah_rm_cons_device_id rm_cons_console_ids[] = {
+	{ .name = GH_RM_DEVICE_CONSOLE },
+	{}
+};
+MODULE_DEVICE_TABLE(gunyah_rsc_mgr, rm_cons_console_ids);
+
+static struct gh_rm_driver rm_cons_console_drv = {
+	.drv = {
+		.name = KBUILD_MODNAME,
+		.probe = rm_cons_console_probe,
+		.remove = rm_cons_console_remove,
+	},
+	.id_table = rm_cons_console_ids,
+};
+module_gh_rm_driver(rm_cons_console_drv);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("Gunyah Console");