적어도 내가 사용하는 모든 프로그래밍 언어에서 파일을 읽거나 쓰려면 먼저 파일을 열어야합니다.
그러나이 열린 작업은 실제로 무엇을합니까?
일반적인 기능에 대한 매뉴얼 페이지는 실제로 ‘읽기 / 쓰기를 위해 파일을 여는 것’이외의 것을 말하지 않습니다.
http://www.cplusplus.com/reference/cstdio/fopen/
https://docs.python.org/3/library/functions.html#open
분명히 함수를 사용하면 파일에 쉽게 액세스 할 수있는 일종의 객체 생성이 필요하다는 것을 알 수 있습니다.
이것을 넣는 또 다른 방법은 open
함수 를 구현하려면 Linux에서 무엇을해야합니까?
답변
거의 모든 고급 언어에서 파일을 여는 함수는 해당 커널 시스템 호출을 둘러싸는 래퍼입니다. 다른 멋진 일도 할 수 있지만 최신 운영 체제에서는 파일을 열면 항상 커널을 거쳐야합니다.
이것이 fopen
라이브러리 함수 또는 파이썬의 open
인수가 open(2)
시스템 호출 의 인수와 매우 유사한 이유 입니다.
파일을 여는 것 외에도 이러한 함수는 일반적으로 읽기 / 쓰기 작업에 사용될 버퍼를 설정합니다. 이 버퍼의 목적은 기본 시스템 호출에 대한 호출이 덜 리턴하는지 여부에 관계없이 N 바이트를 읽을 때마다 해당 라이브러리 호출이 N 바이트를 리턴하도록하기위한 것입니다.
나는 실제로 내 자신의 기능을 구현하는 데 관심이 없다. 당신이 원한다면 도대체 무슨 일인지 이해하는 데있어
유닉스 계열 운영 체제에서, 성공적인 호출 open
은 “파일 디스크립터” 를 반환하는데 이는 사용자 프로세스의 맥락에서 정수일뿐입니다. 결과적으로이 디스크립터는 열린 파일과 상호 작용하는 모든 호출에 전달되며이를 호출 한 후 close
디스크립터는 유효하지 않습니다.
호출 open
은 다양한 검사가 수행되는 유효성 검사 지점과 같은 역할 을 합니다. 모든 조건이 충족되지 않으면 -1
설명자 대신 반환하여 호출이 실패하고 오류 종류가에 표시됩니다 errno
. 필수 점검 사항은 다음과 같습니다.
- 파일이 존재하는지 여부
- 지정된 모드에서이 파일을 열 수있는 호출 프로세스 권한이 있는지 여부 이는 파일 권한, 소유자 ID 및 그룹 ID를 호출 프로세스의 각 ID와 일치시켜 결정됩니다.
커널의 맥락에서 프로세스의 파일 디스크립터와 물리적으로 열린 파일 사이에는 어떤 종류의 매핑이 있어야합니다. 디스크립터에 매핑 된 내부 데이터 구조에는 블록 기반 장치를 처리하는 또 다른 버퍼 또는 현재 읽기 / 쓰기 위치를 가리키는 내부 포인터가 포함될 수 있습니다.
답변
간단한 open()
시스템 호출 버전을 통해이 안내서를 살펴 보는 것이 좋습니다 . 파일을 열 때 장면 뒤에서 발생하는 일을 나타내는 다음 코드 스 니펫을 사용합니다.
0 int sys_open(const char *filename, int flags, int mode) {
1 char *tmp = getname(filename);
2 int fd = get_unused_fd();
3 struct file *f = filp_open(tmp, flags, mode);
4 fd_install(fd, f);
5 putname(tmp);
6 return fd;
7 }
간단히, 다음은 해당 코드가 한 줄씩 수행하는 것입니다.
- 커널 제어 메모리 블록을 할당하고 파일을 사용자 제어 메모리에서 파일로 복사하십시오.
- 사용하지 않는 파일 디스크립터를 선택하십시오. 현재 열려있는 파일의 확장 가능한 목록에 정수 인덱스로 생각할 수 있습니다. 각 프로세스는 커널에 의해 유지 관리되지만 자체 목록이 있습니다. 코드가 직접 액세스 할 수 없습니다. 목록의 항목에는 기본 파일 시스템이 디스크에서 바이트를 가져 오는 데 사용하는 정보 (예 : inode 번호, 프로세스 권한, 열린 플래그 등)가 포함됩니다.
-
이
filp_open
기능에는 구현이 있습니다struct file *filp_open(const char *filename, int flags, int mode) { struct nameidata nd; open_namei(filename, flags, mode, &nd); return dentry_open(nd.dentry, nd.mnt, flags); }
두 가지 작업을 수행합니다.
- 파일 시스템을 사용하여 전달 된 파일 이름 또는 경로에 해당하는 inode (또는 일반적으로 파일 시스템이 사용하는 모든 종류의 내부 식별자)를 조회하십시오.
- 만들기
struct file
아이 노드에 대한 필수 정보와 그것을 돌려줍니다. 이 구조체는 앞서 언급 한 열린 파일 목록의 항목이됩니다.
-
반환 된 구조체를 프로세스의 열린 파일 목록에 저장 ( “설치”)합니다.
- 할당 된 커널 제어 메모리 블록을 해제하십시오.
- 다음과 같은 파일 조작 함수에 전달 될 수있는 파일 기술자를 반환
read()
,write()
및close()
. 이들 각각은 커널에 제어권을 넘겨줍니다. 파일 디스크립터를 사용하여 프로세스 목록에서 해당 파일 포인터를 찾고 해당 파일 포인터의 정보를 사용하여 실제로 읽기, 쓰기 또는 닫기를 수행 할 수 있습니다.
야심이 있다면이 간단한 예제를 open()
Linux 커널 의 시스템 호출 구현 ( 이라는 함수) 과 비교할 수 있습니다 do_sys_open()
. 유사점을 찾는 데 어려움이 없어야합니다.
물론 이것은 호출 할 때 발생하는 “최상위 계층”일뿐입니다. open()
보다 정확하게는 파일을 여는 과정에서 호출되는 최상위 수준의 커널 코드입니다. 고급 프로그래밍 언어는이 위에 추가 계층을 추가 할 수 있습니다. 더 낮은 수준에서 진행되는 것이 많이 있습니다. ( Ruslan 과 pjc50 에게 설명해 주셔서 감사합니다 .) 대략 위에서 아래로 :
open_namei()
및dentry_open()
파일 및 디렉토리에 대한 액세스 메타 데이터 및 콘텐츠에, 또한 커널의 일부 파일 시스템 코드, 호출. 파일 시스템은 디스크에서 원시 바이트를 읽고 파일 및 디렉토리의 트리로 그 바이트 패턴을 해석합니다.- 파일 시스템은 다시 커널의 일부인 블록 장치 계층을 사용 하여 드라이브에서 원시 바이트를 얻습니다. (재미있는 사실 : Linux를 사용하면 블록 장치 계층에서 원시 데이터에 액세스 할 수 있습니다
/dev/sda
.) - 블록 장치 계층은 “읽기 섹터 X”와 같은 중간 레벨 명령 에서 기계 코드의 개별 입력 / 출력 명령 으로 변환하기 위해 커널 코드이기도 한 저장 장치 드라이버를 호출합니다 . 드라이브가 사용할 수있는 다른 통신 표준에 해당하는 IDE , (S) ATA , SCSI , Firewire 등을 포함한 여러 유형의 저장 장치 드라이버 가 있습니다. (이름은 엉망입니다.)
- I / O 명령어는 프로세서 칩과 마더 보드 컨트롤러의 내장 기능을 사용하여 물리적 드라이브로가는 와이어에서 전기 신호를 보내고받습니다. 이것은 소프트웨어가 아닌 하드웨어입니다.
- 와이어의 다른 쪽 끝에서 디스크의 펌웨어 (내장 제어 코드)는 전기 신호를 해석하여 플래터를 회전시키고 헤드 (HDD)를 이동하거나 플래시 ROM 셀 (SSD)을 읽거나 데이터에 액세스하는 데 필요한 모든 것을 해당 유형의 저장 장치.
캐싱으로 인해 다소 올바르지 않을 수도 있습니다 . 😛 진지하게, 내가 빠뜨린 많은 세부 사항이 있습니다. 사람은 (나 아닌)이 전체 과정이 어떻게 작동하는지 설명하는 여러 권의 책을 쓸 수 있습니다. 그러나 그것은 당신에게 아이디어를 줄 것입니다.
답변
이야기하고 싶은 파일 시스템이나 운영 체제는 괜찮습니다. 좋은!
ZX Spectrum에서 LOAD
명령을 초기화 하면 오디오 입력 라인을 읽고 시스템이 타이트한 루프 상태가됩니다.
데이터 시작은 일정한 톤으로 표시되고 그 후에는 긴 펄스 / 짧은 펄스 시퀀스가 뒤 따릅니다. 짧은 펄스는 이진에 대한 0
것이고 긴 펄스는 이진에 대한 것입니다 1
( https://en.wikipedia.org/ wiki / ZX_Spectrum_software ). 타이트로드 루프는 바이트 (8 비트)를 채울 때까지 비트를 수집하여 메모리에 저장하고 메모리 포인터를 증가시킨 다음 루프를 반복하여 더 많은 비트를 스캔합니다.
일반적으로 로더가 가장 먼저 읽는 것은 짧고 고정 된 형식의 헤더로 , 예상되는 바이트 수와 파일 이름, 파일 유형 및로드 주소와 같은 추가 정보를 나타냅니다. 이 짧은 헤더를 읽은 후 프로그램은 기본 대량 데이터로드를 계속할지 또는로드 루틴을 종료하고 사용자에게 적절한 메시지를 표시할지 여부를 결정할 수 있습니다.
파일 끝 상태는 예상 한만큼의 바이트 (소프트웨어에서 고정 된 바이트 수, 소프트웨어에서 하드 와이어 또는 헤더에 표시된 것과 같은 변수 수)를 수신하여 인식 할 수 있습니다. 로딩 루프가 일정 시간 동안 예상 주파수 범위에서 펄스를받지 못하면 오류가 발생했습니다.
이 답변에 대한 작은 배경
설명 된 절차는 일반 오디오 테이프에서 데이터를로드하므로 오디오 입력 (테이프 레코더에 표준 플러그로 연결된)을 스캔해야합니다. LOAD
명령은 기술적으로 동일한 open
파일 -하지만 물리적으로 묶여 실제로 파일을로드. 이것은 테이프 레코더가 컴퓨터에 의해 제어되지 않기 때문에 파일을 열 수는 없지만로드 할 수는 없기 때문입니다.
“tight loop”는 (1) Z80-A (메모리가 제공되는 경우) 인 CPU가 실제로 3.5MHz로 느리고 (2) 스펙트럼에 내부 클럭이 없기 때문에 언급되었습니다. 즉, 모든 T 상태 (명령 시간) 를 정확하게 유지해야 합니다. 단일. 교수. 루프 내에서 정확한 비프 음 타이밍을 유지합니다.
다행히도 CPU 속도가 낮 으면 종이 한 장의 사이클 수를 계산할 수 있다는 점에서 뚜렷한 이점이 있었으므로 실제로 소요되는 시간도 늘어났습니다.
답변
파일을 열 때 정확히 어떤 일이 발생하는지는 운영 체제에 따라 다릅니다. 아래에서는 파일을 열 때 어떤 일이 발생하는지에 대한 아이디어를 제공하고 더 자세히 관심이 있다면 소스 코드를 확인할 수 있으므로 Linux에서 발생하는 일에 대해 설명합니다. 이 답변을 너무 오래 만들 수 있으므로 권한을 다루지 않습니다.
Linux에서 모든 파일은 inode 라는 구조로 인식됩니다.. 각 구조에는 고유 번호가 있으며 모든 파일에는 하나의 inode 번호 만 있습니다. 이 구조는 파일의 메타 데이터 (예 : 파일 크기, 파일 권한, 타임 스탬프 및 디스크 블록에 대한 포인터)를 저장하지만 실제 파일 이름 자체는 아닙니다. 각 파일 (및 디렉토리)에는 파일 이름 항목과 조회 용 inode 번호가 포함됩니다. 파일을 열면 관련 권한이 있다고 가정하면 파일 이름과 연관된 고유 한 inode 번호를 사용하여 파일 디스크립터가 작성됩니다. 많은 프로세스 / 응용 프로그램이 동일한 파일을 가리킬 수 있으므로 inode에는 파일에 대한 총 링크 수를 유지하는 링크 필드가 있습니다. 파일이 디렉토리에 존재하면 링크 수는 1이고, 하드 링크가 있으면 링크 수는 2가되고 프로세스가 파일을 열면 링크 수는 1 씩 증가합니다.
답변
부기, 주로. 여기에는 “파일이 있습니까?”와 같은 다양한 검사가 포함됩니다. “쓰기 위해이 파일을 열 수있는 권한이 있습니까?”
그러나 그것은 모든 커널 요소입니다-당신이 당신의 자신의 장난감 OS를 구현하지 않는 한, 탐구해야 할 것이 많지 않습니다 (재미 있다면, 그것은 좋은 학습 경험입니다). 물론, 파일을 여는 동안받을 수있는 모든 가능한 오류 코드를 계속 배워야 제대로 처리 할 수 있습니다.
코드 수준에서 가장 중요한 부분 은 열려있는 파일에 대한 핸들 을 제공한다는 점 입니다. 파일로 수행하는 다른 모든 작업에 사용합니다. 이 임의의 핸들 대신 파일 이름을 사용할 수 없습니까? 물론 핸들을 사용하면 몇 가지 장점이 있습니다.
- 시스템은 현재 열려있는 모든 파일을 추적하고 파일이 삭제되는 것을 방지 할 수 있습니다 (예 :).
- 최신 OS는 핸들을 중심으로 구축되었습니다. 핸들로 할 수있는 유용한 것들이 많이 있으며, 모든 종류의 핸들은 거의 동일하게 동작합니다. 예를 들어, Windows 파일 핸들에서 비동기 I / O 작업이 완료되면 핸들에 신호가 표시됩니다. 따라서 신호가 날 때까지 핸들을 차단하거나 완전히 비동기식으로 작업을 완료 할 수 있습니다. 파일 핸들을 기다리는 것은 스레드 핸들 (예 : 스레드가 종료 될 때 신호), 프로세스 핸들 (다시 프로세스가 종료 될 때 신호) 또는 소켓 (일부 비동기 작업이 완료 될 때)을 기다리는 것과 동일합니다. 마찬가지로 각 프로세스에서 핸들을 소유하므로 프로세스가 예기치 않게 종료되거나 응용 프로그램이 잘못 작성된 경우 OS는 핸들을 해제 할 수있는 항목을 알고 있습니다.
- 대부분의 작업은 위치 적
read
입니다. 파일의 마지막 위치에서 시작합니다. 핸들을 사용하여 파일의 특정 “열기”를 식별하면 동일한 파일에 대해 여러 개의 동시 핸들을 가질 수 있습니다. 어떤 식 으로든 핸들은 파일로 이동 가능한 창 역할을하며 비동기 I / O 요청을 발행하는 방법으로 매우 유용합니다. - 핸들은 파일 이름보다 훨씬 작습니다. 핸들은 일반적으로 포인터 크기이며 일반적으로 4 또는 8 바이트입니다. 반면, 파일 이름은 수백 바이트를 가질 수 있습니다.
- 응용 프로그램에서 파일을 연 경우에도 핸들을 사용하여 OS 에서 파일 을 이동할 수 있습니다. 핸들은 여전히 유효하며 파일 이름이 변경된 경우에도 여전히 동일한 파일을 가리 킵니다.
물리적 파일 을 사용 하지 않고 통신 채널을 갖도록 프로세스간에 핸들을 공유하는 다른 트릭도 있습니다 . 유닉스 시스템에서는 파일이 장치 및 기타 다양한 가상 채널에도 사용되므로 반드시 필요한 것은 아닙니다. ), 그러나 그들은 실제로 open
작업 자체에 묶여 있지 않으므로 그에 대해서는 다루지 않을 것입니다.
답변
독서를 위해 열 때 실제로 는 아무것도 일어나지 않아도 됩니다. 파일이 존재하고 응용 프로그램에 파일을 읽을 수있는 충분한 권한이 있는지 확인하고 파일에 대한 읽기 명령을 실행할 수있는 핸들을 작성하기 만하면됩니다.
실제 명령이 전달되는 명령에 있습니다.
OS는 종종 핸들과 관련된 버퍼를 채우기 위해 읽기 작업을 시작하여 읽기를 시작합니다. 그런 다음 실제로 읽기를 수행하면 디스크 IO를 기다릴 필요없이 버퍼의 내용을 즉시 반환 할 수 있습니다.
쓰기 위해 새 파일을 열려면 OS에서 새 (현재 비어있는) 파일의 디렉토리에 항목을 추가해야합니다. 그리고 다시 쓰기 명령을 실행할 수있는 핸들이 작성됩니다.
답변
기본적으로 열기 호출은 파일을 찾은 후 나중에 I / O 조작이 파일을 다시 찾을 수 있도록 필요한 것을 기록해야합니다. 그것은 매우 모호하지만, 내가 즉시 생각할 수있는 모든 운영 체제에서 사실입니다. 구체적인 내용은 플랫폼마다 다릅니다. 이미 여기에 많은 답변이 현대 데스크탑 운영 체제에 대해 이야기합니다. CP / M에서 약간의 프로그래밍을 했으므로 CP / M에서 작동하는 방식에 대한 지식을 제공 할 것입니다 (MS-DOS는 아마도 같은 방식으로 작동하지만 보안상의 이유로 오늘날 이와 같이 일반적으로 수행되지는 않습니다) ).
CP / M에는 FCB라는 것이 있습니다 (C를 언급했듯이 구조체라고 부를 수 있습니다. 실제로 다양한 필드를 포함하는 RAM의 35 바이트 연속 영역입니다). FCB에는 파일 이름을 기록하는 필드와 디스크 드라이브를 식별하는 (4 비트) 정수가 있습니다. 그런 다음 커널의 Open File을 호출 할 때이 구조체에 대한 포인터를 CPU의 레지스터 중 하나에 배치하여 포인터를 전달합니다. 얼마 후, 운영 체제가 약간 변경된 구조체로 돌아옵니다. 이 파일에 대한 I / O에 관계없이이 구조체에 대한 포인터를 시스템 호출에 전달합니다.
CP / M은이 FCB와 어떤 관계가 있습니까? 자체 사용을 위해 특정 필드를 예약하고이 필드를 사용하여 파일을 추적하므로 프로그램 내부에서 해당 필드를 건드리지 않는 것이 좋습니다. 파일 열기 작업은 디스크 시작시 테이블을 통해 FCB에있는 것과 동일한 이름을 가진 파일을 찾습니다 ( ‘?’와일드 카드 문자는 모든 문자와 일치 함). 파일을 찾으면 디스크에서 파일의 물리적 위치를 포함하여 일부 정보를 FCB에 복사하므로 후속 I / O 호출은 궁극적으로 BIOS를 호출하여이 위치를 디스크 드라이버로 전달할 수 있습니다. 이 수준에서는 세부 사항이 다릅니다.