| /* Copyright (C) 2021 Free Software Foundation, Inc. |
| Contributed by Oracle. |
| |
| This file is part of GNU Binutils. |
| |
| 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; either version 3, or (at your option) |
| any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with this program; if not, write to the Free Software |
| Foundation, 51 Franklin Street - Fifth Floor, Boston, |
| MA 02110-1301, USA. */ |
| |
| #include "config.h" |
| #include <dlfcn.h> |
| #include <pthread.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <unistd.h> |
| #include <sys/mman.h> |
| #include <sys/param.h> |
| #include <sys/stat.h> |
| |
| #include "gp-defs.h" |
| #include "collector.h" |
| #include "gp-experiment.h" |
| #include "memmgr.h" |
| |
| /* TprintfT(<level>,...) definitions. Adjust per module as needed */ |
| #define DBG_LT0 0 // for high-level configuration, unexpected errors/warnings |
| #define DBG_LT1 1 // for configuration details, warnings |
| #define DBG_LT2 2 |
| #define DBG_LT3 3 |
| |
| /* ------------- Data and prototypes for block management --------- */ |
| #define IO_BLK 0 /* Concurrent requests */ |
| #define IO_SEQ 1 /* All requests are sequential, f.e. JAVA_CLASSES */ |
| #define IO_TXT 2 /* Sequential requests. Text strings. */ |
| #define ST_INIT 0 /* Initial state. Not allocated */ |
| #define ST_FREE 1 /* Available */ |
| #define ST_BUSY 2 /* Not available */ |
| |
| /* IO_BLK, IO_SEQ */ |
| #define NCHUNKS 64 |
| |
| /* IO_TXT */ |
| #define NBUFS 64 /* Number of text buffers */ |
| #define CUR_BUSY(x) ((uint32_t) ((x)>>63)) /* bit 63 */ |
| #define CUR_INDX(x) ((uint32_t) (((x)>>57) & 0x3fULL)) /* bits 62:57 */ |
| #define CUR_FOFF(x) ((x) & 0x01ffffffffffffffULL) /* bits 56: 0 */ |
| #define CUR_MAKE(busy, indx, foff) ((((uint64_t)(busy))<<63) | (((uint64_t)(indx))<<57) | ((uint64_t)(foff)) ) |
| |
| typedef struct Buffer |
| { |
| uint8_t *vaddr; |
| uint32_t left; /* bytes left */ |
| uint32_t state; /* ST_FREE or ST_BUSY */ |
| } Buffer; |
| |
| typedef struct DataHandle |
| { |
| Pckt_type kind; /* obsolete (to be removed) */ |
| int iotype; /* IO_BLK, IO_SEQ, IO_TXT */ |
| int active; |
| char fname[MAXPATHLEN]; /* data file name */ |
| |
| /* IO_BLK, IO_SEQ */ |
| uint32_t nflow; /* number of data flows */ |
| uint32_t *blkstate; /* block states, nflow*NCHUNKS array */ |
| uint32_t *blkoff; /* block offset, nflow*NCHUNKS array */ |
| uint32_t nchnk; /* number of active chunks, probably small for IO_BLK */ |
| uint8_t *chunks[NCHUNKS]; /* chunks (nflow contiguous blocks in virtual memory) */ |
| uint32_t chblk[NCHUNKS]; /* number of active blocks in a chunk */ |
| uint32_t nblk; /* number of blocks in data file */ |
| int exempt; /* if exempt from experiment size limit */ |
| |
| /* IO_TXT */ |
| Buffer *buffers; /* array of text buffers */ |
| uint64_t curpos; /* current buffer and file offset */ |
| } DataHandle; |
| |
| #define PROFILE_DATAHNDL_MAX 16 |
| static DataHandle data_hndls[PROFILE_DATAHNDL_MAX]; |
| static int initialized = 0; |
| static long blksz; /* Block size. Multiple of page size. Power of two to make (x%blksz)==(x&(blksz-1)) fast. */ |
| static long log2blksz; /* log2(blksz) to make (x/blksz)==(x>>log2blksz) fast. */ |
| static uint32_t size_limit; /* Experiment size limit */ |
| static uint32_t cur_size; /* Current experiment size */ |
| static void init (); |
| static void deleteHandle (DataHandle *hndl); |
| static int exp_size_ck (int nblocks, char *fname); |
| |
| /* IO_BLK, IO_SEQ */ |
| static int allocateChunk (DataHandle *hndl, unsigned ichunk); |
| static uint8_t *getBlock (DataHandle *hndl, unsigned iflow, unsigned ichunk); |
| static int remapBlock (DataHandle *hndl, unsigned iflow, unsigned ichunk); |
| static int newBlock (DataHandle *hndl, unsigned iflow, unsigned ichunk); |
| static void deleteBlock (DataHandle *hndl, unsigned iflow, unsigned ichunk); |
| |
| /* IO_TXT */ |
| static int is_not_the_log_file (char *fname); |
| static int mapBuffer (char *fname, Buffer *buf, off64_t foff); |
| static int newBuffer (DataHandle *hndl, uint64_t pos); |
| static void writeBuffer (Buffer *buf, int blk_off, char *src, int len); |
| static void deleteBuffer (Buffer *buf); |
| |
| /* |
| * Common buffer management routines |
| */ |
| static void |
| init () |
| { |
| /* set the block size */ |
| long pgsz = CALL_UTIL (sysconf)(_SC_PAGESIZE); |
| blksz = pgsz; |
| log2blksz = 16; /* ensure a minimum size */ |
| while ((1 << log2blksz) < blksz) |
| log2blksz += 1; |
| blksz = 1L << log2blksz; /* ensure that blksz is a power of two */ |
| TprintfT (DBG_LT1, "iolib init: page size=%ld (0x%lx) blksz=%ld (0x%lx) log2blksz=%ld\n", |
| pgsz, pgsz, (long) blksz, (long) blksz, (long) log2blksz); |
| size_limit = 0; |
| cur_size = 0; |
| initialized = 1; |
| } |
| |
| DataHandle * |
| __collector_create_handle (char *descp) |
| { |
| int exempt = 0; |
| char *desc = descp; |
| if (desc[0] == '*') |
| { |
| desc++; |
| exempt = 1; |
| } |
| if (!initialized) |
| init (); |
| |
| /* set up header for file, file name, etc. */ |
| if (__collector_exp_dir_name == NULL) |
| { |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\">__collector_exp_dir_name==NULL</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_EXPOPEN); |
| return NULL; |
| } |
| char fname[MAXPATHLEN]; |
| CALL_UTIL (strlcpy)(fname, __collector_exp_dir_name, sizeof (fname)); |
| CALL_UTIL (strlcat)(fname, "/", sizeof (fname)); |
| Pckt_type kind = 0; |
| int iotype = IO_BLK; |
| if (__collector_strcmp (desc, SP_HEAPTRACE_FILE) == 0) |
| kind = HEAP_PCKT; |
| else if (__collector_strcmp (desc, SP_SYNCTRACE_FILE) == 0) |
| kind = SYNC_PCKT; |
| else if (__collector_strcmp (desc, SP_IOTRACE_FILE) == 0) |
| kind = IOTRACE_PCKT; |
| else if (__collector_strcmp (desc, SP_RACETRACE_FILE) == 0) |
| kind = RACE_PCKT; |
| else if (__collector_strcmp (desc, SP_PROFILE_FILE) == 0) |
| kind = PROF_PCKT; |
| else if (__collector_strcmp (desc, SP_OMPTRACE_FILE) == 0) |
| kind = OMP_PCKT; |
| else if (__collector_strcmp (desc, SP_HWCNTR_FILE) == 0) |
| kind = HW_PCKT; |
| else if (__collector_strcmp (desc, SP_DEADLOCK_FILE) == 0) |
| kind = DEADLOCK_PCKT; |
| else if (__collector_strcmp (desc, SP_FRINFO_FILE) == 0) |
| CALL_UTIL (strlcat)(fname, "data.", sizeof (fname)); |
| else if (__collector_strcmp (desc, SP_LOG_FILE) == 0) |
| iotype = IO_TXT; |
| else if (__collector_strcmp (desc, SP_MAP_FILE) == 0) |
| iotype = IO_TXT; |
| else if (__collector_strcmp (desc, SP_JCLASSES_FILE) == 0) |
| iotype = IO_SEQ; |
| else |
| { |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\">iolib unknown file desc %s</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_EXPOPEN, desc); |
| return NULL; |
| } |
| |
| CALL_UTIL (strlcat)(fname, desc, sizeof (fname)); |
| TprintfT (DBG_LT1, "createHandle calling open on fname = `%s', desc = `%s' %s\n", |
| fname, desc, (exempt == 0 ? "non-exempt" : "exempt")); |
| |
| /* allocate a handle -- not mt-safe */ |
| DataHandle *hndl = NULL; |
| for (int i = 0; i < PROFILE_DATAHNDL_MAX; ++i) |
| if (data_hndls[i].active == 0) |
| { |
| hndl = &data_hndls[i]; |
| break; |
| } |
| |
| /* out of handles? */ |
| if (hndl == NULL) |
| { |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\">%s</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_NOHNDL, fname); |
| return NULL; |
| } |
| |
| hndl->kind = kind; |
| hndl->nblk = 0; |
| hndl->exempt = exempt; |
| CALL_UTIL (strlcpy)(hndl->fname, fname, sizeof (hndl->fname)); |
| int fd = CALL_UTIL (open)(hndl->fname, |
| O_RDWR | O_CREAT | O_TRUNC | O_EXCL, |
| S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); |
| if (fd < 0) |
| { |
| TprintfT (0, "createHandle open failed -- hndl->fname = `%s', SP_LOG_FILE = `%s': %s\n", |
| hndl->fname, SP_LOG_FILE, CALL_UTIL (strerror)(errno)); |
| if (is_not_the_log_file (hndl->fname) == 0) |
| { |
| char errbuf[4096]; |
| /* If we are trying to create the handle for the log file, write to stderr, not the experiment */ |
| CALL_UTIL (snprintf)(errbuf, sizeof (errbuf), |
| "create_handle: COL_ERROR_LOG_OPEN %s: %s\n", hndl->fname, CALL_UTIL (strerror)(errno)); |
| CALL_UTIL (write)(2, errbuf, CALL_UTIL (strlen)(errbuf)); |
| |
| } |
| else |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\" ec=\"%d\">%s: create_handle</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_FILEOPN, errno, hndl->fname); |
| return NULL; |
| } |
| CALL_UTIL (close)(fd); |
| |
| hndl->iotype = iotype; |
| if (hndl->iotype == IO_TXT) |
| { |
| /* allocate our buffers in virtual memory */ |
| /* later, we will remap buffers individually to the file */ |
| uint8_t *memory = (uint8_t*) CALL_UTIL (mmap64_) (0, |
| (size_t) (NBUFS * blksz), PROT_READ | PROT_WRITE, |
| #if ARCH(SPARC) |
| MAP_SHARED | MAP_ANON, |
| #else |
| MAP_PRIVATE | MAP_ANON, |
| #endif |
| -1, (off64_t) 0); |
| if (memory == MAP_FAILED) |
| { |
| TprintfT (0, "create_handle: can't mmap MAP_ANON (for %s): %s\n", hndl->fname, CALL_UTIL (strerror)(errno)); |
| /* see if this is the log file */ |
| if (is_not_the_log_file (hndl->fname) == 0) |
| { |
| /* If we are trying to map the log file, write to stderr, not to the experiment */ |
| char errbuf[4096]; |
| CALL_UTIL (snprintf)(errbuf, sizeof (errbuf), |
| "create_handle: can't mmap MAP_ANON (for %s): %s\n", hndl->fname, CALL_UTIL (strerror)(errno)); |
| CALL_UTIL (write)(2, errbuf, CALL_UTIL (strlen)(errbuf)); |
| } |
| else /* write the error message into the experiment */ |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\" ec=\"%d\">MAP_ANON (for %s); create_handle</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_FILEMAP, errno, hndl->fname); |
| return NULL; |
| } |
| TprintfT (DBG_LT2, " create_handle IO_TXT data buffer length=%ld (0x%lx) file='%s' memory=%p -- %p\n", |
| (long) (NBUFS * blksz), (long) (NBUFS * blksz), hndl->fname, |
| memory, memory + (NBUFS * blksz) - 1); |
| |
| /* set up an array of buffers, pointing them to the virtual addresses */ |
| TprintfT (DBG_LT2, "create_handle IO_TXT Buffer structures fname = `%s', NBUFS= %d, size = %ld (0x%lx)\n", fname, |
| NBUFS, (long) NBUFS * sizeof (Buffer), (long) NBUFS * sizeof (Buffer)); |
| hndl->buffers = (Buffer*) __collector_allocCSize (__collector_heap, NBUFS * sizeof (Buffer), 1); |
| if (hndl->buffers == NULL) |
| { |
| TprintfT (0, "create_handle allocCSize for hndl->buffers failed\n"); |
| CALL_UTIL (munmap)(memory, NBUFS * blksz); |
| return NULL; |
| } |
| for (int i = 0; i < NBUFS; i++) |
| { |
| Buffer *buf = &hndl->buffers[i]; |
| buf->vaddr = memory + i * blksz; |
| buf->state = ST_FREE; |
| } |
| /* set the file pointer to the beginning of the file */ |
| hndl->curpos = CUR_MAKE (0, 0, 0); |
| } |
| else |
| { |
| if (hndl->iotype == IO_BLK) |
| { |
| long nflow = CALL_UTIL (sysconf)(_SC_NPROCESSORS_ONLN); |
| if (nflow < 16) |
| nflow = 16; |
| hndl->nflow = (uint32_t) nflow; |
| } |
| else if (hndl->iotype == IO_SEQ) |
| hndl->nflow = 1; |
| TprintfT (DBG_LT2, "create_handle calling allocCSize blkstate fname=`%s' nflow=%d NCHUNKS=%d size=%ld (0x%lx)\n", |
| fname, hndl->nflow, NCHUNKS, |
| (long) (hndl->nflow * NCHUNKS * sizeof (uint32_t)), |
| (long) (hndl->nflow * NCHUNKS * sizeof (uint32_t))); |
| uint32_t *blkstate = (uint32_t*) __collector_allocCSize (__collector_heap, hndl->nflow * NCHUNKS * sizeof (uint32_t), 1); |
| if (blkstate == NULL) |
| return NULL; |
| for (int j = 0; j < hndl->nflow * NCHUNKS; ++j) |
| blkstate[j] = ST_INIT; |
| hndl->blkstate = blkstate; |
| TprintfT (DBG_LT2, "create_handle calling allocCSize blkoff fname=`%s' nflow=%d NCHUNKS=%d size=%ld (0x%lx)\n", |
| fname, hndl->nflow, NCHUNKS, |
| (long) (hndl->nflow * NCHUNKS * sizeof (uint32_t)), |
| (long) (hndl->nflow * NCHUNKS * sizeof (uint32_t))); |
| hndl->blkoff = (uint32_t*) __collector_allocCSize (__collector_heap, hndl->nflow * NCHUNKS * sizeof (uint32_t), 1); |
| if (hndl->blkoff == NULL) |
| return NULL; |
| hndl->nchnk = 0; |
| for (int j = 0; j < NCHUNKS; ++j) |
| { |
| hndl->chunks[j] = NULL; |
| hndl->chblk[j] = 0; |
| } |
| } |
| hndl->active = 1; |
| return hndl; |
| } |
| |
| static void |
| deleteHandle (DataHandle *hndl) |
| { |
| if (hndl->active == 0) |
| return; |
| hndl->active = 0; |
| |
| if (hndl->iotype == IO_BLK || hndl->iotype == IO_SEQ) |
| { |
| /* Delete all blocks. */ |
| /* Since access to hndl->active is not synchronized it's still |
| * possible that we leave some blocks undeleted. |
| */ |
| for (int j = 0; j < hndl->nflow * NCHUNKS; ++j) |
| { |
| uint32_t oldstate = hndl->blkstate[j]; |
| if (oldstate != ST_FREE) |
| continue; |
| /* Mark as busy */ |
| uint32_t state = __collector_cas_32 (hndl->blkstate + j, oldstate, ST_BUSY); |
| if (state != oldstate) |
| continue; |
| deleteBlock (hndl, j / NCHUNKS, j % NCHUNKS); |
| } |
| } |
| else if (hndl->iotype == IO_TXT) |
| { |
| /* |
| * First, make sure that buffers are in some "coherent" state: |
| * |
| * At this point, the handle is no longer active. But some threads |
| * might already have passed the active-handle check and are now |
| * trying to schedule writes. So, set the handle pointer to "busy". |
| * This will prevent new writes from being scheduled. Threads that |
| * polling will time out. |
| */ |
| hrtime_t timeout = __collector_gethrtime () + 10 * ((hrtime_t) 1000000000); |
| volatile uint32_t busy = 0; |
| while (1) |
| { |
| uint32_t indx; |
| uint64_t opos, npos, foff; |
| int blk_off; |
| /* read the current pointer */ |
| opos = hndl->curpos; |
| busy = CUR_BUSY (opos); |
| indx = CUR_INDX (opos); |
| foff = CUR_FOFF (opos); |
| if (busy == 1) |
| { |
| if (__collector_gethrtime () > timeout) |
| { |
| TprintfT (0, "deleteHandle ERROR: timeout cleaning up handle for %s\n", hndl->fname); |
| return; |
| } |
| continue; |
| } |
| blk_off = foff & (blksz - 1); |
| if (blk_off > 0) |
| foff += blksz - blk_off; |
| npos = CUR_MAKE (1, indx, foff); |
| |
| /* try to update the handle position atomically */ |
| if (__collector_cas_64p (&hndl->curpos, &opos, &npos) != opos) |
| continue; |
| |
| /* |
| * If the last buffer won't be filled, account for |
| * the white space at the end so that the buffer will |
| * be deleted properly. |
| */ |
| if (blk_off > 0) |
| { |
| Buffer *buf = &hndl->buffers[indx]; |
| if (__collector_subget_32 (&buf->left, blksz - blk_off) == 0) |
| deleteBuffer (buf); |
| } |
| break; |
| } |
| /* wait for buffers to be deleted */ |
| timeout = __collector_gethrtime () + 10 * ((hrtime_t) 1000000000); |
| for (int i = 0; i < NBUFS; i++) |
| { |
| Buffer *buf = &hndl->buffers[i]; |
| while (__collector_cas_32 (&buf->state, ST_FREE, ST_INIT) != ST_FREE) |
| { |
| if (__collector_gethrtime () > timeout) |
| { |
| TprintfT (0, "deleteHandle ERROR: timeout waiting for buffer %d for %s\n", i, hndl->fname); |
| return; |
| } |
| } |
| CALL_UTIL (munmap)(buf->vaddr, blksz); |
| } |
| |
| /* free buffer array */ |
| __collector_freeCSize (__collector_heap, hndl->buffers, NBUFS * sizeof (Buffer)); |
| } |
| } |
| |
| void |
| __collector_delete_handle (DataHandle *hndl) |
| { |
| if (hndl == NULL) |
| return; |
| deleteHandle (hndl); |
| } |
| |
| static int |
| exp_size_ck (int nblocks, char *fname) |
| { |
| if (size_limit == 0) |
| return 0; |
| /* do an atomic add to the cur_size */ |
| uint32_t old_size = cur_size; |
| uint32_t new_size; |
| for (;;) |
| { |
| new_size = __collector_cas_32 (&cur_size, old_size, old_size + nblocks); |
| if (new_size == old_size) |
| { |
| new_size = old_size + nblocks; |
| break; |
| } |
| old_size = new_size; |
| } |
| TprintfT (DBG_LT2, "exp_size_ck() adding %d block(s); new_size = %d, limit = %d blocks; fname = %s\n", |
| nblocks, new_size, size_limit, fname); |
| |
| /* pause the entire collector if we have exceeded the limit */ |
| if (old_size < size_limit && new_size >= size_limit) |
| { |
| TprintfT (0, "exp_size_ck() experiment size limit exceeded; new_size = %ld, limit = %ld blocks; fname = %s\n", |
| (long) new_size, (long) size_limit, fname); |
| (void) __collector_log_write ("<event kind=\"%s\" id=\"%d\">%ld blocks (each %ld bytes)</event>\n", |
| SP_JCMD_CWARN, COL_ERROR_SIZELIM, (long) size_limit, (long) blksz); |
| __collector_pause_m ("size-limit"); |
| __collector_terminate_expt (); |
| return -1; |
| } |
| return 0; |
| } |
| |
| int |
| __collector_set_size_limit (char *par) |
| { |
| if (!initialized) |
| init (); |
| |
| int lim = CALL_UTIL (strtol)(par, &par, 0); |
| size_limit = (uint32_t) ((uint64_t) lim * 1024 * 1024 / blksz); |
| TprintfT (DBG_LT0, "collector_size_limit set to %d MB. = %d blocks\n", |
| lim, size_limit); |
| (void) __collector_log_write ("<setting limit=\"%d\"/>\n", lim); |
| return COL_ERROR_NONE; |
| } |
| |
| /* |
| * IO_BLK and IO_SEQ files |
| */ |
| |
| /* |
| * Allocate a chunk (nflow blocks) contiguously in virtual memory. |
| * Its blocks will be mmapped to the file individually. |
| */ |
| static int |
| allocateChunk (DataHandle *hndl, unsigned ichunk) |
| { |
| /* |
| * hndl->chunks[ichunk] is one of: |
| * - NULL (initial value) |
| * - CHUNK_BUSY (transition state when allocating the chunk) |
| * - some address (the allocated chunk) |
| */ |
| uint8_t *CHUNK_BUSY = (uint8_t *) 1; |
| hrtime_t timeout = 0; |
| while (1) |
| { |
| if (hndl->chunks[ichunk] > CHUNK_BUSY) |
| return 0; /* the chunk has already been allocated */ |
| /* try to allocate the chunk (change: NULL => CHUNK_BUSY) */ |
| if (__collector_cas_ptr (&hndl->chunks[ichunk], NULL, CHUNK_BUSY) == NULL) |
| { |
| /* allocate virtual memory */ |
| uint8_t *newchunk = (uint8_t*) CALL_UTIL (mmap64_) (0, |
| (size_t) (blksz * hndl->nflow), PROT_READ | PROT_WRITE, |
| #if ARCH(SPARC) |
| MAP_SHARED | MAP_ANON, |
| #else |
| MAP_PRIVATE | MAP_ANON, |
| #endif |
| -1, (off64_t) 0); |
| if (newchunk == MAP_FAILED) |
| { |
| deleteHandle (hndl); |
| TprintfT (DBG_LT1, " allocateChunk mmap: start=0x%x length=%ld (0x%lx), offset=%d ret=%p\n", |
| 0, (long) (blksz * hndl->nflow), |
| (long) (blksz * hndl->nflow), 0, newchunk); |
| TprintfT (0, "allocateChunk: can't mmap MAP_ANON (for %s): %s\n", hndl->fname, CALL_UTIL (strerror) (errno)); |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\" ec=\"%d\">MAP_ANON (for %s)</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_FILEMAP, errno, hndl->fname); |
| return 1; |
| } |
| |
| /* assign allocated address to our chunk */ |
| if (__collector_cas_ptr (&hndl->chunks[ichunk], CHUNK_BUSY, newchunk) != CHUNK_BUSY) |
| { |
| TprintfT (0, "allocateChunk: can't release chunk CAS lock for %s\n", hndl->fname); |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\">couldn't release chunk CAS lock (%s)</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_GENERAL, hndl->fname); |
| } |
| __collector_inc_32 (&hndl->nchnk); |
| return 0; |
| } |
| |
| /* check for time out */ |
| if (timeout == 0) |
| timeout = __collector_gethrtime () + 10 * ((hrtime_t) 1000000000); |
| if (__collector_gethrtime () > timeout) |
| { |
| TprintfT (0, "allocateChunk: timeout for %s\n", hndl->fname); |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\">timeout allocating chunk for %s</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_GENERAL, hndl->fname); |
| return 1; |
| } |
| } |
| } |
| |
| /* |
| * Get the address for block (iflow,ichunk). |
| */ |
| static uint8_t * |
| getBlock (DataHandle *hndl, unsigned iflow, unsigned ichunk) |
| { |
| return hndl->chunks[ichunk] + iflow * blksz; |
| } |
| |
| /* |
| * Map block (iflow,ichunk) to the next part of the file. |
| */ |
| static int |
| remapBlock (DataHandle *hndl, unsigned iflow, unsigned ichunk) |
| { |
| int rc = 0; |
| int fd; |
| /* Get the old file nblk and increment it atomically. */ |
| uint32_t oldblk = hndl->nblk; |
| for (;;) |
| { |
| uint32_t newblk = __collector_cas_32 (&hndl->nblk, oldblk, oldblk + 1); |
| if (newblk == oldblk) |
| break; |
| oldblk = newblk; |
| } |
| off64_t offset = (off64_t) oldblk * blksz; |
| |
| /* 6618470: disable thread cancellation */ |
| int old_cstate; |
| pthread_setcancelstate (PTHREAD_CANCEL_DISABLE, &old_cstate); |
| |
| /* Open the file. */ |
| int iter = 0; |
| hrtime_t tso = __collector_gethrtime (); |
| for (;;) |
| { |
| fd = CALL_UTIL (open)(hndl->fname, O_RDWR, 0); |
| if (fd < 0) |
| { |
| if (errno == EMFILE) |
| { |
| /* too many open files */ |
| iter++; |
| if (iter > 1000) |
| { |
| /* we've tried 1000 times; kick error back to caller */ |
| char errmsg[MAXPATHLEN + 50]; |
| hrtime_t teo = __collector_gethrtime (); |
| double deltato = (double) (teo - tso) / 1000000.; |
| (void) CALL_UTIL (snprintf) (errmsg, sizeof (errmsg), |
| " t=%lu, %s: open-retries-failed=%d, %3.6f ms.; remap\n", |
| (unsigned long) __collector_thr_self (), hndl->fname, |
| iter, deltato); |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\">%s</event>\n", |
| SP_JCMD_COMMENT, COL_COMMENT_NONE, errmsg); |
| rc = 1; |
| goto exit; |
| } |
| /* keep trying */ |
| continue; |
| } |
| deleteHandle (hndl); |
| TprintfT (0, "remapBlock: can't open file: %s: %s\n", hndl->fname, STR (CALL_UTIL (strerror)(errno))); |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\" ec=\"%d\">t=%lu, %s: remap </event>\n", |
| SP_JCMD_CERROR, COL_ERROR_FILEOPN, errno, |
| (unsigned long) __collector_thr_self (), |
| hndl->fname); |
| rc = 1; |
| goto exit; |
| } |
| else |
| break; |
| } |
| |
| /* report number of retries of the open due to too many open fd's */ |
| if (iter > 0) |
| { |
| char errmsg[MAXPATHLEN + 50]; |
| hrtime_t teo = __collector_gethrtime (); |
| double deltato = (double) (teo - tso) / 1000000.; |
| (void) CALL_UTIL (snprintf) (errmsg, sizeof (errmsg), |
| " t=%d, %s: open-retries=%lu, %3.6f ms.; remap\n", |
| (unsigned long) __collector_thr_self (), hndl->fname, |
| iter, deltato); |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\">%s</event>\n", |
| SP_JCMD_COMMENT, COL_COMMENT_NONE, errmsg); |
| } |
| |
| /* Ensure disk space is allocated and the block offset is 0 */ |
| uint32_t zero = 0; |
| int n = CALL_UTIL (pwrite64_) (fd, &zero, sizeof (zero), |
| (off64_t) (offset + blksz - sizeof (zero))); |
| if (n <= 0) |
| { |
| deleteHandle (hndl); |
| TprintfT (0, "remapBlock: can't pwrite file: %s : errno=%d\n", hndl->fname, errno); |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\" ec=\"%d\">%s: remap</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_NOSPACE, errno, hndl->fname); |
| CALL_UTIL (close)(fd); |
| rc = 1; |
| goto exit; |
| } |
| hndl->blkoff[iflow * NCHUNKS + ichunk] = 0; |
| |
| /* Map block to file */ |
| uint8_t *bptr = getBlock (hndl, iflow, ichunk); |
| uint8_t *vaddr = (uint8_t *) CALL_UTIL (mmap64_) ((void*) bptr, |
| (size_t) blksz, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, |
| fd, offset); |
| |
| if (vaddr != bptr) |
| { |
| deleteHandle (hndl); |
| TprintfT (DBG_LT1, " remapBlock mmap: start=%p length=%ld (0x%lx) offset=0x%llx ret=%p\n", |
| bptr, (long) blksz, (long) blksz, (long long) offset, vaddr); |
| TprintfT (0, "remapBlock: can't mmap file: %s : errno=%d\n", hndl->fname, errno); |
| (void) __collector_log_write ("<event kind=\"%s\" id=\"%d\" ec=\"%d\">%s: remap</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_FILEMAP, errno, hndl->fname); |
| CALL_UTIL (close)(fd); |
| rc = 1; |
| goto exit; |
| } |
| CALL_UTIL (close)(fd); |
| |
| if (hndl->exempt == 0) |
| exp_size_ck (1, hndl->fname); |
| else |
| Tprintf (DBG_LT1, "exp_size_ck() bypassed for %d block(s); exempt fname = %s\n", |
| 1, hndl->fname); |
| exit: |
| /* Restore the previous cancellation state */ |
| pthread_setcancelstate (old_cstate, NULL); |
| |
| return rc; |
| } |
| |
| static int |
| newBlock (DataHandle *hndl, unsigned iflow, unsigned ichunk) |
| { |
| if (allocateChunk (hndl, ichunk) != 0) |
| return 1; |
| if (remapBlock (hndl, iflow, ichunk) != 0) |
| return 1; |
| |
| /* Update the number of active blocks */ |
| __collector_inc_32 (hndl->chblk + ichunk); |
| return 0; |
| } |
| |
| static void |
| deleteBlock (DataHandle *hndl, unsigned iflow, unsigned ichunk) |
| { |
| uint8_t *bptr = getBlock (hndl, iflow, ichunk); |
| CALL_UTIL (munmap)((void*) bptr, blksz); |
| hndl->blkstate[iflow * NCHUNKS + ichunk] = ST_INIT; |
| |
| /* Update the number of active blocks */ |
| __collector_dec_32 (hndl->chblk + ichunk); |
| } |
| |
| int |
| __collector_write_record (DataHandle *hndl, Common_packet *pckt) |
| { |
| if (hndl == NULL || !hndl->active) |
| return 1; |
| /* fill in the fields of the common packet structure */ |
| if (pckt->type == 0) |
| pckt->type = hndl->kind; |
| if (pckt->tstamp == 0) |
| pckt->tstamp = __collector_gethrtime (); |
| if (pckt->lwp_id == 0) |
| pckt->lwp_id = __collector_lwp_self (); |
| if (pckt->thr_id == 0) |
| pckt->thr_id = __collector_thr_self (); |
| if (pckt->cpu_id == 0) |
| pckt->cpu_id = CALL_UTIL (getcpuid)(); |
| if (pckt->tsize == 0) |
| pckt->tsize = sizeof (Common_packet); |
| TprintfT (DBG_LT3, "collector_write_record to %s, type:%d tsize:%d\n", |
| hndl->fname, pckt->type, pckt->tsize); |
| return __collector_write_packet (hndl, (CM_Packet*) pckt); |
| } |
| |
| int |
| __collector_write_packet (DataHandle *hndl, CM_Packet *pckt) |
| { |
| if (hndl == NULL || !hndl->active) |
| return 1; |
| |
| /* if the experiment is not open, there should be no writes */ |
| if (__collector_expstate != EXP_OPEN) |
| { |
| #ifdef DEBUG |
| char *xstate; |
| switch (__collector_expstate) |
| { |
| case EXP_INIT: |
| xstate = "EXP_INIT"; |
| break; |
| case EXP_OPEN: |
| xstate = "EXP_OPEN"; |
| break; |
| case EXP_PAUSED: |
| xstate = "EXP_PAUSED"; |
| break; |
| case EXP_CLOSED: |
| xstate = "EXP_CLOSED"; |
| break; |
| default: |
| xstate = "Unknown"; |
| break; |
| } |
| TprintfT (0, "collector_write_packet: write to %s while experiment state is %s\n", |
| hndl->fname, xstate); |
| #endif |
| return 1; |
| } |
| int recsz = pckt->tsize; |
| if (recsz > blksz) |
| { |
| TprintfT (0, "collector_write_packet: packet too long: %d (max %ld)\n", recsz, blksz); |
| return 1; |
| } |
| collector_thread_t tid = __collector_no_threads ? __collector_lwp_self () |
| : __collector_thr_self (); |
| unsigned iflow = (unsigned) (((unsigned long) tid) % hndl->nflow); |
| |
| /* Acquire block */ |
| uint32_t *sptr = &hndl->blkstate[iflow * NCHUNKS]; |
| uint32_t state = ST_BUSY; |
| unsigned ichunk; |
| for (ichunk = 0; ichunk < NCHUNKS; ++ichunk) |
| { |
| uint32_t oldstate = sptr[ichunk]; |
| if (oldstate == ST_BUSY) |
| continue; |
| /* Mark as busy */ |
| state = __collector_cas_32 (sptr + ichunk, oldstate, ST_BUSY); |
| if (state == oldstate) |
| break; |
| if (state == ST_BUSY) |
| continue; |
| /* It's possible the state changed from ST_INIT to ST_FREE */ |
| oldstate = state; |
| state = __collector_cas_32 (sptr + ichunk, oldstate, ST_BUSY); |
| if (state == oldstate) |
| break; |
| } |
| |
| if (state == ST_BUSY || ichunk == NCHUNKS) |
| { |
| /* We are out of blocks for this data flow. |
| * We might switch to another flow but for now report and return. |
| */ |
| TprintfT (0, "collector_write_packet: all %d blocks on flow %d for %s are busy\n", |
| NCHUNKS, iflow, hndl->fname); |
| return 1; |
| } |
| |
| if (state == ST_INIT && newBlock (hndl, iflow, ichunk) != 0) |
| return 1; |
| uint8_t *bptr = getBlock (hndl, iflow, ichunk); |
| uint32_t blkoff = hndl->blkoff[iflow * NCHUNKS + ichunk]; |
| if (blkoff + recsz > blksz) |
| { |
| /* The record doesn't fit. Close the block */ |
| if (blkoff < blksz) |
| { |
| Common_packet *closed = (Common_packet *) (bptr + blkoff); |
| closed->type = CLOSED_PCKT; |
| closed->tsize = blksz - blkoff; /* redundant */ |
| } |
| if (remapBlock (hndl, iflow, ichunk) != 0) |
| return 1; |
| blkoff = hndl->blkoff[iflow * NCHUNKS + ichunk]; |
| } |
| if (blkoff + recsz < blksz) |
| { |
| /* Set the empty padding */ |
| Common_packet *empty = (Common_packet *) (bptr + blkoff + recsz); |
| empty->type = EMPTY_PCKT; |
| empty->tsize = blksz - blkoff - recsz; |
| } |
| __collector_memcpy (bptr + blkoff, pckt, recsz); |
| |
| /* Release block */ |
| if (hndl->active == 0) |
| { |
| deleteBlock (hndl, iflow, ichunk); |
| return 0; |
| } |
| hndl->blkoff[iflow * NCHUNKS + ichunk] += recsz; |
| sptr[ichunk] = ST_FREE; |
| return 0; |
| } |
| |
| /* |
| * IO_TXT files |
| * |
| * IO_TXT covers the case where many threads are trying to write text messages |
| * sequentially (atomically) to a file. Examples include SP_LOG_FILE and SP_MAP_FILE. |
| * |
| * The file is not written directly, but by writing to mmapped virtual memory. |
| * The granularity of the mapping is a "Buffer". There may be as many as |
| * NBUFS buffers at any one time. |
| * |
| * The current position of the file is handled via hndl->curpos. |
| * |
| * * It is accessed atomically with 64-bit CAS instructions. |
| * |
| * * This 64-bit word encapsulates: |
| * - busy: a bit to lock access to hndl->curpos |
| * - indx: an index indicating which Buffer to use for the current position |
| * - foff: the file offset |
| * |
| * * The contents are accessed with: |
| * - unpack macros: CUR_BUSY CUR_INDX CUR_FOFF |
| * - pack macro : CUR_MAKE |
| * |
| * Conceptually, what happens when a thread wants to write a message is: |
| * - acquire the hndl->curpos "busy" lock |
| * . acquire and map new Buffers if needed to complete the message |
| * . update the file offset |
| * . release the lock |
| * - write to the corresponding buffers |
| * |
| * Each Buffer has a buf->left field that tracks how many more bytes |
| * need to be written to the Buffer. After a thread writes to a Buffer, |
| * it decrements buf->left atomically. When buf->left reaches 0, the |
| * Buffer (mapping) is deleted, freeing the Buffer for a new mapping. |
| * |
| * The actual implementation has some twists: |
| * |
| * * If the entire text message fits into the current Buffer -- that is, |
| * no new Buffers are needed -- the thread does not acquire the lock. |
| * It simply updates hndl->curpos atomically to the new file offset. |
| * |
| * * There are various timeouts to prevent hangs in case of abnormalities. |
| */ |
| static int |
| is_not_the_log_file (char *fname) |
| { |
| if (CALL_UTIL (strstr)(fname, SP_LOG_FILE) == NULL) |
| return 1; |
| return 0; |
| } |
| |
| static int |
| mapBuffer (char *fname, Buffer *buf, off64_t foff) |
| { |
| int rc = 0; |
| /* open fname */ |
| int fd = CALL_UTIL (open)(fname, O_RDWR, 0); |
| if (fd < 0) |
| { |
| TprintfT (0, "mapBuffer ERROR: can't open file: %s\n", fname); |
| if (is_not_the_log_file (fname)) |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\" ec=\"%d\">%s: mapBuffer</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_FILEOPN, errno, fname); |
| return 1; |
| } |
| TprintfT (DBG_LT2, "mapBuffer pwrite file %s at 0x%llx\n", fname, (long long) foff); |
| |
| /* ensure disk space is allocated */ |
| char nl = '\n'; |
| int n = CALL_UTIL (pwrite64_) (fd, &nl, sizeof (nl), |
| (off64_t) (foff + blksz - sizeof (nl))); |
| if (n <= 0) |
| { |
| TprintfT (0, "mapBuffer ERROR: can't pwrite file %s at 0x%llx\n", fname, |
| (long long) (foff + blksz - sizeof (nl))); |
| if (is_not_the_log_file (fname)) |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\" ec=\"%d\">%s: mapBuffer</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_FILETRNC, errno, fname); |
| rc = 1; |
| goto exit; |
| } |
| /* mmap buf->vaddr to fname at foff */ |
| uint8_t *vaddr = CALL_UTIL (mmap64_) (buf->vaddr, (size_t) blksz, |
| PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, foff); |
| if (vaddr != buf->vaddr) |
| { |
| TprintfT (DBG_LT1, " mapBuffer mmap: start=%p length=%ld (0x%lx) offset=0x%llx ret=%p\n", |
| buf->vaddr, blksz, blksz, (long long) foff, vaddr); |
| TprintfT (0, "mapBuffer ERROR: can't mmap %s: vaddr=%p size=%ld (0x%lx) ret=%p off=0x%llx errno=%d\n", |
| fname, buf->vaddr, blksz, blksz, vaddr, (long long) foff, errno); |
| if (is_not_the_log_file (fname)) |
| __collector_log_write ("<event kind=\"%s\" id=\"%d\" ec=\"%d\">%s: mapBuffer</event>\n", |
| SP_JCMD_CERROR, COL_ERROR_FILEMAP, errno, fname); |
| rc = 1; |
| } |
| else |
| buf->left = blksz; |
| exit: |
| CALL_UTIL (close)(fd); |
| |
| /* Should we check buffer size? Let's not since: |
| * - IO_TXT is typically not going to be that big |
| * - we want log.xml to be treated specially |
| */ |
| /* exp_size_ck( 1, fname ); */ |
| return rc; |
| } |
| |
| static int |
| newBuffer (DataHandle *hndl, uint64_t foff) |
| { |
| /* find a ST_FREE buffer and mark it ST_BUSY */ |
| int ibuf; |
| for (ibuf = 0; ibuf < NBUFS; ibuf++) |
| if (__collector_cas_32 (&hndl->buffers[ibuf].state, ST_FREE, ST_BUSY) == ST_FREE) |
| break; |
| if (ibuf >= NBUFS) |
| { |
| TprintfT (0, "newBuffer ERROR: all buffers busy for %s\n", hndl->fname); |
| return -1; |
| } |
| Buffer *nbuf = hndl->buffers + ibuf; |
| |
| /* map buffer */ |
| if (mapBuffer (hndl->fname, nbuf, foff) != 0) |
| { |
| nbuf->state = ST_FREE; |
| ibuf = -1; |
| goto exit; |
| } |
| exit: |
| return ibuf; |
| } |
| |
| static void |
| writeBuffer (Buffer *buf, int blk_off, char *src, int len) |
| { |
| __collector_memcpy (buf->vaddr + blk_off, src, len); |
| if (__collector_subget_32 (&buf->left, len) == 0) |
| deleteBuffer (buf); |
| } |
| |
| static void |
| deleteBuffer (Buffer *buf) |
| { |
| buf->state = ST_FREE; |
| } |
| |
| int |
| __collector_write_string (DataHandle *hndl, char *src, int len) |
| { |
| if (hndl == NULL || !hndl->active) |
| return 1; |
| if (len <= 0) |
| return 0; |
| |
| hrtime_t timeout = __collector_gethrtime () + 20 * ((hrtime_t) 1000000000); |
| volatile uint32_t busy = 0; |
| while (1) |
| { |
| uint32_t indx; |
| uint64_t opos, foff, base; |
| int blk_off, buf_indices[NBUFS], ibuf, nbufs; |
| |
| /* read and decode the current pointer */ |
| opos = hndl->curpos; |
| busy = CUR_BUSY (opos); |
| indx = CUR_INDX (opos); |
| foff = CUR_FOFF (opos); |
| if (busy == 1) |
| { |
| if (__collector_gethrtime () > timeout) |
| { |
| /* |
| * E.g., if another thread deleted the handle |
| * after we checked hndl->active. |
| */ |
| TprintfT (0, "__collector_write_string ERROR: timeout writing length=%d to text file: %s\n", len, hndl->fname); |
| return 1; |
| } |
| continue; |
| } |
| |
| /* initial block offset */ |
| blk_off = foff & (blksz - 1); |
| |
| /* number of new buffers to map */ |
| int lastbuf = ((foff + len - 1) >> log2blksz); /* last block file index we will write */ |
| int firstbuf = ((foff - 1) >> log2blksz); /* last block file index we have written */ |
| nbufs = lastbuf - firstbuf; |
| TprintfT (DBG_LT2, "__collector_write_string firstbuf = %d, lastbuf = %d, nbufs = %d, log2blksz = %ld\n", |
| firstbuf, lastbuf, nbufs, log2blksz); |
| if (nbufs >= NBUFS) |
| { |
| Tprintf (0, "__collector_write_string ERROR: string of length %d too long to be written to text file: %s\n", len, hndl->fname); |
| return 1; |
| } |
| |
| /* things are simple if we don't need new buffers */ |
| if (nbufs == 0) |
| { |
| /* try to update the handle position atomically */ |
| uint64_t npos = CUR_MAKE (0, indx, foff + len); |
| if (__collector_cas_64p (&hndl->curpos, &opos, &npos) != opos) |
| continue; |
| |
| /* success! copy our string and we're done */ |
| TprintfT (DBG_LT2, "__collector_write_string writeBuffer[%d]: vaddr = %p, len = %d, foff = %lld, '%s'\n", |
| indx, hndl->buffers[indx].vaddr, len, (long long) foff, src); |
| writeBuffer (&hndl->buffers[indx], foff & (blksz - 1), src, len); |
| break; |
| } |
| |
| /* initialize the new signal mask */ |
| sigset_t new_mask; |
| sigset_t old_mask; |
| CALL_UTIL (sigfillset)(&new_mask); |
| |
| /* 6618470: disable thread cancellation */ |
| int old_cstate; |
| pthread_setcancelstate (PTHREAD_CANCEL_DISABLE, &old_cstate); |
| /* block all signals */ |
| CALL_UTIL (sigprocmask)(SIG_SETMASK, &new_mask, &old_mask); |
| |
| /* but if we need new buffers, "lock" the handle pointer */ |
| uint64_t lpos = CUR_MAKE (1, indx, foff); |
| if (__collector_cas_64p (&hndl->curpos, &opos, &lpos) != opos) |
| { |
| /* restore signal mask */ |
| CALL_UTIL (sigprocmask)(SIG_SETMASK, &old_mask, NULL); |
| /* Restore the previous cancellation state */ |
| pthread_setcancelstate (old_cstate, NULL); |
| continue; |
| } |
| |
| /* map new buffers */ |
| base = ((foff - 1) & ~(blksz - 1)); /* last buffer to have been mapped */ |
| for (ibuf = 0; ibuf < nbufs; ibuf++) |
| { |
| base += blksz; |
| buf_indices[ibuf] = newBuffer (hndl, base); |
| if (buf_indices[ibuf] < 0) |
| break; |
| } |
| |
| /* "unlock" the handle pointer */ |
| uint64_t npos = CUR_MAKE (0, indx, foff); |
| if (ibuf == nbufs) |
| npos = CUR_MAKE (0, buf_indices[nbufs - 1], foff + len); |
| if (__collector_cas_64p (&hndl->curpos, &lpos, &npos) != lpos) |
| { |
| TprintfT (0, "__collector_write_string ERROR: file handle corrupted: %s\n", hndl->fname); |
| /* |
| * At this point, the handle is apparently corrupted and |
| * presumably locked. No telling what's going on. Still |
| * let's proceed and write our data and let a later thread |
| * raise an error if it encounters one. |
| */ |
| } |
| |
| /* restore signal mask */ |
| CALL_UTIL (sigprocmask)(SIG_SETMASK, &old_mask, NULL); |
| /* Restore the previous cancellation state */ |
| pthread_setcancelstate (old_cstate, NULL); |
| |
| /* if we couldn't map all the buffers we needed, don't write any part of the string */ |
| if (ibuf < nbufs) |
| { |
| TprintfT (0, "__collector_write_string ERROR: can't map new buffer: %s\n", hndl->fname); |
| return 1; |
| } |
| |
| /* write any data to the old block */ |
| if (blk_off > 0) |
| { |
| TprintfT (DBG_LT2, "__collector_write_string partial writeBuffer[%d]: len=%ld, foff = %d '%s'\n", |
| indx, blksz - blk_off, blk_off, src); |
| writeBuffer (&hndl->buffers[indx], blk_off, src, blksz - blk_off); |
| src += blksz - blk_off; |
| len -= blksz - blk_off; |
| } |
| |
| /* write data to the new blocks */ |
| for (ibuf = 0; ibuf < nbufs; ibuf++) |
| { |
| int clen = blksz; |
| if (clen > len) |
| clen = len; |
| TprintfT (DBG_LT2, "__collector_write_string continue writeBuffer[%d]: len= %d, %s", |
| ibuf, clen, src); |
| writeBuffer (&hndl->buffers[buf_indices[ibuf]], 0, src, clen); |
| src += clen; |
| len -= clen; |
| } |
| break; |
| } |
| return 0; |
| } |
| |