diff options
Diffstat (limited to 'drivers/scsi/cxgb3i/cxgb3i_pdu.c')
-rw-r--r-- | drivers/scsi/cxgb3i/cxgb3i_pdu.c | 402 |
1 files changed, 402 insertions, 0 deletions
diff --git a/drivers/scsi/cxgb3i/cxgb3i_pdu.c b/drivers/scsi/cxgb3i/cxgb3i_pdu.c new file mode 100644 index 000000000000..ce7ce8c6094c --- /dev/null +++ b/drivers/scsi/cxgb3i/cxgb3i_pdu.c @@ -0,0 +1,402 @@ +/* + * cxgb3i_pdu.c: Chelsio S3xx iSCSI driver. + * + * Copyright (c) 2008 Chelsio Communications, Inc. + * Copyright (c) 2008 Mike Christie + * Copyright (c) 2008 Red Hat, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation. + * + * Written by: Karen Xie (kxie@chelsio.com) + */ + +#include <linux/skbuff.h> +#include <linux/crypto.h> +#include <scsi/scsi_cmnd.h> +#include <scsi/scsi_host.h> + +#include "cxgb3i.h" +#include "cxgb3i_pdu.h" + +#ifdef __DEBUG_CXGB3I_RX__ +#define cxgb3i_rx_debug cxgb3i_log_debug +#else +#define cxgb3i_rx_debug(fmt...) +#endif + +#ifdef __DEBUG_CXGB3I_TX__ +#define cxgb3i_tx_debug cxgb3i_log_debug +#else +#define cxgb3i_tx_debug(fmt...) +#endif + +static struct page *pad_page; + +/* + * pdu receive, interact with libiscsi_tcp + */ +static inline int read_pdu_skb(struct iscsi_conn *conn, struct sk_buff *skb, + unsigned int offset, int offloaded) +{ + int status = 0; + int bytes_read; + + bytes_read = iscsi_tcp_recv_skb(conn, skb, offset, offloaded, &status); + switch (status) { + case ISCSI_TCP_CONN_ERR: + return -EIO; + case ISCSI_TCP_SUSPENDED: + /* no transfer - just have caller flush queue */ + return bytes_read; + case ISCSI_TCP_SKB_DONE: + /* + * pdus should always fit in the skb and we should get + * segment done notifcation. + */ + iscsi_conn_printk(KERN_ERR, conn, "Invalid pdu or skb."); + return -EFAULT; + case ISCSI_TCP_SEGMENT_DONE: + return bytes_read; + default: + iscsi_conn_printk(KERN_ERR, conn, "Invalid iscsi_tcp_recv_skb " + "status %d\n", status); + return -EINVAL; + } +} + +static int cxgb3i_conn_read_pdu_skb(struct iscsi_conn *conn, + struct sk_buff *skb) +{ + struct iscsi_tcp_conn *tcp_conn = conn->dd_data; + bool offloaded = 0; + unsigned int offset; + int rc; + + cxgb3i_rx_debug("conn 0x%p, skb 0x%p, len %u, flag 0x%x.\n", + conn, skb, skb->len, skb_ulp_mode(skb)); + + if (!iscsi_tcp_recv_segment_is_hdr(tcp_conn)) { + iscsi_conn_failure(conn, ISCSI_ERR_PROTO); + return -EIO; + } + + if (conn->hdrdgst_en && (skb_ulp_mode(skb) & ULP2_FLAG_HCRC_ERROR)) { + iscsi_conn_failure(conn, ISCSI_ERR_HDR_DGST); + return -EIO; + } + + if (conn->datadgst_en && (skb_ulp_mode(skb) & ULP2_FLAG_DCRC_ERROR)) { + iscsi_conn_failure(conn, ISCSI_ERR_DATA_DGST); + return -EIO; + } + + /* iscsi hdr */ + rc = read_pdu_skb(conn, skb, 0, 0); + if (rc <= 0) + return rc; + + if (iscsi_tcp_recv_segment_is_hdr(tcp_conn)) + return 0; + + offset = rc; + if (conn->hdrdgst_en) + offset += ISCSI_DIGEST_SIZE; + + /* iscsi data */ + if (skb_ulp_mode(skb) & ULP2_FLAG_DATA_DDPED) { + cxgb3i_rx_debug("skb 0x%p, opcode 0x%x, data %u, ddp'ed, " + "itt 0x%x.\n", + skb, + tcp_conn->in.hdr->opcode & ISCSI_OPCODE_MASK, + tcp_conn->in.datalen, + ntohl(tcp_conn->in.hdr->itt)); + offloaded = 1; + } else { + cxgb3i_rx_debug("skb 0x%p, opcode 0x%x, data %u, NOT ddp'ed, " + "itt 0x%x.\n", + skb, + tcp_conn->in.hdr->opcode & ISCSI_OPCODE_MASK, + tcp_conn->in.datalen, + ntohl(tcp_conn->in.hdr->itt)); + offset += sizeof(struct cpl_iscsi_hdr_norss); + } + + rc = read_pdu_skb(conn, skb, offset, offloaded); + if (rc < 0) + return rc; + else + return 0; +} + +/* + * pdu transmit, interact with libiscsi_tcp + */ +static inline void tx_skb_setmode(struct sk_buff *skb, int hcrc, int dcrc) +{ + u8 submode = 0; + + if (hcrc) + submode |= 1; + if (dcrc) + submode |= 2; + skb_ulp_mode(skb) = (ULP_MODE_ISCSI << 4) | submode; +} + +void cxgb3i_conn_cleanup_task(struct iscsi_task *task) +{ + struct iscsi_tcp_task *tcp_task = task->dd_data; + + /* never reached the xmit task callout */ + if (tcp_task->dd_data) + kfree_skb(tcp_task->dd_data); + tcp_task->dd_data = NULL; + + /* MNC - Do we need a check in case this is called but + * cxgb3i_conn_alloc_pdu has never been called on the task */ + cxgb3i_release_itt(task, task->hdr_itt); + iscsi_tcp_cleanup_task(task); +} + +/* + * We do not support ahs yet + */ +int cxgb3i_conn_alloc_pdu(struct iscsi_task *task, u8 opcode) +{ + struct iscsi_tcp_task *tcp_task = task->dd_data; + struct sk_buff *skb; + + task->hdr = NULL; + /* always allocate rooms for AHS */ + skb = alloc_skb(sizeof(struct iscsi_hdr) + ISCSI_MAX_AHS_SIZE + + TX_HEADER_LEN, GFP_ATOMIC); + if (!skb) + return -ENOMEM; + + cxgb3i_tx_debug("task 0x%p, opcode 0x%x, skb 0x%p.\n", + task, opcode, skb); + + tcp_task->dd_data = skb; + skb_reserve(skb, TX_HEADER_LEN); + task->hdr = (struct iscsi_hdr *)skb->data; + task->hdr_max = sizeof(struct iscsi_hdr); + + /* data_out uses scsi_cmd's itt */ + if (opcode != ISCSI_OP_SCSI_DATA_OUT) + cxgb3i_reserve_itt(task, &task->hdr->itt); + + return 0; +} + +int cxgb3i_conn_init_pdu(struct iscsi_task *task, unsigned int offset, + unsigned int count) +{ + struct iscsi_tcp_task *tcp_task = task->dd_data; + struct sk_buff *skb = tcp_task->dd_data; + struct iscsi_conn *conn = task->conn; + struct page *pg; + unsigned int datalen = count; + int i, padlen = iscsi_padding(count); + skb_frag_t *frag; + + cxgb3i_tx_debug("task 0x%p,0x%p, offset %u, count %u, skb 0x%p.\n", + task, task->sc, offset, count, skb); + + skb_put(skb, task->hdr_len); + tx_skb_setmode(skb, conn->hdrdgst_en, datalen ? conn->datadgst_en : 0); + if (!count) + return 0; + + if (task->sc) { + struct scatterlist *sg; + struct scsi_data_buffer *sdb; + unsigned int sgoffset = offset; + struct page *sgpg; + unsigned int sglen; + + sdb = scsi_out(task->sc); + sg = sdb->table.sgl; + + for_each_sg(sdb->table.sgl, sg, sdb->table.nents, i) { + cxgb3i_tx_debug("sg %d, page 0x%p, len %u offset %u\n", + i, sg_page(sg), sg->length, sg->offset); + + if (sgoffset < sg->length) + break; + sgoffset -= sg->length; + } + sgpg = sg_page(sg); + sglen = sg->length - sgoffset; + + do { + int j = skb_shinfo(skb)->nr_frags; + unsigned int copy; + + if (!sglen) { + sg = sg_next(sg); + sgpg = sg_page(sg); + sgoffset = 0; + sglen = sg->length; + ++i; + } + copy = min(sglen, datalen); + if (j && skb_can_coalesce(skb, j, sgpg, + sg->offset + sgoffset)) { + skb_shinfo(skb)->frags[j - 1].size += copy; + } else { + get_page(sgpg); + skb_fill_page_desc(skb, j, sgpg, + sg->offset + sgoffset, copy); + } + sgoffset += copy; + sglen -= copy; + datalen -= copy; + } while (datalen); + } else { + pg = virt_to_page(task->data); + + while (datalen) { + i = skb_shinfo(skb)->nr_frags; + frag = &skb_shinfo(skb)->frags[i]; + + get_page(pg); + frag->page = pg; + frag->page_offset = 0; + frag->size = min((unsigned int)PAGE_SIZE, datalen); + + skb_shinfo(skb)->nr_frags++; + datalen -= frag->size; + pg++; + } + } + + if (padlen) { + i = skb_shinfo(skb)->nr_frags; + frag = &skb_shinfo(skb)->frags[i]; + frag->page = pad_page; + frag->page_offset = 0; + frag->size = padlen; + skb_shinfo(skb)->nr_frags++; + } + + datalen = count + padlen; + skb->data_len += datalen; + skb->truesize += datalen; + skb->len += datalen; + return 0; +} + +int cxgb3i_conn_xmit_pdu(struct iscsi_task *task) +{ + struct iscsi_tcp_task *tcp_task = task->dd_data; + struct sk_buff *skb = tcp_task->dd_data; + struct iscsi_tcp_conn *tcp_conn = task->conn->dd_data; + struct cxgb3i_conn *cconn = tcp_conn->dd_data; + unsigned int datalen; + int err; + + if (!skb) + return 0; + + datalen = skb->data_len; + tcp_task->dd_data = NULL; + err = cxgb3i_c3cn_send_pdus(cconn->cep->c3cn, skb); + cxgb3i_tx_debug("task 0x%p, skb 0x%p, len %u/%u, rv %d.\n", + task, skb, skb->len, skb->data_len, err); + if (err > 0) { + int pdulen = err; + + if (task->conn->hdrdgst_en) + pdulen += ISCSI_DIGEST_SIZE; + if (datalen && task->conn->datadgst_en) + pdulen += ISCSI_DIGEST_SIZE; + + task->conn->txdata_octets += pdulen; + return 0; + } + + if (err < 0 && err != -EAGAIN) { + kfree_skb(skb); + cxgb3i_tx_debug("itt 0x%x, skb 0x%p, len %u/%u, xmit err %d.\n", + task->itt, skb, skb->len, skb->data_len, err); + iscsi_conn_printk(KERN_ERR, task->conn, "xmit err %d.\n", err); + iscsi_conn_failure(task->conn, ISCSI_ERR_XMIT_FAILED); + return err; + } + /* reset skb to send when we are called again */ + tcp_task->dd_data = skb; + return -EAGAIN; +} + +int cxgb3i_pdu_init(void) +{ + pad_page = alloc_page(GFP_KERNEL); + if (!pad_page) + return -ENOMEM; + memset(page_address(pad_page), 0, PAGE_SIZE); + return 0; +} + +void cxgb3i_pdu_cleanup(void) +{ + if (pad_page) { + __free_page(pad_page); + pad_page = NULL; + } +} + +void cxgb3i_conn_pdu_ready(struct s3_conn *c3cn) +{ + struct sk_buff *skb; + unsigned int read = 0; + struct iscsi_conn *conn = c3cn->user_data; + int err = 0; + + cxgb3i_rx_debug("cn 0x%p.\n", c3cn); + + read_lock(&c3cn->callback_lock); + if (unlikely(!conn || conn->suspend_rx)) { + cxgb3i_rx_debug("conn 0x%p, id %d, suspend_rx %lu!\n", + conn, conn ? conn->id : 0xFF, + conn ? conn->suspend_rx : 0xFF); + read_unlock(&c3cn->callback_lock); + return; + } + skb = skb_peek(&c3cn->receive_queue); + while (!err && skb) { + __skb_unlink(skb, &c3cn->receive_queue); + read += skb_ulp_pdulen(skb); + err = cxgb3i_conn_read_pdu_skb(conn, skb); + __kfree_skb(skb); + skb = skb_peek(&c3cn->receive_queue); + } + read_unlock(&c3cn->callback_lock); + if (c3cn) { + c3cn->copied_seq += read; + cxgb3i_c3cn_rx_credits(c3cn, read); + } + conn->rxdata_octets += read; +} + +void cxgb3i_conn_tx_open(struct s3_conn *c3cn) +{ + struct iscsi_conn *conn = c3cn->user_data; + + cxgb3i_tx_debug("cn 0x%p.\n", c3cn); + if (conn) { + cxgb3i_tx_debug("cn 0x%p, cid %d.\n", c3cn, conn->id); + scsi_queue_work(conn->session->host, &conn->xmitwork); + } +} + +void cxgb3i_conn_closing(struct s3_conn *c3cn) +{ + struct iscsi_conn *conn; + + read_lock(&c3cn->callback_lock); + conn = c3cn->user_data; + if (conn && c3cn->state != C3CN_STATE_ESTABLISHED) + iscsi_conn_failure(conn, ISCSI_ERR_CONN_FAILED); + read_unlock(&c3cn->callback_lock); +} |