2-1 컴퓨터 시스템과 하드웨어
시스템 계층 구조
컴퓨터 시스템은 다음과 같은 3개의 요소들이 층을 이루고 있다.
컴퓨터의 시스템 계층 구조는 다음과 같은 특징을 가지고 있다.
1. 사용자는 응용프로그램이나 OS 패키지에 포함된 GUI와 도구 프로그램을 통해 컴퓨터를 활용한다.
2. 하드웨어는 OS의 베타적이고 독립적인 지배를 받는다.
3. 어떤 사용자나 응용프로그램도 직접 하드웨어에 접근할 수 없다. 오직 운영체제를 통해서만 접근 가능하다.
OS는 응용프로그램과 하드웨어 사이에서 중계자 역할을 수행한다. 즉 하드웨어를 사용하는 사람이 하드웨어에 대해 잘 알지 못해도 사용할 수 있도록 추상화시켜준다고 생각할 수 있다.
하드웨어
컴퓨터 하드웨어를 좀 더 자세히 살펴보겠다.
컴퓨터는 크게 CPU, 메모리, 캐쉬, 버스, 입출력 장치 및 시스템 제어화로로 구성되어 있다.
하나씩 간단하게 살펴보자.
CPU는 프로그램의 코드(기계명령)을 수행하는 중앙처리 장치이다. 메모리에 적재된 프로그램을 실행하는 역할을 수행한다.
메모리(RAM)은 CPU에 의해 수행될 프로그램 코드가 적재된다.
Cache 메모리는 기술이 발전하며 벌어지는 CPU와 메모리 간의 속도 격차를 줄여주기 위해 만들어졌다. CPU는 캐시메모리에서 명령과 데이터를 읽어 더 효율적으로 작동한다.
Bus는 하드웨어들이 서로 데이터를 주고 받기 위한 디지털 신호 다발이다.
버스는 지니는 정보에 따라서 주소버스, 데이터 버스, 제어 버스로 나눌 수 있다. 주소 버스는 주소값을, 데이터 버스는 코드나 데이터를, 제어버스는 인터럽트, 메모리 읽기 쓰기 등의 제어 신호를 전달한다.
또 목적에 따라서는 SystemBus와 I/O Bus로 나눌 수 있는데, SystemBus는 빠른 하드웨어(CPU,RAM,Cache)를 위한 것이며 I/O Bus는 더 느린 하드웨어를 위한 것이다. 만약 모든 하드웨어가 같은 버스를 사용한다면 느린 하드웨어 때문에 효율성이 떠러지게 될 것이다.
입출력 장치 및 시스템 회로는 CPU와 입출력 장치에서 중계 역할을 수행한다. CPU의 개입 없이 직접 전송하는 DMAC(Direct Memory Access Controller)와 장치들이 입출력을 완료했을 때 발생시키는 인터럽트 신호를 받아 CPU에게 전달하는 INTC(Intercept Controller) 등이 포함된다.
CPU와 메모리 관계
CPU와 메모리는 그럼 어떤 상호작용을 하는가?
이를 알기 위해선 먼저 32bit, 64bit와 같은 CPU의 주소선을 이해해야 한다. CPU의 성능은 컴퓨터의 성능과 직결된다고 해도 과언이 아니다. 우린 이제부터 32bit CPU를 기준으로 하여 공부해보겠다. 64bit는 크기가 너무 크기 때문이다.
32bit는 32개의 주소선을 갖는 CPU를 의미한다.
주소선이란 주소 버스를 통해 주소를 전달받고 데이터 버스를 내놓을 때 사용되는 통로와 같다.
즉 32bit -> 2^32 개의 서로 다른 주소값을 갖을 수 있다는 것이다.
bit는 0 과 1 을 나타내는 하나의 칸이다. 그런데 이것이 32개 있다고 하면 0~ 2^32-1 개의 주소값이 형성되게 될 것이다.
참고로
2^10 = 1KB |
2^20 = 1KB * 2^10 = 1MB |
2^30 = 1MB * 2^10 = 1GB |
2^40 = 1GB * 2^10 = 1TB |
이다.
그럼 2^32 = 4GB임을 알 수 있을 것이다.
4GB는 CPU가 사용할 수 있는 최대 주소 값이다. 따라서 4GB는 32bit CPU가 사용할 수 있는 최데 메모리 크기라고 할 수 있다.
또한 4GB의 메모리에서 운영체제가 차지는 메모리 영역을 제외하고 사용해야 한다.
CPU는 32개의 주소선(A0-31)을 이용해서 RAM과 상호작용한다. RAM에 직접적으로 연결되는 것이 아닌 주소버스(검정선)를 통해서 RAM에 연결하고, 데이터 버스(초록선)를 통해서 데이터를 전달 받는다.
CPU 명령어는 CPU가 해석하고 이해할 수 있는 기계명령으로 이루어져 있다.
어셈블리어는 기계명령과 1대1로 대응될 수 있도록 만든 컴퓨터 프로그래밍의 로우레벨 언어이다.
우린 기계 명령을 내리기 위해 어셈블리어를 사용하는 것이다.
하지만 어셈블리어는 일반 사용자가 사용하기 어렵기 때문에 더 가독성이 좋고 사용하기 편한 하이레벨 언어(예를 들어 C언어)가 만들어졌다. 하이레벨 언어는 컴파일러나 인터프리터에 의해 어셈블리어로 바뀌고 이것이 CPU에게 전달되어 명령이 실행되는 것이다.
CPU 라이프 사이클
CPU가 한개의 명령을 처리하는 세부 과정을 라이프 사이클이라고 한다.
CPU는 내부에서 명령을 처리하기 위해 레지스터들을 가지고 있다.
CPU레지스터는 다음과 같다.
- PC(Program Counter) 레지스터 - 다음실행할 기계명령의 메모리 주소(프로그램의 코드 영역임)를 저장한다. IP(Instruction Pointer)레지스터라고도 불린다
- IR(Instruction Register) 레지스터 - 실행하기위해 메모리에서 읽어온 명령이 저장되는 레지스터
- SP(Spack Pointer) - 스택 영역의 꼭대기 메모리 주소를 저장하는 레지스터
- Data Register - 연산에 사용할 데이터들을 저장한다
- Status Register - CPU의 상태, 인터럽트 금지 등의 제어 정보를 저장한다
CPU가
mov eax ,[300] 이라는 어셈블리어를 수행한다고 해보자.
해당 어셈블리어는 메모리 300번지의 데이터를 eax 레지스터에 저장하라는 의미이다.
위 명령이 메모리 100번지에 저장되어 있다고 할 때,
1) PC 레지스터가 실행할 명령어의 메모리 주소를 저장해둔다. 그리고 주소 버스를 통해 메모리에 접근해 100번지에 접근한다.
2), 3) 이제 100번지에 저장된 명령어를 데이터 버스를 거쳐 CPU로 보낸다.
4) IR 레지스터가 실행할 명령어를 저장하고 명령어를 수행하기 위해 다시 주소 버스를 통해 300번지에 접근한다.
5) 300번지에 들어 있는 값을 데이터 버스를 통해 CPU로 전송한다
6), 7) 임시 레지스를 거쳐 eax 레지스터에 300번지에 저장된 값을 저장시킨다.
간략하게 살펴보면 이와 같다.
그럼 SP 레지스터가 하는 일은 무엇일까.
일단 Stack이 어디 있다는 것인가?
OS는 프로그램을 실행시킬 때 마다
- Code - 프로그램 코드 적재
- Data - 전역 변수들을 적재
- Heap - 프로그램 실행 중 동적으로 저장되는 데이터 적재
- Stack - 함수호출시 매개변수, 지역변수 그리고 함수 호출 뒤 돌아갈 주소가 적재
위와같은 4가지 공간을 제공한다.
즉 프로그램마다 자신의 스택 공간이 할당되는 것이다.
SP 레지스터는 현재 실행중인 스택 영역의 Top(꼭대기)를 가르킨다.
컨텍스트와 컨텍스트 스위치
이 앞전 포스팅에서 알아본 것과 같이 운영체제는 다중 프로그래밍을 실행한다. 즉 여러 프로그램이 함께 실행되는 것이다.
만약 CPU가 하나라고 한다면, 프로그램이 바뀔 때 CPU가 수행할 값들이 바뀌게 될 것인데 이 문제를 어떻게 해결할 수 있을까?
어떤 프로그램이 실행중인 일체의 상황을 컨텍스트(Context)라고 한다. 뜻 그대로 문맥 정도로 생각해도 좋을 것 같다.
그리고 이 컨텍스트는 메모리와 CPU 레지스터에 담겨있다.
운영체제가 다른 프로그램을 실행하게 된다면, 실행하고 있던 프로그램의 컨텍스트를 다른 곳에 복사시킨다.
RAM 메모리에 적재된 내용은 RAM에 남아 있을 것이기 때문에 CPU 입장에서는 레지스터만 복사시키면 된다.
이렇게 원래 프로그램의 컨텍스트를 다른 곳에 복사시키고 새로 실행할 프로그램의 켄텍스트를 가져오는 일련의 과정을 컨텍스트 스위칭이라고 한다.
예를 들어 실행 프로그램을 프로그램 A에서 B로 변경한다고 해보자.
그럼 A의 컨텍스트, 즉 레지스터의 값들은 메모리의 특정 영역에 저장된다. 해당 메모리 영역은 운영체제만 접근 가능한 메모리 영역으로 PCB(Process Control Block)이라고 한다.
그리고 저장되어 있는 B의 컨텍스트를 CPU가 다시 가져온다.
이렇게 컨텍스트 스위칭이 진행된다.
2-2 컴퓨터 시스템과 운영체제
앞서 설명한 것과 같이 컴퓨터 시스템은 계층구조로 이루어져 있다.
운영체제는 응용프로그램과 하드웨어 사이에 인터페이스 역할을 수행하는데, 이 이유는 계층간의 독립성을 확보하기 위함이다. 즉 운영체제로 하여금 다른 계층들을 추상화시킬 수 있는 것이다.
응용프로그램은 하드웨어에 접근할 때 OS에 시스템 콜을 호출한다.
이렇게 응용 프로그램이 하드웨어에 직접 접근하지 못하도록 하고, 시스템 콜을 사용하는 이유는 응용프로그램들 사이에서 하드웨어 사용 충돌을 막기위함이다.
운영체제와 커널
운영체제가 어떤 기능을 수행하는지 다시 상기해보자.
프로세스와 스레드를 관리한다
디스크에 저장된 프로그램이 메모리에 적재되고 실행되면 이를 프로세스라고 부른다. 프로세스는 한 개 이상의 스레드로 구성된다. 스레드는 프로세스의 실행된위이다. 운영체제는 여러 프로그램의 실행, 즉 여러 프로세스를 동시에 실행하도록 관리한다.
이 외에도 메모리 관리, 파일관리, 장치관리, 사용자 인터페이스 관리, 네트워킹 관리, 보호 및 보안 관리를 수행한다.
운영체제가 프로세스와 스레드를 어떻게 관리하는지에 조금 더 초점을 맞춰보자.
운영체제는 이전 포스팅에서 알아보았듯 도구/GUI 소프트웨어, Kernel, 디바이스 드라이버 등으로 구성된 소프트웨어이다.
도구/GUI는 사용자가 컴퓨터를 편리하게 사용할 수 있도록 돕는다. 예를 들어 파일관리자나, bash 등 우리가 window나 리눅스를 사용하며 접하게 되는 인터페이스 들이다.
커널은 운영체제의 핵심이라고 할 수 있는데, 부팅 후 메모리에 계속 상주하며 운영체제의 핵심 기능을 수행한다.
프로세스와 스레드 관리, 메모리 관리, 파일 시스템 관리, 디바이스 드라이브를 호출 해 장치 입출력을 관리하는 등 운영체제의 기능을 거의 다 수행하고 있다. 응용 프로그램은 커널의 함수를 일반적으로 호출 할 수 없지만, System Call을 통해서 접근할 수 있게 된다.
디바이스 드라이버는 장치를 직접 제어하는 소프트웨어로 장치마다 전담하는 드라이버가 존재한다.
운영체제가 하드웨어와 응용프로그램 사이의 중계자 역할을 수행한다는 것은 지겹도록 말해왔다.
그 중계자 역할을 수행하기 위해 System Call과 Interrup 가 사용되는 것이다.
시스템 콜(System Call)과 인터럽트(Interrupt)란?
시스템 콜은 응용프로그램에서 커널 코드를 실행하는 기법이다.
원래는 응용프로그램이 커널이 존재하는 메모리 영역에 접근할 수 있는 권힌이 없다.
하지만 운영체제는 패키지를 통해 system call library를 제공하고, 이를 통해서 응용프로그램이 커널에 접근할 수 있도록 한다.
라이브러리라는 것은 우리가 일반 프로그래밍을 할 때 한번 쯤 들어봤을 것이다.
라이브러리는
Standard Library와 SystemCall Library 두 종류가 존재한다.
우리가 평소 프로그래밍을 하며 사용하는 라이브러리가 스탠다드 라이브러리이다 ex) printf(), abs() ..
그리고 CPU가 커널의 코드와 데이터가 적재된 매ㅔ모리 영역을 억세스하는 권한을 갖도록 하고 커널에 작성된 함수를 실행시키도록 하는 라이브러리가 시스템 콜 라이브러리이다. ex) fork(), exit(), open() ...
인터럽트는 하드웨어 장치들이 CPU에게 하드웨어 신호(인터럽트 신호)를 물리적으로 발생시켜 입출력, 타이머 완료 등을 CPU에게 알리는 것이다.
인터럽트가 발생하면 CPU는 하던 일을 멈추고 인터럽트 요청을 처리하는 코드를 실행한다, 이 코드를 인터럽트 서비스 루틴(ISR)이라고 한다.
User Space와 kernel Space
만약 응용프로그램이 하드웨에 직접 접근하게 된다면 다른 응용프로그램이 적재한 메모리위에 새로운 내용을 덮어 쓴다거나, 삭제하며 다른 응용프로그램의 실행을 망칠 수 있다. 심지어 커널의 주요 데이터를 훼손해 심각한 문제를 일으킬 수도 있다.
그렇기에 메모리 공간은 User Space와 Kernel Space로 나눠지며, CPU의 실행 모드 또한 UserMode와 KernelMode로 나눠져 있다.
커널 공간이 메모리 영역에서 운영체제가 차지하는 메모리라고 생각할 수 있다. 32bit CPU가 4GB 메모리를 사용할 때, 2GB - 2GB가 각각 user space와 kernel space로 나뉜다. 이는 응용프로그램으로부터 커널 코드와 데이터를 지키려는 목적이다.
응요프로그램은 2GB의 메모리 영역을 할당받는다고 생각하지만 사실 이 주소 공간은 실제 공간이 아닌 가상의 공간이다.
OS가 응요프로그램에게 가상의 주소를 만들어 부여한다.
가상 주소란 응용프로그램들이 자신이 커널 공간을 제외한 모든 공간을 차지하고 있다고 생각하게 하는 주소이다.
OS는 각 응용프로그램에게 할당한 가상 공간과 실제 물리 메모리 공간을 매핑시켜 가상 주소 공간의 충돌을 해결한다.
이를 위해 OS는 각 프로세스 마다 커널 공간에 매핑 테이블을 만들고 가상주소와 실제 메모리 영역을 매핑 시킨다.
한마디로 운영체제에 의해 응용프로그램들은 물리 메모리를 나눠서 쓸 수 있게 된 것이다.
물리 메모리가 부족하게 되면 어떻게 될까?
이럴 때는 물리 메모리 일부를 하드디스크에 저장해둔다. 이를 가상 메모리 기법이라고 한다. 이에 대해선 뒤에서 더 자세하게 배우게 될 것이다.
OS가 제공하는 가상 주소 공간을 통해 모든 응용프로그램은 자신만의 User Space를 가지게 된다.
CPU의 User Mode와 Kernel Mode
CPU는 사용자 모드와 커널 모드 중 하나의 모드로 실행된다.
응용프로그램이 system call을 호출하면 커널모드로 넘어간다. 이는 CPU의 레지스터에 설정된다.
User Mode에서는 UserSpace만 접근이 가능하다.
만약 kernelSpace에 접근하면 예외가 발생한다. 어떤 하드웨어도 Kernel 공간에 직접 접근하거나, 커널이 점유한 메모리에 접근할 수 없다. 또한 특권 명령이라 불리는 기계 명령 또한 실행할 수 없다.
Kernel Mode에서는 모든 메모리 공간에 접근할 수 있으며 하드웨어에 접근할 수 있고, 특권 명령 또한 실행할 수 있다.
User Mode에서 Kernel Mode로 CPU가 전환되려면 System call과 interrupt가 실행되면 된다.
시스템콜은 응용프로그램이 커널에 작성된 커널 함수를 활용하는 방법이고, 인터럽트는 인터럽트 서비스 루틴 자체가 커널에 내장되어 있기에 커널 모드로 전환해야 하는 것이다.
특권 명령에는
I/O 명령, Halt 명령, 인터럽트 플래그, 타이머 설정, 컨텍스츠 스위칭 등이 있다
I/O 명령은 하드웨어들을 제어하거나 입출력과 저장장치를 제어하고 읽거나 쓸 때 사용되는 CPU의 기계 명령이다
Halt 명령은 처리할 작업이 없으면 CPU가 작동중지하며 idel 상태가되는 것는 명령이다.
인터럽트 플래그는 인터럽트 발생시 이를 처리할지 무시할지를 결정하는 bit이다.
타이머 설정은 타이머를 설정하는 것이고, 컨텍스트 스위칭은 앞서 다뤘기 때문에 넘어가겠다.
kernel의 실체
Kernel이 중요하다는 것은 알겠는데 그래서 kernel이 뭔지 모호하게 느껴질 것이다.
커널은 컴파일된 바이너리 형태로 운영체제가 설치되는 하드디스크 특정 영역에 잇다가 부팅시 메모리에 적재된다.
커널은 프로세스도 스레드도 아닌 커널공간에 적재된 함수와 자료구조이다.
커널 그자체는 프로세스가 아님을 기억하자.
커널은 함수이다.
커널은 실행되고 있는 것이 아니라 systemcall에 의해 커널 코드가 호출돼 실행되는 것이다.
프로세스가 시스템콜을 통해 커널에 진입하면, 자신에게 할당된 커널 스택을 활용해 커널에 있는 함수를 호출할 때 매개변수와 지역변수를 저장한다.
하지만 이 커널 스택은 응용프로그램에 속한 것이기에 시스템 콜이 끝나면 이 스택은 함께 사라진다.
응용프로그램 빌딩
앞서 라이브러리에 대해서 잠깐 언급했었는데, 라이브러리의 종류에는 표준 라이브러리와 시스템콜 라이브러리가 있다는 것을 언급했었다.
라이브러리는 응용프로그램에서 활용하도록 미리 함수를 작성해 컴파일하고 바이너리 형태로 만든 파일을 의미한다.
* 여기서 바이너리 파일은 0과1, 이진법으로 작성된 파일을 의미한다.
표준 라이브러리는 프로그램이 언어가 같으면 그 사용법도 같지만 시스템 호출 라이브러리는 OS 마다 다르다.
시스템 호출 라이브러리는 시스템 호출함수와 커널 API로 나눌 수 있다.
표준 라이브러리 기능을 활용하는 것을 function call이라고 하며,
시스템 호출 라이브러리는 system call을 호출해, 커널의 기능을 활용한다.
function call(함수 호출)은 돌아올 주소와 매개변수를 사용자 스택에 저장하고, 함수가 저장된 위치로 점프 후 실행한 다음 원래 주소로 돌아온다. 이는 전부 사용자 코드에서 실행된다.
응용프로그램은 커널에 접근하기 위해 시스템 콜 함수를 통해 간접적으로 커널 함수를 호출한다.
System call은 CPU 모드를 커널 모드로 바꾸고, 커널 공간 내의 시스템 호출 핸들러 코드를 실행한다.
시스템 호출 과정 조금 더 깊게 살펴보자.
커널은 컴퓨터 자원을 관리하며 많은 기능을 실행하는 함수들로 구성되어 있다.
시스템 콜은 trap이라고도 불리기도 하며, 시스템 콜은 64bit AMD 기준으로 호출/반납 - syscall/sysret 명령어를 사용한다.
CPU에서 시스템 콜이 처리되는 일반적인 과정은
호출할 시스템 호출번호(커널 함수 고유 ID), 파라미터가 CPU 레지스테 저장되고, CUP 명령을 실행하면 CPU가 커널 모드로 바뀌고 커널의 시스템 콜 핸들러에 진입하게 된다.
read() 명령이라는 시스템 콜 호출 함수가 실행된다고 해보자.
그럼 read() 함수가 시스템 호출 함수임을 판단하고 syscall 명령(시스템 콜 호출)이 실행된다.
그럼 systemcall을 통해 커널의 시스템 콜 핸들러에 진입하게 된다.
이제 커널 스택(응용프로그램에 속한 것임)에 CPU의 레지스터 값을 저장한다.
그리고 이 레지스터 값을 기반으로 호출할 커널 함수 주소를 알아내고 실행한다.
sys_read()라는 커널 함수를 실행하고, 이 값을 하드 디스크를 거쳐 다시 레지스터에 값으로 리턴한다.]
이제 이 레지스터 값을 CPU에 복구하면 시스템 콜 함수의 호출이 마무리 된다.
하지만 시스템 호출 함수는 그냥 함수 호출보다 더 비용이 시간 비용을 초래한다.
더 느려진다는 의미이다.
표준 라이브러리 fread()와 시스템 호출 라이브러리 read()를 비교해보면
char buf[100];
for(i = 1 ; i<=10; i++)
fread/read (fp,buf, 100);
위 C언어 코드를 실행하면, fread는 시스템 콜을 1번 호출하지만, read는 10번 호출하게 된다.
OS와 인터럽트
커널에 접근하는 방법에는 시스템 콜 이외에도 인터럽트라는 방법이 있다.
인터럽트는 I/O나 저장장치가 OS와 대화하기 위한 방법이다.
장치들이 어떤 상황이 발생했을 때 (이때 사건은 비동기적인 사건이다) CPU에 알리는 하드웨어적인 방법이라고도 할 수 있다.
인터럽트가 발생하고 처리하는 과정은 CPU와 인터럽트 제어 등의 하드웨어가 상호 협력해야 한다.
CPU에는 인터럽트 신호를 수신할 수 있는 핀이 1개 뿐이기 때문에
CPU와 I/O 사이에는 APIC라는 인터럽트 제어기가 존재한다.
APIC는 신호를 받는 I/O APIC와 CPU에 신호를 보내는 Local APIC 장치로 구성된다 .
CPU는 각 인터럽트 번호를 인터럽트 백터라고 하고 관리하며
커널 영역 ISR(인터럽트 서비스 루틴)에 저장한다.
참고로 멀티 코어에 경우네느 인터럽트를 관리하는 코어가 지정되기도 한다. 이는 인터럽트 친화성이랑 관련이 있다.
인터럽트 서비스 루틴은 어디에 저장되어있을까?
ISR은 디바이스 드라이브나 커널 코드에 들어있다. 디바이스 드라이브는 장치를 제어하며 장치로부터 입출력을 수행하는 프로그램이다.
이는 장치마다 필요하고, 장치와 커널 사이에서 제어 명령과 데이터를 전달하는 인터페이스 역할을 수행한다.
커널 코드가 설치된 장치와 무관하게 작성되고 운용될 수 있도록 도와주는 역할을한다. 커널로부터 하드웨어 장치를 추상화시킨다고 생각할 수 있을 것이다.
인터럽트는 다중 프로그래밍의 키와 같다.
다중 프로그래밍은 장치들의 입출력시 다른 일을 하는 것이 기본 원리인데, 인터럽트가 없다면 불가능하기 때문이다.
인터럽트를 끝으로 이번 챕터를 마무리 하겠다.
이번 포스팅에서는 컴퓨터시스템이 하드웨어와 어떤 관계와 계층 구조를 가지고 있는지 살펴보았고
컴퓨터 시스템에서 운영체제가 어떤 역할을 수행하는지, 운영체제의 구성 요소와 커널에 대해 알아보았다.
그 후 커널 공간과 유저 공간, 커널 모드와 유저 모드를 알아보고 이것들이 어떻게 진행되며 상호작용하는지도 알아보았다.
그 핵심에는 시스템콜과 인터럽트가 있었다.
다음 포스팅에서는 프로세스에 대해 더 자세히 알아보도록 하겠다.
'운영체제' 카테고리의 다른 글
[운영체제] 4. 스레드와 멀티 스레딩 (1) | 2025.01.27 |
---|---|
[운영체제] 3. 프로세스와 프로세스 관리 (0) | 2025.01.14 |
[운영체제]1-2 운영체제의 태동 (1) | 2024.12.19 |
[운영체제] 1-1 운영체제 개념 (1) | 2024.12.05 |
[운영체제] 0. 운영체제 공부 시작 (2) | 2024.12.05 |