Tại sao ngăn xếp cuộc gọi có kích thước tối đa tĩnh?


46

Đã làm việc với một vài ngôn ngữ lập trình, tôi luôn tự hỏi tại sao ngăn xếp luồng có kích thước tối đa được xác định trước, thay vì mở rộng tự động theo yêu cầu. 

So sánh, một số cấu trúc cấp cao rất phổ biến (danh sách, bản đồ, v.v.) được tìm thấy trong hầu hết các ngôn ngữ lập trình được thiết kế để phát triển theo yêu cầu trong khi các yếu tố mới được thêm vào, chỉ bị giới hạn về kích thước bởi bộ nhớ khả dụng hoặc bởi các giới hạn tính toán ( ví dụ địa chỉ 32 bit).

Tôi không biết mặc dù có bất kỳ ngôn ngữ lập trình hoặc môi trường thời gian chạy nào trong đó kích thước ngăn xếp tối đa không bị giới hạn trước bởi một số tùy chọn mặc định hoặc trình biên dịch. Đây là lý do tại sao quá nhiều đệ quy sẽ dẫn đến rất nhanh lỗi / ngoại lệ tràn ngăn xếp phổ biến, ngay cả khi chỉ một tỷ lệ tối thiểu của bộ nhớ có sẵn cho một quy trình được sử dụng cho ngăn xếp.

Tại sao hầu hết các môi trường thời gian chạy (nếu không phải tất cả) đều đặt giới hạn tối đa cho kích thước mà ngăn xếp có thể tăng lên khi chạy?


13
Loại ngăn xếp này là không gian địa chỉ liên tục không thể di chuyển âm thầm phía sau hậu trường. Không gian địa chỉ có giá trị trên các hệ thống 32 bit.
CodeInChaos

7
Để giảm sự xuất hiện của các ý tưởng tháp ngà như đệ quy rò rỉ từ giới hàn lâm và gây ra các vấn đề trong thế giới thực như giảm khả năng đọc mã và tăng tổng chi phí sở hữu;)
Brad Thomas

6
@BradThomas Đó là những gì tối ưu hóa cuộc gọi đuôi là dành cho.
JAB

3
@JohnWu: Điều tương tự bây giờ, chỉ một lát sau: hết bộ nhớ.
Jörg W Mittag

1
Trong trường hợp không rõ ràng, một lý do hết bộ nhớ còn tệ hơn là hết stack, đó là (giả sử có một trang bẫy) hết stack chỉ khiến quá trình của bạn thất bại. Hết bộ nhớ có thể khiến bất cứ điều gì thất bại, bất cứ ai tiếp theo cố gắng phân bổ bộ nhớ. Sau đó, một lần nữa, trên một hệ thống không có trang bẫy hoặc các phương tiện khác để phát hiện hết ngăn xếp, việc hết ngăn xếp có thể là thảm họa, đưa bạn vào hành vi không xác định. Trên một hệ thống như vậy, bạn sẽ hết bộ nhớ lưu trữ miễn phí và đơn giản là bạn không thể viết mã với đệ quy không giới hạn.
Steve Jessop

Câu trả lời:


13

Có thể viết một hệ điều hành không yêu cầu ngăn xếp phải liền kề nhau trong không gian địa chỉ. Về cơ bản, bạn cần thêm một chút lộn xộn trong quy ước gọi điện, để đảm bảo rằng:

  1. nếu không có đủ không gian trong phạm vi ngăn xếp hiện tại cho chức năng bạn đang gọi, thì bạn tạo một phạm vi ngăn xếp mới và di chuyển con trỏ ngăn xếp để trỏ đến điểm bắt đầu của nó như là một phần của việc thực hiện cuộc gọi.

  2. khi bạn trở về từ cuộc gọi đó, bạn chuyển trở lại phạm vi ngăn xếp ban đầu. Nhiều khả năng bạn giữ lại cái được tạo ở (1) để sử dụng trong tương lai bởi cùng một chủ đề. Về nguyên tắc, bạn có thể giải phóng nó, nhưng theo cách đó là những trường hợp khá kém hiệu quả khi bạn tiếp tục nhảy qua lại ranh giới trong một vòng lặp và mỗi cuộc gọi đều yêu cầu cấp phát bộ nhớ.

  3. setjmplongjmp, hoặc bất cứ điều gì tương đương với hệ điều hành của bạn để chuyển giao quyền kiểm soát không cục bộ, đang thực hiện và có thể chuyển chính xác trở lại phạm vi ngăn xếp cũ khi được yêu cầu.

Tôi nói "quy ước gọi" - cụ thể tôi nghĩ rằng nó có thể được thực hiện tốt nhất trong phần mở đầu chức năng thay vì người gọi, nhưng trí nhớ của tôi về điều này là mơ hồ.

Lý do có khá nhiều ngôn ngữ chỉ định kích thước ngăn xếp cố định cho một luồng, là vì chúng muốn hoạt động bằng cách sử dụng ngăn xếp gốc, trên các hệ điều hành không làm điều này. Như câu trả lời của mọi người khác, theo giả định rằng mỗi ngăn xếp cần được đặt liền kề trong không gian địa chỉ và không thể di chuyển, bạn cần dành một phạm vi địa chỉ cụ thể để sử dụng cho mỗi luồng. Điều đó có nghĩa là chọn một kích thước lên phía trước. Ngay cả khi không gian địa chỉ của bạn rất lớn và kích thước bạn chọn thực sự lớn, bạn vẫn phải chọn nó ngay khi bạn có hai luồng.

"Aha" bạn nói, "những hệ điều hành được cho là sử dụng ngăn xếp không liền kề này là gì? Tôi cá là đó là một hệ thống học thuật tối nghĩa không có ích gì với tôi!". Vâng, đó là một câu hỏi khác mà may mắn đã được hỏi và trả lời.


36

Các cấu trúc dữ liệu đó thường có các thuộc tính mà ngăn xếp hệ điều hành không có:

  • Danh sách liên kết không yêu cầu không gian địa chỉ liền kề. Vì vậy, họ có thể thêm một phần bộ nhớ từ bất cứ nơi nào họ muốn khi họ phát triển.

  • Ngay cả các bộ sưu tập cần lưu trữ liền kề, như vectơ của C ++, cũng có lợi thế hơn các ngăn xếp hệ điều hành: Chúng có thể khai báo tất cả các con trỏ / iterator không hợp lệ bất cứ khi nào chúng phát triển. Mặt khác, ngăn xếp hệ điều hành cần phải giữ các con trỏ tới ngăn xếp hợp lệ cho đến khi hàm có khung mà mục tiêu thuộc về trả về.

Một ngôn ngữ lập trình hoặc thời gian chạy có thể chọn để thực hiện các ngăn xếp riêng của chúng không liền kề hoặc có thể di chuyển để tránh các giới hạn của các ngăn xếp hệ điều hành. Golang sử dụng các ngăn xếp tùy chỉnh như vậy để hỗ trợ số lượng rất nhiều các thói quen chung, ban đầu được triển khai dưới dạng bộ nhớ không liền kề và bây giờ thông qua các ngăn xếp di động nhờ theo dõi con trỏ (xem bình luận của hobb). Trăn không có chồng, Lua và Erlang cũng có thể sử dụng ngăn xếp tùy chỉnh, nhưng tôi không xác nhận điều đó.

Trên các hệ thống 64 bit, bạn có thể định cấu hình ngăn xếp tương đối lớn với chi phí tương đối thấp, vì không gian địa chỉ rất nhiều và bộ nhớ vật lý chỉ được phân bổ khi bạn thực sự sử dụng nó.


1
Đây là một câu trả lời hay và tôi làm theo ý của bạn nhưng không phải là thuật ngữ khối bộ nhớ "liền kề" trái ngược với "liên tục", vì mỗi đơn vị bộ nhớ có địa chỉ duy nhất của riêng nó?
DanK

2
+1 cho "ngăn xếp cuộc gọi không bị giới hạn" Nó thường được triển khai theo cách đơn giản và hiệu suất, nhưng không nhất thiết phải như vậy.
Paul Draper

Bạn hoàn toàn đúng về Go. Trên thực tế, sự hiểu biết của tôi là các phiên bản cũ có ngăn xếp rời rạc , và các phiên bản mới có ngăn xếp di chuyển . Dù bằng cách nào, đó là một điều cần thiết để cho phép số lượng lớn các con khỉ đột. Việc sắp xếp một vài megabyte mỗi con goroutine cho ngăn xếp sẽ khiến chúng quá đắt để phục vụ mục đích của chúng đúng cách.
hobbs

@hobbs: Vâng, Bắt đầu với các ngăn xếp có thể phát triển, tuy nhiên thật khó để làm cho chúng nhanh. Khi Go có được Bộ thu gom rác chính xác, nó cõng nó để thực hiện các ngăn xếp có thể di chuyển: khi ngăn xếp di chuyển, bản đồ loại chính xác được sử dụng để cập nhật các con trỏ lên ngăn xếp trước đó.
Matthieu M.

26

Trong thực tế, rất khó (và đôi khi không thể) để phát triển ngăn xếp. Để hiểu tại sao đòi hỏi một số hiểu biết về bộ nhớ ảo.

Trong Ye Olde Days của các ứng dụng đơn luồng và bộ nhớ liền kề, ba là ba thành phần của một không gian địa chỉ tiến trình: mã, heap và ngăn xếp. Làm thế nào ba cái đó được đặt ra phụ thuộc vào HĐH, nhưng nói chung, mã xuất hiện trước, bắt đầu từ dưới cùng của bộ nhớ, heap tiếp theo và lớn lên, và ngăn xếp bắt đầu ở trên cùng của bộ nhớ và tăng dần xuống. Cũng có một số bộ nhớ dành riêng cho hệ điều hành, nhưng chúng ta có thể bỏ qua điều đó. Các chương trình trong những ngày đó có phần tràn đầy kịch tính hơn: ngăn xếp sẽ bị rơi vào đống, và tùy thuộc vào việc được cập nhật trước, bạn sẽ làm việc với dữ liệu xấu hoặc trở về từ một chương trình con vào một phần bộ nhớ tùy ý.

Quản lý bộ nhớ đã thay đổi mô hình này phần nào: từ phối cảnh của chương trình, bạn vẫn có ba thành phần của bản đồ bộ nhớ quy trình và chúng thường được tổ chức theo cùng một cách, nhưng bây giờ mỗi thành phần được quản lý như một phân đoạn độc lập và MMU sẽ báo hiệu Hệ điều hành nếu chương trình cố gắng truy cập bộ nhớ ngoài một phân đoạn. Khi bạn đã có bộ nhớ ảo, không cần hoặc không muốn cung cấp cho chương trình quyền truy cập vào toàn bộ không gian địa chỉ của nó. Vì vậy, các phân khúc đã được chỉ định ranh giới cố định.

Vậy tại sao không mong muốn cung cấp cho chương trình quyền truy cập vào không gian địa chỉ đầy đủ của nó? Bởi vì bộ nhớ đó cấu thành một "phí cam kết" chống lại sự hoán đổi; bất cứ lúc nào, bất kỳ hoặc tất cả bộ nhớ cho một chương trình có thể phải được ghi để hoán đổi để nhường chỗ cho bộ nhớ của chương trình khác. Nếu mọi chương trình có khả năng tiêu thụ 2GB trao đổi, thì bạn sẽ phải cung cấp đủ số lượng trao đổi cho tất cả các chương trình của mình hoặc có cơ hội hai chương trình sẽ cần nhiều hơn số tiền họ có thể nhận được.

Tại thời điểm này, giả sử đủ không gian địa chỉ ảo, bạn có thể mở rộng các phân đoạn này nếu cần và phân đoạn dữ liệu (heap) thực tế tăng theo thời gian: bạn bắt đầu với một phân đoạn dữ liệu nhỏ và khi bộ cấp phát bộ nhớ yêu cầu nhiều không gian hơn khi nó cần thiết. Tại thời điểm này, với một ngăn xếp duy nhất, có thể mở rộng phân khúc ngăn xếp một cách vật lý: HĐH có thể bẫy nỗ lực đẩy thứ gì đó ra ngoài phân khúc và thêm bộ nhớ. Nhưng điều này cũng không đặc biệt mong muốn.

Nhập đa luồng. Trong trường hợp này, mỗi luồng có một phân đoạn ngăn xếp độc lập, một lần nữa kích thước cố định. Nhưng bây giờ các phân đoạn được đặt lần lượt trong không gian địa chỉ ảo, vì vậy không có cách nào để mở rộng một phân đoạn mà không di chuyển một phân đoạn khác - điều mà bạn không thể làm được vì chương trình sẽ có khả năng có con trỏ sống trong ngăn xếp. Bạn có thể thay thế để lại một số không gian giữa các phân khúc, nhưng không gian đó sẽ bị lãng phí trong hầu hết các trường hợp. Một cách tiếp cận tốt hơn là đặt gánh nặng lên nhà phát triển ứng dụng: nếu bạn thực sự cần các ngăn xếp sâu, bạn có thể chỉ định điều đó khi tạo luồng.

Ngày nay, với không gian địa chỉ ảo 64 bit, chúng ta có thể tạo các ngăn xếp vô hạn hiệu quả cho số lượng luồng vô hạn hiệu quả. Nhưng một lần nữa, điều đó không đặc biệt mong muốn: trong hầu hết các trường hợp, chồng quá mức cho thấy lỗi với mã của bạn. Cung cấp cho bạn ngăn xếp 1 GB chỉ đơn giản là trì hoãn việc phát hiện ra lỗi đó.


3
Các CPU x86-64 hiện tại chỉ có 48 bit không gian địa chỉ
CodeInChaos

Afaik, linux không mọc ngăn xếp tự động: Khi một quá trình cố gắng để truy cập vào đúng khu vực bên dưới đống hiện phân bổ, các ngắt được xử lý bằng cách chỉ vẽ bản đồ một trang thêm bộ nhớ ngăn xếp, thay vì segfaulting quá trình này.
cmaster

2
@cmaster: đúng, nhưng không phải ý nghĩa của kdgregory bằng cách "phát triển ngăn xếp". Có một phạm vi địa chỉ hiện được chỉ định để sử dụng như ngăn xếp. Bạn đang nói về việc dần dần ánh xạ thêm bộ nhớ vật lý vào phạm vi địa chỉ đó khi cần thiết. kdgregory đang nói rằng khó hoặc không thể tăng phạm vi.
Steve Jessop

x86 không phải là kiến ​​trúc duy nhất và 48 bit vẫn có hiệu lực vô hạn
kdgregory

1
BTW, tôi nhớ những ngày tôi làm việc với x86 là không vui lắm, chủ yếu là vì nhu cầu xử lý phân khúc. Tôi rất thích các dự án trên nền tảng MC68k ;-)
kdgregory

4

Ngăn xếp có kích thước tối đa cố định không phổ biến.

Thật khó để hiểu đúng: độ sâu của ngăn xếp tuân theo Phân phối Luật Sức mạnh, điều đó có nghĩa là cho dù bạn có kích thước ngăn xếp nhỏ đến đâu, vẫn sẽ có một phần đáng kể các chức năng với các ngăn xếp nhỏ hơn (vì vậy, bạn lãng phí không gian), và cho dù bạn có tạo ra nó lớn đến đâu, vẫn sẽ có các hàm với các ngăn xếp thậm chí còn lớn hơn (vì vậy bạn buộc một lỗi tràn ngăn xếp đối với các hàm không có lỗi). Nói cách khác: bất kỳ kích thước nào bạn chọn, nó sẽ luôn luôn quá nhỏ và quá lớn cùng một lúc.

Bạn có thể khắc phục vấn đề đầu tiên bằng cách cho phép các ngăn xếp bắt đầu nhỏ và phát triển linh hoạt, nhưng sau đó bạn vẫn gặp vấn đề thứ hai. Và nếu bạn cho phép ngăn xếp phát triển linh hoạt bằng mọi giá, thì tại sao lại đặt giới hạn tùy ý cho nó?

Có những hệ thống mà các ngăn xếp có thể phát triển linh hoạt và không có kích thước tối đa: Erlang, Go, Smalltalk và Scheme chẳng hạn. Có rất nhiều cách để thực hiện một cái gì đó như thế:

  • ngăn xếp di chuyển: khi ngăn xếp liền kề không thể phát triển được nữa bởi vì có một thứ khác trên đường đi, di chuyển nó đến một vị trí khác trong bộ nhớ, với nhiều không gian trống hơn
  • ngăn xếp rời rạc: thay vì phân bổ toàn bộ ngăn xếp trong một không gian bộ nhớ liền kề, hãy phân bổ nó trong nhiều không gian bộ nhớ
  • Ngăn xếp phân bổ heap: thay vì có các vùng nhớ riêng cho stack và heap, chỉ cần phân bổ stack trên heap; như bạn nhận thấy, các cấu trúc dữ liệu được phân bổ theo đống có xu hướng không có vấn đề phát triển và thu hẹp khi cần thiết
  • hoàn toàn không sử dụng ngăn xếp: đó cũng là một tùy chọn, ví dụ: thay vì theo dõi trạng thái chức năng trong một ngăn xếp, hãy để chức năng chuyển tiếp cho callee

Ngay khi bạn có các cấu trúc luồng điều khiển không cục bộ mạnh mẽ, ý tưởng về một ngăn xếp liền kề duy nhất sẽ xuất hiện ngoài cửa sổ: ví dụ: các trường hợp ngoại lệ và tiếp tục có thể tiếp tục, sẽ "rẽ nhánh" ngăn xếp, vì vậy bạn thực sự kết thúc với một mạng ngăn xếp (ví dụ như được thực hiện với một chồng spaghetti). Ngoài ra, các hệ thống có ngăn xếp có thể sửa đổi hạng nhất, chẳng hạn như Smalltalk khá nhiều yêu cầu ngăn xếp spaghetti hoặc một cái gì đó tương tự.


1

HĐH phải đưa ra một khối liền kề khi yêu cầu ngăn xếp. Cách duy nhất nó có thể làm là nếu kích thước tối đa được chỉ định.

Ví dụ: giả sử bộ nhớ trông như thế này trong khi yêu cầu (đại diện X được sử dụng, Os không được sử dụng):

XOOOXOOXOOOOOX

Nếu một yêu cầu cho kích thước ngăn xếp là 6, câu trả lời của hệ điều hành sẽ trả lời là không, ngay cả khi có nhiều hơn 6. Nếu một yêu cầu cho một ngăn xếp có kích thước 3, câu trả lời của HĐH sẽ là một trong các khu vực của 3 vị trí trống (Os) liên tiếp.

Ngoài ra, người ta có thể thấy khó khăn trong việc cho phép tăng trưởng khi chiếm chỗ tiếp giáp tiếp theo.

Các đối tượng khác được đề cập (Danh sách, v.v.) không đi vào ngăn xếp, chúng kết thúc trên đống ở các khu vực không tiếp giáp hoặc phân mảnh, vì vậy khi chúng lớn lên, chúng chỉ cần lấy không gian, chúng không yêu cầu tiếp giáp như chúng quản lý khác nhau.

Hầu hết các hệ thống đặt giá trị hợp lý cho kích thước ngăn xếp, bạn có thể ghi đè lên khi luồng được xây dựng nếu cần kích thước lớn hơn.


1

Trên linux, đây hoàn toàn là một giới hạn tài nguyên tồn tại để tiêu diệt các tiến trình chạy trốn trước khi chúng tiêu thụ lượng tài nguyên có hại. Trên hệ thống debian của tôi, đoạn mã sau

#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);
}

tạo ra đầu ra

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

Lưu ý rằng giới hạn cứng được đặt thành RLIM_INFINITY: Quá trình được phép tăng giới hạn mềm lên bất kỳ số tiền nào . Tuy nhiên, miễn là lập trình viên không có lý do để tin rằng chương trình thực sự cần số lượng bộ nhớ ngăn xếp bất thường, quá trình sẽ bị hủy khi vượt quá kích thước ngăn xếp của tám mebibytes.

Do giới hạn này, một quá trình chạy trốn (đệ quy vô hạn vô ý) đã bị giết trong một thời gian dài trước khi nó bắt đầu tiêu thụ một lượng lớn bộ nhớ đến mức hệ thống buộc phải bắt đầu hoán đổi. Điều này có thể tạo ra sự khác biệt giữa một quá trình bị sập và một máy chủ bị sập. Tuy nhiên, nó không giới hạn các chương trình có nhu cầu chính đáng cho một ngăn xếp lớn, họ chỉ cần đặt giới hạn mềm thành một số giá trị phù hợp.


Về mặt kỹ thuật, các ngăn xếp phát triển linh hoạt: Khi giới hạn mềm được đặt thành tám mebibyte, điều đó không có nghĩa là lượng bộ nhớ này thực sự đã được ánh xạ. Điều này sẽ khá lãng phí vì hầu hết các chương trình không bao giờ đạt được bất kỳ nơi nào gần giới hạn mềm tương ứng của chúng. Thay vào đó, kernel sẽ phát hiện các truy cập bên dưới ngăn xếp và chỉ cần ánh xạ trong các trang bộ nhớ khi cần thiết. Do đó, giới hạn thực sự duy nhất về kích thước ngăn xếp là bộ nhớ khả dụng trên các hệ thống 64 bit (phân mảnh không gian địa chỉ khá lý thuyết với kích thước không gian địa chỉ 16 zebibyte).


2
Đó là ngăn xếp cho chủ đề đầu tiên. Các luồng mới phải phân bổ các ngăn xếp mới và bị giới hạn bởi vì chúng sẽ chạy vào các đối tượng khác.
Zan Lynx

0

Các tối đa ngăn xếp kích thước là tĩnh bởi vì đó là định nghĩa của "tối đa" . Bất kỳ loại tối đa trên bất cứ thứ gì là một con số giới hạn, cố định theo thỏa thuận. Nếu nó hoạt động như một mục tiêu di chuyển tự phát, thì đó không phải là tối đa.

Ngăn xếp trên các hệ điều hành bộ nhớ ảo trên thực tế phát triển linh hoạt, lên đến mức tối đa .

Nói về điều đó, nó không phải là tĩnh. Thay vào đó, nó có thể được cấu hình, trên cơ sở mỗi quá trình hoặc mỗi luồng, thậm chí.

Nếu câu hỏi là "tại sao có tối đa ngăn xếp kích thước" (là một áp đặt nhân tạo, thường là ít hơn rất nhiều so với bộ nhớ có sẵn)?

Một lý do là hầu hết các thuật toán không yêu cầu một lượng không gian ngăn xếp khổng lồ. Một ngăn xếp lớn là một dấu hiệu của một đệ quy chạy trốn có thể . Thật tốt khi dừng đệ quy chạy trốn trước khi phân bổ tất cả bộ nhớ khả dụng. Một vấn đề trông giống như đệ quy chạy trốn là suy thoái sử dụng ngăn xếp, có thể được kích hoạt bởi một trường hợp thử nghiệm không mong muốn. Ví dụ, giả sử một trình phân tích cú pháp cho toán tử nhị phân, toán tử infix hoạt động bằng cách đệ quy trên toán hạng bên phải: phân tích toán hạng đầu tiên, toán tử quét, phân tích phần còn lại của biểu thức. Điều này có nghĩa là độ sâu ngăn xếp tỷ lệ thuận với độ dài của biểu thức : a op b op c op d .... Một trường hợp thử nghiệm lớn của hình thức này sẽ yêu cầu một ngăn xếp rất lớn. Hủy bỏ chương trình khi nó đạt đến giới hạn ngăn xếp hợp lý sẽ nắm bắt được điều này.

Một lý do khác cho kích thước ngăn xếp tối đa cố định là không gian ảo cho ngăn xếp đó có thể được dành riêng thông qua một loại ánh xạ đặc biệt và do đó được đảm bảo. Được đảm bảo có nghĩa là không gian sẽ không được trao cho một phân bổ khác mà ngăn xếp sau đó sẽ va chạm với nó trước khi đạt đến giới hạn. Tham số kích thước ngăn xếp tối đa là bắt buộc để yêu cầu ánh xạ này.

Chủ đề cần một kích thước ngăn xếp tối đa cho một lý do tương tự như thế này. Ngăn xếp của chúng được tạo ra một cách linh hoạt và không thể di chuyển nếu chúng va chạm với thứ gì đó; không gian ảo phải được đặt trước và kích thước được yêu cầu cho phân bổ đó.


@Lynn Không hỏi tại sao kích thước tối đa là tĩnh, (s) anh ấy hỏi tại sao nó được xác định trước.
Will Calderwood
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.