호출 스택의 정적 최대 크기는 왜됩니까?


46

몇 가지 프로그래밍 언어로 작업 한 결과, 필자는 스레드 스택이 필요에 따라 자동으로 확장되는 대신 미리 정의 된 최대 크기를 갖는 이유를 항상 궁금했습니다. 

이에 비해 대부분의 프로그래밍 언어에서 볼 수있는 매우 일반적인 일부 고수준 구조 (목록, 맵 등)는 새로운 요소가 추가되는 동안 필요에 따라 확장되도록 설계되었으며, 사용 가능한 메모리 또는 계산 제한에 따라 크기가 제한됩니다 ( 예를 들어 32 비트 주소 지정).

최대 스택 크기가 일부 기본 또는 컴파일러 옵션에 의해 사전 제한되지 않는 프로그래밍 언어 또는 런타임 환경에 대해서는 알지 못합니다. 이것이 프로세스에 사용 가능한 메모리의 최소 백분율 만 스택에 사용되는 경우에도 너무 많은 재귀가 유비쿼터스 스택 오버플로 오류 / 예외를 초래하는 이유입니다.

대부분의 런타임 환경이 런타임시 스택이 커질 수있는 크기의 최대 한계를 설정하는 이유는 무엇입니까?


13
이러한 종류의 스택은 연속적인 주소 공간으로, 장면 뒤에서 조용히 이동할 수 없습니다. 주소 공간은 32 비트 시스템에서 중요합니다.
코드 InChaos

7
학계에서 발생하는 재귀와 같은 상아탑 아이디어의 발생을 줄이고 코드 가독성 감소 및 총 소유 비용 증가와 같은 현실에서 문제를 발생시키기 위해;)
Brad Thomas

6
@BradThomas 이것이 테일 콜 최적화를위한 것입니다.
JAB

3
@ JohnWu : 조금 후에, 지금하는 것과 같은 것 : 메모리가 부족합니다.
Jörg W Mittag

1
경우는 분명 아니다, 하나의 이유는 메모리가 부족하면 스택이 부족보다 더 나쁜, 즉에만 원인이 스택 부족 (트랩 페이지가있다 랬)입니다 당신의 실패 과정을. 메모리가 실행이 발생할 수 아무것도 다음 시도는 메모리 할당을 할 누구든지 실패를. 그런 다음 다시 트랩 페이지가없는 시스템이나 스택을 감지하는 다른 방법을 사용하면 스택 부족이 치명적일 수 있으므로 정의되지 않은 동작이 발생할 수 있습니다. 그러한 시스템에서는 여유 저장 공간이 부족할 것이므로 무제한 재귀 코드를 작성할 수는 없습니다.
Steve Jessop

답변:


13

주소 공간에서 스택이 연속 될 필요가없는 운영 체제를 작성할 수 있습니다. 기본적으로 호출 규칙에서 약간의 혼란이 필요합니다.

  1. 현재 스택 범위에 호출중인 함수에 대한 공간이 충분하지 않으면 새 스택 범위를 만들고 스택 포인터를 호출의 일부로 시작을 가리 키도록 이동합니다.

  2. 해당 호출에서 돌아 오면 원래 스택 범위로 다시 전송됩니다. 나중에 같은 스레드에서 사용하기 위해 (1)에서 생성 된 것을 유지할 가능성이 높습니다. 원칙적으로 릴리스 할 수는 있지만 루프에서 경계를 가로 질러 계속 호핑하고 모든 호출에 메모리 할당이 필요한 비효율적 인 경우가 있습니다.

  3. setjmplongjmp, 또는 OS 제어의 비 로컬 전송이 동등한 무엇이든, 행위에에 필요할 때 제대로 된 스택 범위로 다시 이동할 수 있습니다.

나는 "호출 규약"이라고 말합니다. 구체적으로 말하면 호출자가 아닌 함수 프롤로그에서 가장 잘 수행된다고 생각하지만 이것에 대한 기억은 흐릿합니다.

꽤 많은 언어가 스레드의 고정 스택 크기를 지정하는 이유 는이 를 수행하지 않는 OS에서 기본 스택을 사용하여 작업하기 때문입니다. 다른 사람의 답변에서 알 수 있듯이 각 스택은 주소 공간에서 연속적이어야하며 이동할 수 없다는 가정하에 각 스레드에서 사용할 특정 주소 범위를 예약해야합니다. 즉, 크기를 미리 선택해야합니다. 주소 공간이 방대하고 선택한 크기가 실제로 큰 경우에도 두 개의 스레드가있는 즉시이를 선택해야합니다.

"아하 (Aha)"는 "비 연속 스택을 사용하는 OS는 무엇이라고 생각합니까? 나에게 쓸모가없는 모호한 학문 시스템입니다."라고 말합니다. 글쎄, 그것은 다행히도 이미 묻고 대답 한 또 다른 질문 입니다.


36

이러한 데이터 구조에는 일반적으로 OS 스택에없는 속성이 있습니다.

  • 연결된 목록에는 인접한 주소 공간이 필요하지 않습니다. 따라서 성장할 때 원하는 곳에서 메모리를 추가 할 수 있습니다.

  • C ++의 벡터와 같이 연속 스토리지가 필요한 컬렉션조차도 OS 스택에 비해 이점이 있습니다. 모든 포인터 / 반복자가 커질 때마다 유효하지 않은 것으로 선언 할 수 있습니다. 반면, OS 스택은 대상이 속한 프레임의 함수가 반환 될 때까지 스택에 대한 포인터를 유효하게 유지해야합니다.

프로그래밍 언어 또는 런타임은 OS 스택의 제한을 피하기 위해 비 연속적이거나 움직일 수있는 자체 스택을 구현하도록 선택할 수 있습니다. Golang은 이러한 사용자 지정 스택을 사용하여 원래 비 연속 메모리로 구현되고 포인터 추적 덕분에 이동식 스택을 통해 매우 많은 수의 공동 루틴을 지원합니다 (호브의 설명 참조). 스택리스 파이썬, Lua 및 Erlang도 사용자 정의 스택을 사용할 수 있지만 확인하지는 못했습니다.

64 비트 시스템에서는 주소 공간이 충분하고 실제 메모리를 사용할 때만 실제 메모리가 할당되므로 상대적으로 저렴한 스택으로 비교적 큰 스택을 구성 할 수 있습니다.


1
이것은 좋은 대답이며 귀하의 의미를 따르지만 각 메모리 장치마다 고유 한 주소가 있기 때문에 "연속적"과 반대되는 "연속적"메모리 블록이라는 용어가 아닙니까?
DanK

2
"콜 스택을 제한 할 필요는 없습니다"+1 단순성과 성능을 위해 종종 그러한 방식으로 구현되지만 반드시 그럴 필요는 없습니다.
Paul Draper

당신은 Go에 대해 옳습니다. 사실, 이전 버전에는 불연속 스택이 있었고 새 버전에는 이동식 스택 이 있다는 것을 이해 합니다. 어느 쪽이든, 많은 수의 고 루틴을 허용해야합니다. 스택에 대해 고 루틴 당 몇 메가 바이트를 사전 할당하면 목적을 달성하기에 너무 비쌉니다.
hobbs

@ hobbs : 그렇습니다. Go는 확장 가능한 스택으로 시작했지만 빠르게 만들기는 어려웠습니다. Go가 정확한 가비지 콜렉터를 얻었을 때 이동 가능한 스택을 구현하기 위해 피기 백을 지원했습니다. 스택이 이동하면 정확한 유형 맵이 포인터를 이전 스택에 업데이트하는 데 사용됩니다.
Matthieu M.

26

실제로 스택을 늘리는 것은 어렵고 때로는 불가능합니다. 가상 메모리에 대한 이해가 필요한 이유를 이해하려면.

Ye Olde Days의 단일 스레드 응용 프로그램과 연속 메모리에서 프로세스 주소 공간의 세 가지 구성 요소는 코드, 힙 및 스택이었습니다. 이 세 가지 방법은 OS에 따라 다르지만 일반적으로 코드는 메모리의 맨 아래부터 시작하여 힙이 다음에 올라가고 위로 올라 갔으며 스택은 메모리의 맨 위에서 시작하여 내려갔습니다. 운영 체제 용으로 예약 된 메모리도 있지만 무시할 수 있습니다. 당시의 프로그램은 스택 오버플로가 다소 급격했습니다. 스택이 힙에 충돌하고 어떤 업데이트가 먼저 업데이트되었는지에 따라 불량 데이터로 작업하거나 서브 루틴에서 메모리의 임의의 부분으로 돌아갑니다.

메모리 관리는이 모델을 다소 변경했습니다. 프로그램 관점에서 프로세스 메모리 맵의 3 가지 구성 요소가 여전히 존재하지만 일반적으로 동일한 방식으로 구성되었지만 이제 각 구성 요소는 독립적 인 세그먼트로 관리되었으며 MMU는 프로그램이 세그먼트 외부의 메모리에 액세스하려고 한 경우 OS 가상 메모리를했다하면이 필요가 없었다 또는 원하는 전체 주소 공간에 대한 프로그램 액세스 권한을 부여 할 수 있습니다. 따라서 세그먼트에는 고정 경계가 할당되었습니다.

그렇다면 프로그램에 전체 주소 공간에 대한 액세스 권한을 부여하는 것이 바람직하지 않은 이유는 무엇입니까? 그 메모리는 스왑에 대한 "커밋 요금"을 구성하기 때문에; 언제든지 다른 프로그램의 메모리를위한 공간을 만들기 위해 하나의 프로그램에 대한 메모리의 일부 또는 전부를 교체해야 할 수도 있습니다. 모든 프로그램이 잠재적으로 2GB의 스왑을 소비 할 수 있다면 모든 프로그램에 충분한 스왑을 제공하거나 두 프로그램이 얻을 수있는 것보다 더 많은 것을 필요로 할 가능성을 가져야합니다.

이 시점에서 충분한 가상 주소 공간을 가정 하면 필요한 경우 이러한 세그먼트를 확장 할 있으며 실제로 데이터 세그먼트 (힙)는 시간이 지남에 따라 커집니다. 작은 데이터 세그먼트로 시작하고 메모리 할당자가 더 많은 공간을 요청할 때 필요합니다. 이 시점에서 단일 스택으로 물리적으로 스택 세그먼트를 확장 할 수 있었을 것입니다. OS는 세그먼트 외부로 무언가를 밀어 내고 더 많은 메모리를 추가하려는 시도를 막을 수 있습니다. 그러나 이것은 특히 바람직하지 않습니다.

멀티 스레딩을 입력하십시오. 이 경우 각 스레드에는 독립적 인 스택 세그먼트가 있으며 다시 고정 크기입니다. 그러나 이제 세그먼트는 가상 주소 공간에 하나씩 배치되므로 다른 세그먼트를 이동하지 않고 한 세그먼트를 확장 할 수있는 방법이 없습니다. 프로그램은 스택에있는 메모리에 대한 포인터를 잠재적으로 가질 수 있기 때문에 수행 할 수 없습니다. 세그먼트 사이에 약간의 공간을 남겨 둘 수도 있지만 거의 모든 경우에 해당 공간이 낭비됩니다. 더 좋은 방법은 응용 프로그램 개발자에게 부담을주는 것이 었습니다. 실제로 딥 스택이 필요한 경우 스레드를 만들 때 지정할 수 있습니다.

오늘날 64 비트 가상 주소 공간을 통해 사실상 무한한 수의 스레드를 위해 사실상 무한 스택을 만들 수있었습니다. 그러나 다시 말하지만, 특히 바람직하지는 않습니다. 거의 모든 경우에 스택 오버 로우는 코드의 버그를 나타냅니다. 1GB 스택을 제공하면 해당 버그 발견이 지연됩니다.


3
현재 x86-64 CPU는 48 비트의 주소 공간 만 갖습니다
CodeInChaos

Afaik, Linux 스택을 동적으로 확장합니다. 프로세스가 현재 할당 된 스택 바로 아래 영역에 액세스하려고하면 프로세스를 보호하는 대신 스택 메모리의 추가 페이지를 매핑하여 인터럽트를 처리합니다.
cmaster

2
@ cmaster : 사실이지만 kdgregory가 "스택을 늘리십시오"라는 의미는 아닙니다. 현재 스택으로 사용하도록 지정된 주소 범위가 있습니다. 필요에 따라 더 많은 실제 메모리를 해당 주소 범위에 점차적으로 매핑하는 것에 대해 이야기하고 있습니다. kdgregory는 범위를 늘리는 것이 어렵거나 불가능하다고 말합니다.
Steve Jessop

x86은 유일한 아키텍처가 아니며 48 비트는 여전히 유효합니다
kdgregory

1
BTW, 나는 주로 세분화를 처리해야하기 때문에 x86을 사용하는 일이 그리 재미 있지 않다는 것을 기억합니다. 나는 많은 ;-) MC68k 플랫폼에서 프로젝트를 선호
kdgregory

4

고정 된 최대 크기를 갖는 스택은 어디에나 있지 않다.

스택 깊이는 전력 법칙 분포를 따릅니다. 즉, 스택 크기를 작게 만들더라도 스택 크기가 더 작은 경우에도 여전히 공간이 낭비됩니다. 아무리 커도 스택이 더 큰 함수는 여전히 존재합니다 (따라서 오류가없는 함수에 대해서는 스택 오버플로 오류를 발생시킵니다). 즉, 어떤 크기를 선택하든 항상 동시에 너무 작고 너무 큽니다.

스택이 작게 시작하고 동적으로 커지도록하여 첫 번째 문제를 해결할 수 있지만 여전히 두 번째 문제가 있습니다. 어쨌든 스택이 동적으로 커지도록 허용한다면 왜 임의의 제한을 두어야합니까?

스택이 동적으로 커지고 최대 크기가없는 시스템이 있습니다 (예 : Erlang, Go, Smalltalk 및 Scheme). 다음과 같은 것을 구현하는 많은 방법이 있습니다.

  • 움직일 수있는 스택 : 다른 스택이있어 인접한 스택이 더 이상 커질 수없는 경우 더 많은 여유 공간이있는 메모리의 다른 위치로 옮깁니다.
  • 불연속 스택 : 단일 스택 메모리 공간에 전체 스택을 할당하는 대신 여러 메모리 공간에 할당
  • 힙 할당 스택 : 스택과 힙에 별도의 메모리 영역을 두지 않고 스택을 힙에 할당하면됩니다. 알다시피 힙 할당 데이터 구조는 필요에 따라 확장 및 축소에 문제가없는 경향이 있습니다.
  • 스택을 전혀 사용하지 마십시오. 예를 들어 스택에서 함수 상태를 추적하는 대신 옵션을 사용하여 함수가 수신자에게 연속성을 전달하도록합니다.

강력한 비 로컬 제어 흐름 구성을 가지 자마자 단일 연속 스택이라는 개념은 어쨌든 창 밖으로 나옵니다. 예를 들어 재개 가능한 예외 및 연속은 스택을 "포크"하므로 실제로 네트워크로 끝납니다. 스택 (예 : 스파게티 스택으로 구현). 또한 스몰 토크와 같은 일급 수정 가능한 스택이있는 시스템에는 스파게티 스택 또는 이와 유사한 것이 거의 필요합니다.


1

OS는 스택이 요청 될 때 연속적인 블록을 제공해야합니다. 그렇게 할 수있는 유일한 방법은 최대 크기가 지정된 경우입니다.

예를 들어, 요청하는 동안 메모리가 다음과 같다고 가정 해 봅시다 (X는 사용, Os는 사용되지 않음).

XOOOXOOXOOOOOX

스택 크기가 6 인 요청이 있으면 6 개가 넘는 경우에도 OS 응답이 아니요로 응답합니다. 크기가 3 인 스택을 요청하면 OS 응답은 3 개의 빈 슬롯 (O) 영역 중 하나입니다.

또한, 다음 인접 슬롯이 점유 될 때 성장을 허용하는 것이 어렵다는 것을 알 수 있습니다.

언급 된 다른 객체 (목록 등)는 스택에 가지 않고 비 연속적이거나 조각난 영역에서 힙으로 끝나므로 커질 때 공간을 잡아서 그대로 필요하지 않습니다. 다르게 관리했습니다.

대부분의 시스템은 스택 크기에 대해 합리적인 값을 설정하므로 더 큰 크기가 필요한 경우 스레드가 생성 될 때이를 무시할 수 있습니다.


1

리눅스에서 이것은 순전히 자원을 소비하기 전에 런 어웨이 프로세스를 죽이기 위해 존재하는 자원 제한입니다. 내 데비안 시스템에서 다음 코드

#include <sys/resource.h>
#include <stdio.h>

int main() {
    struct rlimit limits;
    getrlimit(RLIMIT_STACK, &limits);
    printf("   soft limit = 0x%016lx\n", limits.rlim_cur);
    printf("   hard limit = 0x%016lx\n", limits.rlim_max);
    printf("RLIM_INFINITY = 0x%016lx\n", RLIM_INFINITY);
}

출력을 생성

   soft limit = 0x0000000000800000
   hard limit = 0xffffffffffffffff
RLIM_INFINITY = 0xffffffffffffffff

하드 한계는 다음으로 설정됩니다 RLIM_INFINITY. 프로세스는 소프트 한계를 임의의 양 으로 올릴 수 있습니다. 그러나 프로그래머가 프로그램에 실제로 비정상적인 양의 스택 메모리가 필요하다고 믿을만한 이유가 없으면 스택 크기가 8 메가 바이트를 초과하면 프로세스가 종료됩니다.

이 한계로 인해 대량의 메모리를 사용하기 시작하기 전에 런 어웨이 프로세스 (의도하지 않은 무한 재귀)가 오래 종료되어 시스템이 강제로 스왑을 시작합니다. 이로 인해 충돌 한 프로세스와 충돌 한 서버가 달라질 수 있습니다. 그러나 대규모 스택이 필요한 합법적 인 요구가있는 프로그램을 제한하는 것은 아니며 소프트 제한을 적절한 값으로 설정하기 만하면됩니다.


기술적으로 스택은 동적으로 증가합니다. 소프트 제한이 8MB로 설정되었다고해서이 메모리 양이 실제로 매핑 된 것은 아닙니다. 대부분의 프로그램이 각각의 소프트 한계 근처에 있지 않기 때문에 이는 다소 낭비입니다. 오히려 커널은 스택 아래의 액세스를 감지하고 필요에 따라 메모리 페이지에 매핑합니다. 따라서 스택 크기에 대한 실제 제한은 64 비트 시스템에서 사용 가능한 메모리뿐입니다 (주소 공간 조각화는 16 제비 바이트 주소 공간 크기에서 이론적입니다).


2
그것은 첫 번째 스레드의 스택입니다. 새 스레드는 새 스택을 할당해야하며 다른 개체로 실행되므로 제한됩니다.
Zan Lynx

0

최대 그게 때문에 스택의 크기는 정적 의 정의 "최대" . 모든 것에 대한 최대 종류는 고정되고 합의 된 제한 수치입니다. 자발적으로 움직이는 목표로 행동한다면 최대치가 아닙니다.

가상 메모리 운영 체제의 스택은 실제로 최대 동적으로 증가합니다 .

말하자면 정적 일 필요는 없습니다. 오히려 프로세스 또는 스레드별로 구성 할 수도 있습니다.

질문에 " 최대 스택 크기 왜 있습니까?"(인위적으로 부과 된 것, 일반적으로 사용 가능한 메모리보다 훨씬 적음)?

한 가지 이유는 대부분의 알고리즘에 엄청난 양의 스택 공간이 필요하지 않기 때문입니다. 큰 스택은 런 어웨이 재귀 가능성을 나타냅니다 . 사용 가능한 모든 메모리를 할당하기 전에 런 어웨이 재귀를 중지하는 것이 좋습니다. 런 어웨이 재귀처럼 보이는 문제는 스택 사용이 저하 된 것으로 예상치 못한 테스트 사례로 인해 발생할 수 있습니다. 예를 들어, 오른쪽 피연산자 (첫 번째 피연산자 구문 분석, 스캔 연산자, 나머지 표현식 구문 분석)를 반복하여 이진 삽입 연산자의 구문 분석기가 작동한다고 가정하십시오. 이는 스택 깊이가 표현식의 길이에 비례한다는 것을 의미합니다 a op b op c op d .... 이 형식의 거대한 테스트 사례에는 큰 스택이 필요합니다. 적절한 스택 제한에 도달하면 프로그램을 중단하면이 문제가 발생합니다.

고정 된 최대 스택 크기의 또 다른 이유는 해당 스택의 가상 공간이 특별한 종류의 매핑을 통해 예약되어 보장 될 수 있기 때문입니다. 보장은 공간이 다른 할당에 주어지지 않고 스택이 한계에 도달하기 전에 스택과 충돌한다는 것을 의미합니다. 이 매핑을 요청하려면 최대 스택 크기 매개 변수가 필요합니다.

이와 비슷한 이유로 스레드에는 최대 스택 크기가 필요합니다. 스택은 동적으로 생성되며 충돌 할 경우 이동할 수 없습니다. 가상 공간을 미리 예약해야하며 해당 할당에 크기가 필요합니다.


@Lynn 최대 크기가 정적 인 이유를 묻지 않았으며 사전 정의 된 이유를 물었습니다.
Will Calderwood
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.