ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [xv6] Local APIC
    프로젝트/운영체제 만들기 2023. 7. 21. 02:46

    글의 참고

    - https://github.com/mit-pdos/xv6-public/tree/master

    - MultiProcessor Specification 1.4

    - 64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf [Order Number: 253668-060US]

    - xv6 - DRAFT as of September 4, 2018


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - Local APIC 소스 코드 분석

    : Local APIC에 대한 내용은 이 글을 참고하자. 일단, 먼저 `xv6`에서 Local APIC를 사용하는 필요한 유틸리티 함수들을 알아보자.

    ....
    volatile uint *lapic;  // Initialized in mp.c

    //PAGEBREAK!
    static void lapicw(int index, int value)
    {
      lapic[index] = value;
      lapic[ID];  // wait for write to finish, by reading
    }

    ....
    ....

    int lapicid(void)
    {
      if (!lapic)
        return 0;
      return lapic[ID] >> 24;
    }

    // Acknowledge interrupt.
    void lapiceoi(void)
    {
      if(lapic)
        lapicw(EOI, 0);
    }

    // Spin for a given number of microseconds.
    // On real hardware would want to tune this dynamically.
    void microdelay(int us)
    {
    }

    ....
    ....

    : 먼저 전역 변수로 `lapic`는 MP 테이블을 파싱하는 코드에서 할당된다. Local APIC 와의 통신은 메모리 맵 방식을 사용한다. 이 때문에 캐쉬를 허용하면 안된다. 그래서 `volatile` 키워드를 사용한다. `lapicw`는 오프셋을 받아서, Local APIC의 특정 레지스터에 값을 쓰는 함수이다. `lapicid`는 `Local APIC ID` 레지스터 값을 읽어온다. `microdelay` 함수는 비어있다. 이 함수는 실제 하드웨어에 사양에 따라 동적으로 세팅하기 위해 남겨둔 함수라고 한다. 

     

    : 참고로, `xv6`에서는 PIT를 아예 사용하지 않는다. 정확히 말하면, x86 기반의 `xv6`는 PIT를 사용하지 않는다. MIPS 버전의 `xv6`가 있는데, 해당 버전에서는 PIT를 사용하는 코드가 보인다. 이 얘기를 하는 이유는, `xv6`는 타이머로 PIT가 아닌 APIC 타이머를 사용하기 때문이다. 

     

    : 이 글에서는 `xv6`의 Local APIC 소스를 분석해본다. 참고로, lapicinit 함수는 BSP 및 AP가 자신들의 Local APIC를 초기화하기 위해서 최초에 한 번 호출하는 함수다.

    // Local APIC registers, divided by 4 for use as uint[] indices.
    #define ID      (0x0020/4)   // ID
    #define VER     (0x0030/4)   // Version
    #define TPR     (0x0080/4)   // Task Priority
    #define EOI     (0x00B0/4)   // EOI
    #define SVR     (0x00F0/4)   // Spurious Interrupt Vector
      #define ENABLE     0x00000100   // Unit Enable
    #define ESR     (0x0280/4)   // Error Status
    #define ICRLO   (0x0300/4)   // Interrupt Command
      #define INIT       0x00000500   // INIT/RESET
      #define STARTUP    0x00000600   // Startup IPI
      #define DELIVS     0x00001000   // Delivery status
      #define ASSERT     0x00004000   // Assert interrupt (vs deassert)
      #define DEASSERT   0x00000000
      #define LEVEL      0x00008000   // Level triggered
      #define BCAST      0x00080000   // Send to all APICs, including self.
      #define BUSY       0x00001000
      #define FIXED      0x00000000
    #define ICRHI   (0x0310/4)   // Interrupt Command [63:32]
    #define TIMER   (0x0320/4)   // Local Vector Table 0 (TIMER)
      #define X1         0x0000000B   // divide counts by 1
      #define PERIODIC   0x00020000   // Periodic
    #define PCINT   (0x0340/4)   // Performance Counter LVT
    #define LINT0   (0x0350/4)   // Local Vector Table 1 (LINT0)
    #define LINT1   (0x0360/4)   // Local Vector Table 2 (LINT1)
    #define ERROR   (0x0370/4)   // Local Vector Table 3 (ERROR)
      #define MASKED     0x00010000   // Interrupt masked
    #define TICR    (0x0380/4)   // Timer Initial Count
    #define TCCR    (0x0390/4)   // Timer Current Count
    #define TDCR    (0x03E0/4)   // Timer Divide Configuration

    volatile uint *lapic;  // Initialized in mp.c

    ....

    ....

    void lapicinit(void)

    {
      if(!lapic)
        return;

      // 첫 번째 블락
      // Enable local APIC; set spurious interrupt vector.
      lapicw(SVR, ENABLE | (T_IRQ0 + IRQ_SPURIOUS)); // --- 1

      // 두 번째 블락
      // The timer repeatedly counts down at bus frequency
      // from lapic[TICR] and then issues an interrupt.
      // If xv6 cared more about precise timekeeping,
      // TICR would be calibrated using an external time source.
      lapicw(TDCR, X1);
      lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));
      lapicw(TICR, 10000000);

      // 세 번째 블락
      // Disable logical interrupt lines.
      lapicw(LINT0, MASKED);
      lapicw(LINT1, MASKED);

      // 네 번째 블락
      // Disable performance counter overflow interrupts
      // on machines that provide that interrupt entry.
      if(((lapic[VER]>>16) & 0xFF) >= 4)
        lapicw(PCINT, MASKED);

      // 다섯 번째 블락
      // Map error interrupt to IRQ_ERROR.
      lapicw(ERROR, T_IRQ0 + IRQ_ERROR);

      // 여섯 번째 블락
      // Clear error status register (requires back-to-back writes).
      lapicw(ESR, 0);
      lapicw(ESR, 0);

      // 일곱 번째 블락
      // Ack any outstanding interrupts.
      lapicw(EOI, 0);

      // 여덝 번째 블락
      // Send an Init Level De-Assert to synchronise arbitration ID's.
      lapicw(ICRHI, 0);
      lapicw(ICRLO, BCAST | INIT | LEVEL);
      while(lapic[ICRLO] & DELIVS)
        ;

      // 아홉 번째 블락
      // Enable interrupts on the APIC (but not on the processor).
      lapicw(TPR, 0);
    }

    : 위에서 매크로 값들을 `4`로 나누는 코드가 있다. 해당 코드들은 `lapic`가 `uint` 형이라서 그렇다. `uchar` 였다면, 4로 나누지 않았을 것이다(`lapic[ID]` == `lapic+ID`는 같기 때문이다. 즉, 1을 더할 때 마다 4바이트씩 이동해서 4로 나눈것이다). 소스 분석 내용은 아래와 같다. 

     

    1" Local APIC를 활성화하기 위해 2가지 조건이 필요하다. 이 글을 참고하자. `xv6`는 `IA32_APIC_BASE_MSR`를 검사하지 않는다. 즉, 전역적으로 Local APIC는 이미 활성화되어 있다고 전제하는 것 같다. 그래서 `spurious-interrupt vector register` 에서만 특정 프로세서의 Local APIC만 활성화하는 것 같다. 그리고 추가적으로 `spurious-interrupt vector` 번호를 전달한다. 그 값은 63(T_IRQ0(32) + IRQ_SPURIOUS(31))이다.

    2" 그리고 `APIC 타이머`를 설정한다. 먼저, DIVIDE를 설정한다. 즉, 현재 APIC 타이머가 동작하는 주파수를 낮추겠다는 뜻이다. 분배 설정 레지스터에 `0x0B`을 쓴다. 이 값이면, DIVIDE는 1이 된다. 즉, 그대로 나누지 않게다는 뜻이다. 주파수를 그대로 쓰겠다는 것인데, APIC 타이머의 주파수는 내부 버스 주파수이거나 코어의 크리스탈 주파수가 된다. 이 값은 CPUID 0x15를 통해 알 수 있다. 만약, 10ms에 한 번씩 타이머 인터럽트를 발생시키고 싶다면, 먼저 APIC 타이머의 주파수를 알아낸 뒤, 10ms에 인터럽트가 발생할 수 있도록, 계산을 해야 한다. 그러나 좀 더 멋진 방법으로 이 값을 알아낼 수 있다(그러나, 이 방법은 PIT가 먼저 브링-업이 되어있다고 전제한다). `xv6`는 타이머 인터럽트로 `32`번을 사용하며, 타이머 모드는 `주기 모드`를 사용한다. 즉, 자동으로 카운트 레지스터가 0이 되면, 자동으로 리로드된다. 그리고, 카운트 레지스터에 `10000000`을 쓴다. 

    3" LINT[1:0] 핀은 Local APIC에 붙어있는 핀이다. 거의 사용하지 않는다. 그러므로, MASK 한다. 너무 간단하게 말해서 그냥 넘어가는 것처럼 들리겠지만, 실제 LINT 핀은 정말 사용하지 않는다.

    4" `Local APIC Version Register`에서 버전 정보를 읽는 코드인데, 저렇게 쉬프트 연산을 하는 이유는 [23:16]에 현재 시스템의 `Local APIC`의 버전 정보를 알 수 있는 정보가 들어있기 때문이다. 사실, 스펙에는 [7:0] 비트가 버전 정보 라고 필드라고 명시되어 있다. 그러나, 이 정보는 Local APIC가 프로세서 온 칩으로 되어있는지, 아니면 프로세서와 별개로 설계로 되었는지에 대해서 알려준다. 더 정확하게, 어떤 프로세서인지를 말해주는 정보가 [23:16] 비트이다.

    5" LVT에는 `Error Register` 엔트리가 하나있다. 이 레지스터에는 에러 인터럽트 벡터를 작성할 수 있다. APIC가 내부적으로 에럴르 감지하면, 에러 레지스터에 작성된 인터럽트 벡터가 프로세서로 전달된다.

    6" `ESR` 은 `에러 상태 레지스터`라고 해서 인터럽트 처리중에 에러를 감지하면, 해당 에러에 대한 내용을 ESR에 쓴다. 이 레지스터는 특히하게도 읽기전에 임의의 값을 이 레지스터에 써야한다. 그렇면, 이전의 기록되었던 에러 로그들이 모두 초기화된다.

    7"  `EOI`는 x86에서 현재 인터럽트를 처리를 모두 완료했다는 뜻이 된다. 즉, 프로세서는 다음 인터럽트를 처리할 수 있다는 뜻이 된다. 여기서 `EOI`를 발생시킨 이유는 이전에 처리되지 못했거나 현재 처리중인 인터럽트(미해결 인터럽트)를 리셋하겠다는 의미다.

    8" 이 코드는 모든 AP`s 들에게 `INIT IPI`를 전송하는 코드다. 시스템에 존재하는 모든 AP`s 들은 `INIT IPI`를 받으면, `wait-for SIPI` 상태가 된다. 즉, SIPI를 기다리는 상태가 된다. `INIT IPI`시에 전송되는 플래그에 주목할 필요가 있다. 기본적으로 `INIT IPI`는 트리거 모드가 `LEVEL`이고, 딜리버리 모드는 `INIT(b101)`이 된다. 그리고, 모든 AP`s 들에게 전송할 것이므로, `Destination Shortand`에 `b10`을 쓰고 있다. 즉, 브로드 캐스팅 하겠다는 소리다. 그런데, 여기서 `ASSERT`가 아닌, `DE-ASSERT`를 하고 있다. 이렇게 `INIT IPI`를 De-assert하면, `Arb ID` 레지스터가 `APIC ID` 레지스터의 값으로 리로딩된다(10.4.7.4 Local APIC State After It Receives an INIT-Deassert IPI 참고). 이렇게 `Arb ID`를 설정하는 이유는 APIC 버스의 우선순위 점유권을 Local APIC ID 값이 큰 프로세서에게 먼저 할당하기 위해서다. 그러나, APIC 버스에서 중재 방식은 우선순위가 고정된 값이 아니기 때문에, 이 부분이 크게 중요한 내용은 아닌 듯 싶다. APIC 버스의 중재 방식은 이 글을 참고하자. 그런데, 이 코드 위에 주석을 보면, `Send an Init Level De-Assert to synchronise arbitration ID's` 라는 내용이 있다. 이 내용은 펜티엠 6 계열 프로세서만 통하던 방법이다. 즉, 펜티엄 4 혹은 제온 프로세서 이상에서는 적용되지 않는다.

    9" `TPR` 에 0을 쓰면, 모든 인터럽트를 허용한다는 의미다. 만약, 이 값이 15면 모든 인터럽트를 허용하지 않는다.

     

    : 그리고 `lapicstartup` 이라는 함수가 있는데, 이 함수는 BSP 만 실행하는 함수다. 그런데, 먼저 이 함수를 보기전에 main 함수에서 어떻게 이 함수가 호출되는지를 알아야 한다.

    #define CMOS_PORT    0x70
    #define CMOS_RETURN  0x71

    // Start additional processor running entry code at addr.
    // See Appendix B of MultiProcessor Specification.
    void lapicstartap(uchar apicid, uint addr)
    {
      int i;
      ushort *wrv;

      // 첫 번째 블락
      // "The BSP must initialize CMOS shutdown code to 0AH
      // and the warm reset vector (DWORD based at 40:67) to point at
      // the AP startup code prior to the [universal startup algorithm]."
      outb(CMOS_PORT, 0xF);  // offset 0xF is shutdown code
      outb(CMOS_PORT+1, 0x0A);
      wrv = (ushort*)P2V((0x40<<4 | 0x67));  // Warm reset vector
      wrv[0] = 0;
      wrv[1] = addr >> 4;

      // 두 번째 블락 
      // "Universal startup algorithm."
      // Send INIT (level-triggered) interrupt to reset other CPU.
      lapicw(ICRHI, apicid<<24);
      lapicw(ICRLO, INIT | LEVEL | ASSERT);
      microdelay(200);
      lapicw(ICRLO, INIT | LEVEL);
      microdelay(100);    // should be 10ms, but too slow in Bochs!

      // 세 번째 블락
      // Send startup IPI (twice!) to enter code.
      // Regular hardware is supposed to only accept a STARTUP
      // when it is in the halted state due to an INIT.  So the second
      // should be ignored, but it is part of the official Intel algorithm.
      // Bochs complains about the second one. Too bad for Bochs.
      for(i = 0; i < 2; i++){
        lapicw(ICRHI, apicid<<24);
        lapicw(ICRLO, STARTUP | (addr>>12));
        microdelay(200);
      }
    }

    : 위의 내용은 `MultiProcessor Specification` 에 명시되어 있는 내용이다. 이론적인 부분은 이 글을 참고하도록 하고, 여기서는 소스 코드애 대해 설명한다.

     

    : 위에서 갑자기 CMOS라는 내용이 나오는데, 너무 복잡하게 생각할 필요는 없다. 간단하게 설명하면, BIOS의 중요한 데이터를 저장하는 `비-휘발성 RAM` 이라고 보면 된다. CMOS는 대개 RTC 칩에 들어가있다. 그래서 CMOS에 접근하는 인터페이스와 RTC에 접근하는 주소가 동일하다. 

     

    : CMOS에 접근하는 내용은 이 글을 참고하자. CMOS에서 0x0F 레지스터는 이전에 리셋이 된 이유(Shotdown Code)가 명시되어 있다(자세한 내용은 이 글을 참고하자). 첫 번째 블락을 설명하면, BSP가 AP들을 `웜-리셋 벡터(40:67)`에 `AP startup code`를 세팅하고, `BIOS shutdown code(0x09)`에 0x0A를 설정해서 AP를 `WARM-RESET` 시킬 준비를 하는 것이다. 이 과정을 가장 먼저하는 이유는 `MultiProcessor Specification` 스펙에 아래와 같이 명시되어 있기 때문이다.

    ....
    The BSP must initialize BIOS shutdown code to 0AH and the warm reset vector (DWORD based at 40:67) to point to the AP startup code `prior to` executing the following sequence
    ....

    - 참고 : MultiProcessor Specification 1.4

    : `40:67`에 AP의 엔트리 포인트 주소를 넣는다. 그런데, 4바이트를 넣고 있는다. 그 이유 또한 위에서 40:67에 `DWORD` 사이즈 주소를 넣으라고 되어있다. 스펙에서는 이 과정을 하고 나서 AP를 깨우는 과정을 진행하라고 명시하고 있다.

     

    : 그런데, `WARM-RESET VECTOR`에 4바이트를 넣는 것은 알겠는데, 왜 `wrv[0] = 0; wrv[1] = addr >> 4;` 이런식으로 AP의 스타트업 코드 주소를 넣을까? 해당 내용은 `BIOS Data Area` 영역이다. `인텔 공식 문서`와 `BIOS 부트 시퀀스 문서` 에서는 찾지 못했지만, BIOS 관련 꾀나 유명한 사이트에서 해당 내용을 찾을 수 있었다.

    ....
    [67h 2 bytes] : Adapter ROM offset address
    [69h 2 bytes] : Adapter ROM segment address
    ....

    - 참고 : http://www.bioscentral.com/misc/bda.htm

    : 즉, 하위 2바이트는 오프셋이고, 상위 2바이트는 세그먼트 어드레스다. 그래서 `wrv[1] = addr >> 4` 이런 코드가 나오는 것이다.

     

    : 두 번째 블락은 실제 AP를 깨우는 시퀀스를 따른다. `MultiProcessor Specification 1.4`에서는 AP를 활성화 시키기 위해서 아래와 같은 시퀀스를 따르도록 명시한다.

    BSP sends AP an INIT IPI
    BSP DELAYs (10mSec)
    If ( APIC_VERSION is not an 82489DX ) {
      BSP sends AP a STARTUP IPI
      BSP DELAYs (200µSEC)
      BSP sends AP a STARTUP IPI
      BSP DELAYs (200µSEC)
    }
    BSP verifies synchronization with executing AP

    - 참고 : Example B-1. Universal Start-up Algorithm

    : 두 번째 블락에서는 먼저 깨울 프로세서의 Local APIC ID를 ICR 상위 32비트에 삽입한다. 그리고, `INIT IPI`는 레벨 트리거이기 때문에, 직접 수동으로 HIGH/LOW를 컨트롤한다. 즉, 2번 WRITE 를 해야 한다.

    ....
    `INIT IPI` is an Interprocessor Interrupt with trigger mode set to level and delivery mode set to “101” (bits 8 to 10 of the ICR). INIT IPIs should always be programmed as level triggered; the operating system must perform `two writes` to the ICR to assert and then deassert this delivery mode.
    ....

    - 참고 : MultiProcessor Specification 1.4

     

    : 그런데, 스펙과는 다르게 `INIT IPI`를 실행하는데, 레벨을 HIGH/LOW 하는 부분에서 중간에 200 마이크로 딜레이가 존재한다. 이 내용은 스펙에서는 명시되어 있지 않은 부분이다. 이렇게 딜레이를 준 이유를 추측해보면, 레벨 트리거 특성상 HIGH로 잡아놓으면 계속 HIGH가 된다. 그런데, HIGH 후에 빠른 시점안에 LOW 코드가 와야한다. 그런데, 즉각적으로 LOW 코드가 와버리면, 시그널은 받는 쪽에서 인식하지 못할 수 도 있다. 그래서 딜레이로 준 것으로 보인다. 물론, QEMU에서는 즉각적으로 인식이 되서 아마 `microdelay` 함수 내부를 구현하지 않은 것 같다. 즉, 딜레이가 없는 것과 같다.

     

    : 그 뒤에 100 마이크로 딜레이가 주어진다. 스펙에서는 10ms 로 나오는데, 이 부분은 좀 더 분석이 필요해보인다. 한 가지 더 의문점이 있다. 스펙에서는 BSP가 `INIT IPI`를 HIGH 하는 시점을 기준으로 10ms인지, LOW 하는 기준으로 10ms 인지를 명시하지 않고 있다. `INIT IPI`에 대한 시퀀스 다이어그램이 존재하지 않아서 인데, 이 부분에 대해서도 좀 더 알아볼 필요가 있다. 

     

    : 세 번째 블락에서 또한, 스펙에서 명시된 대로 2번 SIPI를 200 마이크로 딜레이 간격으로 전송한다. 그리고, `SIPI`는 `INIT IPI`와는 다르게 `엣지 트리거` 방식으로 동작한다. 위에서 `lapicw(ICRHI, apicid<<24); lapicw(ICRLO, STARTUP | (addr>>12));` 엣지 트리거 상수가 보이지 않는 이유는 엣지 트리거가 0이고 레벨 트리거가 1이기 때문이다. 즉, 엣지 트리거를 사용하려면, 15번 비트에 0을 쓰면 된다는 것이다.

    ....
    These local APICs recognize the STARTUP IPI, which is an APIC Interprocessor Interrupt with trigger mode set to edge and delivery mode set to “110” (bits 8 through 10 of the ICR).
    ....

    The STARTUP IPI causes the target processor to start executing in Real Mode from address 000VV000h, where VV is an 8-bit vector that is part of the IPI message. Startup vectors are limited to a `4-kilobyte page boundary` in the first megabyte of the address space.
    ....

    If the target processor is in the halted state immediately after RESET or INIT, a STARTUP IPI causes it to leave that state and start executing. The effect is to set CS:IP to VV00:0000h.
    ....

    - 참고 : MultiProcessor Specification 1.4

    : 위에서 `SIPI` 를 통해서 AP`s 가 시작되는 엔트리 포인트 주소는 4KB 정렬된 주소다. 그리고 ICR의 `VECTOR` 필드가 8비트인 점을 고려하면, 엔트리 포인트의 범위는 1MB 안쪽에 이어야 한다는 점이다. 그렇기 때문에, 리얼 모드에서 AP`s 들의 엔트리 포인트 주소 범위중 `000VV000h` 범위의 값만 유효하게 된다.

     

    : 그리고, AP`s 주소 `000VV000h`은 `VV00:0000h`가 같은 형태로 변경된다. 즉, `000VV000h` 에서 `VV`는 세그먼트 어드레스에 할당되서 `000VV000h` << 4`를 거치게 된다.

     

    : `lapicinit`과 `lapicstartup` 함수는 모두 main.c 파일에서만 호출되는 함수다. `lapicinit`은 모든 프로세서가 자신의 Local APIC를 초기화하기 위해서 호출하는 함수고, `lapicstartup` 함수는 BSP가 AP`s 들을 활성화시키기 위해서 딱 한번 호출하는 함수다. 이 함수들이 호출되는 과정을 간단하게만 알아보자.

     

    : main 함수는 BSP만 호출하는 함수다. BSP는 MP 자료 구조를 초기화(`mpinit`)하고, 곧 바로 BSP의 Local APIC를 초기화한다. `lapicinit`에서 BSP가 `INIT IPI`를 각 AP`s 들에게 전송한다. 이 시점에서는 AP`s 들을 활성화시키기 위해서라기 보다는 APIC 버스의 우선순위를 중재하는 과정이라고 보는게 맞다. 이 과정을 통해 AP`s 들에게 버스의 우선권이 먼저 주어진다.

    // Bootstrap processor starts running C code here.
    // Allocate a real stack and switch to it, first
    // doing some setup required for memory allocator to work.
    int main(void)
    {
      kinit1(end, P2V(4*1024*1024)); // phys page allocator
      kvmalloc();      // kernel page table
      mpinit();        // detect other processors
      lapicinit();     // interrupt controller
      seginit();       // segment descriptors
      picinit();       // disable pic
      ioapicinit();    // another interrupt controller
      consoleinit();   // console hardware
      uartinit();      // serial port
      pinit();         // process table
      tvinit();        // trap vectors
      binit();         // buffer cache
      fileinit();      // file table
      ideinit();       // disk 
      startothers();   // start other processors
      kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()
      userinit();      // first user process
      mpmain();        // finish this processor's setup
    }

    ....
    ....

    // Start the non-boot (AP) processors.
    static void startothers(void)
    {
      extern uchar _binary_entryother_start[], _binary_entryother_size[];
      uchar *code;
      struct cpu *c;
      char *stack;

      // Write entry code to unused memory at 0x7000.
      // The linker has placed the image of entryother.S in
      // _binary_entryother_start.
      code = P2V(0x7000);
      memmove(code, _binary_entryother_start, (uint)_binary_entryother_size);

      for(c = cpus; c < cpus+ncpu; c++){
        if(c == mycpu())  // We've started already.
          continue;

        // Tell entryother.S what stack to use, where to enter, and what
        // pgdir to use. We cannot use kpgdir yet, because the AP processor
        // is running in low  memory, so we use entrypgdir for the APs too.
        stack = kalloc();
        *(void**)(code-4) = stack + KSTACKSIZE;
        *(void(**)(void))(code-8) = mpenter;
        *(int**)(code-12) = (void *) V2P(entrypgdir);

        lapicstartap(c->apicid, V2P(code));

        // wait for cpu to finish mpmain()
        while(c->started == 0)
          ;
      }
    }

    : BSP는 그 후에 `startothers` 함수를 호출한다. 여기서 루프를 돌면서, 시스템에 존재하는 모든 AP`s 들을 활성화하고, 활성화된 AP가 `mpmain` 함수 호출을 마무리 할 때 까지 대기한다. AP의 진행 과정에 대한 자세한 코드 분석은 이 글을 참고하자.

    '프로젝트 > 운영체제 만들기' 카테고리의 다른 글

    [xv6] Application Processor  (0) 2023.07.23
    CMOS  (0) 2023.07.22
    [컴퓨터 구조] Local APIC  (0) 2023.07.19
    [멀티프로세서] Multi-Processor Specification(MPS)  (0) 2023.07.18
    [xv6] Scheduling  (0) 2023.07.17
Designed by Tistory.