我创建了两个程序。一个是myThreads.c,另一个是myThreads_test.c,它们都在同一目录中。在该目录中,我还有libslack.c,可用于其列表功能。最后,我确实有一个myThreads.h

我尝试使用以下命令进行编译:

gcc -o  myThreads_test.c myThreads.c -DHAVE_PTHREAD_RWLOCK=1 -lslack  -lrt

要么
gcc -o  myThreads.c myThreads_test.c -DHAVE_PTHREAD_RWLOCK=1 -lslack  -lrt

并得到以下错误:
   /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 0 has      invalid symbol index 11
   /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 1 has invalid symbol index 12
   /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 2 has invalid symbol index 2
   /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 3 has invalid symbol index 2
   /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 4 has invalid symbol index 11
   /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 5 has invalid symbol index 13
   /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 6 has invalid symbol index 13
   /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 7 has invalid symbol index 13
   /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 8 has invalid symbol index 2
   /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 9 has invalid symbol index 2
  /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 10 has invalid symbol index 12
  /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 11 has invalid symbol index 13
  /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 12 has invalid symbol index 13
  /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 13 has invalid symbol index 13
  /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 14 has invalid symbol index 13
  /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 15 has invalid symbol index 13
  /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 16 has invalid symbol index 13
  /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 17 has invalid symbol index 13
  /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 18 has invalid symbol index 13
  /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 19 has invalid symbol index 13
 /usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 20 has invalid symbol index 13
/usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 21 has invalid symbol index 13
/usr/bin/ld: /usr/lib/debug/usr/lib/i386-linux-gnu/crt1.o(.debug_info): relocation 22 has invalid symbol index 21
/usr/lib/gcc/i686-linux-gnu/4.6/../../../i386-linux-gnu/crt1.o: In function `_start':
(.text+0x18): undefined reference to `main'
 collect2: ld returned 1 exit status

这很奇怪,因为链接器似乎在抛出错误而不是编译器。除此之外,根据以上编译命令中c程序的顺序,将删除myThreads.c或删除myThreads_test.c。我以前见过。

因此,我尝试使用上面的不同行进行编译。我觉得这与我的编译方式有关。

这是myThreads.c
/*!
 @file myThreads.c
 An implementation of a package for creating, using and
 managing preemptive threads in linux.
*/

// Assignment 1
// Author: Georges Krinker
// Student #: 260369844
// Course: ECSE 427 (OS)
// Note: This file has been commented using doxygen style.
//       The API can be found at gkrinker.com/locker/OS/pa1
//       Refer to doxygen.org for more info

 /********************************************//**
 *  Includes
 ***********************************************/


#include <signal.h>
#include <sys/time.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "myThreads.h"

/********************************************//**
 *  Defines
 ***********************************************/

#define MAX_NUMBER_OF_THREADS 20    ///< Limit on number of threads that can be created
#define MAX_NUMBER_OF_SEMAPHORES 5  ///< Limit on number of semaphores that can be created
#define DEFAULT_QUANTUM 250         ///< Default value for quantum if non is specified

/**
 * Thread States
 */
#define BLOCKED 0
#define RUNNABLE 1
#define EXIT 2
#define RUNNING 3
#define NOTHREAD -1

 /**
  * Error Handling
  */

#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while(0)

/********************************************//**
 *  Global Variables
 ***********************************************/

static ucontext_t mainContext;                          ///< Main context for initial thread
static int quantum;                                     ///< Quantum
static int threads_completed;                           ///< 1 if all threads finished execution, ie EXIT state
static int currentThread;                               ///< ID of currently running thread
static long dispatcherCount;                            ///< Number of times the dispatcher has been called.

static List *runQueue;                                  ///< Thread Run Queue. Holds indices to the tcb table
static mythread_control_block table[MAX_NUMBER_OF_THREADS];                     ///< Holds thread control blocks
static semaphore semaphores[MAX_NUMBER_OF_SEMAPHORES];      ///< Holds semaphores
static int tableIndex;                                  ///< Holds the next free table index in the table
static int semaphoresIndex;                             ///< Holds the enxt free semaphore index in the table

static sigset_t block_alarm;                            ///< The signal set that will be blocked

/********************************************//**
 *  Functions
 ***********************************************/

 /**
 * This function initializes all the global data structures for the thread system.
 *
 * Creates a new Run Queue and initilaizes the indices for both the
 * TCB table and Semaphore table to 0. It then initializes the TCB
 * and semaphore table. Finally, [TO BE COMPLETED]
 */
 int mythread_init(){
    runQueue = list_create(NULL);
    tableIndex=0;
    semaphoresIndex=0;
    currentThread = -1; //No threads have been created yet.
    int i;
    quantum = DEFAULT_QUANTUM;
    threads_completed = 0;

    //Set all Thread Statuses to 'No Thread' in the TCB table
    for(i=0;i<MAX_NUMBER_OF_THREADS;i++){
        table[i].status=NOTHREAD;
    }
    // Set all semaphores to inactive
    int j;
    for(j=0; j<MAX_NUMBER_OF_SEMAPHORES; j++)
        semaphores[j].active = 0;

    sigemptyset (&block_alarm);
    sigaddset (&block_alarm, SIGALRM);
}

/**
 * This function creates a new thread.
 *
 * The function is responsible for allocating the stack and setting
 * up the user context appropriately. The newly created thread
 * starts running the threadfunc function when it starts. The
 * threadname is stored in the TCB and is printed for info
 * purposes The newly created thread is in the RUNNABLE
 * state when inserted into the system. It is added to the run queue.
 * @return the ID of the newly created thread
 * @param *threadfunc() function to be called on context switch
 * @param stacksize size of stack to be allocated to
 */
int mythread_create(char *threadName, void (*threadfunc)(), int stacksize){

    // Handle the error if any
    if (getcontext(&table[tableIndex].context) == -1)
      handle_error("ERROR IN GETCONTEXT!");

    // Allocate a stack for the new thread and set it's return context
    table[tableIndex].context.uc_stack.ss_size = stacksize;             //Set stack size
    table[tableIndex].context.uc_stack.ss_sp = malloc(stacksize);       // allocate that space
    table[tableIndex].context.uc_link = &mainContext;                   // set the return context as the main Context

    // Modify the context and fill out the thread info on the TCB table
    makecontext(&table[tableIndex].context, threadfunc, 0);             // set the function call and 0 arguments
    table[tableIndex].name = threadName;                                // set the thread name
    table[tableIndex].threadID = tableIndex;                            // set the thread ID
    table[tableIndex].status = RUNNABLE;                                // set the thread as RUNNABLE
    runQueue = list_append_int(runQueue, tableIndex);                   // add it to the run queue

    tableIndex++;                                                       //point to the next free slot

    return tableIndex-1;                                                // returns ID of thread
}
/**
 * This function is called at the end of the function that was invoked
 * by the thread.
 *
 * The function firstly masks signals, sets the status of that
 * thread to 'EXIT' and then unblocks signals before returning.
 */
void mythread_exit(){
    sigprocmask (SIG_BLOCK, &block_alarm, NULL);
    table[currentThread].status=EXIT;
    sigprocmask (SIG_UNBLOCK, &block_alarm, NULL);
}
/**
 * Sets the quantum for the round robin (RR) scheduler.
 *
 * @param quantum_size quantum size in us
 */
void set_quantum_size(int quantum_size){
    quantum = quantum_size;
}

/**
 * Schedules threads in a Round Robin fashion.
 *
 * The function starts by checking if there are no more threads to run.
 * If that is the case, it sets threads_completed to 1 so that
 * run_threads() will terminate and resume to the mainContext.
 * If there are still runnable threads, the dispatcher switches threads
 * by swapping context, and putting the expired thread at the end of the
 * run queue. Finally, if there is only one runnable thread, that thread
 * is allowed to run for one more quantum.
 */
void dispatcher(){
    dispatcherCount++;
    //if there are no more threads to run, and the current thread is done, set
    // threads_completed to 1
    if(list_empty(runQueue) && table[currentThread].status==EXIT){
            threads_completed=1;
            swapcontext(&table[currentThread].context, &mainContext);
        }
        //else switch to the next thread
    else if(!list_empty(runQueue)){
        int nextThread = list_shift_int(runQueue);
        int tempCurrent = currentThread;
        currentThread = nextThread;
        // if the current thread (which is yielding to the next runnable thread) is runnable, queue it
        //back in the runqueue
        if(table[tempCurrent].status != EXIT && table[tempCurrent].status!=BLOCKED){
                runQueue = list_append_int(runQueue, tempCurrent);
                table[tempCurrent].status = RUNNABLE;
        }
        table[nextThread].status = RUNNING; //set the the enxt thread to run
        sigprocmask (SIG_BLOCK, &block_alarm, NULL); //stop the dispatcher from being called while updating timing
        clock_gettime(CLOCK_REALTIME, &table[tempCurrent].stopTime); //record stop time
        table[tempCurrent].elapsedTime += (double) (table[tempCurrent].stopTime.tv_nsec - table[tempCurrent].startTime.tv_nsec); //update elapsed time
        clock_gettime(CLOCK_REALTIME, &table[currentThread].startTime);//record start timer of new thread
                sigprocmask (SIG_UNBLOCK, &block_alarm, NULL);
        //switch context
        swapcontext(&table[tempCurrent].context, &table[nextThread].context);
    }
    else{
        //Keep the only thread that's runnable for 1 more quantum (do nothing!)
    }
}

/**
 * The runthreads() switches control from the main thread to one of the threads in the runqueue.
 *
 * The function starts by defining a signal timer that triggers every
 * quantum usecs and calls the dispatcher. The function then starts the
 * first thread by doing a context switch.
 */
void runthreads(){
    //Set up the signal alarm to call dispatcher() every quantum interval
    struct itimerval tval;
    sigset(SIGALRM, dispatcher);
    tval.it_interval.tv_sec = 0;
    tval.it_interval.tv_usec = quantum;
    tval.it_value.tv_sec = 0;
    tval.it_value.tv_usec = quantum;
    setitimer(ITIMER_REAL, &tval, 0);

    // If there is nothing to run, throw an error!
    if(list_empty(runQueue)){
        handle_error("In runthreads: RUN QUEUE IS EMPTY!");
    }
    else{
        int nextThread = list_shift_int(runQueue);                          //find the next thread to run
        currentThread = nextThread;                                         //make it the current thread
        table[currentThread].status = RUNNING;                              //set it to running
        sigprocmask (SIG_BLOCK, &block_alarm, NULL);                        //stop signal from interrupting the timing calculations
        clock_gettime(CLOCK_REALTIME, &table[currentThread].startTime);     //record start time of the new current thread
        sigprocmask (SIG_UNBLOCK, &block_alarm, NULL);
        swapcontext(&mainContext, &table[nextThread].context);              //swap context
    }
    //only stop when there are no more threads to run!
    while(!threads_completed);
}
/**
 * This function creates a semaphore and sets its initial value to the given parameter.
 *
 * @param value initial semaphore value
 */
int create_semaphore(int value){
    // handle error as usual if value is less than 0
    if(value < 0)
           handle_error("semaphore initialization failed - value less than 0.");
    else{
        semaphores[semaphoresIndex].initialValue = value;                               //set initial value to value
        semaphores[semaphoresIndex].value = semaphores[semaphoresIndex].initialValue;   //set value
        semaphores[semaphoresIndex].active = 1;                                         // make it ative now
        semaphores[semaphoresIndex].waitingThreads = list_create(NULL);                 //create a waitingThreads list for it
    }
    semaphoresIndex++;

    return semaphoresIndex-1; //return ID
}

/**
 * Calls wait() on a Semaphore
 *
 * When a thread calls this function, the value of the semaphore is decremented.
 * If the value goes below 0, the thread is put into a WAIT state. That means
 * calling thread is taken out of the runqueue if the value of the semaphore
 * goes below 0.
 * @param semaphore index of semaphore to be manipulated in the table
 *
 */
void semaphore_wait(int semaphore){
    long oldDispatcherCount=dispatcherCount;
    //if trying to access a semaphore that doesn't show or is inactive handle it
    if(semaphore > semaphoresIndex || semaphores[semaphore].active==0)
        handle_error("Wrong semaphore index in wait()");

    sigprocmask (SIG_BLOCK, &block_alarm, NULL);                                //mask signals for changing semaphore value (indivisibility)
    if(--semaphores[semaphore].value<0){                                        // if value < 0, add thread to waiting queue
        list_append_int(semaphores[semaphore].waitingThreads, currentThread);
        table[currentThread].status = BLOCKED;                                  // change the status of the semaphore to BLOCKED
    }
    sigprocmask (SIG_UNBLOCK, &block_alarm, NULL);                              // Re-nable signals
    while(dispatcherCount == oldDispatcherCount);                               //wait for the scheduler to switch threads (could be made more efficient)
}
/**
 * Calls signal() on a Semaphore
 *
 * When a thread calls this function the value of the semaphore is incremented.
 * If the value is not greater than 0, then we should at least have one thread
 * waiting on it. The thread at the top of the wait queue associated with the
 * semaphore is dequeued from the wait queue and queued in the run queue. The
 * thread state is then set to RUNNABLE.
 * @param semaphore index of semaphore to be manipulated in the table
 *
 */
void semaphore_signal(int semaphore){
    //if trying to access a semaphore that doesn't show or is inactive handle it
    if(semaphore > semaphoresIndex || semaphores[semaphore].active <0)
        handle_error("Wrong semaphore index in signal()");

    if(++semaphores[semaphore].value<=0){                                       // if there are threads waiting...
        if(list_empty(semaphores[semaphore].waitingThreads))                    //contradiction! No thread waiting? Error!
            handle_error("in Semaphore signal: there should be at least one waiting thread in semaphore");
        //Dequeue a waiting thread and set it to RUNNABLE. Then add it to the run queue
        int readyThread = list_shift_int(semaphores[semaphore].waitingThreads);
        table[readyThread].status = RUNNABLE;
        list_append_int(runQueue, readyThread);
    }
}
/**
 * Destroys a semaphore.
 *
 * A call to this function while threads are waiting on the semaphore should fail.
 * That is the removal process should fail with an appropriate error message.
 * If there are no threads waiting, this function will proceed with the removal
 * after checking whether the current value is the same as the initial value of
 * the semaphore.  If the values are different, then a warning message is
 * printed before the semaphore is destroyed.
 * @param semaphore index of semaphore to be manipulated in the table
 *
 */
void destroy_semaphore(int semaphore){
    //if trying to access a semaphore that doesn't show or is inactive handle it
    if(semaphore > semaphoresIndex || semaphores[semaphore].active ==0)
        handle_error("Wrong semaphore index in destroy()");
    //initil and final value check
    if(semaphores[semaphore].value != semaphores[semaphore].initialValue){
        puts("\nWARNING: Destroying a semaphore with different initial and final values!\n");
    }
    //check that there are no threads waiting on this baby
    if(!list_empty(semaphores[semaphore].waitingThreads)){
        printf("Semaphore %i was not destroyed since one or more threads are waiting on it..\n",semaphore);
    }
    //destroy it by setting it to inactive!
    else{
    semaphores[semaphore].active = 0;
    }
}
/**
 * Prints the state of all threads that are maintained by the library at
 * any given time.
 *
 * For each thread, it prints the following information in a tabular
 * form: thread name, thread state (print as a string RUNNING, BLOCKED,
 * EXIT, etc), and amount of time run on CPU.
 *
 */
void my_threads_state(){
    int i;
    char* string;
    for(i=0; i<MAX_NUMBER_OF_THREADS; i++){
        if(table[i].status != -1){
            switch(table[i].status){
                case 0: string = "BLOCKED"; break;
                case 1: string = "RUNNABLE"; break;
                case 3: string = "RUNNING"; break;
                case 2: string = "EXIT"; break;
            }
            printf("thread name: %s with ID number: %i is in state: %s and has run for %fms.\n",
             table[i].name, i, string, table[i].elapsedTime/1000000);
        }
    }
}

这是myThreads.h
/*!
 @file myThreads.h
 An implementation of a package for creating, using and
 managing preemptive threads in linux.
*/

// Multiple-include guard
#ifndef __myThreads_H_
#define __myThreads_H_

 #include <ucontext.h>
 #include <slack/std.h>
 #include <slack/list.h>

/********************************************//**
 *  Structures
 ***********************************************/

 /*! \brief Structure that represents the thread control block (TCB).
 */

 typedef struct _mythread_control_block {
    ucontext_t context;
    char *name;
    int threadID; //Between 0-# of Threads
    int status; // Any of the above defines
    struct timespec startTime;  //when it was created
    struct timespec stopTime;   // when it was stopped
    double elapsedTime; //start-stop in ns

 }mythread_control_block;

 typedef struct _mythread_semaphore{
    int value;
    int initialValue;
    int active;
    List *waitingThreads; // holds indices to the semaphores table
}semaphore;

/********************************************//**
 *  Functions
 ***********************************************/

 /**
 * This function initializes all the global data structures for the thread system.
 *
 * Creates a new Run Queue and initilaizes the indices for both the
 * TCB table and Semaphore table to 0. It then initializes the TCB
 * and semaphore table. Finally, [TO BE COMPLETED]
 */
 int init_my_threads();
/**
 * This function creates a new thread.
 *
 * The function is responsible for allocating the stack and setting
 * up the user context appropriately. The newly created thread
 * starts running the threadfunc function when it starts. The
 * threadname is stored in the TCB and is printed for info
 * purposes The newly created thread is in the RUNNABLE
 * state when inserted into the system. It is added to the run queue.
 * @return the ID of the newly created thread
 * @param *threadfunc() function to be called on context switch
 * @param stacksize size of stack to be allocated to
 */
int mythread_create(char *threadName, void (*threadfunc)(), int stacksize);

/**
 * This function is called at the end of the function that was invoked
 * by the thread.
 *
 * The function firstly masks signals, sets the status of that
 * thread to 'EXIT' and then unblocks signals before returning.
 */
void mythread_exit();

/**
 * Sets the quantum for the round robin (RR) scheduler.
 *
 * @param quantum_size quantum size in us
 */
void set_quantum_size(int quantum_size);

/**
 * Schedules threads in a Round Robin fashion.
 *
 * The function starts by checking if there are no more threads to run.
 * If that is the case, it sets threads_completed to 1 so that
 * run_threads() will terminate and resume to the mainContext.
 * If there are still runnable threads, the dispatcher switches threads
 * by swapping context, and putting the expired thread at the end of the
 * run queue. Finally, if there is only one runnable thread, that thread
 * is allowed to run for one more quantum.
 */
void dispatcher();

/**
 * The runthreads() switches control from the main thread to one of the threads in the runqueue.
 *
 * The function starts by defining a signal timer that triggers every
 * quantum usecs and calls the dispatcher. The function then starts the
 * first thread by doing a context switch.
 */
void runthreads();

/**
 * This function creates a semaphore and sets its initial value to the given parameter.
 *
 * @param value initial semaphore value
 */
 int create_semaphore(int value);

 /**
 * Calls wait() on a Semaphore
 *
 * When a thread calls this function, the value of the semaphore is decremented.
 * If the value goes below 0, the thread is put into a WAIT state. That means
 * calling thread is taken out of the runqueue if the value of the semaphore
 * goes below 0.
 * @param semaphore index of semaphore to be manipulated in the table
 *
 */
void semaphore_wait(int semaphore);

/**
 * Calls signal() on a Semaphore
 *
 * When a thread calls this function the value of the semaphore is incremented.
 * If the value is not greater than 0, then we should at least have one thread
 * waiting on it. The thread at the top of the wait queue associated with the
 * semaphore is dequeued from the wait queue and queued in the run queue. The
 * thread state is then set to RUNNABLE.
 * @param semaphore index of semaphore to be manipulated in the table
 *
 */
 void semaphore_signal(int semaphore);

/**
 * Prints the state of all threads that are maintained by the library at
 * any given time.
 *
 * For each thread, it prints the following information in a tabular
 * form: thread name, thread state (print as a string RUNNING, BLOCKED,
 * EXIT, etc), and amount of time run on CPU.
 *
 */
void my_threads_state();

#endif

最后,这是myThreads_test.c
/*!
 @file myThreads_test.c
 A test program for testing myThreads.c
*/

// Assignment 1
// Author: Georges Krinker
// Student #: 260369844
// Course: ECSE 427 (OS)
// Note: This file has been commented using doxygen style.
//       The API can be found at gkrinker.com/locker/OS/pa1
//       Refer to doxygen.org for more info

 /********************************************//**
 *  Includes
 ***********************************************/

#include <stdio.h>
#include <stdlib.h>
#include "myThreads.h"

 /********************************************//**
 *  Global Variables
 ***********************************************/

 int mutex;                                         //Mutex used by shared resources

 int a;
 int b;
 int mult;

 void multiply() {
    int i;
    for(i=0;i<1000;++i){
    semaphore_wait(mutex);
    mult=mult+(a*b);
    a++;
    b++;
    semaphore_signal(mutex);
    }
    mythread_exit();
 }

 int main(){
    a=0;
    b=0;
    mult=0;
    mutex=create_semaphore(1);

    int threads = 12;                               // How many threads are to be created
    char* names[] = {
        "thread 0",
        "thread 1",
        "thread 2",
        "thread 3",
        "thread 4",
        "thread 5",
        "thread 6",
        "thread 7",
        "thread 8",
        "thread 9",
        "thread 10",
        "thread 11"
    };

    mythread_init();                                // Initialize Package

    set_quantum_size(50);                           // set quantum to 50

    //Create threads

    int i=0;
    int dummy = 0;

    for(i=0; i<threads; i++)
    {
       dummy=mythread_create(names[i], (void *) &multiply, 100);
    }

    runthreads();                               // Run threads

    my_threads_state();                         // See state

    printf("The value of mult is %d\n", mult);

    return 0;

 }

最佳答案

-o表示您要指定输出文件。从而,

gcc -o  myThreads.c myThreads_test.c ...

没有多大意义。
尝试分别编译两个文件:
gcc myThreads.c -o myThreads ...
gcc myThreads_test.c -o myThreads_test ...

其中myThreadsmyThreads_test将是可执行文件名。

关于c - 许多无效的符号索引和对main的 undefined reference ,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/15224880/

10-11 16:47