728x90

Kubernetes에서 서비스 어카운트란?

Kubernetes에서 서비스 어카운트(Service Account)는 클러스터 내의 애플리케이션이 Kubernetes API 서버와 상호작용할 수 있도록 인증 정보를 제공하는 역할을 함

보통 사용자가 직접 API 서버와 상호작용하기 위해 사용자 계정(User Account)을 사용하는 것과는 달리, 서비스 어카운트는 애플리케이션(즉, 파드)이 API 서버와 통신할 수 있도록 만들어진 특수한 계정임

Kubernetes 클러스터 내에서 파드가 실행 중일 때, 이 파드가 클러스터의 상태를 모니터링하거나 다른 리소스에 접근하기 위해서는 Kubernetes API 서버에 인증된 요청을 보내야 함

이를 가능하게 하는 것이 바로 서비스 어카운트 토큰(serviceAccountToken)

이 토큰을 통해 파드는 자신을 인증하고 필요한 작업을 수행할 수 있음

서비스 어카운트 토큰 자동 마운트

서비스 어카운트가 제공하는 토큰은 파드가 Kubernetes API에 접근할 때 중요한 역할을 하며, 이는 자동으로 파드에 마운트됨 Kubernetes에서는 기본적으로 파드가 생성될 때 서비스 어카운트의 토큰을 자동으로 마운트하도록 설정되어 있음

이 설정은 서비스어카운트나, 파드의 메타데이터에서 automountServiceAccountToken: true로 설정됨

apiVersion: v1
kind: ServiceAccount
metadata:
  name: example-account
automountServiceAccountToken: true
apiVersion: v1
kind: Pod
metadata:
  name: example-pod
spec:
  serviceAccountName: example-account
  automountServiceAccountToken: true
  ...

위와 같이 설정된 경우, Kubernetes는 파드 생성 시 서비스 어카운트 토큰을 파드의 파일 시스템에 Projected Volume을 통해 자동으로 주입함

이 토큰은 보통 /var/run/secrets/kubernetes.io/serviceaccount/token 경로에 파일로 저장됨

애플리케이션은 이 경로에 있는 토큰을 읽어 Kubernetes API 서버에 요청을 보낼 때 사용할 수 있음

서비스 어카운트 토큰의 만료와 갱신

Kubernetes 서비스 어카운트 토큰은 기본적으로 expirationSeconds 설정을 통해 토큰이 약 1시간 후에 만료되도록 구성됨

만료된 토큰은 더 이상 유효하지 않으므로 새로운 토큰이 필요함

다행히도, Kubernetes는 이 과정도 자동으로 처리함

즉, 파드에 마운트된 토큰이 만료되면, Kubernetes는 자동으로 새로운 토큰을 생성하고 기존 토큰이 저장된 경로(/var/run/secrets/kubernetes.io/serviceaccount/token)에 업데이트함

이를 통해 애플리케이션은 지속적으로 유효한 토큰을 사용하여 Kubernetes API에 접근할 수 있음

이렇게 자동으로 갱신되는 메커니즘 덕분에 관리자는 토큰 만료에 신경 쓰지 않아도 되고, 애플리케이션은 중단 없이 Kubernetes API 서버와 상호작용할 수 있음

 

아래 파이썬 코드를 실행하면 파드에 마운트 되어있는 서비스 어카운트 토큰을 이용하여 kubernetes api를 호출해 볼 수 있음

import ssl
from kubernetes import client, config

config = client.Configuration()

config.api_key['authorization'] = open('/var/run/secrets/kubernetes.io/serviceaccount/token').read()
config.api_key_prefix['authorization'] = 'Bearer'
config.host = 'https://kubernetes.default'
config.ssl_ca_cert = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
config.verify_ssl=True

api_client = client.CoreV1Api(client.ApiClient(config))
ret = api_client.list_namespaced_pod("example-namespace", watch=False)

print("Listing pods with their IPs:")
for i in ret.items:
    print(f"{i.status.pod_ip}\t{i.metadata.name}")

보안 관점에서의 서비스 어카운트 관리

서비스 어카운트 토큰이 Kubernetes 클러스터에서 중요한 역할을 하기 때문에 이를 안전하게 관리하는 것이 매우 중요함

보안 관점에서 중요한 원칙 중 하나는 최소 권한 원칙(Least Privilege Principle)

각 서비스 어카운트는 그들이 수행해야 할 작업에 필요한 최소한의 권한만을 가져야 함

Kubernetes에서는 Role, RoleBinding, ClusterRole, ClusterRoleBinding을 통해 각 서비스 어카운트에 부여된 권한을 제한할 수 있음

만약 특정 파드가 Kubernetes API에 접근할 필요가 없다면, automountServiceAccountToken: false로 설정하여 서비스 어카운트 토큰이 자동으로 마운트되지 않도록 설정하는 것이 좋음

728x90
728x90

Kubernetes 클러스터에서 애플리케이션의 보안을 유지하고, 다양한 리소스에 대한 접근을 제어하는 것은 매우 중요함

이를 위해 Kubernetes는 RBAC(Role-Based Access Control)와 ServiceAccount를 제공하여, 클러스터 내에서의 접근 권한을 체계적으로 관리할 수 있음

Kubernetes RBAC(Role-Based Access Control)란?

Kubernetes의 RBAC는 사용자나 서비스가 클러스터 내에서 어떤 리소스에 접근하고, 어떤 동작을 수행할 수 있는지를 제어하는 메커니즘임

RBAC를 통해 특정 리소스에 대해 읽기, 쓰기 등의 권한을 세밀하게 설정할 수 있음

RBAC는 Role, ClusterRole, RoleBinding, ClusterRoleBinding으로 구성됨

  • Role: 특정 네임스페이스 내에서 리소스에 대한 권한을 정의함
  • ClusterRole: 클러스터 전체에서 권한을 정의함
  • RoleBinding: 네임스페이스 내에서 특정 주체(사용자, 그룹, ServiceAccount)에게 Role을 할당함
  • ClusterRoleBinding: 클러스터 전체에서 특정 주체에게 ClusterRole을 할당함

ServiceAccount란?

ServiceAccount는 Kubernetes 클러스터 내에서 파드(Pod)가 Kubernetes API 서버에 접근할 수 있도록 해주는 계정임

기본적으로 Kubernetes에서 파드는 ServiceAccount와 연동되며, 이 계정을 통해 Kubernetes API에 인증된 요청을 할 수 있음

apiVersion: v1
kind: ServiceAccount
metadata:
  name: example-serviceaccount
  namespace: example-namespace

RBAC와 ServiceAccount 연동하기

RBAC는 ServiceAccount와 함께 사용되어, 파드나 애플리케이션이 특정 리소스에 대해 어떤 권한을 가질지 제어할 수 있음

예를 들어, 특정 파드가 pods 리소스에 대한 읽기 권한만 가지도록 설정할 수 있음

이를 위해 Role과 RoleBinding을 활용할 수 있음

Role 정의

Role은 네임스페이스 내에서 리소스에 대한 권한을 정의함

아래는 pods 리소스에 대해 get, list, watch 권한을 가진 Role을 정의한 예시임

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: example-namespace
  name: example-role
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]

RoleBinding 정의

RoleBinding은 특정 ServiceAccount가 위에서 정의한 Role의 권한을 사용할 수 있도록 바인딩하는 역할을 함

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: example-rolebinding
  namespace: example-namespace
subjects:
- kind: ServiceAccount
  name: example-serviceaccount
  namespace: example-namespace
roleRef:
  kind: Role
  name: example-role
  apiGroup: rbac.authorization.k8s.io

위의 예시에서 example-serviceaccount는 example-role에서 정의된 권한을 사용하여 pods 리소스에 대한 get, list, watch 동작을 수행할 수 있음

ClusterRole과 ClusterRoleBinding

Role과 RoleBinding은 네임스페이스 내에서만 작동하지만, 클러스터 전역에서 권한을 설정하고자 할 때는 ClusterRole과 ClusterRoleBinding을 사용함

ClusterRole 정의

ClusterRole은 클러스터 전체에서 리소스에 대한 권한을 정의할 수 있음

예를 들어, 클러스터 내의 모든 nodes와 pods에 대한 읽기 권한을 정의한 예시는 다음과 같음

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: example-clusterrole
rules:
- apiGroups: [""]
  resources: ["nodes", "pods"]
  verbs: ["get", "list", "watch"]

ClusterRoleBinding 정의

ClusterRoleBinding은 특정 ServiceAccount가 클러스터 전역에서 ClusterRole을 사용할 수 있도록 바인딩하는 역할을 함

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: example-clusterrolebinding
subjects:
- kind: ServiceAccount
  name: example-serviceaccount
  namespace: example-namespace
roleRef:
  kind: ClusterRole
  name: example-clusterrole
  apiGroup: rbac.authorization.k8s.io

이 예시에서 example-serviceaccount는 클러스터 전체에서 nodes와 pods 리소스에 대해 읽기 권한을 가짐

 

728x90
728x90

Go 언어에서 메서드를 정의할 때, 구조체와 메서드의 관계를 결정하는 중요한 요소 중 하나는 리시버(Receiver)의 타입임

리시버는 메서드가 호출될 때 어떤 방식으로 구조체 데이터를 다룰지 결정하며, 값 리시버포인터 리시버 두 가지 방식이 있음

값 리시버란?

값 리시버는 메서드 호출 시, 구조체의 복사본을 리시버로 받는 방식

즉, 메서드가 호출될 때 원본 구조체가 아닌, 그 복사된 값이 메서드로 전달됨

package main

import "fmt"

type Counter struct {
    count int
}

func (c Counter) Increment() {
    c.count++
}

func (c Counter) GetCount() int {
    return c.count
}

func main() {
    counter := Counter{}
    counter.Increment()
    fmt.Println(counter.GetCount())  // 출력: 0
}

위 코드에서 Increment() 메서드는 값 리시버를 사용함

메서드가 호출될 때 Counter 구조체의 복사본이 전달되기 때문에, Increment()가 count 값을 증가시키더라도 원본 구조체의 값은 변하지 않음

따라서 GetCount() 메서드로 값을 확인하면 여전히 0이 출력됨

Increment 메서드는 아무런 의미가 없는 메서드라고 볼 수 있음

포인터 리시버란?

포인터 리시버는 메서드 호출 시, 구조체의 메모리 주소를 전달받아 원본 데이터를 직접 수정할 수 있는 방식

포인터 리시버를 사용하면 메서드 내부에서 구조체의 상태를 변경할 수 있으며, 메모리 주소를 다루기 때문에 큰 구조체를 복사하지 않고도 성능을 최적화할 수 있음

package main

import "fmt"

type Counter struct {
    count int
}

func (c *Counter) Increment() {
    c.count++
}

func (c *Counter) GetCount() int {
    return c.count
}

func main() {
    counter := Counter{}
    counter.Increment()
    fmt.Println(counter.GetCount())  // 출력: 1
}

위 코드에서 Increment() 메서드는 포인터 리시버를 사용함

이 메서드는 구조체의 포인터를 전달받아, 원본 구조체의 count 값을 직접 수정할 수 있음

따라서 Increment() 메서드를 호출한 후 GetCount() 메서드를 통해 수정된 값을 확인할 수 있으며, 출력은 1이 됨

값 리시버와 포인터 리시버의 차이점

구분 값 리시버 포인터 리시버
구조체 전달 방식 구조체의 복사본 전달 구조체의 메모리 주소 전달
원본 수정 여부 원본 수정 불가능 원본 수정 가능
메모리 효율성 구조체가 클 경우 비효율적 (복사 비용) 메모리 효율적
상태 변경 메서드 내에서 상태를 변경할 수 없음 메서드 내에서 상태를 변경할 수 있음

성능 차이

값 리시버는 구조체의 복사본을 사용하기 때문에, 큰 구조체에서는 메모리 복사로 인해 성능 저하가 발생할 수 있음

반면, 포인터 리시버는 구조체의 메모리 주소를 사용하므로 성능적으로 더 효율적

상태 변경

값 리시버는 구조체의 상태를 변경하지 않음

즉, 복사본에서만 변경이 일어나므로 원래의 구조체 상태에는 영향을 미치지 않음

반면, 포인터 리시버는 원본 구조체의 데이터를 직접 수정할 수 있어 상태 변경이 필요한 경우 적합함

언제 값 리시버를 사용하고, 언제 포인터 리시버를 사용해야 할까

  • 값 리시버가 유리한 경우
    • 구조체가 작고, 복사 비용이 적으며, 동시성 문제를 피해야 하는 경우 값 리시버가 유리함
    • 값 리시버는 복사본을 사용하기 때문에 원본 데이터의 상태를 변경할 수 없으며, 여러 고루틴이 안전하게 동시 접근할 수 있음
  • 포인터 리시버가 유리한 경우
    • 구조체가 크거나, 원본 상태를 변경할 필요가 있는 경우 포인터 리시버가 유리함
    • 그러나 동시성 문제를 신경 써야 하며, 성능 이점을 얻으려면 구조체 크기와 복사 비용을 고려해야 함

값 리시버와 포인터 리시버를 모두 사용하는 예제

하나의 구조체에 값 리시버와 포인터 리시버를 모두 사용할 수 있음

package main

import "fmt"

type Person struct {
    name string
    age  int
}

// 값 리시버: 상태를 변경하지 않음
func (p Person) GetName() string {
    return p.name
}

// 포인터 리시버: 상태를 변경함
func (p *Person) HaveBirthday() {
    p.age++
}

func main() {
    person := Person{name: "Alice", age: 25}

    fmt.Println("이름:", person.GetName()) // 값 리시버 사용
    person.HaveBirthday()                 // 포인터 리시버 사용
    fmt.Println("나이:", person.age)       // 출력: 26
}

GetName() 메서드는 값 리시버를 사용하여 데이터를 읽기만 하며, HaveBirthday() 메서드는 포인터 리시버를 사용하여 age 필드를 직접 수정함

 

 

728x90

'Go' 카테고리의 다른 글

Go 함수와 메서드 차이  (0) 2024.06.24
Go 구조체, 메서드, 인터페이스  (0) 2024.06.19
728x90

CPU가 명령어를 가져와 실행하는 과정

  1. 명령어 인출
  2. 명령어 실행
  3. 다음 명령어 인출

명령어를 가져오는 기준 : Program Counter (PC)

  • PC는 CPU내부의 레지스터 중 하나로, 프로그램이 메모리에 저장된 여러 명령어들 중 어디부터 실행해야 할지를 알려주는 '위치 표시기'라고 할 수 있음
  • PC는 현재 실행 중이거나 다음에 실행할 명령어가 메모리의 어느 위치에 있는지를 가리킴
  • CPU가 명령어를 인출한 후, PC는 다음 명령어의 주소를 가리키도록 자동으로 증가함
  • 프로그램이 조건에 따라 다른 명령어로 이동해야 할 때(예: if 문, for 문에서의 점프), PC의 값을 변경하여 새로운 위치로 이동하게 함

최초의 PC 레지스터 값은 어떻게 설정될까?

  1. 컴파일 과정
    • 코드가 작성된 후, 컴파일러는 이 코드를 기계 명령어(바이너리 코드)로 변환하여 실행 파일을 생성함
    • 이 실행 파일에는 프로그램의 명령어와 데이터를 포함하고 있으며, 디스크에 저장됨
  2. 프로그램 실행 요청
    • 사용자가 프로그램을 실행하면, 운영 체제가 이 요청을 처리함
  3. 프로그램의 메모리 로드
    • 운영 체제는 실행 파일을 디스크에서 읽어 들여, 메모리(RAM)의 특정 위치에 로드함
    • 이 과정에서 프로그램의 코드와 데이터가 메모리에 배치됨
    • 특히, 프로그램의 시작 지점(엔트리 포인트)이 메모리의 특정 주소에 할당됨
  4. 프로그램 카운터(PC) 설정
    • 운영 체제는 프로그램을 메모리에 로드한 후, 프로그램의 엔트리 포인트 주소(즉, 첫 번째 명령어가 위치한 주소)를 PC 레지스터에 설정함
    • 이 엔트리 포인트 주소는 실행 파일의 헤더에 정의되어 있음
  5. 프로그램 실행 시작
    • PC 레지스터가 프로그램의 첫 번째 명령어를 가리키게 되면, CPU는 이 명령어를 인출(fetch)하여 실행(decode & execute)함
    • 이후, PC는 자동으로 다음 명령어의 주소를 가리키도록 업데이트되며, 프로그램이 순차적으로 실행됨
728x90

'Computer Science' 카테고리의 다른 글

멀티태스킹과 프로세스 관리  (0) 2024.08.14
메모리 단편화  (0) 2024.08.14
운영체제의 역할과 필요성  (0) 2024.08.08
정적 라이브러리와 동적 라이브러리  (3) 2024.07.24
링커의 심벌해석 대상  (3) 2024.07.24
728x90

코드를 작성하다 보면 자연스럽게 if-else 구문을 많이 사용하게 됨

조건문을 통해 로직을 처리하는 것은 필수적이지만, 조건이 복잡해지고 여러 조건이 중첩되면서 코드가 읽기 어렵고 유지보수가 힘들어지는 경우가 많음

특히, 중첩된 if-else는 코드의 가독성을 떨어뜨리는 주요 원인 중 하나임

이 글에서는 중첩된 if-else 구문을 리팩토링하는 여러 가지 기법을 소개하고, 실제로 적용할 수 있는 예시를 통해 코드의 품질을 향상시키는 방법을 정리함

Early Return 패턴

Early Return 패턴은 함수의 실행 중 특정 조건을 만족하지 않으면 일찍 반환(return)하는 방식

Early Return 패턴을 활용하면 불필요한 중첩을 방지할 수 있음

# before
def process_data(data):
    if data is not None:
        if data.is_valid():
            if data.has_permission():
                return "Processing data"
            else:
                return "No permission"
        else:
            return "Invalid data"
    else:
        return "No data"
        
 # after
 def process_data(data):
    if data is None:
        return "No data"
    if not data.is_valid():
        return "Invalid data"
    if not data.has_permission():
        return "No permission"
    
    return "Processing data"

Early Return을 사용하면 코드의 중첩을 줄여 가독성을 크게 향상시킬 수 있음

조건이 충족되지 않을 경우 빠르게 반환(Return)하면 이후 로직을 간결하게 처리할 수 있음

 

Guard Clauses 패턴

Guard Clauses는 특정 조건을 검사해 빠르게 반환하거나 예외를 던지는 패턴

이 방식은 Early Return과 유사하지만, 주로 예외 처리나 특정한 조건을 걸러내는 데 자주 사용됨

# before
def calculate_discount(price, user):
    if price > 0:
        if user.is_premium():
            if user.has_discount_coupon():
                return price * 0.7
    return price


# after
def calculate_discount(price, user):
    if price <= 0:
        raise ValueError("Price must be greater than zero")
    
    if not user.is_premium():
        return price
    
    if not user.has_discount_coupon():
        return price
    
    return price * 0.7

 

조건문을 함수로 분리

Early Return, Guard Clauses 패턴을 사용할 때 조건이 많은 경우 

각 조건을 별도의 함수로 분리해 코드의 가독성과 재사용성을 높일 수 있음

# before
def validate_user(user):
    if user.is_active:
        if user.has_subscription:
            if user.is_email_verified:
                return True
    return False


# after
def is_active_user(user):
    return user.is_active

def has_valid_subscription(user):
    return user.has_subscription

def is_email_verified(user):
    return user.is_email_verified

def validate_user(user):
    if not is_active_user(user):
        return False
    if not has_valid_subscription(user):
        return False
    if not is_email_verified(user):
        return False
    return True

 

데이터 구조를 활용해 조건 제거하기

때로는 복잡한 조건문을 데이터 구조를 사용해 간결하게 표현할 수도 있음

특히 다중 조건을 처리할 때 유용함

# before
def get_discount(user_type):
    if user_type == "premium":
        return 20
    elif user_type == "standard":
        return 10
    elif user_type == "basic":
        return 5
    else:
        return 0


# after
def get_discount(user_type):
    discounts = {
        "premium": 20,
        "standard": 10,
        "basic": 5,
    }
    return discounts.get(user_type, 0)
728x90

'Python' 카테고리의 다른 글

파이썬 클린코드  (0) 2024.04.28
파이썬 계약에 의한 디자인(Design by Contract, DbC)  (1) 2024.04.21
Python 정규 표현식 기초  (0) 2024.04.13
Python 추상클래스  (0) 2024.04.12
Python Singleton 패턴  (0) 2024.04.12
728x90

멀티태스킹과 프로세스 관리는 현대 컴퓨터 시스템의 중요한 요소로, 여러 프로그램이 동시에 실행되면서도 각각의 프로그램이 원활하게 작동할 수 있도록 관리하는 역할을 수행함

이 글에서는 멀티태스킹의 개념과 이를 가능하게 하는 프로세스 관리의 기초 원리를 정리함

1. 멀티태스킹의 개념

멀티태스킹은 컴퓨터 시스템이 여러 작업(프로그램)을 동시에 처리하는 것처럼 보이게 만드는 기술임

실제로는 단일 CPU 코어에서 한 번에 하나의 작업만 처리할 수 있지만, 운영체제는 빠른 속도로 작업을 전환하여 사용자가 여러 프로그램이 동시에 실행되는 것처럼 느끼도록 함

1.1 시분할 시스템과 멀티태스킹

시분할 시스템은 CPU 시간을 여러 작업 간에 나누어 사용하게 하는 방식으로, 멀티태스킹의 기본 원리임

운영체제는 각 작업에 일정한 시간 간격을 할당하고, 이 시간이 지나면 다른 작업으로 전환함

이 과정을 매우 빠르게 반복하여 사용자는 여러 작업이 동시에 실행되는 것처럼 느끼게 됨

 

프로그램 A를 먼저 실행했다가 잠시 중지하고 프로그램 B의 실행으로 넘어갔다가 중지하고 다시 프로그램 A를 실행

이 과정에서는 프로그램 A를 어디까지 실행했고, 어떤 데이터를 사용하고 있었는지 등의 문맥(Context)을 기반으로 전환함

1.2 문맥 전환(Context Switching)

문맥 전환은 CPU가 하나의 프로세스에서 다른 프로세스로 전환할 때 발생하는 과정임

이 과정에서 운영체제는 현재 프로세스의 상태(레지스터 값, 프로그램 카운터 등)를 저장하고, 다음에 실행할 프로세스의 상태를 복원함

문맥 전환은 멀티태스킹의 핵심 기술로, 이를 통해 여러 프로세스가 공존할 수 있음

2. 프로세스 관리의 중요성

프로세스 관리는 멀티태스킹 시스템에서 각 프로그램(프로세스)의 실행을 효과적으로 제어하고 조정하는 역할을 수행함

운영체제는 프로세스의 생성, 실행, 일시 중지, 종료 등을 관리하며, 이를 통해 시스템 자원의 효율적인 사용을 보장함

2.1 프로세스의 구조

프로세스는 프로그램이 메모리에 적재되어 실행 중인 상태를 의미하며, 다음과 같은 구조로 이루어짐

  • 코드 영역: 실행할 기계 명령어가 저장된 영역으로, 프로세스의 실제 작업을 수행
  • 데이터 영역: 전역 변수와 같은 데이터가 저장되는 영역
  • 힙 영역: 동적 메모리 할당을 위해 사용되는 영역
  • 스택 영역: 함수 호출 시 매개변수, 반환 주소, 지역 변수가 저장되는 영역

2.2 프로세스 생성과 종료

프로세스는 프로그램이 실행될 때 운영체제에 의해 생성됨

프로세스 생성 과정은 다음과 같음

  1. 프로세스 식별자(PID) 할당: 운영체제는 각 프로세스에 고유한 식별자를 할당함
  2. 메모리 할당: 코드, 데이터, 힙, 스택 영역이 메모리에 할당됨
  3. 실행 준비: 운영체제는 프로세스를 준비 큐에 배치하여 CPU 할당을 대기하게 함

프로세스가 종료되면 운영체제는 해당 프로세스의 메모리를 해제하고, 프로세스 관련 자원을 반환함

2.3 프로세스 스케줄링

프로세스 스케줄링은 CPU 자원을 여러 프로세스에 할당하는 과정임

스케줄링 알고리즘은 프로세스의 우선순위, 실행 시간, 입출력 대기 시간 등을 고려하여 CPU 시간을 효율적으로 분배함

  • 선점형 스케줄링: 높은 우선순위를 가진 프로세스가 CPU를 차지할 수 있도록 낮은 우선순위의 프로세스를 중지시킴
  • 비선점형 스케줄링: 현재 실행 중인 프로세스가 완료될 때까지 다른 프로세스가 CPU를 차지하지 않음

스케줄링 알고리즘의 선택은 시스템의 응답 속도, 처리량, 공정성 등에 영향을 미침

3. 멀티태스킹의 도전 과제

멀티태스킹은 강력한 기능을 제공하지만, 동시에 몇 가지 도전 과제를 동반함

3.1 상호 배제와 동기화

여러 프로세스가 동시에 실행될 때, 특정 자원(예: 파일, 메모리)을 동시에 사용하려고 하면 충돌이 발생할 수 있음

이를 방지하기 위해 운영체제는 상호 배제(Mutual Exclusion) 메커니즘과 동기화(Synchronization) 기법을 사용함

  • 뮤텍스(Mutex): 한 번에 하나의 프로세스만 자원에 접근할 수 있도록 보장함
  • 세마포어(Semaphore): 자원 접근을 카운팅하여 여러 프로세스가 제한된 자원에 접근할 수 있도록 조정함

3.2 데드락(교착 상태)

데드락은 두 개 이상의 프로세스가 서로의 자원을 기다리며 무한 대기 상태에 빠지는 현상

운영체제는 데드락을 예방, 탐지, 회복하기 위한 다양한 기법을 제공함

4. 결론

멀티태스킹과 프로세스 관리는 현대 운영체제의 핵심적인 기능으로, 여러 프로그램이 동시에 실행될 수 있도록 자원 관리를 효율적으로 수행함

문맥 전환, 프로세스 스케줄링, 상호 배제와 같은 기술을 통해 시스템의 안정성과 성능을 유지하며, 사용자에게 원활한 컴퓨팅 환경을 제공함

운영체제의 이러한 기능들은 복잡한 시스템 자원을 관리하고, 여러 작업이 원활하게 수행될 수 있도록 지원함으로써 현대 컴퓨터 시스템의 근간을 이루고 있음

728x90

'Computer Science' 카테고리의 다른 글

CPU 명령어 인출과 실행 과정  (1) 2024.08.21
메모리 단편화  (0) 2024.08.14
운영체제의 역할과 필요성  (0) 2024.08.08
정적 라이브러리와 동적 라이브러리  (3) 2024.07.24
링커의 심벌해석 대상  (3) 2024.07.24
728x90

메모리 단편화는 메모리를 효율적으로 사용하지 못해 발생하는 현상으로, 내부 단편화와 외부 단편화로 나뉨

내부 단편화 (Internal Fragmentation)

내부 단편화는 메모리 할당 단위의 고정 크기로 인해 할당된 메모리 블록 내에서 남는 공간이 생기는 현상

예를 들어, 운영체제가 8KB 크기의 고정된 메모리 블록을 할당한다고 가정할 때, 어떤 프로그램이 6KB의 메모리가 필요하면 2KB의 메모리가 남게 됨

이 2KB의 공간은 다른 용도로 사용할 수 없으며, 결과적으로 메모리 공간이 낭비됨

이처럼 할당된 메모리 블록 내에서 남는 공간을 내부 단편화라 함

내부 단편화 해결 방법

 

  • 가변 크기 블록 할당 (Variable-sized Partitioning)
    • 고정된 크기의 메모리 블록 대신 가변 크기의 블록을 사용하여 필요한 만큼의 메모리를 할당
    • 프로그램이 실제로 필요한 메모리 양에 맞춰 메모리를 할당할 수 있어 내부 단편화를 줄일 수 있음
  • 슬랩 할당자 (Slab Allocator)
    • 운영체제에서 특정 크기의 메모리 블록을 자주 사용하는 경우, 이러한 블록을 미리 준비해두는 방식으로 내부 단편화를 줄임
    • 슬랩 할당자는 주로 커널에서 사용되며, 효율적인 메모리 사용을 보장함

 

외부 단편화 (External Fragmentation)

외부 단편화는 할당 가능한 메모리 공간이 비연속적으로 존재하여 총 여유 공간이 충분함에도 메모리를 할당하지 못하는 현상

다양한 크기의 메모리 블록이 할당되고 해제되면서, 메모리 공간에는 다양한 크기의 빈 블록이 흩어져 남게 됨

이로 인해, 충분한 전체 메모리 공간이 존재하더라도 연속된 메모리 블록이 없어 새로운 프로그램을 위해 메모리를 할당할 수 없는 상황이 발생할 수 있음

이러한 비연속적인 빈 공간을 외부 단편화라 함

외부 단편화 해결 방법

  • 메모리 압축 (Memory Compaction)
    • 외부 단편화를 줄이기 위해, 메모리의 사용 중인 블록을 하나로 모아 연속된 빈 공간을 확보하는 방법
    • 메모리 이동이 필요하기 때문에 비용이 발생할 수 있지만, 큰 연속된 메모리 블록을 할당해야 할 때 유용함
  • 가상 메모리 (Virtual Memory)
    • 가상 메모리 기법을 사용하면 프로그램은 연속적인 메모리 공간에 접근하는 것처럼 보이지만, 실제로는 비연속적인 물리적 메모리 위치를 사용할 수 있음
    • 이로 인해 외부 단편화 문제를 어느 정도 완화할 수 있음
  • 최적 적합 할당 (Best Fit Allocation)
    • 메모리 할당 시, 사용할 수 있는 빈 공간 중에서 가장 작은 빈 블록에 메모리를 할당하여 외부 단편화를 줄이려는 방법
    • 이는 남은 작은 빈 공간을 최소화하는 데 도움이 됨
728x90
728x90

운영체제는 컴퓨터 시스템에서 핵심적인 역할을 담당하는 소프트웨어로, 사용자가 하드웨어와 소프트웨어를 효율적으로 사용할 수 있도록 다양한 기능을 제공함

1. 운영체제의 역할

1.1 프로세스 관리

운영체제는 여러 프로그램이 동시에 실행되는 멀티태스킹 환경을 지원함

프로세스 관리의 핵심은 다음과 같음

  • 프로세스 생성 및 종료: 프로그램이 실행되면 운영체제가 새로운 프로세스를 생성하고, 프로그램 종료 시 이를 제거함
  • 프로세스 스케줄링: CPU 시간을 효율적으로 분배하여 여러 프로세스가 마치 동시에 실행되는 것처럼 보이게 함
    이는 CPU 스케줄링 알고리즘을 통해 이루어짐
  • 문맥 전환: 프로세스 간 전환 시 필요한 상황 정보를 저장하고 복원하여 프로그램의 상태를 유지함

1.2 메모리 관리

메모리는 모든 프로그램 실행에 필수적인 자원으로, 운영체제는 메모리의 효율적 사용을 보장함

  • 메모리 할당 및 해제: 프로그램이 필요로 하는 메모리 공간을 할당하고, 필요가 없어지면 해제함
  • 가상 메모리 관리: 물리적 메모리의 크기를 초과하는 프로그램 실행을 가능하게 하며, 페이지 교체 알고리즘을 통해 효율적인 메모리 사용을 보장함

1.3 파일 시스템 관리

운영체제는 파일의 저장, 검색, 보호 등과 관련된 다양한 기능을 제공함

  • 파일 저장 및 검색: 파일을 디스크에 저장하고 검색할 수 있는 인터페이스를 제공함
  • 파일 권한 관리: 사용자 및 그룹에 따라 파일 접근 권한을 설정하고 관리함

1.4 하드웨어 및 장치 관리

운영체제는 하드웨어와 소프트웨어 간의 매개 역할을 하여 하드웨어 자원의 효율적 사용을 보장함.

  • 장치 드라이버: 운영체제가 하드웨어 장치와 소통할 수 있도록 지원하는 소프트웨어로, 모든 프로그램이 특정 하드웨어와 직접 상호작용하지 않도록 추상화 계층을 제공함
  • 입출력 관리: 다양한 입출력 장치(프린터, 디스크 등)의 효율적 사용과 관리

1.5 사용자 인터페이스 제공

운영체제는 사용자와 시스템 간의 상호작용을 지원하는 다양한 인터페이스를 제공함

  • 그래픽 사용자 인터페이스(GUI): 사용자가 아이콘과 메뉴를 통해 컴퓨터를 쉽게 사용할 수 있도록 지원함
  • 명령어 인터페이스(CLI): 명령어 입력을 통해 컴퓨터와 상호작용할 수 있는 환경을 제공함

2. 운영체제의 필요성

2.1 자원 관리의 효율성

운영체제가 없다면 사용자가 직접 메모리 할당, CPU 스케줄링 등을 처리해야 하며, 이는 매우 복잡하고 비효율적일 수 있음

운영체제는 이러한 자원 관리를 자동으로 수행하여 시스템의 효율성을 극대화함

2.2 프로그램 실행의 용이성

운영체제는 프로그램 실행을 간단하게 만들어 줌

프로그래머는 프로그램을 작성하고 컴파일한 후 운영체제에 의해 자동으로 메모리에 적재되고 실행됨

이를 통해 개발자는 복잡한 하드웨어 관리에서 벗어나 프로그램 개발에 집중할 수 있음

2.3 하드웨어 추상화

운영체제는 하드웨어와 소프트웨어 간의 추상화 계층을 제공하여, 프로그래머가 특정 하드웨어에 대한 지식 없이도 프로그램을 개발할 수 있게 함

이는 하드웨어의 다양성을 프로그램에 반영하지 않고도 동일한 소프트웨어가 여러 시스템에서 실행될 수 있도록 지원함

2.4 보안 및 안정성

운영체제는 프로그램 간의 상호작용과 데이터 공유를 관리하여 시스템의 보안을 유지함

또한, 프로세스 격리 및 사용자 권한 관리를 통해 시스템의 안정성을 보장함

2.5 사용자 경험 향상

운영체제는 사용자에게 친숙한 환경을 제공함으로써, 컴퓨터 사용 경험을 향상함

GUI를 통해 사용자는 복잡한 명령어 입력 없이도 쉽게 컴퓨터를 사용할 수 있음

3. 결론

운영체제는 현대 컴퓨터 시스템에서 필수적인 구성 요소로, 하드웨어와 소프트웨어 간의 매개 역할을 수행하고 시스템 자원을 효율적으로 관리함으로써 사용자와 프로그래머에게 다양한 편의를 제공함

운영체제의 이러한 기능들은 컴퓨터 시스템의 효율성과 안정성을 유지하는 데 핵심적임

운영체제가 없다면 프로그래머와 사용자는 시스템 자원 관리를 직접 해야 하므로 컴퓨터 사용이 매우 비효율적이 되고, 오류 발생 가능성도 높아짐

운영체제의 중요성과 필요성을 이해하는 것은 컴퓨터 시스템의 전반적인 작동 원리를 이해하는 데 필수적임

728x90

'Computer Science' 카테고리의 다른 글

멀티태스킹과 프로세스 관리  (0) 2024.08.14
메모리 단편화  (0) 2024.08.14
정적 라이브러리와 동적 라이브러리  (3) 2024.07.24
링커의 심벌해석 대상  (3) 2024.07.24
지역 심벌과 전역 심벌  (0) 2024.07.24
728x90

소프트웨어 개발에서 라이브러리는 코드의 재사용성을 높이고, 개발 시간을 단축하는 데 중요한 역할을 함

라이브러리는 크게 정적 라이브러리와 동적 라이브러리로 구분됨

이번 글에서는 정적 라이브러리와 동적 라이브러리의 개념과 차이점, 그리고 라이브러리 생성 및 링크 과정에 대해 정리함

정적 라이브러리(Static Library)란?

정적 라이브러리는 링크 타임에 프로그램에 결합되며, 코드가 실행 파일에 포함되는 라이브러리

 

특징

  • 확장자: .a (Unix 계열), .lib (Windows)
  • 링크 시점: 컴파일 후 링크 타임에 실행 파일에 결합됨
  • 파일 크기: 모든 필요한 라이브러리 코드가 포함되어 실행 파일 크기가 커질 수 있음
  • 독립성: 실행 파일에 라이브러리의 모든 코드가 포함되므로, 실행 시 별도의 라이브러리가 필요하지 않음
  • 업데이트: 라이브러리를 업데이트하려면 실행 파일을 다시 컴파일하고 링크해야 함

로드 방식

  • 링크 타임에 포함: 정적 라이브러리는 프로그램이 링크될 때 실행 파일에 포함됨
    이는 라이브러리의 코드가 실행 파일에 직접 결합되어 독립적인 실행 파일이 생성됨을 의미함
    결과적으로, 실행 시 별도의 라이브러리 파일이 필요하지 않음

동적 라이브러리(Dynamic Library)

동적 라이브러리는 코드가 실행파일에 포함되지 않으며, 일반적으로 실행 시간에 로드되는 라이브러리

 

특징

  • 확장자: .so (Unix 계열), .dll (Windows)
  • 링크 시점: 실행 시간에 라이브러리가 로드됨
  • 파일 크기: 라이브러리 코드가 포함되지 않기 때문에 실행 파일 크기가 정적라이브러리에 비해 작음
  • 공유성: 여러 프로그램이 하나의 라이브러리를 공유하여 메모리 사용량을 줄일 수 있음
  • 업데이트: 라이브러리를 업데이트해도 실행 파일을 다시 컴파일할 필요가 없음

로드 방식

  • 런타임 로드: 일반적으로 프로그램이 실행되는 동안 동적 라이브러리가 로드됨
    이 방식은 라이브러리 파일이 실행 파일과 별도로 유지되며, 필요할 때마다 로드
  • 프리로드: 일부 시스템에서는 프로그램이 실행될 때 특정 동적 라이브러리를 미리 로드할 수 있음
    이를 통해 실행 시간 동안 필요한 라이브러리가 즉시 사용 가능함
    환경 변수(LD_PRELOAD 등)를 사용하여 특정 라이브러리를 미리 로드할 수 있음

라이브러리 생성 과정

라이브러리를 생성하는 과정은 여러 객체 파일을 하나의 라이브러리 파일로 묶는 것을 포함
정적 라이브러리와 동적 라이브러리 생성 과정은 다소 다름

정적 라이브러리 생성

  1. 소스 코드 컴파일
    • 소스 코드를 컴파일하여 객체 파일을 생성함
    • gcc -c libmath.c -o libmath.o
  2. 정적 라이브러리 생성
    • 여러 객체 파일을 하나의 정적 라이브러리 파일로 묶음
    • ar rcs libmath.a libmath.o

동적 라이브러리 생성

  1. 소스 코드 컴파일
    • 동적 라이브러리를 생성할 때는 위치 독립 코드(Position Independent Code)를 생성해야 함
    • gcc -fPIC -c libmath.c -o libmath.o
  2. 동적 라이브러리 생성
    • 객체 파일을 동적 라이브러리로 묶음
    • gcc -shared -o libmath.so libmath.o

라이브러리 링크 과정

생성된 라이브러리를 사용하여 실행 파일을 만드는 과정은 링크 단계에서 이루어짐

 

정적 라이브러리 링크

정적 라이브러리를 포함하여 실행 파일을 생성함
정적 라이브러리의 모든 코드가 실행 파일에 포함

  • gcc -o myprogram myprogram.c -L. -lmath

동적 라이브러리 링크

동적 라이브러리를 포함하여 실행 파일을 생성함
실행 파일에는 동적 라이브러리를 참조하는 코드만 포함되고, 라이브러리는 실행 시 로드됨

  • gcc -o myprogram myprogram.c -L. -lmath

동적 라이브러리 로드 설정

동적 라이브러리를 실행 파일이 실행되는 동안 찾을 수 있도록 환경 변수를 설정

  • export LD_LIBRARY_PATH=.

결론

정적 라이브러리와 동적 라이브러리는 각각의 장단점을 가지고 있으며, 특정 상황에 따라 적절히 선택하여 사용해야 함

라이브러리 생성과 링크는 소프트웨어 개발의 서로 다른 단계이지만, 실행 파일을 만들기 위해 모두 필요함

생성된 라이브러리를 링크 과정에서 사용하여 최종 실행 파일을 만드는 것이 이 두 과정의 주요 역할임

개발 환경과 배포 방식에 따라 적합한 라이브러리 방식을 선택하여 효율적인 소프트웨어 개발을 할 수 있음

728x90

'Computer Science' 카테고리의 다른 글

메모리 단편화  (0) 2024.08.14
운영체제의 역할과 필요성  (0) 2024.08.08
링커의 심벌해석 대상  (3) 2024.07.24
지역 심벌과 전역 심벌  (0) 2024.07.24
링커란?  (0) 2024.07.17
728x90

링커의 심벌 해석 대상은 주로 전역 심벌과 외부 심벌임. 링커는 여러 개의 객체 파일을 결합하여 실행 가능한 프로그램을 생성하는 과정에서 심벌 해석을 수행함

이 과정에서 링커는 각 객체 파일에 포함된 심벌을 분석하고 올바른 메모리 주소를 할당하여 참조를 해결함

링커의 심벌 해석 대상

  1. 전역 심벌(Global Symbols)
    • 정의된 전역 변수와 함수: 각 객체 파일에서 정의된 전역 변수와 함수는 링커에 의해 심벌 테이블에 추가되고, 프로그램 전체에서 참조될 수 있도록 메모리 주소가 할당됨
    • 예시: int g_a = 1; 와 같은 전역 변수나 int func_a(int x, int y); 와 같은 함수 정의
  2. 외부 심벌(External Symbols)
    • 다른 객체 파일에서 정의된 변수와 함수: 한 객체 파일에서 선언되었지만 정의되지 않은 변수나 함수는 외부 심벌로 간주되며, 링커가 다른 객체 파일에서 해당 심벌의 정의를 찾아 연결함
    • 예시: extern int g_e; 와 같은 외부 변수 선언이나 다른 객체 파일에서 정의된 함수 참조

심벌 해석 과정

링커의 심벌 해석 과정은 다음과 같은 단계를 포함함

  1. 심벌 수집(Symbol Collection)
    • 각 객체 파일의 심벌 테이블을 수집하고, 전역 심벌과 외부 심벌을 추출함
  2. 심벌 해석(Symbol Resolution)
    • 전역 심벌과 외부 심벌을 매칭하여 참조를 해결함. 예를 들어, 한 객체 파일에서 extern으로 선언된 심벌을 다른 객체 파일에서 정의된 심벌과 연결함
    • 정의되지 않은 외부 심벌이 발견되면 링커 오류가 발생함
  3. 심벌
    • 에 실제 메모리 주소를 할당함. 이를 통해 각 객체 파일에서 참조된 심벌이 올바른 메모리 위치를 가리키도록 함
  4. 재배치(Relocation)
    • 심벌 해석과 주소 할당 후, 각 객체 파일의 코드와 데이터를 재배치하여 올바른 메모리 위치로 이동함

예시 코드에서의 링커 심벌 해석 대상

int g_a = 1;               // 전역 변수
extern int g_e;           // 외부 변수
int func_a(int x, int y); // 함수 참조

// 함수 구현
int func_b()
{
    int m = g_a + 2;
    return func_a(m + g_e);
}

 

이 예제 코드에서 링커의 심벌 해석 대상은 다음과 같음

  • 전역 변수 g_a: 객체 파일에서 정의된 전역 변수로, 링커는 이 변수를 참조하는 모든 곳에 대해 올바른 메모리 주소를 할당함
  • 외부 변수 g_e: 다른 객체 파일에서 정의된 변수로, 링커는 이 변수를 참조하는 모든 곳에서 정의된 실제 심벌과 연결함
  • 함수 func_a: 다른 객체 파일에서 정의된 함수로, 링커는 이 함수를 참조하는 모든 곳에서 정의된 실제 심벌과 연결함
  • 함수 func_b: 객체 파일에서 정의된 함수로, 링커는 이 함수의 코드와 데이터를 올바른 메모리 위치로 재배치함

이러한 과정을 통해 링커는 프로그램을 실행 가능한 상태로 만들며, 모든 심벌 참조가 올바르게 해결되도록 보장함

728x90

'Computer Science' 카테고리의 다른 글

운영체제의 역할과 필요성  (0) 2024.08.08
정적 라이브러리와 동적 라이브러리  (3) 2024.07.24
지역 심벌과 전역 심벌  (0) 2024.07.24
링커란?  (0) 2024.07.17
컴파일 언어와 인터프리터 언어 비교  (0) 2024.07.10

+ Recent posts