alistair23-linux/drivers/tty/ipwireless/tty.c
Peter Hurley a9c3f68f3c tty: Fix low_latency BUG
The user-settable knob, low_latency, has been the source of
several BUG reports which stem from flush_to_ldisc() running
in interrupt context. Since 3.12, which added several sleeping
locks (termios_rwsem and buf->lock) to the input processing path,
the frequency of these BUG reports has increased.

Note that changes in 3.12 did not introduce this regression;
sleeping locks were first added to the input processing path
with the removal of the BKL from N_TTY in commit
a88a69c912,
'n_tty: Fix loss of echoed characters and remove bkl from n_tty'
and later in commit 38db89799b,
'tty: throttling race fix'. Since those changes, executing
flush_to_ldisc() in interrupt_context (ie, low_latency set), is unsafe.

However, since most devices do not validate if the low_latency
setting is appropriate for the context (process or interrupt) in
which they receive data, some reports are due to misconfiguration.
Further, serial dma devices for which dma fails, resort to
interrupt receiving as a backup without resetting low_latency.

Historically, low_latency was used to force wake-up the reading
process rather than wait for the next scheduler tick. The
effect was to trim multiple milliseconds of latency from
when the process would receive new data.

Recent tests [1] have shown that the reading process now receives
data with only 10's of microseconds latency without low_latency set.

Remove the low_latency rx steering from tty_flip_buffer_push();
however, leave the knob as an optional hint to drivers that can
tune their rx fifos and such like. Cleanup stale code comments
regarding low_latency.

[1] https://lkml.org/lkml/2014/2/20/434

"Yay.. thats an annoying historical pain in the butt gone."
	-- Alan Cox

Reported-by: Beat Bolli <bbolli@ewanet.ch>
Reported-by: Pavel Roskin <proski@gnu.org>
Acked-by: David Sterba <dsterba@suse.cz>
Cc: Grant Edwards <grant.b.edwards@gmail.com>
Cc: Stanislaw Gruszka <sgruszka@redhat.com>
Cc: Hal Murray <murray+fedora@ip-64-139-1-69.sjc.megapath.net>
Cc: <stable@vger.kernel.org> # 3.12.x+
Signed-off-by: Peter Hurley <peter@hurleysoftware.com>
Signed-off-by: Alan Cox <alan@linux.intel.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
2014-02-28 16:31:00 -08:00

649 lines
14 KiB
C

/*
* IPWireless 3G PCMCIA Network Driver
*
* Original code
* by Stephen Blackheath <stephen@blacksapphire.com>,
* Ben Martel <benm@symmetric.co.nz>
*
* Copyrighted as follows:
* Copyright (C) 2004 by Symmetric Systems Ltd (NZ)
*
* Various driver changes and rewrites, port to new kernels
* Copyright (C) 2006-2007 Jiri Kosina
*
* Misc code cleanups and updates
* Copyright (C) 2007 David Sterba
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/ppp_defs.h>
#include <linux/if.h>
#include <linux/ppp-ioctl.h>
#include <linux/sched.h>
#include <linux/serial.h>
#include <linux/slab.h>
#include <linux/tty.h>
#include <linux/tty_driver.h>
#include <linux/tty_flip.h>
#include <linux/uaccess.h>
#include "tty.h"
#include "network.h"
#include "hardware.h"
#include "main.h"
#define IPWIRELESS_PCMCIA_START (0)
#define IPWIRELESS_PCMCIA_MINORS (24)
#define IPWIRELESS_PCMCIA_MINOR_RANGE (8)
#define TTYTYPE_MODEM (0)
#define TTYTYPE_MONITOR (1)
#define TTYTYPE_RAS_RAW (2)
struct ipw_tty {
struct tty_port port;
int index;
struct ipw_hardware *hardware;
unsigned int channel_idx;
unsigned int secondary_channel_idx;
int tty_type;
struct ipw_network *network;
unsigned int control_lines;
struct mutex ipw_tty_mutex;
int tx_bytes_queued;
int closing;
};
static struct ipw_tty *ttys[IPWIRELESS_PCMCIA_MINORS];
static struct tty_driver *ipw_tty_driver;
static char *tty_type_name(int tty_type)
{
static char *channel_names[] = {
"modem",
"monitor",
"RAS-raw"
};
return channel_names[tty_type];
}
static struct ipw_tty *get_tty(int index)
{
/*
* The 'ras_raw' channel is only available when 'loopback' mode
* is enabled.
* Number of minor starts with 16 (_RANGE * _RAS_RAW).
*/
if (!ipwireless_loopback && index >=
IPWIRELESS_PCMCIA_MINOR_RANGE * TTYTYPE_RAS_RAW)
return NULL;
return ttys[index];
}
static int ipw_open(struct tty_struct *linux_tty, struct file *filp)
{
struct ipw_tty *tty = get_tty(linux_tty->index);
if (!tty)
return -ENODEV;
mutex_lock(&tty->ipw_tty_mutex);
if (tty->closing) {
mutex_unlock(&tty->ipw_tty_mutex);
return -ENODEV;
}
if (tty->port.count == 0)
tty->tx_bytes_queued = 0;
tty->port.count++;
tty->port.tty = linux_tty;
linux_tty->driver_data = tty;
tty->port.low_latency = 1;
if (tty->tty_type == TTYTYPE_MODEM)
ipwireless_ppp_open(tty->network);
mutex_unlock(&tty->ipw_tty_mutex);
return 0;
}
static void do_ipw_close(struct ipw_tty *tty)
{
tty->port.count--;
if (tty->port.count == 0) {
struct tty_struct *linux_tty = tty->port.tty;
if (linux_tty != NULL) {
tty->port.tty = NULL;
linux_tty->driver_data = NULL;
if (tty->tty_type == TTYTYPE_MODEM)
ipwireless_ppp_close(tty->network);
}
}
}
static void ipw_hangup(struct tty_struct *linux_tty)
{
struct ipw_tty *tty = linux_tty->driver_data;
if (!tty)
return;
mutex_lock(&tty->ipw_tty_mutex);
if (tty->port.count == 0) {
mutex_unlock(&tty->ipw_tty_mutex);
return;
}
do_ipw_close(tty);
mutex_unlock(&tty->ipw_tty_mutex);
}
static void ipw_close(struct tty_struct *linux_tty, struct file *filp)
{
ipw_hangup(linux_tty);
}
/* Take data received from hardware, and send it out the tty */
void ipwireless_tty_received(struct ipw_tty *tty, unsigned char *data,
unsigned int length)
{
int work = 0;
mutex_lock(&tty->ipw_tty_mutex);
if (!tty->port.count) {
mutex_unlock(&tty->ipw_tty_mutex);
return;
}
mutex_unlock(&tty->ipw_tty_mutex);
work = tty_insert_flip_string(&tty->port, data, length);
if (work != length)
printk(KERN_DEBUG IPWIRELESS_PCCARD_NAME
": %d chars not inserted to flip buffer!\n",
length - work);
if (work)
tty_flip_buffer_push(&tty->port);
}
static void ipw_write_packet_sent_callback(void *callback_data,
unsigned int packet_length)
{
struct ipw_tty *tty = callback_data;
/*
* Packet has been sent, so we subtract the number of bytes from our
* tally of outstanding TX bytes.
*/
tty->tx_bytes_queued -= packet_length;
}
static int ipw_write(struct tty_struct *linux_tty,
const unsigned char *buf, int count)
{
struct ipw_tty *tty = linux_tty->driver_data;
int room, ret;
if (!tty)
return -ENODEV;
mutex_lock(&tty->ipw_tty_mutex);
if (!tty->port.count) {
mutex_unlock(&tty->ipw_tty_mutex);
return -EINVAL;
}
room = IPWIRELESS_TX_QUEUE_SIZE - tty->tx_bytes_queued;
if (room < 0)
room = 0;
/* Don't allow caller to write any more than we have room for */
if (count > room)
count = room;
if (count == 0) {
mutex_unlock(&tty->ipw_tty_mutex);
return 0;
}
ret = ipwireless_send_packet(tty->hardware, IPW_CHANNEL_RAS,
buf, count,
ipw_write_packet_sent_callback, tty);
if (ret == -1) {
mutex_unlock(&tty->ipw_tty_mutex);
return 0;
}
tty->tx_bytes_queued += count;
mutex_unlock(&tty->ipw_tty_mutex);
return count;
}
static int ipw_write_room(struct tty_struct *linux_tty)
{
struct ipw_tty *tty = linux_tty->driver_data;
int room;
/* FIXME: Exactly how is the tty object locked here .. */
if (!tty)
return -ENODEV;
if (!tty->port.count)
return -EINVAL;
room = IPWIRELESS_TX_QUEUE_SIZE - tty->tx_bytes_queued;
if (room < 0)
room = 0;
return room;
}
static int ipwireless_get_serial_info(struct ipw_tty *tty,
struct serial_struct __user *retinfo)
{
struct serial_struct tmp;
if (!retinfo)
return (-EFAULT);
memset(&tmp, 0, sizeof(tmp));
tmp.type = PORT_UNKNOWN;
tmp.line = tty->index;
tmp.port = 0;
tmp.irq = 0;
tmp.flags = 0;
tmp.baud_base = 115200;
tmp.close_delay = 0;
tmp.closing_wait = 0;
tmp.custom_divisor = 0;
tmp.hub6 = 0;
if (copy_to_user(retinfo, &tmp, sizeof(*retinfo)))
return -EFAULT;
return 0;
}
static int ipw_chars_in_buffer(struct tty_struct *linux_tty)
{
struct ipw_tty *tty = linux_tty->driver_data;
if (!tty)
return 0;
if (!tty->port.count)
return 0;
return tty->tx_bytes_queued;
}
static int get_control_lines(struct ipw_tty *tty)
{
unsigned int my = tty->control_lines;
unsigned int out = 0;
if (my & IPW_CONTROL_LINE_RTS)
out |= TIOCM_RTS;
if (my & IPW_CONTROL_LINE_DTR)
out |= TIOCM_DTR;
if (my & IPW_CONTROL_LINE_CTS)
out |= TIOCM_CTS;
if (my & IPW_CONTROL_LINE_DSR)
out |= TIOCM_DSR;
if (my & IPW_CONTROL_LINE_DCD)
out |= TIOCM_CD;
return out;
}
static int set_control_lines(struct ipw_tty *tty, unsigned int set,
unsigned int clear)
{
int ret;
if (set & TIOCM_RTS) {
ret = ipwireless_set_RTS(tty->hardware, tty->channel_idx, 1);
if (ret)
return ret;
if (tty->secondary_channel_idx != -1) {
ret = ipwireless_set_RTS(tty->hardware,
tty->secondary_channel_idx, 1);
if (ret)
return ret;
}
}
if (set & TIOCM_DTR) {
ret = ipwireless_set_DTR(tty->hardware, tty->channel_idx, 1);
if (ret)
return ret;
if (tty->secondary_channel_idx != -1) {
ret = ipwireless_set_DTR(tty->hardware,
tty->secondary_channel_idx, 1);
if (ret)
return ret;
}
}
if (clear & TIOCM_RTS) {
ret = ipwireless_set_RTS(tty->hardware, tty->channel_idx, 0);
if (tty->secondary_channel_idx != -1) {
ret = ipwireless_set_RTS(tty->hardware,
tty->secondary_channel_idx, 0);
if (ret)
return ret;
}
}
if (clear & TIOCM_DTR) {
ret = ipwireless_set_DTR(tty->hardware, tty->channel_idx, 0);
if (tty->secondary_channel_idx != -1) {
ret = ipwireless_set_DTR(tty->hardware,
tty->secondary_channel_idx, 0);
if (ret)
return ret;
}
}
return 0;
}
static int ipw_tiocmget(struct tty_struct *linux_tty)
{
struct ipw_tty *tty = linux_tty->driver_data;
/* FIXME: Exactly how is the tty object locked here .. */
if (!tty)
return -ENODEV;
if (!tty->port.count)
return -EINVAL;
return get_control_lines(tty);
}
static int
ipw_tiocmset(struct tty_struct *linux_tty,
unsigned int set, unsigned int clear)
{
struct ipw_tty *tty = linux_tty->driver_data;
/* FIXME: Exactly how is the tty object locked here .. */
if (!tty)
return -ENODEV;
if (!tty->port.count)
return -EINVAL;
return set_control_lines(tty, set, clear);
}
static int ipw_ioctl(struct tty_struct *linux_tty,
unsigned int cmd, unsigned long arg)
{
struct ipw_tty *tty = linux_tty->driver_data;
if (!tty)
return -ENODEV;
if (!tty->port.count)
return -EINVAL;
/* FIXME: Exactly how is the tty object locked here .. */
switch (cmd) {
case TIOCGSERIAL:
return ipwireless_get_serial_info(tty, (void __user *) arg);
case TIOCSSERIAL:
return 0; /* Keeps the PCMCIA scripts happy. */
}
if (tty->tty_type == TTYTYPE_MODEM) {
switch (cmd) {
case PPPIOCGCHAN:
{
int chan = ipwireless_ppp_channel_index(
tty->network);
if (chan < 0)
return -ENODEV;
if (put_user(chan, (int __user *) arg))
return -EFAULT;
}
return 0;
case PPPIOCGUNIT:
{
int unit = ipwireless_ppp_unit_number(
tty->network);
if (unit < 0)
return -ENODEV;
if (put_user(unit, (int __user *) arg))
return -EFAULT;
}
return 0;
case FIONREAD:
{
int val = 0;
if (put_user(val, (int __user *) arg))
return -EFAULT;
}
return 0;
case TCFLSH:
return tty_perform_flush(linux_tty, arg);
}
}
return -ENOIOCTLCMD;
}
static int add_tty(int j,
struct ipw_hardware *hardware,
struct ipw_network *network, int channel_idx,
int secondary_channel_idx, int tty_type)
{
ttys[j] = kzalloc(sizeof(struct ipw_tty), GFP_KERNEL);
if (!ttys[j])
return -ENOMEM;
ttys[j]->index = j;
ttys[j]->hardware = hardware;
ttys[j]->channel_idx = channel_idx;
ttys[j]->secondary_channel_idx = secondary_channel_idx;
ttys[j]->network = network;
ttys[j]->tty_type = tty_type;
mutex_init(&ttys[j]->ipw_tty_mutex);
tty_port_init(&ttys[j]->port);
tty_port_register_device(&ttys[j]->port, ipw_tty_driver, j, NULL);
ipwireless_associate_network_tty(network, channel_idx, ttys[j]);
if (secondary_channel_idx != -1)
ipwireless_associate_network_tty(network,
secondary_channel_idx,
ttys[j]);
/* check if we provide raw device (if loopback is enabled) */
if (get_tty(j))
printk(KERN_INFO IPWIRELESS_PCCARD_NAME
": registering %s device ttyIPWp%d\n",
tty_type_name(tty_type), j);
return 0;
}
struct ipw_tty *ipwireless_tty_create(struct ipw_hardware *hardware,
struct ipw_network *network)
{
int i, j;
for (i = 0; i < IPWIRELESS_PCMCIA_MINOR_RANGE; i++) {
int allfree = 1;
for (j = i; j < IPWIRELESS_PCMCIA_MINORS;
j += IPWIRELESS_PCMCIA_MINOR_RANGE)
if (ttys[j] != NULL) {
allfree = 0;
break;
}
if (allfree) {
j = i;
if (add_tty(j, hardware, network,
IPW_CHANNEL_DIALLER, IPW_CHANNEL_RAS,
TTYTYPE_MODEM))
return NULL;
j += IPWIRELESS_PCMCIA_MINOR_RANGE;
if (add_tty(j, hardware, network,
IPW_CHANNEL_DIALLER, -1,
TTYTYPE_MONITOR))
return NULL;
j += IPWIRELESS_PCMCIA_MINOR_RANGE;
if (add_tty(j, hardware, network,
IPW_CHANNEL_RAS, -1,
TTYTYPE_RAS_RAW))
return NULL;
return ttys[i];
}
}
return NULL;
}
/*
* Must be called before ipwireless_network_free().
*/
void ipwireless_tty_free(struct ipw_tty *tty)
{
int j;
struct ipw_network *network = ttys[tty->index]->network;
for (j = tty->index; j < IPWIRELESS_PCMCIA_MINORS;
j += IPWIRELESS_PCMCIA_MINOR_RANGE) {
struct ipw_tty *ttyj = ttys[j];
if (ttyj) {
mutex_lock(&ttyj->ipw_tty_mutex);
if (get_tty(j))
printk(KERN_INFO IPWIRELESS_PCCARD_NAME
": deregistering %s device ttyIPWp%d\n",
tty_type_name(ttyj->tty_type), j);
ttyj->closing = 1;
if (ttyj->port.tty != NULL) {
mutex_unlock(&ttyj->ipw_tty_mutex);
tty_vhangup(ttyj->port.tty);
/* FIXME: Exactly how is the tty object locked here
against a parallel ioctl etc */
/* FIXME2: hangup does not mean all processes
* are gone */
mutex_lock(&ttyj->ipw_tty_mutex);
}
while (ttyj->port.count)
do_ipw_close(ttyj);
ipwireless_disassociate_network_ttys(network,
ttyj->channel_idx);
tty_unregister_device(ipw_tty_driver, j);
tty_port_destroy(&ttyj->port);
ttys[j] = NULL;
mutex_unlock(&ttyj->ipw_tty_mutex);
kfree(ttyj);
}
}
}
static const struct tty_operations tty_ops = {
.open = ipw_open,
.close = ipw_close,
.hangup = ipw_hangup,
.write = ipw_write,
.write_room = ipw_write_room,
.ioctl = ipw_ioctl,
.chars_in_buffer = ipw_chars_in_buffer,
.tiocmget = ipw_tiocmget,
.tiocmset = ipw_tiocmset,
};
int ipwireless_tty_init(void)
{
int result;
ipw_tty_driver = alloc_tty_driver(IPWIRELESS_PCMCIA_MINORS);
if (!ipw_tty_driver)
return -ENOMEM;
ipw_tty_driver->driver_name = IPWIRELESS_PCCARD_NAME;
ipw_tty_driver->name = "ttyIPWp";
ipw_tty_driver->major = 0;
ipw_tty_driver->minor_start = IPWIRELESS_PCMCIA_START;
ipw_tty_driver->type = TTY_DRIVER_TYPE_SERIAL;
ipw_tty_driver->subtype = SERIAL_TYPE_NORMAL;
ipw_tty_driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;
ipw_tty_driver->init_termios = tty_std_termios;
ipw_tty_driver->init_termios.c_cflag =
B9600 | CS8 | CREAD | HUPCL | CLOCAL;
ipw_tty_driver->init_termios.c_ispeed = 9600;
ipw_tty_driver->init_termios.c_ospeed = 9600;
tty_set_operations(ipw_tty_driver, &tty_ops);
result = tty_register_driver(ipw_tty_driver);
if (result) {
printk(KERN_ERR IPWIRELESS_PCCARD_NAME
": failed to register tty driver\n");
put_tty_driver(ipw_tty_driver);
return result;
}
return 0;
}
void ipwireless_tty_release(void)
{
int ret;
ret = tty_unregister_driver(ipw_tty_driver);
put_tty_driver(ipw_tty_driver);
if (ret != 0)
printk(KERN_ERR IPWIRELESS_PCCARD_NAME
": tty_unregister_driver failed with code %d\n", ret);
}
int ipwireless_tty_is_modem(struct ipw_tty *tty)
{
return tty->tty_type == TTYTYPE_MODEM;
}
void
ipwireless_tty_notify_control_line_change(struct ipw_tty *tty,
unsigned int channel_idx,
unsigned int control_lines,
unsigned int changed_mask)
{
unsigned int old_control_lines = tty->control_lines;
tty->control_lines = (tty->control_lines & ~changed_mask)
| (control_lines & changed_mask);
/*
* If DCD is de-asserted, we close the tty so pppd can tell that we
* have gone offline.
*/
if ((old_control_lines & IPW_CONTROL_LINE_DCD)
&& !(tty->control_lines & IPW_CONTROL_LINE_DCD)
&& tty->port.tty) {
tty_hangup(tty->port.tty);
}
}