Received: by 2002:a25:8b91:0:0:0:0:0 with SMTP id j17csp2181405ybl; Thu, 19 Dec 2019 09:17:05 -0800 (PST) X-Google-Smtp-Source: APXvYqxKVmMQe5cZItEZK5X4an93JMQU4f9nLU7nSk9ecCaujrXQIrttIyp9q8SdIqUH8E3kiKgq X-Received: by 2002:a9d:32e:: with SMTP id 43mr9795083otv.301.1576775824704; Thu, 19 Dec 2019 09:17:04 -0800 (PST) ARC-Seal: i=1; a=rsa-sha256; t=1576775824; cv=none; d=google.com; s=arc-20160816; b=HbNZ/Melo6qb7A/DyQYnac7xtD/16oF2rKZgwv2I9D3kfl4QIoIbsydDx4S206/buy voUojcOFuV1r3yZsYXwHT/84/pkRmbxwrrKlxsTNY93qHfLvDJQQn42JofEY3b2pFRDr P7q0PEpw6PWW8zTRAwduR1ciAIzzv1koGow2nyaXxmKWabsrKMmpXzJn2tClKs2vf/f6 pbNTrUBhd/WKJ9YLXYfgvSVwi4WgklOKNeNfdS/YKxuTPXNKjsvUO51K15ChVkwsNwKL gNnUGqJfSiqf0wr57oyrSwOV54/8dPgcpjz8pHwc5muBkWdMqr6fDBIi2EohrBiqUEBz 42bQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; h=list-id:precedence:sender:content-transfer-encoding:mime-version :references:in-reply-to:message-id:date:subject:cc:to:from :dkim-signature; bh=C2M6sI+GaPDwiZ86mCdlGOwzavWJLidzR+bx2oQQ/mo=; b=L8yCIU6bQNnPpKBn8D7JYdaISlnFz1SiK4/c7FspOQhJhx/s5p9WU6ZD00/PK0KaW5 l6fIf9gQ5jTYuzmdE+DuuwvdfPWGfCYyLu+pJ6JnxnJZDHQ5cViQyyA07HDCKH8746BM Xrd1j2bbir0z8+m2uld6QmMoov6x8FL3y9nPVRFBkH2ENYgmVz6zAd/1G3QLE5/TRUBf v63gYVGuV88t7SSD3zLsdnADSpnFd7K9Wn+Rt3efK1WK7Ova4OuPopcNEgg4xPbkPu1d roSZKBNr5pZ7/MqqvxQfmkQOz7cd7fwQvc3w7sYQcslVNh0G4HiV5Fz5L/xO8V10i/Cd p9gg== ARC-Authentication-Results: i=1; mx.google.com; dkim=pass header.i=@bgdev-pl.20150623.gappssmtp.com header.s=20150623 header.b=nyAuIVTa; spf=pass (google.com: best guess record for domain of linux-kernel-owner@vger.kernel.org designates 209.132.180.67 as permitted sender) smtp.mailfrom=linux-kernel-owner@vger.kernel.org Return-Path: Received: from vger.kernel.org (vger.kernel.org. [209.132.180.67]) by mx.google.com with ESMTP id i6si3556012otp.5.2019.12.19.09.16.50; Thu, 19 Dec 2019 09:17:04 -0800 (PST) Received-SPF: pass (google.com: best guess record for domain of linux-kernel-owner@vger.kernel.org designates 209.132.180.67 as permitted sender) client-ip=209.132.180.67; Authentication-Results: mx.google.com; dkim=pass header.i=@bgdev-pl.20150623.gappssmtp.com header.s=20150623 header.b=nyAuIVTa; spf=pass (google.com: best guess record for domain of linux-kernel-owner@vger.kernel.org designates 209.132.180.67 as permitted sender) smtp.mailfrom=linux-kernel-owner@vger.kernel.org Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1727189AbfLSRPw (ORCPT + 99 others); Thu, 19 Dec 2019 12:15:52 -0500 Received: from mail-wm1-f65.google.com ([209.85.128.65]:35419 "EHLO mail-wm1-f65.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1727167AbfLSRPv (ORCPT ); Thu, 19 Dec 2019 12:15:51 -0500 Received: by mail-wm1-f65.google.com with SMTP id p17so6467750wmb.0 for ; Thu, 19 Dec 2019 09:15:50 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bgdev-pl.20150623.gappssmtp.com; s=20150623; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=C2M6sI+GaPDwiZ86mCdlGOwzavWJLidzR+bx2oQQ/mo=; b=nyAuIVTayku53ZNj5rodyH5uhOx51b/6Hg/TN5OKhAHxkfmt4IJxcaBubRU7mKoSkS rHUdPnIY3YgE9y6YQU5IErBzQSLrGhAQoC254UNCGRp2/nsM2ovOtATx4zq6EEUeqpYe S6hl8c9D8DhIWWEUdbDqdSKDwdQl1DejKZH91KmGb9cdB9tMw6bkR2P2uvhpVs5rE8Cd EeJXQXQfzTMILAQdk4PgDcBDmQul/I56SMoFzHIeDS9snWo3ACFgjQqiP2G6k0gK0UEf pf65ppBcu53OPNeoi60SGXWTydImHpAQjJxh20Wm6ltU/Df12pHY/VwOfJCgm7BGBaeK VQog== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=C2M6sI+GaPDwiZ86mCdlGOwzavWJLidzR+bx2oQQ/mo=; b=bnD4MvdztJO6SDqBDt1Lu6ZPIUbFZ46msvBRQQ31cnYY4sUOqp9VVmU2PJ0ZgIEyWm K2pIi74KRAolc7fKMxteTvbkYwVSQP2rX37i5n9ev3NcW+IPezHj34TbcIsT5crpnH0h HplAZ/6u6rL9JakXqKrVTG7FKqfEMdH1+VG8QiOQ4WFHF+BsL7pBCHvEFLQPSXGp5N3r NGiqpzn48chwwVqrSdhTBx/bW53X2nS1K/olTWPU2/UrPUNBiZrFbR8wngPv54lHMoyv y3qFotULOU/aXb7oBStDc01t3A3O7LXbteGr2AL1d58JPxFpWHUxYPRWM8SwXWBjh5EC iprA== X-Gm-Message-State: APjAAAXOrnR51BEIFROtchDjxHIKYBWlx9/zN4YlTJVmRiBFA77Iz8n9 8vXwR1WleonTJZM1suJRlJ91QCc5Aqs= X-Received: by 2002:a1c:a543:: with SMTP id o64mr10740519wme.73.1576775748762; Thu, 19 Dec 2019 09:15:48 -0800 (PST) Received: from debian-brgl.home ([2a01:cb1d:af:5b00:6d6c:8493:1ab5:dad7]) by smtp.gmail.com with ESMTPSA id q6sm7401428wrx.72.2019.12.19.09.15.47 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 19 Dec 2019 09:15:48 -0800 (PST) From: Bartosz Golaszewski To: Kent Gibson , Linus Walleij , Andy Shevchenko , Greg Kroah-Hartman Cc: linux-gpio@vger.kernel.org, linux-kernel@vger.kernel.org, Bartosz Golaszewski Subject: [PATCH v3 12/13] gpiolib: add new ioctl() for monitoring changes in line info Date: Thu, 19 Dec 2019 18:15:27 +0100 Message-Id: <20191219171528.6348-13-brgl@bgdev.pl> X-Mailer: git-send-email 2.23.0 In-Reply-To: <20191219171528.6348-1-brgl@bgdev.pl> References: <20191219171528.6348-1-brgl@bgdev.pl> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Sender: linux-kernel-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org From: Bartosz Golaszewski Currently there is no way for user-space to be informed about changes in status of GPIO lines e.g. when someone else requests the line or its config changes. We can only periodically re-read the line-info. This is fine for simple one-off user-space tools, but any daemon that provides a centralized access to GPIO chips would benefit hugely from an event driven line info synchronization. This patch adds a new ioctl() that allows user-space processes to reuse the file descriptor associated with the character device for watching any changes in line properties. Every such event contains the updated line information. Currently the events are generated on three types of status changes: when a line is requested, when it's released and when its config is changed. The first two are self-explanatory. For the third one: this will only happen when another user-space process calls the new SET_CONFIG ioctl() as any changes that can happen from within the kernel (i.e. set_transitory() or set_debounce()) are of no interest to user-space. Signed-off-by: Bartosz Golaszewski --- drivers/gpio/gpiolib.c | 191 ++++++++++++++++++++++++++++++++++++-- drivers/gpio/gpiolib.h | 1 + include/uapi/linux/gpio.h | 24 +++++ tools/gpio/gpio-watch | Bin 0 -> 26528 bytes 4 files changed, 208 insertions(+), 8 deletions(-) create mode 100755 tools/gpio/gpio-watch diff --git a/drivers/gpio/gpiolib.c b/drivers/gpio/gpiolib.c index 41abf17640de..a422086e6973 100644 --- a/drivers/gpio/gpiolib.c +++ b/drivers/gpio/gpiolib.c @@ -547,6 +547,9 @@ static long linehandle_set_config(struct linehandle_state *lh, if (ret) return ret; } + + atomic_notifier_call_chain(&desc->gdev->notifier, + GPIOLINE_CHANGED_CONFIG, desc); } return 0; } @@ -1203,14 +1206,25 @@ static void gpio_desc_to_lineinfo(struct gpio_desc *desc, spin_unlock_irqrestore(&gpio_lock, flags); } +struct gpio_chardev_data { + struct gpio_device *gdev; + wait_queue_head_t wait; + DECLARE_KFIFO(events, struct gpioline_info_changed, 32); + struct notifier_block lineinfo_changed_nb; + unsigned long *watched_lines; +}; + /* * gpio_ioctl() - ioctl handler for the GPIO chardev */ static long gpio_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { - struct gpio_device *gdev = filp->private_data; + struct gpio_chardev_data *priv = filp->private_data; + struct gpio_device *gdev = priv->gdev; struct gpio_chip *chip = gdev->chip; void __user *ip = (void __user *)arg; + struct gpio_desc *desc; + __u32 offset; /* We fail any subsequent ioctl():s when the chip is gone */ if (!chip) @@ -1232,9 +1246,9 @@ static long gpio_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) if (copy_to_user(ip, &chipinfo, sizeof(chipinfo))) return -EFAULT; return 0; - } else if (cmd == GPIO_GET_LINEINFO_IOCTL) { + } else if (cmd == GPIO_GET_LINEINFO_IOCTL || + cmd == GPIO_GET_LINEINFO_WATCH_IOCTL) { struct gpioline_info lineinfo; - struct gpio_desc *desc; if (copy_from_user(&lineinfo, ip, sizeof(lineinfo))) return -EFAULT; @@ -1247,11 +1261,25 @@ static long gpio_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) if (copy_to_user(ip, &lineinfo, sizeof(lineinfo))) return -EFAULT; + + if (cmd == GPIO_GET_LINEINFO_WATCH_IOCTL) + set_bit(desc_to_gpio(desc), priv->watched_lines); + return 0; } else if (cmd == GPIO_GET_LINEHANDLE_IOCTL) { return linehandle_create(gdev, ip); } else if (cmd == GPIO_GET_LINEEVENT_IOCTL) { return lineevent_create(gdev, ip); + } else if (cmd == GPIO_GET_LINEINFO_UNWATCH_IOCTL) { + if (copy_from_user(&offset, ip, sizeof(offset))) + return -EFAULT; + + desc = gpiochip_get_desc(chip, offset); + if (IS_ERR(desc)) + return PTR_ERR(desc); + + clear_bit(desc_to_gpio(desc), &desc->flags); + return 0; } return -EINVAL; } @@ -1264,6 +1292,105 @@ static long gpio_ioctl_compat(struct file *filp, unsigned int cmd, } #endif +static struct gpio_chardev_data * +to_gpio_chardev_data(struct notifier_block *nb) +{ + return container_of(nb, struct gpio_chardev_data, lineinfo_changed_nb); +} + +static int lineinfo_changed_notify(struct notifier_block *nb, + unsigned long action, void *data) +{ + struct gpio_chardev_data *priv = to_gpio_chardev_data(nb); + struct gpioline_info_changed chg; + struct gpio_desc *desc = data; + int ret = NOTIFY_DONE; + + if (test_bit(desc_to_gpio(desc), priv->watched_lines)) { + memset(&chg, 0, sizeof(chg)); + chg.info.line_offset = gpio_chip_hwgpio(desc); + chg.event_type = action; + chg.timestamp = ktime_get_real_ns(); + gpio_desc_to_lineinfo(desc, &chg.info); + + ret = kfifo_in_spinlocked(&priv->events, &chg, + 1, &priv->wait.lock); + if (ret) + wake_up_poll(&priv->wait, EPOLLIN); + else + pr_debug_ratelimited( + "%s: lineinfo event FIFO is full - event dropped\n", + __func__); + + ret = NOTIFY_OK; + } + + return ret; +} + +static __poll_t lineinfo_watch_poll(struct file *filep, + struct poll_table_struct *pollt) +{ + struct gpio_chardev_data *priv = filep->private_data; + __poll_t events = 0; + + poll_wait(filep, &priv->wait, pollt); + + if (!kfifo_is_empty_spinlocked_noirqsave(&priv->events, + &priv->wait.lock)) + events = EPOLLIN | EPOLLRDNORM; + + return events; +} + +static ssize_t lineinfo_watch_read(struct file *filep, char __user *buf, + size_t count, loff_t *off) +{ + struct gpio_chardev_data *priv = filep->private_data; + struct gpioline_info_changed event; + ssize_t bytes_read = 0; + int ret; + + if (count < sizeof(event)) + return -EINVAL; + + do { + spin_lock(&priv->wait.lock); + if (kfifo_is_empty(&priv->events)) { + if (bytes_read) { + spin_unlock(&priv->wait.lock); + return bytes_read; + } + + if (filep->f_flags & O_NONBLOCK) { + spin_unlock(&priv->wait.lock); + return -EAGAIN; + } + + ret = wait_event_interruptible_locked(priv->wait, + !kfifo_is_empty(&priv->events)); + if (ret) { + spin_unlock(&priv->wait.lock); + return ret; + } + } + + ret = kfifo_out(&priv->events, &event, 1); + spin_unlock(&priv->wait.lock); + if (ret != 1) { + ret = -EIO; + break; + /* We should never get here. See lineevent_read(). */ + } + + if (copy_to_user(buf + bytes_read, &event, sizeof(event))) + return -EFAULT; + bytes_read += sizeof(event); + } while (count >= bytes_read + sizeof(event)); + + return bytes_read; +} + /** * gpio_chrdev_open() - open the chardev for ioctl operations * @inode: inode for this chardev @@ -1274,14 +1401,48 @@ static int gpio_chrdev_open(struct inode *inode, struct file *filp) { struct gpio_device *gdev = container_of(inode->i_cdev, struct gpio_device, chrdev); + struct gpio_chardev_data *priv; + int ret = -ENOMEM; /* Fail on open if the backing gpiochip is gone */ if (!gdev->chip) return -ENODEV; + + priv = kzalloc(sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->watched_lines = bitmap_zalloc(gdev->chip->ngpio, GFP_KERNEL); + if (!priv->watched_lines) + goto out_free_priv; + + init_waitqueue_head(&priv->wait); + INIT_KFIFO(priv->events); + priv->gdev = gdev; + + priv->lineinfo_changed_nb.notifier_call = lineinfo_changed_notify; + ret = atomic_notifier_chain_register(&gdev->notifier, + &priv->lineinfo_changed_nb); + if (ret) + goto out_free_bitmap; + get_device(&gdev->dev); - filp->private_data = gdev; + filp->private_data = priv; + + ret = nonseekable_open(inode, filp); + if (ret) + goto out_unregister_notifier; + + return ret; - return nonseekable_open(inode, filp); +out_unregister_notifier: + atomic_notifier_chain_unregister(&gdev->notifier, + &priv->lineinfo_changed_nb); +out_free_bitmap: + bitmap_free(priv->watched_lines); +out_free_priv: + kfree(priv); + return ret; } /** @@ -1292,17 +1453,23 @@ static int gpio_chrdev_open(struct inode *inode, struct file *filp) */ static int gpio_chrdev_release(struct inode *inode, struct file *filp) { - struct gpio_device *gdev = container_of(inode->i_cdev, - struct gpio_device, chrdev); + struct gpio_chardev_data *priv = filp->private_data; + struct gpio_device *gdev = priv->gdev; + bitmap_free(priv->watched_lines); + atomic_notifier_chain_unregister(&gdev->notifier, + &priv->lineinfo_changed_nb); put_device(&gdev->dev); + kfree(priv); + return 0; } - static const struct file_operations gpio_fileops = { .release = gpio_chrdev_release, .open = gpio_chrdev_open, + .poll = lineinfo_watch_poll, + .read = lineinfo_watch_read, .owner = THIS_MODULE, .llseek = no_llseek, .unlocked_ioctl = gpio_ioctl, @@ -1513,6 +1680,8 @@ int gpiochip_add_data_with_key(struct gpio_chip *chip, void *data, for (i = 0; i < chip->ngpio; i++) gdev->descs[i].gdev = gdev; + ATOMIC_INIT_NOTIFIER_HEAD(&gdev->notifier); + #ifdef CONFIG_PINCTRL INIT_LIST_HEAD(&gdev->pin_ranges); #endif @@ -2852,6 +3021,8 @@ static int gpiod_request_commit(struct gpio_desc *desc, const char *label) } done: spin_unlock_irqrestore(&gpio_lock, flags); + atomic_notifier_call_chain(&desc->gdev->notifier, + GPIOLINE_CHANGED_REQUESTED, desc); return ret; } @@ -2949,6 +3120,9 @@ static bool gpiod_free_commit(struct gpio_desc *desc) } spin_unlock_irqrestore(&gpio_lock, flags); + atomic_notifier_call_chain(&desc->gdev->notifier, + GPIOLINE_CHANGED_RELEASED, desc); + return ret; } @@ -3111,6 +3285,7 @@ static int gpio_set_bias(struct gpio_chip *chip, struct gpio_desc *desc) if (ret != -ENOTSUPP) return ret; } + return 0; } diff --git a/drivers/gpio/gpiolib.h b/drivers/gpio/gpiolib.h index a1cbeabadc69..8e3969616cfe 100644 --- a/drivers/gpio/gpiolib.h +++ b/drivers/gpio/gpiolib.h @@ -54,6 +54,7 @@ struct gpio_device { const char *label; void *data; struct list_head list; + struct atomic_notifier_head notifier; #ifdef CONFIG_PINCTRL /* diff --git a/include/uapi/linux/gpio.h b/include/uapi/linux/gpio.h index 799cf823d493..a37ad3d97c8d 100644 --- a/include/uapi/linux/gpio.h +++ b/include/uapi/linux/gpio.h @@ -59,6 +59,28 @@ struct gpioline_info { /* Maximum number of requested handles */ #define GPIOHANDLES_MAX 64 +/* Possible line status change events */ +enum { + GPIOLINE_CHANGED_REQUESTED = 1, + GPIOLINE_CHANGED_RELEASED, + GPIOLINE_CHANGED_CONFIG, +}; + +/** + * struct gpioline_info_changed - Information about a change in status + * of a GPIO line + * @info: updated line information + * @timestamp: estimate of time of status change occurrence, in nanoseconds + * and GPIOLINE_CHANGED_CONFIG + * @event_type: one of GPIOLINE_CHANGED_REQUESTED, GPIOLINE_CHANGED_RELEASED + */ +struct gpioline_info_changed { + struct gpioline_info info; + __u64 timestamp; + __u32 event_type; + __u32 padding[5]; /* for future use */ +}; + /* Linerequest flags */ #define GPIOHANDLE_REQUEST_INPUT (1UL << 0) #define GPIOHANDLE_REQUEST_OUTPUT (1UL << 1) @@ -176,6 +198,8 @@ struct gpioevent_data { #define GPIO_GET_CHIPINFO_IOCTL _IOR(0xB4, 0x01, struct gpiochip_info) #define GPIO_GET_LINEINFO_IOCTL _IOWR(0xB4, 0x02, struct gpioline_info) +#define GPIO_GET_LINEINFO_WATCH_IOCTL _IOWR(0xB4, 0x0b, struct gpioline_info) +#define GPIO_GET_LINEINFO_UNWATCH_IOCTL _IOWR(0xB4, 0x0c, __u32) #define GPIO_GET_LINEHANDLE_IOCTL _IOWR(0xB4, 0x03, struct gpiohandle_request) #define GPIO_GET_LINEEVENT_IOCTL _IOWR(0xB4, 0x04, struct gpioevent_request) diff --git a/tools/gpio/gpio-watch b/tools/gpio/gpio-watch new file mode 100755 index 0000000000000000000000000000000000000000..d67bf021afad6b6eec40cf10b673c5127e55b0da GIT binary patch literal 26528 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⪻gd?Nc&?5gJsR}) z&5B!rP)u3>VOV~UtRw@wf=T3$G`VrSgwz!HU5JdaQ^+jW_iH_d_m?XJuQD$_T%|vw1sHOD*$mSRf2&HLpH~?6R=qcK`E2i#Rr>sX z$*`UUHLR}xr?ft|pYUrz4V$Yv9Q&=M#2*165!UDTRfbzwP{Zo-zgVTu@3jm!{gm?Q zJ%JVkbC~$~pZ|YBwZBX=r0311GJSqeekA}V63Z#7L^J=NkU@Qq%jfrL{{IlqYyY`_ zvL3hNz0f5Q*5~)`=V(n3(mDl-XVz!<5^~D*MOce=DLAZ22CT>MHDr|Q4~LcHu-0ck ztM!?F9SGTC|4)ZiPIdu(zsfG2-vUPEYw;qV^_LVm3L#la4BbE&S2^l@P8;m1%B-$W z>jL^SdXDhQhH|U(%erI%{qJ9|^s*cnZKlc{LH4;hSk`0t0}JTS>{k-^v!I66@h5vcc9e?_A5hd&+W$*I qUW