Message ID | 20201222101156.98308-18-kashyap.desai@broadcom.com (mailing list archive) |
---|---|
State | Changes Requested |
Headers | show |
Series | Introducing mpi3mr driver | expand |
Hi Kashyap, I love your patch! Perhaps something to improve: [auto build test WARNING on mkp-scsi/for-next] [also build test WARNING on scsi/for-next v5.10 next-20201223] [If your patch is applied to the wrong git tree, kindly drop us a note. And when submitting patch, we suggest to use '--base' as documented in https://git-scm.com/docs/git-format-patch] url: https://github.com/0day-ci/linux/commits/Kashyap-Desai/Introducing-mpi3mr-driver/20201222-181732 base: https://git.kernel.org/pub/scm/linux/kernel/git/mkp/scsi.git for-next config: powerpc-allyesconfig (attached as .config) compiler: powerpc64-linux-gcc (GCC) 9.3.0 reproduce (this is a W=1 build): wget https://raw.githubusercontent.com/intel/lkp-tests/master/sbin/make.cross -O ~/bin/make.cross chmod +x ~/bin/make.cross # https://github.com/0day-ci/linux/commit/877f4e22deefcac84c14ed953f360f8131e02d14 git remote add linux-review https://github.com/0day-ci/linux git fetch --no-tags linux-review Kashyap-Desai/Introducing-mpi3mr-driver/20201222-181732 git checkout 877f4e22deefcac84c14ed953f360f8131e02d14 # save the attached .config to linux build tree COMPILER_INSTALL_PATH=$HOME/0day COMPILER=gcc-9.3.0 make.cross ARCH=powerpc If you fix the issue, kindly add following tag as appropriate Reported-by: kernel test robot <lkp@intel.com> All warnings (new ones prefixed by >>): drivers/scsi/mpi3mr/mpi3mr_fw.c: In function 'mpi3mr_isr_poll': >> drivers/scsi/mpi3mr/mpi3mr_fw.c:481:6: warning: variable 'num_admin_replies' set but not used [-Wunused-but-set-variable] 481 | u32 num_admin_replies = 0, num_op_reply = 0; | ^~~~~~~~~~~~~~~~~ vim +/num_admin_replies +481 drivers/scsi/mpi3mr/mpi3mr_fw.c 465 466 /** 467 * mpi3mr_isr_poll - Reply queue polling routine 468 * @irq: IRQ 469 * @privdata: Interrupt info 470 * 471 * poll for pending I/O completions in a loop until pending I/Os 472 * present or controller queue depth I/Os are processed. 473 * 474 * Return: IRQ_NONE or IRQ_HANDLED 475 */ 476 static irqreturn_t mpi3mr_isr_poll(int irq, void *privdata) 477 { 478 struct mpi3mr_intr_info *intr_info = privdata; 479 struct mpi3mr_ioc *mrioc; 480 u16 midx; > 481 u32 num_admin_replies = 0, num_op_reply = 0; 482 483 if (!intr_info || !intr_info->op_reply_q) 484 return IRQ_NONE; 485 486 mrioc = intr_info->mrioc; 487 midx = intr_info->msix_index; 488 489 /* Poll for pending IOs completions */ 490 do { 491 if (!mrioc->intr_enabled) 492 break; 493 494 if (!midx) 495 num_admin_replies = mpi3mr_process_admin_reply_q(mrioc); 496 if (intr_info->op_reply_q) 497 num_op_reply += 498 mpi3mr_process_op_reply_q(mrioc, intr_info); 499 500 usleep_range(mrioc->irqpoll_sleep, 10 * mrioc->irqpoll_sleep); 501 502 } while (atomic_read(&intr_info->op_reply_q->pend_ios) && 503 (num_op_reply < mrioc->max_host_ios)); 504 505 intr_info->op_reply_q->enable_irq_poll = false; 506 enable_irq(pci_irq_vector(mrioc->pdev, midx)); 507 508 return IRQ_HANDLED; 509 } 510 --- 0-DAY CI Kernel Test Service, Intel Corporation https://lists.01.org/hyperkitty/list/kbuild-all@lists.01.org
On 12/22/20 11:11 AM, Kashyap Desai wrote: > Register driver for threaded interrupt. > > By default, driver will attempt io completion from interrupt context > (primary handler). Since driver tracks per reply queue outstanding ios, > it will schedule threaded ISR if there are any outstanding IOs expected > on that particular reply queue. Threaded ISR (secondary handler) will loop > for IO completion as long as there are outstanding IOs > (speculative method using same per reply queue outstanding counter) > or it has completed some X amount of commands (something like budget). > > Signed-off-by: Kashyap Desai <kashyap.desai@broadcom.com> > Cc: sathya.prakash@broadcom.com > --- > drivers/scsi/mpi3mr/mpi3mr.h | 12 ++++++ > drivers/scsi/mpi3mr/mpi3mr_fw.c | 75 +++++++++++++++++++++++++++++++-- > 2 files changed, 84 insertions(+), 3 deletions(-) > > diff --git a/drivers/scsi/mpi3mr/mpi3mr.h b/drivers/scsi/mpi3mr/mpi3mr.h > index 74b6b4b6e322..41a8689b46c9 100644 > --- a/drivers/scsi/mpi3mr/mpi3mr.h > +++ b/drivers/scsi/mpi3mr/mpi3mr.h > @@ -144,6 +144,10 @@ extern struct list_head mrioc_list; > /* Default target device queue depth */ > #define MPI3MR_DEFAULT_SDEV_QD 32 > > +/* Definitions for Threaded IRQ poll*/ > +#define MPI3MR_IRQ_POLL_SLEEP 2 > +#define MPI3MR_IRQ_POLL_TRIGGER_IOCOUNT 8 > + > /* SGE Flag definition */ > #define MPI3MR_SGEFLAGS_SYSTEM_SIMPLE_END_OF_LIST \ > (MPI3_SGE_FLAGS_ELEMENT_TYPE_SIMPLE | MPI3_SGE_FLAGS_DLAS_SYSTEM | \ > @@ -295,6 +299,9 @@ struct op_req_qinfo { > * @q_segment_list: Segment list base virtual address > * @q_segment_list_dma: Segment list base DMA address > * @ephase: Expected phased identifier for the reply queue > + * @pend_ios: Number of IOs pending in HW for this queue > + * @enable_irq_poll: Flag to indicate polling is enabled > + * @in_use: Queue is handled by poll/ISR > */ > struct op_reply_qinfo { > u16 ci; > @@ -306,6 +313,9 @@ struct op_reply_qinfo { > void *q_segment_list; > dma_addr_t q_segment_list_dma; > u8 ephase; > + atomic_t pend_ios; > + bool enable_irq_poll; > + atomic_t in_use; > }; > > /** > @@ -559,6 +569,7 @@ struct scmd_priv { > * @shost: Scsi_Host pointer > * @id: Controller ID > * @cpu_count: Number of online CPUs > + * @irqpoll_sleep: usleep unit used in threaded isr irqpoll > * @name: Controller ASCII name > * @driver_name: Driver ASCII name > * @sysif_regs: System interface registers virtual address > @@ -660,6 +671,7 @@ struct mpi3mr_ioc { > u8 id; > int cpu_count; > bool enable_segqueue; > + u32 irqpoll_sleep; > > char name[MPI3MR_NAME_LENGTH]; > char driver_name[MPI3MR_NAME_LENGTH]; > diff --git a/drivers/scsi/mpi3mr/mpi3mr_fw.c b/drivers/scsi/mpi3mr/mpi3mr_fw.c > index ba4bfcc17809..4c4e21fb4ef3 100644 > --- a/drivers/scsi/mpi3mr/mpi3mr_fw.c > +++ b/drivers/scsi/mpi3mr/mpi3mr_fw.c > @@ -346,12 +346,16 @@ static int mpi3mr_process_op_reply_q(struct mpi3mr_ioc *mrioc, > > reply_qidx = op_reply_q->qid - 1; > > + if (!atomic_add_unless(&op_reply_q->in_use, 1, 1)) > + return 0; > + > exp_phase = op_reply_q->ephase; > reply_ci = op_reply_q->ci; > > reply_desc = mpi3mr_get_reply_desc(op_reply_q, reply_ci); > if ((le16_to_cpu(reply_desc->ReplyFlags) & > MPI3_REPLY_DESCRIPT_FLAGS_PHASE_MASK) != exp_phase) { > + atomic_dec(&op_reply_q->in_use); > return 0; > } > > @@ -364,6 +368,7 @@ static int mpi3mr_process_op_reply_q(struct mpi3mr_ioc *mrioc, > > mpi3mr_process_op_reply_desc(mrioc, reply_desc, &reply_dma, > reply_qidx); > + atomic_dec(&op_reply_q->pend_ios); > if (reply_dma) > mpi3mr_repost_reply_buf(mrioc, reply_dma); > num_op_reply++; > @@ -378,6 +383,14 @@ static int mpi3mr_process_op_reply_q(struct mpi3mr_ioc *mrioc, > if ((le16_to_cpu(reply_desc->ReplyFlags) & > MPI3_REPLY_DESCRIPT_FLAGS_PHASE_MASK) != exp_phase) > break; > + /* > + * Exit completion loop to avoid CPU lockup > + * Ensure remaining completion happens from threaded ISR. > + */ > + if (num_op_reply > mrioc->max_host_ios) { > + intr_info->op_reply_q->enable_irq_poll = true; > + break; > + } > > } while (1); > > @@ -386,6 +399,7 @@ static int mpi3mr_process_op_reply_q(struct mpi3mr_ioc *mrioc, > &mrioc->sysif_regs->OperQueueIndexes[reply_qidx].ConsumerIndex); > op_reply_q->ci = reply_ci; > op_reply_q->ephase = exp_phase; > + atomic_dec(&op_reply_q->in_use); > > return num_op_reply; > } > @@ -395,7 +409,7 @@ static irqreturn_t mpi3mr_isr_primary(int irq, void *privdata) > struct mpi3mr_intr_info *intr_info = privdata; > struct mpi3mr_ioc *mrioc; > u16 midx; > - u32 num_admin_replies = 0; > + u32 num_admin_replies = 0, num_op_reply = 0; > > if (!intr_info) > return IRQ_NONE; > @@ -409,8 +423,10 @@ static irqreturn_t mpi3mr_isr_primary(int irq, void *privdata) > > if (!midx) > num_admin_replies = mpi3mr_process_admin_reply_q(mrioc); > + if (intr_info->op_reply_q) > + num_op_reply = mpi3mr_process_op_reply_q(mrioc, intr_info); > > - if (num_admin_replies) > + if (num_admin_replies || num_op_reply) > return IRQ_HANDLED; > else > return IRQ_NONE; > @@ -431,7 +447,20 @@ static irqreturn_t mpi3mr_isr(int irq, void *privdata) > /* Call primary ISR routine */ > ret = mpi3mr_isr_primary(irq, privdata); > > - return ret; > + /* > + * If more IOs are expected, schedule IRQ polling thread. > + * Otherwise exit from ISR. > + */ > + if (!intr_info->op_reply_q) > + return ret; > + > + if (!intr_info->op_reply_q->enable_irq_poll || > + !atomic_read(&intr_info->op_reply_q->pend_ios)) > + return ret; > + > + disable_irq_nosync(pci_irq_vector(mrioc->pdev, midx)); > + > + return IRQ_WAKE_THREAD; > } > > /** > @@ -446,6 +475,36 @@ static irqreturn_t mpi3mr_isr(int irq, void *privdata) > */ > static irqreturn_t mpi3mr_isr_poll(int irq, void *privdata) > { > + struct mpi3mr_intr_info *intr_info = privdata; > + struct mpi3mr_ioc *mrioc; > + u16 midx; > + u32 num_admin_replies = 0, num_op_reply = 0; > + > + if (!intr_info || !intr_info->op_reply_q) > + return IRQ_NONE; > + > + mrioc = intr_info->mrioc; > + midx = intr_info->msix_index; > + > + /* Poll for pending IOs completions */ > + do { > + if (!mrioc->intr_enabled) > + break; > + > + if (!midx) > + num_admin_replies = mpi3mr_process_admin_reply_q(mrioc); > + if (intr_info->op_reply_q) > + num_op_reply += > + mpi3mr_process_op_reply_q(mrioc, intr_info); > + > + usleep_range(mrioc->irqpoll_sleep, 10 * mrioc->irqpoll_sleep); > + > + } while (atomic_read(&intr_info->op_reply_q->pend_ios) && > + (num_op_reply < mrioc->max_host_ios)); > + > + intr_info->op_reply_q->enable_irq_poll = false; > + enable_irq(pci_irq_vector(mrioc->pdev, midx)); > + > return IRQ_HANDLED; > } > > @@ -1161,6 +1220,9 @@ static int mpi3mr_create_op_reply_q(struct mpi3mr_ioc *mrioc, u16 qidx) > op_reply_q->num_replies = MPI3MR_OP_REP_Q_QD; > op_reply_q->ci = 0; > op_reply_q->ephase = 1; > + atomic_set(&op_reply_q->pend_ios, 0); > + atomic_set(&op_reply_q->in_use, 0); > + op_reply_q->enable_irq_poll = false; > > if (!op_reply_q->q_segments) { > retval = mpi3mr_alloc_op_reply_q_segments(mrioc, qidx); > @@ -1482,6 +1544,10 @@ int mpi3mr_op_request_post(struct mpi3mr_ioc *mrioc, > pi = 0; > op_req_q->pi = pi; > > + if (atomic_inc_return(&mrioc->op_reply_qinfo[reply_qidx].pend_ios) > + > MPI3MR_IRQ_POLL_TRIGGER_IOCOUNT) > + mrioc->op_reply_qinfo[reply_qidx].enable_irq_poll = true; > + > writel(op_req_q->pi, > &mrioc->sysif_regs->OperQueueIndexes[reply_qidx].ProducerIndex); > > @@ -2783,6 +2849,7 @@ int mpi3mr_init_ioc(struct mpi3mr_ioc *mrioc, u8 re_init) > u32 ioc_status, ioc_config, i; > Mpi3IOCFactsData_t facts_data; > > + mrioc->irqpoll_sleep = MPI3MR_IRQ_POLL_SLEEP; > mrioc->change_count = 0; > if (!re_init) { > mrioc->cpu_count = num_online_cpus(); > @@ -3068,6 +3135,8 @@ static void mpi3mr_memset_buffers(struct mpi3mr_ioc *mrioc) > mrioc->op_reply_qinfo[i].ci = 0; > mrioc->op_reply_qinfo[i].num_replies = 0; > mrioc->op_reply_qinfo[i].ephase = 0; > + atomic_set(&mrioc->op_reply_qinfo[i].pend_ios, 0); > + atomic_set(&mrioc->op_reply_qinfo[i].in_use, 0); > mpi3mr_memset_op_reply_q_buffers(mrioc, i); > > mrioc->req_qinfo[i].ci = 0; > Reviewed-by: Hannes Reinecke <hare@suse.de> Cheers, Hannes
diff --git a/drivers/scsi/mpi3mr/mpi3mr.h b/drivers/scsi/mpi3mr/mpi3mr.h index 74b6b4b6e322..41a8689b46c9 100644 --- a/drivers/scsi/mpi3mr/mpi3mr.h +++ b/drivers/scsi/mpi3mr/mpi3mr.h @@ -144,6 +144,10 @@ extern struct list_head mrioc_list; /* Default target device queue depth */ #define MPI3MR_DEFAULT_SDEV_QD 32 +/* Definitions for Threaded IRQ poll*/ +#define MPI3MR_IRQ_POLL_SLEEP 2 +#define MPI3MR_IRQ_POLL_TRIGGER_IOCOUNT 8 + /* SGE Flag definition */ #define MPI3MR_SGEFLAGS_SYSTEM_SIMPLE_END_OF_LIST \ (MPI3_SGE_FLAGS_ELEMENT_TYPE_SIMPLE | MPI3_SGE_FLAGS_DLAS_SYSTEM | \ @@ -295,6 +299,9 @@ struct op_req_qinfo { * @q_segment_list: Segment list base virtual address * @q_segment_list_dma: Segment list base DMA address * @ephase: Expected phased identifier for the reply queue + * @pend_ios: Number of IOs pending in HW for this queue + * @enable_irq_poll: Flag to indicate polling is enabled + * @in_use: Queue is handled by poll/ISR */ struct op_reply_qinfo { u16 ci; @@ -306,6 +313,9 @@ struct op_reply_qinfo { void *q_segment_list; dma_addr_t q_segment_list_dma; u8 ephase; + atomic_t pend_ios; + bool enable_irq_poll; + atomic_t in_use; }; /** @@ -559,6 +569,7 @@ struct scmd_priv { * @shost: Scsi_Host pointer * @id: Controller ID * @cpu_count: Number of online CPUs + * @irqpoll_sleep: usleep unit used in threaded isr irqpoll * @name: Controller ASCII name * @driver_name: Driver ASCII name * @sysif_regs: System interface registers virtual address @@ -660,6 +671,7 @@ struct mpi3mr_ioc { u8 id; int cpu_count; bool enable_segqueue; + u32 irqpoll_sleep; char name[MPI3MR_NAME_LENGTH]; char driver_name[MPI3MR_NAME_LENGTH]; diff --git a/drivers/scsi/mpi3mr/mpi3mr_fw.c b/drivers/scsi/mpi3mr/mpi3mr_fw.c index ba4bfcc17809..4c4e21fb4ef3 100644 --- a/drivers/scsi/mpi3mr/mpi3mr_fw.c +++ b/drivers/scsi/mpi3mr/mpi3mr_fw.c @@ -346,12 +346,16 @@ static int mpi3mr_process_op_reply_q(struct mpi3mr_ioc *mrioc, reply_qidx = op_reply_q->qid - 1; + if (!atomic_add_unless(&op_reply_q->in_use, 1, 1)) + return 0; + exp_phase = op_reply_q->ephase; reply_ci = op_reply_q->ci; reply_desc = mpi3mr_get_reply_desc(op_reply_q, reply_ci); if ((le16_to_cpu(reply_desc->ReplyFlags) & MPI3_REPLY_DESCRIPT_FLAGS_PHASE_MASK) != exp_phase) { + atomic_dec(&op_reply_q->in_use); return 0; } @@ -364,6 +368,7 @@ static int mpi3mr_process_op_reply_q(struct mpi3mr_ioc *mrioc, mpi3mr_process_op_reply_desc(mrioc, reply_desc, &reply_dma, reply_qidx); + atomic_dec(&op_reply_q->pend_ios); if (reply_dma) mpi3mr_repost_reply_buf(mrioc, reply_dma); num_op_reply++; @@ -378,6 +383,14 @@ static int mpi3mr_process_op_reply_q(struct mpi3mr_ioc *mrioc, if ((le16_to_cpu(reply_desc->ReplyFlags) & MPI3_REPLY_DESCRIPT_FLAGS_PHASE_MASK) != exp_phase) break; + /* + * Exit completion loop to avoid CPU lockup + * Ensure remaining completion happens from threaded ISR. + */ + if (num_op_reply > mrioc->max_host_ios) { + intr_info->op_reply_q->enable_irq_poll = true; + break; + } } while (1); @@ -386,6 +399,7 @@ static int mpi3mr_process_op_reply_q(struct mpi3mr_ioc *mrioc, &mrioc->sysif_regs->OperQueueIndexes[reply_qidx].ConsumerIndex); op_reply_q->ci = reply_ci; op_reply_q->ephase = exp_phase; + atomic_dec(&op_reply_q->in_use); return num_op_reply; } @@ -395,7 +409,7 @@ static irqreturn_t mpi3mr_isr_primary(int irq, void *privdata) struct mpi3mr_intr_info *intr_info = privdata; struct mpi3mr_ioc *mrioc; u16 midx; - u32 num_admin_replies = 0; + u32 num_admin_replies = 0, num_op_reply = 0; if (!intr_info) return IRQ_NONE; @@ -409,8 +423,10 @@ static irqreturn_t mpi3mr_isr_primary(int irq, void *privdata) if (!midx) num_admin_replies = mpi3mr_process_admin_reply_q(mrioc); + if (intr_info->op_reply_q) + num_op_reply = mpi3mr_process_op_reply_q(mrioc, intr_info); - if (num_admin_replies) + if (num_admin_replies || num_op_reply) return IRQ_HANDLED; else return IRQ_NONE; @@ -431,7 +447,20 @@ static irqreturn_t mpi3mr_isr(int irq, void *privdata) /* Call primary ISR routine */ ret = mpi3mr_isr_primary(irq, privdata); - return ret; + /* + * If more IOs are expected, schedule IRQ polling thread. + * Otherwise exit from ISR. + */ + if (!intr_info->op_reply_q) + return ret; + + if (!intr_info->op_reply_q->enable_irq_poll || + !atomic_read(&intr_info->op_reply_q->pend_ios)) + return ret; + + disable_irq_nosync(pci_irq_vector(mrioc->pdev, midx)); + + return IRQ_WAKE_THREAD; } /** @@ -446,6 +475,36 @@ static irqreturn_t mpi3mr_isr(int irq, void *privdata) */ static irqreturn_t mpi3mr_isr_poll(int irq, void *privdata) { + struct mpi3mr_intr_info *intr_info = privdata; + struct mpi3mr_ioc *mrioc; + u16 midx; + u32 num_admin_replies = 0, num_op_reply = 0; + + if (!intr_info || !intr_info->op_reply_q) + return IRQ_NONE; + + mrioc = intr_info->mrioc; + midx = intr_info->msix_index; + + /* Poll for pending IOs completions */ + do { + if (!mrioc->intr_enabled) + break; + + if (!midx) + num_admin_replies = mpi3mr_process_admin_reply_q(mrioc); + if (intr_info->op_reply_q) + num_op_reply += + mpi3mr_process_op_reply_q(mrioc, intr_info); + + usleep_range(mrioc->irqpoll_sleep, 10 * mrioc->irqpoll_sleep); + + } while (atomic_read(&intr_info->op_reply_q->pend_ios) && + (num_op_reply < mrioc->max_host_ios)); + + intr_info->op_reply_q->enable_irq_poll = false; + enable_irq(pci_irq_vector(mrioc->pdev, midx)); + return IRQ_HANDLED; } @@ -1161,6 +1220,9 @@ static int mpi3mr_create_op_reply_q(struct mpi3mr_ioc *mrioc, u16 qidx) op_reply_q->num_replies = MPI3MR_OP_REP_Q_QD; op_reply_q->ci = 0; op_reply_q->ephase = 1; + atomic_set(&op_reply_q->pend_ios, 0); + atomic_set(&op_reply_q->in_use, 0); + op_reply_q->enable_irq_poll = false; if (!op_reply_q->q_segments) { retval = mpi3mr_alloc_op_reply_q_segments(mrioc, qidx); @@ -1482,6 +1544,10 @@ int mpi3mr_op_request_post(struct mpi3mr_ioc *mrioc, pi = 0; op_req_q->pi = pi; + if (atomic_inc_return(&mrioc->op_reply_qinfo[reply_qidx].pend_ios) + > MPI3MR_IRQ_POLL_TRIGGER_IOCOUNT) + mrioc->op_reply_qinfo[reply_qidx].enable_irq_poll = true; + writel(op_req_q->pi, &mrioc->sysif_regs->OperQueueIndexes[reply_qidx].ProducerIndex); @@ -2783,6 +2849,7 @@ int mpi3mr_init_ioc(struct mpi3mr_ioc *mrioc, u8 re_init) u32 ioc_status, ioc_config, i; Mpi3IOCFactsData_t facts_data; + mrioc->irqpoll_sleep = MPI3MR_IRQ_POLL_SLEEP; mrioc->change_count = 0; if (!re_init) { mrioc->cpu_count = num_online_cpus(); @@ -3068,6 +3135,8 @@ static void mpi3mr_memset_buffers(struct mpi3mr_ioc *mrioc) mrioc->op_reply_qinfo[i].ci = 0; mrioc->op_reply_qinfo[i].num_replies = 0; mrioc->op_reply_qinfo[i].ephase = 0; + atomic_set(&mrioc->op_reply_qinfo[i].pend_ios, 0); + atomic_set(&mrioc->op_reply_qinfo[i].in_use, 0); mpi3mr_memset_op_reply_q_buffers(mrioc, i); mrioc->req_qinfo[i].ci = 0;
Register driver for threaded interrupt. By default, driver will attempt io completion from interrupt context (primary handler). Since driver tracks per reply queue outstanding ios, it will schedule threaded ISR if there are any outstanding IOs expected on that particular reply queue. Threaded ISR (secondary handler) will loop for IO completion as long as there are outstanding IOs (speculative method using same per reply queue outstanding counter) or it has completed some X amount of commands (something like budget). Signed-off-by: Kashyap Desai <kashyap.desai@broadcom.com> Cc: sathya.prakash@broadcom.com --- drivers/scsi/mpi3mr/mpi3mr.h | 12 ++++++ drivers/scsi/mpi3mr/mpi3mr_fw.c | 75 +++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 3 deletions(-)