| /* |
| * 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); |
| } |