Electrónica, Electricidad y Sonido
Programación
1. Introducción
¿Qué es Software de sistemas?
Es un Software que hace de puente entre las aplicaciones y el hardware.
Software .- Programas con sus instrucciones, etc.
Sistema .- Relacionado con el control y la gestión de un sistema informático
Los procesadores de lenguajes y los sistemas operativos son un tipo de software.
Es irrelevante para el usuario final, y vital para los programadores.
Aplicaciones y Software de Sistemas
APLICACIONES | SW DE SISTEMAS |
Ejecutan una aplicación | Controlan el sistema |
Maneja datos de la aplicación | Maneja recursos de la máquina |
Da servicio al usuario final | Ofrece servicios al programador y a las aplicaciones |
Escrito en lenguaje de alto nivel | Partes en ensamblador |
Solo está en ejecución mientras dura la aplicación | Puede ejecutarse durante todo el tiempo que la máquina esté encendida |
Aplicación a medida | Siempre es un paquete |
Generalmente es un proceso paso a paso | Suele manejar varios procesos simultáneamente |
Estas diferencias evolucionan:
Lenguaje C .- Software de Sistemas en Lenguajes de Alto Nivel
Desarrollos de Compiladores o S.O. para proyectos de investigación
Los usuarios del software son:
Aplicaciones .- El usuario final
Software de Sistemas .- Desarrollador de aplicaciones
Es necesario un entorno adecuado.
Tipos de Software de Sistemas
Procesadores de Lenguajes
Permiten que los programas escritos en lenguajes de alto nivel sean ejecutados en una máquina.
El hardware solo entiende instrucciones máquina de bajo nivel.
Caso de los traductores, donde nos encontramos los compiladores y los ensambladores, y de los intérpretes.
Compilador .- Traduce un programa en alto nivel a instrucciones máquina ejecutables por una CPU. Cuando se ejecuta una aplicación, el compilador no está presente. Así un programa escrito en MODULA es imposible de ejecutar. El compilador nos ofrece un puente real entre dos entornos diferentes.
Intérprete .- Traduce y ejecuta sentencia a sentencia un programa. Es un programa que está ejecutándose, toma una sentencia de la aplicación y la ejecuta en la CPU.
Los procesadores de lenguaje crean una Máquina Virtual; el programador obtiene una máquina que entiende Modula o Cobol. El Software de Sistemas ha creado un entorno más sencillo y conveniente que el entorno real.
Sistemas Operativos
Controlan las operaciones de la máquina mientras se ejecutan las aplicaciones. Realiza muchas funciones de gestión de recursos (memoria, discos, seguridad, arranque/parada, etc.). Cuando se arranca una máquina se carga el S.O. y mantiene el control del sistema durante todo el tiempo.
Las aplicaciones manejan su E/S mediante peticiones al S.O., el cual envía las órdenes apropiadas al hardware.
El S.O. ofrece un entorno mucho más simple que la máquina real, Máquina Virtual, algunos S.O.'s ofrecen varias.
Es un claro puente entre el hardware y las aplicaciones.
Protege el acceso directo a la E/S por las aplicaciones, el acceso se produce de forma controlada a través de llamadas al S.O.
Traduce las peticiones de las aplicaciones (system call) a las instrucciones necesarias para acceder al hardware real.
Utilidades
Suelen ser una extensión libre del S.O. Se trata de tareas de bajo nivel; por ejemplo, al copiar un disco completo, sería copiar todos los sectores y pistas. Suelen realizar una única tarea.
Otras clase de Software de Sistemas
Ofrecen algún tipo de función del sistema que no es específica de ninguna aplicación:
-
Editores
-
Gestores de Bases de Datos
-
Programas de ordenación
-
Emuladores de pantalla o teleproceso
-
Etc.
Necesidad del Software de Sistemas
-
Se puede escribir un programa en código máquina
-
Aplicaciones de bajo nivel controlan el hardware
-
Sin compiladores ni S.O., para llevar los programa a la memoria para su ejecución:
-
Cargador (software de sistema) carga los programas del usuario
-
Escrito en PROM
-
Mediante interruptores
-
Sin S.O. solo podemos cargar una sola aplicación
-
Programa incrustados (embbebed)
-
Una única función (lavadoras, misiles, etc.)
-
Sin procesadores de lenguajes
-
Programas en ensamblador o lenguaje máquina
-
Ventajas:
-
Productividad
-
Calidad
-
Evitar duplicidades de código en las aplicaciones
-
Manejo de periféricos
2. Ensambladores
Conceptos básicos sobre ensambladores
La función de un ensamblador es traducir un programa en lenguaje ensamblador al formato interno máquina que se puede ejecutar.
Se supone que un ensamblador es un programa muy simple; pues el lenguaje ensamblador especifica todas las instrucciones máquina, más o menos un reformateo. Sin embargo, realiza muchas más cosas:
-
Direccionamiento simbólico
-
Macros
-
Suelen tener un tamaño parecido a un compilador
-
Puede ser un tipo de procesador de lenguajes elaborado
Código fuente .- Lenguaje de entrada que acepta el traductor.
Código objeto .- Código máquina traducido que genera el traductor.
Un ensamblador es un traductor que genera código máquina binario a partir de un código fuente en lenguaje ensamblador.
Un emulador es un programa ejecutándose sobre una máquina que interpreta y ejecuta las instrucciones de un programa escrito para otra máquina.
Lenguaje Ensamblador
Es un método de programación donde cada instrucción máquina es especificada por el programador, pero con varias ayudas en el lenguaje que lo hacen más conveniente que el código máquina puro.
Los lenguajes ensambladores son diferentes para cada máquina:
-
Conjunto de instrucciones máquina propio
-
Pueden existir similitudes
-
Las instrucciones reales deben corresponderse con las de la máquina
-
La terminología usada puede variar ligeramente
El formato estándar es una instrucción por línea, que consta de cuatro campos:
Etiquetas .- Opcionales
Código Operación .- Es un campo obligatorio, excepto cuando es una línea de comentarios
Operando .- El número depende de la arquitectura de la máquina
Comentarios .- Opcionales
NEXT LDA TOTAL Para valor final
Características del lenguaje ensamblador
Códigos de operación mnemónicos
-
La mayor diferencia entre el código máquina y el lenguaje ensamblador son los mnemónicos
-
Los códigos de operación de las instrucciones se representan mediante un nombre corto (mnemónico)
-
Se corresponde con su código numérico
LDA 00000001
-
Mayor legibilidad
-
Operación que realiza cada línea
-
El diseño del hardware suele añadir el diseño de su lenguaje ensamblador
Direccionamiento Simbólico
-
Posibilidad de referirse a una dirección mediante un símbolo
-
Evita utilizar direcciones absolutas
-
Cercano al concepto de nombres de variables en los L. A. N.
-
Libera al programador de calcular direcciones
-
Facilita la modificación de los programas
-
Permite la reubicación (o la facilita en gran medida) de los programas; todas las referencias a direcciones simbólicas se deberán ensamblar como relativas al inicio del programa.
Pseudo-instrucciones
-
Las pseudo-instrucciones o directivas son órdenes (o información) directamente para el ensamblador
-
Nunca se ejecxutan
-
Se procesan en tiempo de ensamblado
-
Uso:
-
Definir la dirección inicial
-
Reservar espacio para los datos
-
Ordenes para el listado; saltos de página, ...
-
Asignar un valor constante a un símbolo
-
ORG, EQU, DS, DC, DEFW, DEFS, BSS, MACRO, END
Valores
-
Necesidad de poder indicar valores
-
Areas de datos inicializadas
CONVRATE DEFW 27
-
Valores numéricos:
1CH, X'1C', H1C
00011100B
-
Tipos de datos, sólo una posibilidad de representación de los datos
Expresiones
REC - 1
LDA TOTAL + 8
TOTAL - 20C0
Código de LDA - 01
Instrucción: 0120C8
-
Suma y resta suele estar soportada
-
Multiplicación y división a veces (símbolos absolutos y relativos)
Literales
-
Los cálculos que incluyen constantes necesitan tener almacenadas esas constantes
LDA VAR1
MUL CIEN
. . .
. . .
CIEN DEFW 100
-
El literal se puede usar donde se necesita un valor
MUL = 100
-
El ensamblador se encarga de buscarle las posiciones de memoria que necesite.
-
Normalmente, al final del programa
-
No se debe confundir con una dirección explícita como operando
MUL 100
Macros
-
Unen ciertas secuencias de instrucciones que se repiten frecuentemente
-
Evita reescribirlas varias veces
-
Concepto general aplicable a todo tipo de lenguajes
-
Pseudo-instrucciones
MACRO y END
-
Permiten parámetros simbólicos
-
Diferencia entre macro y subrutina
-
Tiempo de ensamblado contra tiempo de ejecución
-
Subrutinas menos código pero menos eficientes
-
Las macros repiten su código cada vez, pero no tienen prólogo/epílogo de las subrutinas
MACRO
INCR &LOC, &VAL
LDA &LOC
ADD =&VAL
STO &LOC
MEND
-
El programador puede utilizar una nueva instrucción del tipo
INCR total, 4
-
El ensamblador la sustituye por
LDA TOTAL
ADD =4
STO TOTAL
Ensamblado Condicional
Funciones de un ensamblador
Un ensamblador es como un software que traduce código fuente en lenguaje ensamblador a código máquina.
Existe un proceso de traducción, sobre todo para manejar las direcciones simbólicas, macros, literales y otros.
Traducción de Códigos de Operación
Los códigos de operación mnemónicos deben ser reemplazados por sus códigos de operación numéricos. Se suele emplear una Tabla de Instrucciones (constante).
Las instrucciones de máquinas reales suelen tener tamaños diferentes. A veces, un mismo código para varias cosas.
Asignación de direcciones
Debe determinar las direcciones reales donde se cargará cada instrucción. Se usa un contador de direcciones.
-
Al pirncipio de un programa se asigna un valor inicial al contador de direcciones (ORG)
-
Se incrementa con el tamaño de cada instrucción explorada
-
Cuando el programador usa el símbolo “ * “, el ensamblador utiliza el valor actual de su contador de direcciones
El valor del contador de direcciones puede ser una dirección absoluta o relativa al comienzo del programa,
-
Código reubicable
-
Necesidad de reubicación por parte del enlazador o del cargador
Resolución de direcciones simbólicas
Debe reemplazar los símbolos que aparecen como operando en las instrucciones, y utilizar la dirección numérica real correspondiente.
Se suele realizar en la segunda pasada consultando la Tabla de Símbolos.
Conversión de constantes a binario
Todos los valores definidos son valores decimales, caracteres, etc. Se deben evaluar y calcular un valor binario que se inserte en el código ejecutable.
Evaluación de expresiones
Evaluar las expresiones y calcular un valor binario que se pueda insertar en el código.
Asignación de almacenamiento a literales
Se deben asignar posiciones de memoria a los literales; calcular su valor y almacenar en la dirección que se le asigna. También se debe insertar el valor de su dirección en las instrucciones que lo utlizan.
Cálculo de los campos de dirección
Cualquier expresión utilizada en un campo de operando se debe calcular. Según los tipos de direccionamiento utilizado se calcula la Dirección Efectiva (D.E.) del operando.
El valor necesario para calcular la dirección real donde está almacenado el dato se inserta en el código generado.
LDA 2000 (DE: 2000)
LDA* 2000 (DE: 2000 + IX)
Expansión de Macros
Las instrucciones que componen una macro se deben insertar en el código cada vez que aparezca una referencia a la macro. Esto incluye las sustituciones de los parámetros simbólicos.
Como se incluyen nuevas instrucciones, se deben realizar todas las funciones descritas para cada instrucción que compone la macro.
Tareas varias
-
Mensajes de error
-
Listados de código fuente y código objeto en octal/hexadecimal
-
Generar un fichero en el código objeto
Mecánica de un Ensamblador
Se realizan varias pasadas por el código fuente (se lee éste de principio a fin varias veces). La información se recoge en cada pasada, en un conjunto de tablas.
Se usará en la pasada final para general el código objeto. La primera pasada lee el código fuente original. Las siguientes pueden volver a leerlo o utilizar algún formato de código intermedio.
La mayoría de los ensambladores realizan dos pasadas. Cuando realizan expansión de macros suelen realizar una pasada previa para dicha expansión.
Primera pasada
-
Examinar las instrucciones
-
Incrementar el Contador de Posiciones de Memoria
-
Construir la Tabla de Símbolos
-
Construir la Tabla de Literales
Segunda pasada
-
Utilizar la tabla de símbolos
-
Generar el Código Objeto Completo
Tabla de Símbolos
-
Nombre de todos los identificadores
-
Sus direcciones (valores)
Es posible realizar un ensamblador de 1 paso:
-
Prohibir referencias futuras
-
Mantener simultáneamente todo el código objeto que se construye en memoria (modificable)
-
Examinar el código hasta encontrar la definición del símbolo y continuar por donde iba
-
Otras técnicas
Ejemplo
* Ejemplo de programa en ensamblador
* Subrut. que saca por impresora A-Z
ORG 2000H Dir. Inicial
LDI `A' Carc. Inicial
NEXT OUT 2 Salida
STO CHARVAL Salva
SUB ='Z' Es la Z?
JZ EXIT Si es Z, fin
LDA CHARVAL Ingr. Char
ADD =1 Sig. Char
JUMP NEXT Continuar
EXIT JUM* 0 Termina
***** Datos *****
CHARVAL DEFW 0 Valor Carac.
END
-
Tabla de símbolos
NEXT 2003
CHARVAL 201C
EXIT 2018
-
Tabla de literales
`Z' 201E
1 2020
-
Código generado (dirección inicial 2000H)
-
09 00 41
2003 32 00 02
-
02 20 1C
-
04 20 1E
200C 23 20 18
200F 01 20 1C
2012 02 20 20
-
21 20 03
2018 A1 00 00 Por salto indexado
201B 00
201C 00 00
201E 00 5ª
-
00 01
Cuestiones prácticas
-
La tabla de símbolos es el corazón del ensamblador
-
Un ensamblador eficiente tendrá un acceso eficiente a su T.S.
-
50 símbolos (programa pequeño)
-
1.000 símbolos (programa grande)
-
Ordenación alfabética; búsqueda binaria
-
Tablas Hash
-
Listado de referencias cruzadas
-
Listado tabla de símbolos
Enlazadores y Cargadores
Programación Modular
En programas grandes (+100.000 líneas) es conveniente no tener que recompilar todo el programa cuando se realizan pequeños cambios. Por esta razón se encuentran separados en módulos compilables que se integran para su ejecución.
El proceso de integrar varios módulos para formar un único programa ejecutable se denomina enlazado (linking) o edición de enlaces (linkage editing). Los enlaces entre módulos suelen estar basados en las llamadas a procedimientos externos.
La compilación es independiente para cada módulo. Estos módulos son integrados por el enlazador o editor de enlaces antes de su ejecución.
Es un diseño estructurado mediante técnicas de ingeniería software. Estas son sus ventajas:
-
Mayor facilidad en los cambios
-
Varios programadores pueden escribir simultáneamente diferentes módulos
-
Las distintas funciones del programa se pueden separar y aislar, evitando los efectos laterales
-
Reutilización de módulos comunes
-
Diferentes lenguajes de programación en diferentes módulos
-
Depuración por módulos. Pruebas independientes y posterior integración
La mayoría de los traductores no generan un código ejecutable (cargar y ejecutar). Crean un código objeto reubicable; es un formato procesable por un editor de enlaces (MS-DOS: ficheros .OBJ).
El editor de enlaces genera un módulo cargable; en MS-DOS los ficheros .COM o .EXE. Se trata de un fichero con los mismos bits que se cargarán en la memoria RAM para su ejecución.
Ensamblado para reubicación
Al final, hay que unir todos los módulos para generar un único programa objeto reubicable.
El problema es que no se conoce la posición definitiva en memoria de los módulos dentro del programa principal. Esta depende del origen asignado al primer módulo y de los tamaños de todos los módulos que le preceden.
La solución podría consistir en ensamblar todos los módulos juntos; pero resulta muy ineficaz y habría que repetir reiteradamente el trabajo de ensamblar módulos no modificados.
Entonces, encontramos la solución en ensamblar sólo aquellos módulos que han sido modificados. Para ello es necesario un programa enlazador o montador.
Un enlazador compacta todos los módulos, procura que todas las referencias del programa global sean correctas. El ensamblador deberá preparar los módulos objetos para facilitar el trabajo del enlazador.
El problema de la reubicación reside en que posicionar un código en una dirección de memoria determinada se reduce a sus posiciones absolutas. Por esta razón hay que ensamblar los módulos como si empezaran en la posición cero (0), y construir una Tabla con todas las Direcciones Absolutas (TDA).
El cargador o el enlazador, una vez conocido el origen real X del programa o del módulo, incrementan en X todas las direcciones de la TDA.
Los ensambladores suelen poder generar tanto código absoluto como código reubicable. Así tenemos:
-
Ensambladores absolutos
-
Ensambladores relativos
Si existe una pseudo-instrucción ORG se comportan como ensambladores absolutos. Si, por el contrario, no aparece, se supone que no se conoce el origen, se ensambla con origen cero y se geenera una TDA.
Pseudo-instrucciones EXPORT e IMPORT
Los problemas de la programación modular son la reubicación y las referencias externas (un módulo necesita utilizar datos o código de otros módulos).
El ensamblador no puede generar completa la Tabla de Símbolos y se produce un error de programación. Para evitarlo hay que usar las pseudo-instrucciones EXPORT e IMPORT.
EXPORT .- Se especifican todos los nombres simbólicos de un módulo que serán referenciados por otros módulos.
IMPORT .- Se identifican todas las referencias externas de un módulo:
-
Permite reconocer una referencia externa
-
No se puede completar la Tabla de Símbolos
El enlazador resolverá las referencias externas. El ensamblador dejará el trabajo preparado creando dos tablas adicionales:
-
Tabla de Símbolos Importados (TSI)
-
Tabla de Símbolos Exportados (TSE)
Tabla de Símbolos Importados (TSI)
-
Una anotación por cada aparición de un símbolo importado
-
Nombre simbólico
-
Dirección relativa dentro del módulo
-
Tipo
Tabla de Símbolos Exportados (TSE)
-
Nombre simbólico
-
Valor de la dirección
-
Tipo
Programa Enlazador
El objetivo es convertir uno o más módulos en un único módulo ejecutable. Sus funciones son reubicar y resolver las referencias externas.
Necesita que el ensamblador genere para cada módulo la siguiente información:
-
Indicación de módulo absoluto (o reubicable)
-
Dirección de carga (solo módulos absolutos)
-
TDA, Tabla de Direcciones Absolutas
-
TSI, Tabla de Símbolos Importados
-
TSE, Tabla de Símbolos Exportados
Necesita que usuario le informe de los módulos que se deben enlazar y de las direcciones absolutas deseadas.
Primera función: Generar todas las direcciones absolutas
Para determinar la dirección de comienzo del módulo:
-
Existe una sentencia ORG y es un módulo absoluto
-
Indicación explícita del usuario al enlazador
-
Dirección final del módulo anterior
Segunda función: Proceder a la reubicación del módulo, conocida la dirección inicial
Se deben incrementar en el valor de la dirección inicial del módulo todas las direcciones absolutas de su TDA; así como todas las direcciones de símbolos exportados que están en su TSE (puede hacerlo el cargador).
Una vez realizada la reubicación de todos los módulos, se conoce la dirección absoluta de todos los símbolos exportados.
Para resolver las referencias externas se genera una Tabla Global de Símbolos Exportados (TGSE):
-
Uniendo todas las TSE
-
La TGSE debe estar bien estructurada pues se realizarán numerosas búsquedas sobre ella
-
Hay que comprobar que no existen referencias externas no resueltas
Para resolver las referencias externas de cada módulo se toman las entradas de su TSI y se busca su dirección absoluta en la TGSE; si no existe se da un error.
Tercera función: Copiar todos los módulos en un solo bloque ejecutable
Debe ser manejable por el cargador para proceder a su ejecución; generalmente, se crea un nuevo fichero con extensión de ejecutable (.EXE, .COM, etc.).
Fichero ejecutables
Fichero ejecutable con una única dirección de comienzo
-
Es necesario almacenar una imagen exacta del contenido de la memoria principal para la ejecución del programa.
-
Se añade únicamente una cabecera con la dirección inicial y su longitud.
-
Método muy sencillo
-
Genera ficheros innecesariamente grandes
Fichero ejecutable con dirección por palabra
-
Se almacena cada palabra efectiva del ejecutable con su dirección
-
Permite eliminar las zonas intermedias no usadas
-
Añade demasiada cantidad de información sobre las direcciones
Fichero ejecutable con división en bloques con dirección de comienzo
-
Dividir el ejecutable en pequeños bloques con direcciones sucesivas
-
Añadir la dirección inicial y la longitud de cada bloque
Librerías de programas
Existen librerías generales, matemáticas, de entrada/salida, etc.
Una serie de módulos ensamblados de forma reubicable dan mayor trabajo al enlazador. Este busca los módulos externos en todos los ficheros del sistema con extensión de librería (.lib) y en los ficheros especificados explícitamente.
Programa cargador
Su objetivo es leer un fichero ejecutable almacenado en memoria auxiliar (DD, Cinta, etc.) y traspasarlo a la memoria principal para su ejecución. Debe ser compatible con el formato de salida generado por el enlazador.
Para poder cargar los programas en diferentes zonas de memoria, el cargador reubicador necesita de una Tabla General de Direcciones Absoluta generada por el enlazador combinando la TDA y la TSI de los módulos.
Para que un programa se ejecute, se necesita que el contador de programa apunte a la primera instrucción ejecutable.
5. Sistemas operativos
Introducción
Genéricamente un sistema operativo, S.O. realiza principalmente dos funciones:
Compartir los recursos
Suministrar una máquina virtual o extendida
Gestión de los recursos
Un S.O. debe compartir los recursos del computador entre un número de usuarios simultáneos del sistema. El objetivo es incrementar la disponibilidad del sistema, y al mismo tiempo maximizar la utilización de los recursos compartidos.
Los recursos que se comparten son:
Procesadores
Memoria
E/S
Datos
La importancia de cada recurso depende de su coste. Actualmente hay un decremento de los costes, pero siempre existen recursos escasos.
Máquina virtual
Trata de transformar el hardware en una máquina más fácil de utilizar. Algunas áreas donde la máquina virtual difiere del hardware son:
E/S
Memoria
Sistema de ficheros
Protección y manejo de errores
Interacción con programas
Control de los programas
La naturaleza exacta de una máquina virtual depende de la aplicación en la cual será usada. Su diseño también estará fuertemente influenciado por el uso al que se destine el sistema. Los Sistemas de Propósito General son un problema.
Tipos de Sistemas Operativos
S.O. Mono-Usuario
Ofrecen un máquina virtual para un único usuario
Apropiados para sistemas dedicados a una sola función (CP/M, MS-DOS, ...)
Objetivos:
Lenguaje de comandos fácil de usar
Sistema de ficheros simple
Facilidades de E/S para terminal, disco e impresora
Actualmente, muchos microordenadores soportan multi-tareas
Control de Procesos (Tiempo Real)
Control por parte de un sistema computador de un proceso industrial
La característica común de estas aplicaciones es el feedback:
Recibe entradas desde el proceso controlado
Calcula una respuesta para mantener la estabilidad del sistema
Inicia el mecanismo para dar dicha respuesta
Problema .- Hay un tiempo crítico en el cual se debe dar la respuesta
Su principal función es suministrar la máxima exactitud con la mínima intervención del operador, y si el hardware falla, fallos seguros
Sistemas de Consultas de Ficheros
Sistemas con un gran conjunto de datos, o base de datos, que se consulta para obtener información
Respuesta en un tiempo corto (<1minuto)
Capacidad de modificación de los datos para actualizaciones
Ejemplos: Sistemas de gestión empresarial, sistemas médicos, etc.
El acceso a la información no debe implicar conocimientos (hardware o software) de la organización de la base de datos
El S.O. debe facilitar el acceso a los datos, la respuesta
Sistemas de Procesamiento de Transacciones
Base de datos que se modifica constantemente (reservas de líneas aéreas, operaciones bancarias, etc.)
Necesidad de actualización instantánea de la base de datos
Problema: Accesos simultáneos y limitaciones en el tiempo de respuesta
Deben ser transparentes para el usuario individual
El S.O. debe resolverlos dando la impresión al usuario de que es el único en el sistema
Sistemas de Propósito General
Sistemas con un gran número de usuarios que realizan una gran variedad de tareas
Se diseñan para manejar un flujo continuo de trabajo en la forma de trabajos (jobs) que se ejecutarán en el computador
Cada trabajo realiza una tarea específica para un determinado usuario y, típicamente, ejecuta uno o más programas
Debe tener un gran número de programas de utilidades (software sistemas)
Debe manejar una amplia variedad de periféricos
Suministrar y controlar estas facilidades, junto con la organización del flujo de trabajo, son las funciones genéricas de un S.O. de propósito general
Sistemas Batch .- Cuando un trabajo entra en el computador el usuario no tiene ya más contacto con el trabajo hasta que este termina y, típicamente, ejecuta uno o más programas
Sistemas multi-acceso o interactivos .- El usuario puede ejecutar uno o más programas tecleando en el terminal; y utilizarlo para monitorizar y controlar la ejecución de sus programas
La mayoría de los sistemas operativos combinan ambos modos de operación:
Modo Interactivo .- Desarrollo de programas y preparación de documentos
Modo Batch .- Tareas rutinarias no interactivas y trabajos con un gran tiempo de ejecución
Funciones y Características de un Sistema Operativo
Funciones de un S.O. Un poco de historia
El punto de partida fue la máquina sola (hardware), es decir, la CPU, la memoria, los periféricos E/S. Sin el software la carga y la ejecución de programas eran realizadas por el usuario.
Algunos ejemplos son los computadores con procesamiento por lotes, con entradas por tarjetas perforadas, con salida por impresora. Las actividades realizadas por el operador para cada trabajo eran:
Poner las tarjetas del programa fuente en el lector
Inicializar un programa de lectura de tarjetas
Arrancar un compilador que compile el programa fuente
Poner las tarjetas de datos en el lector
Iniciar la ejecución del programa compilado
Recoger los resultados de la impresora
La velocidad de la máquina venía determinada por las pulsaciones del operador y su velocidad alimentando los periféricos.
Job Control Language
Primera Mejora .- Automatizar carga, lectura, compilación y ejecución
Funciones Operador .- Cargar las tarjetas y recoger los resultados en la impresora
Precio Pagado .- Dedicar recursos a un programa de control de trabajos
Necesidad de tarjetas de control
El programa de control debe ser capaz de:
Interpretar un lenguaje de control de trabajos (job control language)
Manejar los errores de un trabajo para que no afecten a los siguientes
Velocidad Máquina .- Velocidad E/S
Off-Lining
Mejora .- Realizar toda la E/S sobre cintas magnéticas
La conversión de tarjetas perforadas a cinta se realiza en un ordenador satélite de bajo coste
La transferencia de las cintas magnéticas se hace a mano
Precio .- Conjunto de rutinas para codificación y empaquetado de datos en cinta magnética
Como la cinta es un medio de almacenamiento serie, no existía el concepto de planificación de trabajos
Interrupciones y canales
Mejora .- Eliminar la dependencia de E/S. Solapar E/S con procesamiento.
Dos características HW: Canal e Interrupciones
Un canal es un dispositivo que controla uno o más periféricos, realizando las transferencias de datos entre Memoria y los Periféricos con una mínima intervención de la CPU
Una interrupción es una señal que transfiere el control de la CPU a una posición fija, salvando simultáneamente el valor previo del CP.
Una interrupción desde un canal le indica a la CPU que la transferencia de datos ha terminado
Permiten leer trabajo desde cualquier tipo de dispositivo y ejecutarlos de uno en uno en cualquier orden
Precio .- Rutina de Manejo de Interrupciones; Planificador (scheduler)
El planificador sirve para decidir cuál de los trabajos seleccionados en el disco será el siguiente en ejecutarse
Mitad 60. Single-Stream Batch Monitor
Multiprogramación
Mejora .- Ejecutar varios programas simultáneamente
Es posible mantener varios programas al mismo tiempo en memoria dividiendo el tiempo de CPU entre ellos en función de los recursos que cada uno necesita
Objetivo .- Conseguir una utilización óptima de los recursos
Precio .- Control de los Recursos y la protección de los trabajos
Principios 70. Multi-Stream Batch Monitor
Multi-Acceso
Mejora .- Permitir interacción entre usuario y ordenador; muy importante en el desarrollo y la depuración de programas
Aceptar entradas desde terminales remotos, sistema multi-acceso
Precio .- Control y protección de usuarios simultáneos
Principios 70 - Mediados 70. Multi-Acceso + Multi-Programación
Nuevas facilidades - Redes de ordenadores
Sistemas con varios S.O. simultáneamente
Facilidades de comunicación entre ordenadores
Redes de ordenadores. S.O. distribuidos
Un sistema operativo debe realizar las siguientes funciones:
Secuenciamiento de trabajos
Interpretación de un lenguaje de control
Manejo de errores
Manejo de E/S
Manejo de interrupciones
Planificación (scheduling)
Control de recursos
Protección
Multi-Acceso
Adicionalmente:
Ofrecer un buen interfaz con el usuario (usuario)
Permitir contabilidad de los recursos (administrador)
Características de un Sistema Operativo
Concurrencia
Concurrencia es la existencia simultánea de varias actividades
Ejemplos .-
Solapamiento E/S y procesamiento CPU
Coexistencia de varios programas en Memoria Principal
Problemas .-
Conmutación entre actividades
Protección de una actividad frente a los efectos de otras
Sincronización entre actividades mutuamente dependientes
Compartir recursos
Actividades concurrentes pueden necesitar compartir recursos o información
Motivos
Coste .- No tiene sentido suministrar todos los recursos a todos los usuarios por separado
Aprovechar el trabajo realizado .- Usar programas y rutinas de otros
Datos compartidos .- Usar los mismos datos para diferentes programas, posiblemente de varios usuarios
Evitar redundancias .- Compartir una única copia de un programa (editor, compilador, ...)
Problemas
Asignación de recursos
Acceso simultáneo a datos
Ejecución simultánea de programas
Protección contra corrupción de datos y programas
Almacenamiento a largo plazo
Necesidad de datos y programas compartidos implica la necesidad de almacenar la información a largo plazo
Evita almacenamiento en medios externos
Problemas .-
Facilidad de acceso
Protección contra interferencias (maliciosas o no)
Protección contra fallos del sistema
Tratar sucesos no determinísticos
Un S.O. debe ser determinístico en el sentido que un mismo programa que se ejecuta con los mismos datos debe producir siempre el mismo resultado
Indeterminado, debe responder a sucesos que ocurrirán en un orden no predecible
Peticiones de recursos, errores de ejecución, interrupciones de los periféricos, ...
Un S.O. debe diseñarse para tratar cualquier secuencia de eventos
Características deseables
Eficiencia
Dificultad para encontrar un único criterio que la mida
Criterios
Tiempo medio entre trabajos por lotes
Tiempo de CPU no usado
Tiempo de espera para trabajos por lotes
Tiempo de respuesta en sistemas multiacceso
Utilización de recursos
“throughput, jobs/h”; velocidad de trabajo en terminales
No se pueden satisfacer simultáneamente
Coste recursos humanos vs. Coste recursos materiales
Confiabilidad
Idealmente, un S.O. libre de errores y capacidad de tratar todas las posibles contingencias
Práctica, nunca es posible
Mantenibilidad
Debe ser posible mantener el S.O.
Mejorándolo
Corrigiendo errores
El S.O. debe ser:
Modular
Interfaces entre módulos claramente definidos
Bien documentado
Pequeño tamaño
El espacio que ocupa el S.O. es un espacio perdido para las aplicaciones
Gran tamaño aumenta las posibilidades de errores y de tiempo de desarrollo
Programas, Procesos y Procesadores
Procesadores
Un proceso puede llevarse a cabo gracias a un Agente en el cual se ejecuta el programa asociado a este proceso, un procesador.
Un procesador ejecuta un proceso; un proceso se ejecuta en un procesador
Un procesador es algo en lo que se ejecutan instrucciones:
Hardware
Hardware + Software
Concurrencia
Ocurre con la activación de varios procesos al mismo tiempo.
Concurrencia verdadera. N procesadores y N procesos concurrentes,
Concurrencia aparente. Si hay menos procesadores que procesos; conmutación en un procesador de un proceso a otro
Un procesador debe almacenar cierta información:
Sobre el proceso que se está ejecutando
Antes de poder conmutar a otro proceso
Debe ser suficiente par permitir la reanudación del proceso posteriormente
El procesamiento es concurrente; si se realizara una foto del sistema completo se encontrarían varios procesos entre su punto de entrada y su punto de salida, concurrencia verdadera y aparente.
Procesos No Deterministas
Proceso como secuencia de acciones que pueden ser interrumpidas entre cualquier paso. Estas interrupciones ocurren en un orden impredecible.
Una interrupción debe ir asociada con el almacenamiento de la información necesaria para reanudar el proceso interrumpido.
Un proceso es una secuencia de acciones (dinámico). Un programa es una secuencia de instrucciones (estático). Un procesador es un agente para ejecutar un proceso.
No determinista y concurrencia: Interrupción de procesos entre acciones y conmutación de procesos en el procesador.
Se da la necesidad de mantener la información suficiente sobre los procesos para que éstos puedan reanudarse en momentos posteriores.
Procesos concurrentes
omunicación entre procesos
Los procesos en un sistema computador no actúan aislados. Deben cooperar para alcanzar los objetivos deseados y deben competir por los recursos del sistema
Cooperación y competición: comunicación entre procesos
Exclusión mutua
Sincronización
Interbloqueo
Semáforos
Signal(s)
Efecto: Incrementar el valor del semáforo `S' en uno; la operación de incremento debe ser una operación
rementar(S)
Signal(s)
Incrementar(s)
val(sem) = C(sem) + ns(sem) - nw(sem)
val(sem) es el valor del semáforo sem
C(sem) es su valor inicial
ns(sem) es el número de operaciones signal sobre él
nw(sem) es el número de operaciones wait sobre él
Por definición,
val(sem) >= 0
Se deriva la importante relación
nw(sem) <= ns(sem) + C(sem)
que cumple la igualdad estricta si:
val(sem) = 0
Exclusión mútua
Protección frente a accesos simultáneos a recursos no compartibles
Prevenir que los procesos ejecuten concurrentemente las zonas del programa donde se accede al recurso
Sección crítica.- esas zonas del programa
Exclusión mutua de recursos mediante exclusión mutua de las secciones críticas
Exclusión mutua de una sección crítica.
Encerrándola con wait y signal
wait(mutex);
critical section
signal(mutex);
Donde mutex es el nombre de un semáforo y su valor inicial es 1
Formalmente.
nw(mutex) <= ns(mutex) + 1
Como máximo un proceso puede estar dentro de la sección crítica; la entrada se prohibe solo si otro proceso está ya dentro
Un proceso es detenido solo si el valor de mutex es 0
nw(mutex) = ns(mutex) + 1
Ejemplo
Programa Proceso A
. . .
wait(mutex);
añadir elemento a la cola;
signal(mutex);
. . .
. . .
Programa Proceso B
. . .
. . .
wait(mutex);
quitar elemento de la cola;
signal(mutex);
. . .
. . .
Ejemplo sin semáforos
while puerta = CERRADA do
op_nula;
puerta := CERRADA;
sección crítica
puerta := ABIERTA;
La razón se debe en la separación de la pregunta por la puerta abierta y la operación de cerrar puerta
Los semáforos evitan dificultades similares porque wait y signal son operaciones indivisibles
Existen microprocesadores con instrucciones máquinas de TEST AND SET absolutamente indivisibles
Sincronización
Proceso A no puede pasar de un punto L1 hasta que otro proceso B no haya alcanzado un punto L2
Programa Proceso A
. . .
L1: wait(sem_sync);
. . .
. . .
Programa Proceso B
. . .
L2: signal(sem_sync);
. . .
. . .
donde sem_sync es un semáforo con valor inicial 0.
Formalmente:
nw(sem_sync) <= ns(sem_sync)
lo cual implica que A no puede pasar de L1 antes de que B pase por L2.
Caso de los productores - consumidores
Un conjunto de procesos productores y un conjunto de procesos consumidores
que se comunican por medio de un buffer
dentro del cual los productores depositan items
y los consumidores extraen estos items
Proceso Productor .- produce item - deposita item en el buffer
Proceso Consumidor .- consume item - extrae item del buffer
Buffer de capacidad limitada, máximo N items de igual tamaño
Los problemas aparecen cuando el buffer está lleno o está vacío
0 <= d - e = N
El problema es la protección contra accesos simultáneos al buffer
Programa Productor
repeat indefinidamente
begin
producir item;
wait(s_lleno);
wait(sem_buff);
depositar item en buffer;
signal(sem_buff);
signal(s_vacio);
end
Programa consumidor
repeat indefinidamente
begin
wait(s_vacio);
wait(sem_buff);
extraer item del buffer;
signal(sem_buff);
signal(s_lleno);
consumir item;
end
Sincronización mediante semáforos s_lleno y s_vacio, que se inicializan a N y 0 respectivamente. Exclusión mutua mediante semáforo sem_buff que se inicializa a 1.
Semáforos de Sincronización
nw(s_lleno) <= ns(s_lleno) + N
nw(s_vacio) <= ns(s_vacio)
Observando el orden de las operaciones de los programas tenemos que:
ns(s_vacio) <= d <= nw(s_lleno)
ns(s_lleno) <= e <= nw(s_vacio)
y por tanto
d <= nw(s_lleno)
<= ns(s_lleno) + N
<= e + N
similarmente
e <= nw(s_vacio)
<= ns(s_vacio)
<= d
combinando ambas
e <= d <= e + N
Interbloqueo (deadlock)
Puede ocurrir cuando varios procesos compiten por un mismo recurso
Ejemplo .- Procesos A y B que utilizan los semáforos x e Y cuyos valores iniciales son 1 en ambos casos
Programa Proceso A
. . .
wait(X)
. . .
wait(Y)
. . .
. . .
Programa Proceso B
. . .
wait(Y)
. . .
wait(X)
. . .
. . .
La compartibilidad de un semáforo está determinada por su valor inicial
Si el valor es n (n>1), el semáforo puede compartirse por n procesos
Si el valor es 1, el semáforo no se puede compartir
Otro ejemplo más complejo: productor - consumidor invirtiendo el orden de las operaciones wait del consumidor
wait(sem_buff);
wait(s_vacio);
extraer item del buffer;
signal(sem_buff);
signal(s_lleno);
Interbloqueo puede ocurrir como resultado de una secuencia de operaciones wait en un orden incorrecto
Monitores
La utilización indisciplinada de los semáforos facilita el que se produzcan errores:
Un programador puede fácilmente colocar operaciones wait y signal en los lugares equivocados
O puede olvidar ponerlos donde o cuando debe. Ejemplo: datos compartidos que no se protegen con wait-signal
Construcciones en lenguajes de programación que:
Obliguen al programador a declarar datos/recursos compartidos explícitamente
Y que impongan la exclusión mutua en el acceso a los mismos
Un monitor consta de:
Los datos que son un objeto compartido
Un conjunto de procedimientos que pueden ser invocados para acceder al objeto
Un trozo de programa que inicializa el objeto (este programa se ejecuta únicamente una vez, cuando se crea el objeto)
Ejemplo .- Un buffer para el paso de datos entre un proceso productor y un proceso consumidor
El espacio para el buffer y su puntero
Dos procedimientos, depositar y extraer
Un troza de programa que inicialice los punteros al comienzo del buffer
Un lenguaje que incorpore monitores. Un compilador debe asegurar que los procedimientos de cada monitor se implementan como secciones críticas mutuamente excluyente, generando operaciones wait y signal. La responsabilidad de la exclusión mutua pasa del programador al compilador.
El diseñador de S.O. obtiene la ventaja de restringir todas las operaciones sobre objetos compartidos a un conjunto de procedimientos bien definidos y con la seguridad de que estas operaciones son mutuamente excluyentes.
La sincronización con propósitos distintos a la exclusión mutua siguen siendo responsabilidad del programador.
Núcleo del sistema
El principal interfaz entre el hardware básico de la máquina y el S.O. lo ofrece el núcleo del sistema, que debe suministrar un entorno en el cual puedan existir los procesos:
Manejador de interrupciones
Conmutación de procesos
Mecanismos para la comunicación entre procesos
Prestaciones hardware esenciales
Mecanismo interrupciones
Actividades E/S solapadas con procesamiento en la CPU
Interrumpir cuando termina la transferencia E/S
Un mecanismo interrupción que por lo menos salve el valor del CP del proceso interrumpido y transfiera el control a una posición conocida de memoria
Rutinas interrupción .- Determinan la fuente de la interrupción y la tratan de manera adecuada
Protección memoria
Proteger la memoria usada por un proceso de accesos no autorizados de otros procesos
Mecanismos de protección en el direccionamiento hardware de la memoria
Conjunto de instrucciones privilegiadas
Instrucciones reservadas para uso exclusivo del S.O.
Funciones típicas que realizan:
Habilitar/Deshabilitar Interrupciones
Conmutación de procesos
Acceso registros usados por el hardware de protección de memoria
Entrada/Salida
Parada y control del procesador
Mayoría de computadores operan en más de un modo, típicamente en dos modos:
Modo supervisor .- Pueden usarse las instrucciones privilegiadas
Modo usuario .- No pueden usarse las instrucciones privilegiadas
El cambio de modo usuario a supervisor se realiza automáticamente
Un proceso de usuario llama al S.O. para ejecutar alguna función que necesita utilizar instrucciones privilegiadas (system call)
Se produce una interrupción
Ocurre un error en un proceso de usuario (interrupciones internas o traps). P.E.- Intento de ejecutar una instrucción privilegiada
El cambio de supervisor a usuario se produce ejecutando una instrucción privilegiada
Reloj tiempo real
Para poder implementar cualquier política de planificación y poder controlar los recursos del sistema es esencial contar con un reloj hardware para generar interrupciones a intervalos fijos de tiempo real
Perfil del núcleo
El núcleo consta de tres partes:
El Manejador de Interrupciones de Primer Nivel (MIPN)
El despachador (dispatcher) o Planificador de bajo nivel
Dos procedimientos o rutinas que implementan las primitivas wait y signal
Es la parte del S.O. más dependiente de la máquina. Típicamente, se escribía en lenguaje ensamblador.
Representación de procesos
Estructura de datos con información acerca del proceso. La representación física de los procesos en el sistema viene dada por el descriptor de proceso (bloque de control o vector de estado).
El descriptor de proceso es una zona de memoria que contiene toda la información relevante acerca del proceso:
Identificación (nombre proceso)
Estado
Otras informaciones: Punteros memoria y recursos, punteros a colas diversas, zona reserva registros, etc.
Estados de un proceso
Ejecución .- Si un procesador está ejecutando sus instrucciones
Preparado .- Podría utilizar un procesador si alguno estuviera disponible
Espera o bloqueado .- No puede utilizar un procesador aunque alguno estuviera disponible. Generalmente, está esperando que ocurra algún suceso que le permita continuar
El estado de un proceso es la información clave para el despachador cuando tiene que asignar el procesador
Almacenar toda la información que hay que salvar cuando el proceso pierde el control del procesador:
Valores de los registros
Valores de cualquier registro utilizado en el direccionamiento de memoria
El formato de esta parte del descriptor depende del hardware
Descriptor de Proceso hardware, entorno volatil del proceso .- Sub-conjunto de las facilidades del sistema compartidas y modificables que son accesibles al proceso
Estructura de procesos .- Unión de todos los Descriptores de Procesos del sistema; una lista simple encadenada
Tabla Central del Sistema .- Aparece la Estructura de Procesos; es la estructura de datos básica del S.O. y desde la que accede a las diversas estructuras que posee
Manejador de Interrupciones
El Manejador de Interrupciones de Primer Nivel (MIPN) es responsable de responder a todas las excepciones del sistema; éstas son las siguientes:
Interrupciones .- Sucesos externos asíncronos. Señal externa que le llega al procesador y que hace que el computador altere su flujo de ejecución de instrucciones
Traps o interrupciones SW .- Sucesos internos síncronos. Errores y system calls
Las funciones del MIPN son:
Determinar la fuente de interrupción
Iniciar el servicio de la interrupción
Siempre que se entra al MIPN se para a modo supervisor. En cuanto al hardware debe salvar como mínimo el contador de programa.
También deben salvarse los registros que utilice el MIPN, si no lo hace el hardware lo debe hacer el MIPN como su primera operación; una alternativa será un conjunto extra de registros.
La determinación de la fuente de interrupción será más o menos fácil dependiendo del hardware:
HW distinga entre las distintas fuentes y se salte a distintas posiciones de memoria según la fuente
HW solo indica interrupción, SW realizará una secuencia de tests sobre los distintos flags de estado de las posibles fuentes HW
Métodos intermedios
Interrupciones con prioridad
Normalmente, las interrupciones se deshabilitan cuando se cede el control al MIPN
Asegura que los valores de registros salvados no se machacan si ocurre otra interrupción
Nuevas interrupciones quedan pendientes
Problema: interrupciones con prioridades
El MIPN deshabilita las interrupciones con igual o menor prioridad
Tratamiento de una interrupción
El MIPN llama a una Rutina de Tratamiento de Interrupción (RTI) apropiada al tipo de interrupción recibida.
Conviene que sean cortas
La RTI realiza unas pocas acciones y el resto las deja para un proceso que se ejecutará en modo usuario posteriormente
Cuando ocurre una interrupción puede cambiar el estado de algún proceso
Espera Preparado
Las RTI son las encargadas de modificar el campo de estado del descriptor del proceso afectado
Es posible que el proceso que estaba en ejecución pierda el control de la CPU
Misión del dispatcher
Despachador
Se trata de un Planificador de Bajo Nivel, cuya función es asignar la CPU entre los distintos procesos del sistema.
Es llamado cuando el proceso en ejecución no puede continuar o cuando se sospecha que existe otro proceso que aprovecharía mejor el procesador:
Después de una interrupción que cambia el estado de un proceso
Después de una system call cuyo resultado sea que el proceso en ejecución queda temporalmente imposibilitado para continuar
Después de un error cuyo resultado es la suspensión del proceso en ejecución mientras se trata el error
Estos son todos casos especiales de excepciones. El despachador se invoca después de todas las excepciones.
Operaciones
Si el proceso en ejecución es el más adecuado para ejecutarse le devuelve el control en el punto salvado por la interrupción y termina
Si no es el más adecuado, salva el contexto volátil del proceso en su descriptor
Recupera el contexto volátil del proceso seleccionado para ejecutarse
Transfiere el control al nuevo proceso seleccionado, en la posición indicada por el CP recuperado
El proceso más adecuado para ejecutarse:
Ordenar todos los procesos preparados según algún tipo de prioridad
La asignación de prioridades en el planificador de alto nivel
Ejemplo
Cola proceso
Cola ordenada por prioridades creciente de modo que el siguiente proceso elegible esté el primero de la cola
RTI que modifica estado proceso
Introducir el descriptor del proceso en la cola en la posición adecuada
Problema
Proceso nulo
Alternativas
Varias clases de procesos ejecutables
Resumen
Mecanismo interrupción
Salvar CP
Salvar otros registros (opcional)
Llamar MIPN
MIPN
Salvar registro que utiliza (si no se salvaron)
Identificar interrupción (ayudado por hardware)
Llamar a una RTI
RTI
Atender la interrupción
Posibilidad de modificar el estado de un proceso
Despachador
¿Conmutación de proceso? Si no, continuar con el proceso en ejecución
Salvar contexto volátil del proceso en ejecución
Recuperar contexto volátil del proceso en la cola
Transferir control al nuevo proceso
Implementación wait y signal
Hay que implementar algún mecanismo de comunicación entre procesos, para ello se emplean las operaciones wait y signal. Las cuales se incluyen en el núcleo porque:
Deben estar disponibles para todos los procesos
Su resultado puede ser bloquear un proceso, provocando una llamada al dispatcher
Una RTI puede despertar un proceso mediante la ejecución de signal sobre el mismo semáforo donde el proceso ejecutó un wait
Bloquear y desbloquear
La operación wait implica un bloqueo potencial del proceso, y signal la liberación de un proceso
Implementación mediante una cola asociada a cada semáforo
wait(s)
if s<>0 then s:=s-1
else añadir proceso cola y
estado proceso = ESPERA
signal(s)
if cola VACIA then s:=s+1
else quitar proceso cola y
estado proceso = PREPARADO
Encolar y desencolar
Para saber qué proceso se quita de la cola después de un signal, hay que conocer su estructura:
Primero Entrar, Primero salir (FIFO)
Por prioridades
Diferentes semáforos pueden tener diferentes tipos de organización de cola
Asignación del procesador
wait y signal pueden alterar el estado de un proceso
Si lo modifican se debe llamar al dispatcher
Por simplicidad, aunque no se modifique el estado de ningún proceso, siempre se llama al disptacher; éste debe reanudar el proceso en ejecución que será el primero en la cola
Indivisibilidad
wait y signal deben ser operaciones indivisibles
necesidad de alguna clase de operación de cierre
Implementar la operación de cierre
Deshabilitando las interrupciones
Instrucción de Test and Set
Instrucción para intercambiar el contenido de dos posiciones de memoria
Los dos mecanismos de cierre mediante instrucciones especiales provocan una espera ocupada
Diferencias entre wait/signal y operaciones de cierre
wait y signal | Lock y Unlock | |
Propósito | Sincronización Genérica procesos | Exclusión Mutua de procesos desde wait/signal |
Nivel Implementación | Software | Hardware |
Mecanismo Retardo | Colas | Espera Ocupada/ Inhibir Interrupciones |
Tiempo Retardo Típico | Varios segundos | Varios microsegundos |
Gestor de memoria
El siguiente nivel después del Núcleo es el Gestor de Memoria. Es necesario cuando un proceso que no tiene asignada algo de memoria apenas puede realizar pocas cosas o ninguna.
Objetivos
Reubicación
Memoria debe ser compartida
El programador no conoce donde se cargará su programa; no puede escribir sus programas en términos de direcciones absolutas
La memoria asignada a los programas no permanece fija durante toda la ejecución
No puede realizar la reubicación al cargar
Puede ser necesario mover los programa en memoria
La región de memoria asignada a un proceso puede cambiar durante el tiempo de vida del proceso
S.O. responsable de transformar las direcciones usadas por el programador a las direcciones físicas asignadas al proceso
Protección
Ningún proceso tiene permitido modificar el contenido de las posiciones de memoria asignadas a otro proceso
Las comprobaciones en Tiempo de Compilación no sirven; hay lenguajes que permiten cálculos de direcciones dinámicamente en tiempo de ejecución
Todas las referencias de memoria generadas por un proceso deben ser comprobadas en tiempo de ejecución
Compartir la memoria
Existen ocasiones donde varios procesos deben acceder a las mismas posiciones de memoria
Gestor de Memoria debe permitir accesos controlados a las áreas de memoria compartida
Organización Lógica
Espacio de direcciones lineal o de una dimensión; reflejo del hardware pero no de la forma en que se escriben los programas
Mayoría de programas son estructurados; referencian distintas áreas de datos modificables o no modificables
Segmentación del espacio de direcciones
Codificarlos independientemente
Referencias entre segmentos se pasan a tiempo de ejecución
Darles diferentes grados de protección
Crear mecanismos mediante los cuales se puedan compartir los segmentos entre varios procesos
Organización física
Históricamente, dos niveles de almacenamiento
Compromiso entre velocidad/coste
Acceso M.P. varios nanosegundos
Acceso discos varios milisegundos
Unicamente la información en M.P. es accesible directamente por la CPU
Transferencias de Información entre Memoria Principal y Memoria Secundaria
Responsabilidad del Programador : Overlays
Compiladores pueden generar automáticamente el código para los overlays; en muchos casos les falta información
Si hay reubicación dinámica es imposible hacerlo
La responsabilidad de mover información entre los dos niveles de memoria debe ser del S.O.
Memoria Virtual
Es un mecanismo de transformación de direcciones; convierte las direcciones usadas por el programador en las correspondientes posiciones de memoria física realmente asignadas al programa durante su ejecución.
Las direcciones virtuales son referenciadas por el programa al ejecutarse o utilizadas por el programador. Las direcciones reales son posiciones de memoria disponibles en la memoria principal.
El espacio de direcciones virtuales (o Espacio de Direcciones), N, viene dado por el conjunto de direcciones virtuales que pueden ser referenciadas por un proceso.
El espacio de direcciones reales (o Espacio de Memoria), M, se da por el rango de posiciones de memoria en el sistema computador.
El mecanismo de transformación se puede poner como:
f : N M
El espacio de memoria es generalmente lineal, y su tamaño viene dado por la cantidad de Memoria Principal incluida en la configuración.
El espacio de direcciones no necesita ser lineal, generalmente es de dos dimensiones; y su tamaño puede ser mayor, igual o menor que el del Espacio de Memoria.
Suele permitir al programador utilizar un rango de direcciones diferentes al real; el programador ve una memoria virtual con características diferentes a las de la memoria real.
Implementación de memoria virtual
Registros base y límite
La reubicación y la protección se pueden conseguir con un mecanismo simple de registros base y límite
Cuando un proceso se carga en memoria la dirección de la posición más baja que usa se pone en un registro base
Todas las direcciones del programa se interpretan como relativas a este registro base
f(a) = B + a
a Dirección del programa
B Dirección base del proceso
Para la reubicación hay que mover el proceso y dar el nuevo valor al registro base
Para la protección se añade un segundo registro que contendrá la dirección de la posición más allá que puede utilizar el proceso, el registro límite
La transformación de direcciones se realiza según el siguiente esquema
if a<0 then Violacion Memoria
a':= B + a
if a'>limite then Violacion Memoria
a' es la posicion solicitada
El problema es el tiempo consumido en las operaciones, los registros deben implementarse en hardware rápido
Una alternativa es usar longitud en lugar de la dirección limite'
if a<0 OR a>longitud
then Violacion Memoria
a' := B + a
a' es la posicion solicitada
Paginación
Con el Método Registros Base-Límite, el Espacio de Direcciones es necesariamente menor o igual que el Espacio de Memoria
Es necesario que la Memoria Virtual sea mayor que la memoria física disponible
Almacenamiento de nivel uno .- La memoria secundaria aparece como una extensión de la memoria principal (1.960, Universidad Manchester - Atlas)
Este almacenamiento puede realizarse mediante una técnica denominada paginación
Paginación
El Espacio de Direcciones Virtuales se divide en páginas de igual tamaño (p.e.: 512 bytes)
La Memoria Principal también se divide en marcos de páginas del mismo tamaño que las páginas
Los marcos de páginas se dividen entre los procesos actualmente presentes en el sistema; en un instante determinado, un proceso tendrá unas pocas páginas residentes en Memoria Principal (página activa) y el resto residentes en la Memoria Secundaria (página inactiva)
El mecanismo de paginación tiene dos funciones
Realizar la operación de transformación de direcciones
Transferir las páginas entre Memoria Secundaria y Memoria Principal
Los bits de mayor orden de las direcciones se interpretan como un número de página
Los de menor orden como un desplazamiento, en bytes, dentro de la página
Páginas de 2n bytes: los n bits de menor peso de las direcciones representan el desplazamiento y el resto el número de página
Estos cálculos son función exclusiva del hardware y son transparentes al programador, éste solo sabe que puede programar en un gran espacio de direcciones secuenciales
La transformación de direcciones se realiza por medio de una Tabla de Páginas donde
La entrada p-ésima contiene la posición p' del marco de página donde reside la página p
El desplazamiento, b, se le suma a p' para obtener la posición requerida
La transformación se puede poner como:
f(a) = f(p,b) = p' + b
donde
a Dirección de programa
p Número de página
b Desplazamiento
y si z es el tamaño de página, entonces:
p = parte entera (a/z)
b = resto (a/z)
Las referencias a una página que no está presente en Memoria Principal se conocen como fallo de página; esto provoca que el mecanismo de paginación inicie la transferencia de la página solicitada desde la Memoria Secundaria a la Memoria Principal y que se actualice adecuadamente la Tabla de Páginas
En el bit de presencia, para cada entrada, se indica si la página está o no presente en Memoria Principal
Si el bit de presencia no está activo, es necesario conocer la posición de la página en Memoria Secundaria (en la misma Tabla o en otra diferente)
El problema es cuando no existe ningún marco de página vacío cuando ocurre un fallo de página, por lo que alguna página debe moverse a Memoria Secundaria para poder cargar la nueva página
El Algoritmo de Cambio de Páginas se basa en diversas informaciones sobre las páginas
Es necesario que cada entrada de la Tabla de Páginas contenga unos pocos bits cuya información podría ser
Cuántas veces ha sido referenciada la página
El tiempo de la última referencia a la página
Bit de modificación indicando se la página ha sido modificada (escrita)
Estas informaciones las mantiene el hardware. Así como realiza las operaciones de transformación de direcciones; excepto cuando ocurre un fallo de página que será el S.O. el encargado de buscar la página solicitada en la Memoria Secundaria y, si es necesario, elegir la página que hay que quitar de Memoria Principal
El tiempo necesario para cada referencia a memoria es doble debido a la necesidad de acceder primero a la Tabla de Páginas
Tabla de Páginas en un conjunto de Registros de alta velocidad de acceso
Memoria Asociativa, búsqueda simultánea en paralelo
Gran cantidad de Memoria Principal que puede ocupar la propia Tabla de Páginas
Puede reducirse al número de páginas realmente usadas y acceder mediante hashing en lugar de mediante indexación
Incremento en el tiempo de acceso a memoria de algunas referencias
Tiempo de cálculo de la función de hash
Si existen colisiones en alguna entrada, se deben realizar más de una entrada mediante la función de hash
Segmentación
Refleja la división lógica entre datos y código
Divide el espacio de direcciones en segmentos, cada segmento se corresponde con un procedimiento, módulo o conjunto de datos
La forma más simple es asignar a cada segmento un par de registros base-límite; esto implicaría pocos segmentos por razones económicas y no realiza ningún tipo de protección
Usar un gran número de segmentos y referenciarlos por los nombres dados por el programador; el espacio de direcciones tendrá dos dimensiones: nombre segmento y dirección
La dirección virtual se da como (s, a) donde
s número del segmento
a dirección dentro del segmento
En la Tabla de Segmentos para cada proceso, la entrada s-ésima contiene posición base y longitud (descriptor segmento)
La operación de transformación será
Extraer la dirección de programa (s, a)
Usar s como índice de la tabla de segmento
Si a<0 or a>1 entonces Violación de memoria
(b+a) es la posición buscada
Protección .- Conjunto de Bits de Protección que especifican los modos de acceso a cada segmento; estos bits pueden ser diferentes en cada descriptor
Compartir segmentos .- Solo se necesita tener un descriptor del segmento en la tabla de segmentos de cada proceso que lo comparte
Diferencias con la paginación
El propósito de la Segmentación es la división lógica del espacio de direcciones; el propósito de la Paginación es la división física de la memoria para implementar el almacenamiento de nivel - uno
Las páginas son de un tamaño fijo determinado por la arquitectura de la máquina; los segmentos pueden ser de cualquier tamaño determinado por el usuario
La transformación de las direcciones la realiza el hardware en la paginación; la división entre segmentos y desplazamiento es puramente lógica
Un desbordamiento de un byte en una página incrementa automáticamente el número de página; en el caso de desbordamiento en un segmento se produce una Violación de Memoria
La principal complicación es que para programas grandes no es posible mantener todos los segmentos en la Memoria Principal, resulta más complicado si se comparte la memoria (multiprocesamiento)
La solución reside en emplear paginación o sustituir segmentos completos en memoria
Segmentación paginada .- Cada segmento consta de varias páginas y posee su propia Tabla de Páginas
La entrada de la Tabla de Segmentos apuntará a la tabla de Páginas de cada segmento
La operación de transformación de direcciones consistirá en
extraer la dirección de programa (s,a)
usar s como indice de la tabla de segmento
si la entrada s esta vacia entonces crear una nueva Tabla de Pagina (vacia); en otro caso, extraer la direccion de la Tabla de Paginas
separar la direccion de desplazamiento a, en un numero de pagina, p, y un desplazamiento, d, dentro de la pagina
usar p como un indice de la Tabla de Paginas
si a<0 or a>1 entonces Violacion Memoria
(b+a) es la posicion buscada
Si no se emplea Paginación, un fallo de segmento provocará que el segmento solicitado se traiga completo a la Memoria Principal
Ventajas del uso de páginas
No se necesita tener completamente en Memoria Principal los segmentos
Las páginas de un mismo segmento pueden ocupar posiciones que no sean contiguas
Ejemplo .- Intel 80386, segmentos paginados. Unidad paginación y Unidad segmentación separadas
Estrategias de asignación de memoria
Básicamente las estrategias de gestión de memoria se dividen en tres grandes grupos:
Estrategias de sustitución .- qué información será retirada de la memoria principal
Estrategias de búsqueda .- cuando la información será cargada en memoria
Estrategias de colocación .- donde se colocará la información en memoria principal
Las estrategias de sustitución y de colocación son diferentes para sistemas paginados y no paginados. Tamaño fijo de las páginas frente al tamaño variable de los bloques de información en sistemas no paginados.
Estrategias de colocación en sistemas no paginados
Cada cierto tiempo los segmentos (bloques de información) son insertados o sacados de la memoria
Cuando el sistema está en equilibrio, la memoria tendrá trozos ocupados con información y otros vacíos
Cualquier estrategia de colocación debe mantener una lista de las posiciones y tamaños de todos los huecos sin asignar, lista de huecos
La tarea será decidir qué hueco se usa y actualizar la lista de huecos después de cada inserción
Si el segmento que hay que poner en Memoria Principal es menor que el hueco encontrado, se pone en las posiciones más bajas del hueco creando un único hueco de menor tamaño
Minimizar la fragmentación
Si es mayor que cualquier hueco disponible, mover los segmentos en memoria para crear un hueco de suficiente tamaño
Algoritmos .- El tamaño de los huecos lo designaremos por x1, ..., xn
Mejor encaja .- La lista de huecos se ordena según tamaños crecientes x1 " x2 " ... " xn. Si s es el tamaño del segmento, buscar el hueco i más pequeño que cumpla s " x i
Peor encaja .- La lista de huecos se ordena según tamaños decrecientes, x1 " x2 " ... " xn. Colocar el segmento en el primer hueco de la lista, es decir, en el más grande
Primero encontrado .- La lista se ordena según las direcciones base de los segmentos; se busca el menor hueco que cumpla s " x i , es decir, el primero que se encuentre donde quepa el segmento
Primero encontrado con rotación .- Es una variación del anterior donde la posición de la cabecera de la lista va avanzando cíclicamente un elemento después de cada búsqueda; evita la existencia de un gran número de huecos pequeños al principio de la memoria, lo cual consume mucho tiempo de búsqueda
Buddy (compañero) .-
Los tamaños de los segmentos deben ser potencias de 2, s = 2i, para algún i menor que un límite k
Se mantienen listas separados de huecos para cada tamaño 21, 22, ..., 2i, ..., 2k
Un hueco se quita de la lista (i+1) dividiéndolo en dos huecos de tamaño mitad que se ponen en la lista (i)
Dos huecos contiguos en la misma lista (i) crearán un único hueco de la lista (i+1)
Se implementa mediante un procedimiento recursivo
procedure coger_hueco(i)
begin
if i=k+1 then fallo;
if lista-i vacia then
begin
coger_hueco(i+1);
dividir hueco en dos
`compañeros';
ponerlos en la lista-i
end;
coger el primer hueco en la lista-i
end
Fragmentación
Un problema común a todas las estrategias de colocación es la fragmentación
La memoria se va dividiendo en huecos cada vez más pequeños que no se pueden utilizar en la práctica
Necesidad de compactación de la memoria, mover todos los segmentos hacia una parte de la memoria y crear un único hueco del tamaño máximo posible
Estrategias de colocación en sistemas paginados
Poner p páginas en Memoria Principal consiste únicamente en buscar p marcos de páginas libres; posiblemente usando una estrategia de sustitución
Existe fragmentación interna, debido a que el tamaño solicitado por un proceso no suele ser un múltiplo exacto del tamaño de página
Una parte del último marco de página asignado estará generalmente desperdiciado. Como media se desperdiciará la mitad del tamaño de página por segmento presenta en memoria
Estrategias de sustitución en sistemas paginados
Decidir que bloques de información serán relegados a la Memoria Secundaria cuando no se encuentre espacio suficiente para un nuevo bloque
Idealmente, se desearía sustituir las páginas que tardarán más en ser referenciadas
Algoritmo irrealizable
Inferir del comportamiento pasado, los posibles comportamientos futuros
Las páginas que no han sido modificadas no es necesario transferirlas de nuevo a Memoria Secundaria
Los algoritmos más comunes de sustitución son:
Menos recientemente usada (LRU; Less Recently Used) .- Sustituir la página que se ha usado menos recientemente; necesita almacenar la secuencia de accesos a todas las páginas
Menos frecuentemente usada (LFU; Less Frecuently Used) .- Sustituir la página que se ha usado menos frecuentemente durante un intervalo de tiempo; problema con las últimas páginas cargadas
FIFO (First In First Out) .- Sustituir la página más antigua en memoria; almacenar los tiempos de carga de cada página.
Problema con página antiguas que se usan mucho;
p.e.: S.O.
Asociar a cada página dos bits de información
Bit R de referencia, se activará cuando la página se lee o escribe; cuando se referencia
Bit M de modificación, se activará cuando se escribe en una página
No usada recientemente (NRU; No Recently Used) .- Aproximación simple al LRU, retira una página de la clase de numeración inferior, eligiéndola al azar dentro de dicha clase
- Clase 0 R=0 M=0 - Clase 2 R=1 M=0
- Clase 1 R=0 M=1 - Clase 3 R=1 M=1
Cuando se carga una nueva página R y M serán cero. En cada interrupción de reloj todos los bits R se hacen cero
FIFO Modificado o FIFO R-M .- Inspecciona los bits R y M y aplica FIFO a la clase de numeración inferior
Segunda Oportunidad .- Otra variación del algoritmo FIFO. Examina la página más antigua
Si R=0 la sustituye
Si R=1, hace R=0, la coloca como la página más nuev ay examina la siguiente
Si todas tienen R=0 quita la más antigua
Páginas al azar
Estrategias de sustitución en sistemas no paginados
Objetivo .- Sustituir el segmento o bloque de información que será menos referenciado en el futuro inmediato
Son aplicables las mismas estrategias que en sistemas paginados
Diferencia porque no todos los segmentos ocupan la misma cantidad de memoria
El tamaño del segmento influirá en la decisión del segmento que se sustituirá
Algoritmo más simple .- Sustituir el segmento cuyo tamaño unido a los posibles huecos contiguos a él, dejen libre el suficiente espacio de memoria para el nuevo segmento
Si existen varios segmentos que cumplen la condición anterior; se puede usar uno de los algoritmos de sistemas paginados, p.e.- LRU
Si no existe ninguno, elegir el menor conjunto de segmentos contiguos que dejen libre suficiente espacio de memoria
La elección por tamaño puede quitar segmentos que se referenciarán inmediatamente
LRU y algo de compactación cuando sea necesario
Tamaño del segmento, referencias futuras y compactación producirán algoritmos complejos
Estrategias de búsqueda
Objetivo .- Determinar cuando mover un bloque de información desde la memoria secundaria a la memoria principal
Por demanda
En anticipación
Las estrategias de demanda son más fáciles de implementar
Cuando se produce un fallo de página/segmento se genera una petición de búsqueda del nuevo bloque
Las estrategias de anticipación se basan en las predicciones del comportamiento futuro de los programas
La naturaleza de la construcción de programas
Inferir del comportamiento pasado de un proceso
Operación en un contexto .- En cualquier intervalo de tiempo un programa tiende a operar dentro de un módulo lógico particular
Principio de localidad (Denning, 1970)
Las referencias de un programa tienden a estar agrupadas dentro de un reducido número de posiciones de memoria y estas posiciones solo tienden a cambiar intermitentemente
Propiedad empírica (observada) más que teórica
Nunca está garantizada pero es probable
Durante un intervalo de tiempo, un proceso tiende a concentrar sus referencias en un subconjunto determinado de páginas
Localidad temporal .- Las posiciones referenciadas recientemente tienen una alta prioridad de ser referenciadas próximamente
Localidad espacial .- Las posiciones de memoria cercanas a las posiciones referenciadas recientemente es muy probable que también sean referenciadas
Modelo del conjunto de trabajo
El modelo del conjunto de trabajo del comportamiento de los programas (Denning, 1968) es un intento de explicar el rendimiento de los sistemas paginados en un entorno de multiprogramación
Los procesos para ejecutarse correctamente deben tener su conjunto de trabajo en memoria principal. Si el grado de multiprogramación rebasa un cierto nivel se produce trasiego continuo entre memoria secundaria y memoria principal.
Un proceso necesita un cierto número mínimo de páginas, llamado conjunto de trabajo, residentes en memoria principal, antes de poder usar efectivamente el procesador.
Para evitar el trasiego el grado de multiprogramación no debe ser mayor que el nivel para el cual todos los procesos pueden mantener su conjunto de trabajo en memoria principal.
Basado en el Principio de Localidad; en un instante de tiempo dado, t,
w(t,h) = { página i | página i E N y página i aparece en las últimas h referencias }
Las páginas del conjunto de trabajo cambian lentamente.
Estrategia de sustitución
Ejecutar un proceso si y solo si su conjunto de trabajo está completo en memoria principal
Y nunca sustituir una página que forme parte del conjunto de trabajo de un proceso
Implica una relación entre la Gestión de Memoria y la Gestión del Procesador.
Conclusión .- El verdadero conjunto de trabajo de un proceso es el conjunto de páginas que deben estar en memoria principal para que el proceso se ejecute eficazmente.
Los conjuntos de trabajo son transitorios y el siguiente conjunto de trabajo del proceso puede diferir sustancialmente del anterior.
Ejemplo de implentación
La mayoría de las estrategias están basadas en el sentido común y en la experiencia
Elección basada en una simulación con una mezcla de los trabajos típicos del sistema
Podemos añadir al entorno volátil del descriptor de cada proceso
Una copia del contenido de los registros base y límite
O un puntero a su tabla de segmentos
O un puntero a su tabla de páginas
Si el sistema es no paginado, introduciendo en la tabla central la lista de huecos libres
Entrada y Salida
Es una de las áreas más sórdidas del diseño del S.O., pues existen gran variedad de dispositivos periféricos que pueden diferir en:
Velocidad .- Diferencias de varios órdenes de magnitud en la velocidad de transferencia de datos (1M cps - 1 cps)
Unidad de transferencia .- Caracteres, words, bytes, bloques o registros
Representación de datos .- Codificación de diferentes formas, incluso en el mismo dispositivo
Operaciones permitidas .- Clase de operaciones que pueden realizar; p.e.: entrada o salida
Condiciones de error .- Los fallos pueden tener causas diversas
Objetivos de diseño
Independencia de código de caracteres
No es deseable que el programador necesite conocer los detalles de los códigos de caracteres utilizados
El sistema E/S debe reconocer diferentes códigos y presentar un único formato estándar al usuario
Independencia del dispositivo
Un programa debería ser independiente de los dispositivos particulares que le pueden ser asignados y del tipo de dispositivo usado para sus E/S
Eficiencia
La E/S suele ser un típico cuello de botella del S.O.
Debe ser lo más eficiente posible
Tratamiento uniforme de los dispositivos
Manejar todos los dispositivos de manera uniforme
Difícil en la práctica
La independencia del código de caracteres implica la existencia de una representación interna de todos los caracteres uniforme
Código de caracteres interno
Traducción .- inmediatamente después de la entrada; e, inmediatemante antes de la salida
La independencia de los dispositivos implica que un programa debe trabajar sobre dispositivos virtuales
Streams (Atlas), files (Unix), o data sets (IBM)ç
La asociación de los streams con los dispositivos reales la hace el S.O. basada en cierta información suministrada por el usuario
Especificar solo el tipo de dispositivo
Una lista de descriptores de stream, apuntada desde el descriptor de proceso
La asignación de un dispositivo a un proceso se hace cuando el proceso utiliza la primera vez el correspondiente stream
El proceso abre el stream
El stream se cierra, explícitamente desde el proceso o cuando el proceso finaliza
El sistema de E/S debería ser construido de forma que las características del dispositivo estén claramente asociadas con los dispositivos y no con las rutinas que los manejan (manejadores de dispostivos).
Las características distintivas de cada dispositivo se deben codificar en un descriptor de dispositivo. Este descriptor se utiliza como fuente de información para los manejadores:
Identificación
Instrucciones para operar con el dispositivo
Punteros a las tablas de traducción de caracteres
Status actual: ocupado, libre o caído
Proceso de usuario actual: un puntero al descriptor de proceso que está utilizando actualmente
Todos los descriptores pueden reunirse juntos en una estructura de dispositivos cuya cabecera está en la Tabla Central.
Procedimientos de E/S
DOIO (stream, modo, cantidad, destino, semáforo)
DOIO .- Nombre de un procedimiento E/S del sistema
stream .- número del stream en el que tiene lugar la E/S
modo .- que operación se solicita (opcional, el código de caracteres)
cantidad.- cantidad de datos a transferir
semáforo .- dirección de un semáforo de petición servida el cual será señalizado cuando termina la operación de E/S
DOIO es reentrante y su función consiste en:
Asociar el número de stream con el dispositivo físico apropiado de la lista de descriptores de streams del proceso
Chequear los parámetros consultando la información en el descriptor del stream
Iniciar el servicio de la petición, ensambla los parámetros de la petición en un Bloque de Petición de E/S (IORB) que añade a una cola de bloques similares de otras peticiones
Cola de Peticiones del Dispositivo unida al descriptor del dispositivo y servida por un proceso separado denominado Manejador de Dispositivo
El procedimiento de E/S señala un semáforo de petición pendiente (descriptor del dispositivo) como indicación al Manejador. Este avisará al proceso de usuario que finalizó la operación E/S señalando un semáforo de petición servida (parámetro de DOIO).
La rutina de E/S completa es:
procedure DOIO(stream,modo,cantidad,destino,semaforo)
begin
buscar dispositivo en descriptor del proceso;
chequear parámetros;
if error then salida_con_error;
montar IORB;
insertar IORB en cola peticiones dispositivo;
signal(pet_pendiente)
end;
Manejadores de dispositivo
Es un proceso responsable de tratar las peticiones de un dispositivo. Hay un manejador separado para cada dispositivo
Usar programas compartidos
Diferencias, en las características almacenadas en los descriptores
Su operación es un ciclo continuo durante el cual
Retira un IORB de la cola
Inicia la correspondiente operación E/S
Espera hasta que termina la operación
Lo notifica al proceso origen
El ciclo e una operación de entrada es:
repeat indefinidamente
begin
wait(pet_pendiente);
coger IORB de cola de peticiones;
extraer detalles de la petición;
iniciar operaciones E/S;
wait(op_terminada);
if error then rellenar informacion error;
traducir carácter(es) si es necesario;
transferir datos al destino;
signal(pet_servida);
borrar IORB;
end;
Notas
signal(pet_pendiente) desde el procedimiento E/S cada vez que pone un IORB en la cola
La cola puede tener un algoritmo de selección basada, por ejemplo, en prioridades de E/S
La instrucción de inicio operación E/S puede extraerse de las características en el descriptor
signal(op_terminada) desde la rutina de interrupción
localizar descriptor dispositivo;
signal(op_terminada);
Chequeo de errores, status dispositivo. Errores, se deja la información en la dirección de error indicada en el IORB
Traducción de caracteres según modo en el IORB y las Tablas del Descriptor
Operaciones de salida se coge el dato de la fuente y se traduce antes de las operación
La dirección del semaforo pet_servida se pasa al manejador como un componente del IORB
Es suministrada por el proceso que solicita la E/S como un parámetro del procedimiento de E/S
Desventaja .- Proceso solicitante debe tomar la responsabilidad de la sincronización de sus acciones
Alternativa .- Se da la responsabilidad de sincronización al procedimiento de E/S poniendo el semaforo “pet_servida” local al procedimiento y añadiendo las operaciones
wait(pet_servida);
test direccion error;
El usuario tiene la libertad para continuar en paralelo pero debe detectar el fin de las operaciones de E/S. La responsabilidad está en el S.O. pero se pierde libertad de operación.
Buffering
Las transferencias de entrada las realiza el S.O. en un área de memoria llamada buffer de entrada; el proceso de usuario toma sus datos del buffer y únicamente quedará suspendido si el buffer está vacío.
Las transferencias de salida las realiza el proceso directamente sobre el buffer de salida; el S.O. vaciará el buffer completamente cuando esté lleno.
No evita los casos en los cuales un proceso demanda E/S a una velocidad mayor que la que puede dar el dispositivo. Evita los picos de demanda.
El S.O. asigna espacio para los buffers cuando se abre un stream, y apunta sus direcciones en el descriptor del stream.
El procedimiento de E/S varía ligeramente:
Una petición de entrada extrae el siguiente item del buffer apropiado y lo pasa directamente al proceso
Solo si el buffer está vacío, genera un IORB y señala al manejador
Cuando se abre un stream, se generan suficiente IORB para llenar el buffer
DOBUFFIO(stream,modo,destino)
La cantidad será un solo item
Las esperas por buffer lleno/vacío quedan ocultas en el procedimiento de E/S, no hay necesidad de pasar un semáforo
Tipo, dirección de buffers y semáforos en el descriptor
Dispositivos de fichero
Dispositivos secuenciales .- El nombre es información suficiente para conocer la fuente/destino
Otros dispositivos, tienen la característica de poder seleccionar una zona particular en la que se puede efectuar la transferencia de datos; es necesario especificar qué lugar del medio y no solo su nombre. Cada zona de datos es un fichero.
Dispositivos de fichero o dispositivos con estructura de fichero
Cada fichero tiene un nombre único
Este nombre es utilizado por el S.O. para determinar la posición del fichero en el medio correspondiente
Cada stream tiene que estar asociado con el nombre de un fichero determinado.
DEFINE ENT_1 DISCO3:TESTDATA
Cada vez que se abre un fichero se crea un descriptor de fichero para que contenga la información necesaria para las futuras transferencias:
Dirección del dispositivo
Posición del fichero
El descriptor de fichero añade información extra a la asociación de los streams con los dispositivos físicos.
Spooling
Dispositivos compartidos .- Pueden manejar peticiones sucesivas de diferentes procesos.
Dispositivos no compartidos .- Solo un proceso
Asignación de dispositivos no compartidos
El objetivo es distribuir uniformemente la carga de peticiones y reducir la posibilidad de cuello de botella (SPOOLING) para dispositivos de E/S muy utilizados.
El procedimiento de E/S ejecuta las transferencias sobre un medio intermedio (disco). Para mover los datos entre el disco y el dispositivo requerido se utiliza un proceso especializado llamado spooler.
Ejemplo .- Spooler de impresora
Las salidas por impresora a un fichero; estos ficheros actúan como impresora virtual
Cola de ficheros, esperando su impresión
Función.- coger los ficheros de la cola y enviarlos a la impresora
repeat indefinidamente
begin
wait(algo_al_spool);
coger fichero de cola;
abrir fichero;
repeat until EOF;
begin
DOIO(parametros para lectura disco);
wait(pet_servida de disco);
DOIO(parametros salida impresora);
wait(pet_servida de impresora);
end;
end;
Notas
Buffering datos entre disco e impresora
Semaforo “algo_al_spool” es señalado desde un proceso que cierra un stream de impresora
La cola no tiene por qué ser FIFO, p.e.- ficheros cortos más prioritarios
Igualar la presión de la demanda de periféricos muy utilizados
Reduce la posibilidad de interbloqueo (deadlock)
Relativamente fácil generar varias copias
Necesidad de gran cantidad de espacio de disco
Fuente tráfico en el canal de disco
No es válido para Sistemas de Tiempo Real
Conclusiones
El sistema de E/S es independiente del código de caracteres y del dispositivo físico, tiene un tratamiento uniforme de estos últimos
Se pierde eficiencia; los procedimientos de E/S y los manejadores son generales, pero trabajan más lentos que los especializados
Una mejora en la práctica sería cambiarlo por programas específicos para determinados dispositivos
Sistema de ficheros
Objetivos
Almacenamiento a largo plazo
Almacenamiento en línea
Recuperar información
Almacenar grandes cantidades de datos
Siempre accesible
Sistemas de Propósito General; datos y programas en el sistema (solo terminales)
Compartir la información
Permitir a los usuarios compartir la información
Programas y datos de otros usuarios
Base de Datos General
Software de Sistema; Librerías
Si se comparte, debe estar siempre almacenada
El almacenamiento a largo plazo se realiza en la Memoria Secundaria (discos o cintas)
El sistema de ficheros suministra los medio para organizar y acceder a estos datos
Ficheros de tamaño arbitrario
Colección de datos la cual es considerada como una entidad lógica por el usuario
El fichero en la unidad lógica que almacena y maneja el sistema de ficheros
El medio de almacenamiento está generalmente dividido en bloques; a cada fichero se le asigna el número apropiado de bloques.
Un sistema de fichero debe:
Permitir la creación y borrado de ficheros
Permitir el acceso para lectura y escritura
Realizar automáticamente la gestión del espacio de memoria secundaria
Permitir la referencia a los ficheros contra fallos del sistema
Permitir compartir ficheros entre usuarios que cooperan y protegerlos contra accesos no autorizados
Directorios
El acceso a un fichero no es más que una transformación de su nombre simbólico en una posición física en Memoria Secundaria.
Un directorio de ficheros es una tabla que contiene información sobre las posiciones de los ficheros que incluye algún medio de protección.
Por seguridad se divide el directorio en dos niveles:
Directorio Ficheros Maestro (MFD)
Directorio Ficheros Usuario (UFD)
Seguridad
Chequeo de identidad al nivel de Directorio Maestro (MFD)
Mismo nombre en distintos directorios de usuarios
full name o nombre completo del fichero
Carácter separador
La información en Directorio de Usuario (UFD):
Nombre fichero
Posición física en memoria secundaria
Tipo de fichero (char, binario reubicable, binario ejecutable, librerías, etc.)
Control de acceso
Información administrativa (t. creación, t. última modificación, etc.)
Si extendemos este concepto a una estructura multinivel, cada directorio puede contener ficheros y/o directorios
Usuarios de un proyecto de un departamento
Protección .- chequeo según progresa en el árbol de directorios
Nombre completo, todo el camino (path) y el nombre individual
Longitud del nombre completo: Directorio actual
Seguridad
El problema de seguridad reside en la compartición de ficheros de otros usuarios.
Es necesario un modo de especificar qué usuarios pueden acceder a los ficheros de otro propietario además de especificar qué tipo acceso se permite.
Se asocia a cada fichero un conjunto de privilegios de acceso aplicables a varias clases de usuarios.
Clases de usuarios
El propietario (O)
Miembros de su grupo (G)
Otros o resto del mundo (W)
Privilegios de acceso
Lectura (R)
Escritura (W)
Ejecución (E)
Borrado (D)
Ejemplo: O:RWED,G:RW,W:R
La máscara de protección reside en el UFD del propietario como parte de la entrada de cada fichero. Al propietario le está permitido cambiarlas.
Los usuarios se dividen en grupos, así en su identificación debe incluir su grupo. Surge un problema cuando se desea pertenecer a varios grupos, es necesario un comando de cambio de grupo.
Como alternativa se utiliza la técnica más general, la lista de control de acceso:
Para cada fichero/directorio, información completa acerca de los usuario con tipo de acceso autorizado
Consiste en una o más entradas control acceso, cada una especifica la clase de acceso permitido para un usuario o grupo de usuarios
Sin embargo, aparece otro problema, pues consume una gran cantidad de espacio y hace más lento el acceso a los ficheros. Como ventaja tiene su gran flexibilidad.
Otra técnica es que el propietario permita crear links o enlaces a otros usuarios desde sus UFD a entradas suyas. En UNIX los link van directamente al fichero y no a la entrada del UFD; lo hace indistinguible del original.
Organización de la Memoria Secundaria
Un fichero tiene asignado un conjunto de bloques. Es necesaria alguna técnica de almacenamiento dinámico.
Tipos de Organización de los Bloques de un Fichero:
Ficheros Contiguos
Enlace de Bloques
Mapa de Ficheros
Bloques Indice
Ficheros contiguos
Todos los bloques del fichero están adyacentes
La entrada del UFD apunta al primer bloque y contiene su longitud
Fácil de implementar
Ningún gasto de memoria suplementario
Mucha fragmentación; compactación periódica
Dificultad para borrar o insertar bloques en mitad del fichero
Problema .- Asignar espacio cuando no se conoce el final del fichero
Ventaja .- Acceso aleatorio mediante una simple suma aritmética
Enlace de bloques
Unos pocos bytes de cada bloque se usan como puntero al siguiente bloque del fichero; puntero nulo, (0)
La entrada en el UFD apunta al primer bloque de la cadena de bloques del fichero
Gasto de memoria suplementaria: los bytes de cada puntero
Gran número de accesos al disco para llegar al EOF
Acceso necesariamente secuencial; seguir la cadena
Mapa de ficheros
El estado del disco se registra en un mapa de ficheros o tabla de asignación de ficheros
Cada bloque se representa por una entrada de la tabla o mapa
La entrada en el UFD apunta a la entrada en la tabla para el primer bloque del fichero
Cada entrada del mapa apunta a la posición en el mapa del siguiente bloque del fichero
Gasto de memoria .- Tamaño del mapa de ficheros, depende del tamaño de cada entrada del mapa
Información adicional con un número de identificación único para cada fichero
Se puede incluir un puntero al último bloque de cada fichero
Suele ser necesario almacenar el mapa de ficheros en el propio disco
Para leer N bloques pueden ser necesarios N accesos extras al disco para leer el mapa (que tendrá varios bloques)
Conviene tener los bloques de un mismo fichero cercanos para que estén en el mismo bloque de mapa
Bloques índice
Los punteros de enlace para cada fichero son almacenados en un bloque índice en disco
Si el fichero es grande necesitará varios bloques índice
La entrada en el UFD apuntará al primer bloque índice del fichero
Gasto de memoria .- Un puntero por cada bloque, más la mitad del último bloque índice que no estará lleno
Para ficheros pequeños se podría almacenar un índice primario en la entrada del directorio (UNIX)
Ventaja .- El acceso no tiene que ser secuencial . Nombre del fichero y desplazamiento en el bloque índice
Tiempo de acceso .- Solo consultar el bloque índice (o varios si es muy grande)
Problema .- Insertar o borrar bloques en mitad del fichero
Fin de fichero (EOF)
Con una marca especial, que no puede interpretarse como un dato
Problema en ficheros binarios
Conocer siempre la longitud del fichero
Gestión del espacio libre
Todos los bloques libres como si fueran un único fichero
Ficheros contiguos .- Cada serie de bloques contiguos formarán un fichero libre
Bloque índice .- Lista de bloques libre, pero todas las operaciones se deben realizar por el final de la lista
Enlace bloques y mapa ficheros .- Cadena de Bloques Libres. Las operaciones se pueden realizar en la cabecera o en el final de la cadena
Para borrar N bloques
Bloque índice Borrar N punteros
Resto Sólo dos punteros
Alternativa .- Mapa de Bits
Una zona de memoria donde cada bit representa el estado (ocupado o libre) de cada bloque de Memoria Secundaria
Puede ser grande y será necesario almacenarlo en Memoria Secundaria
Peligroso en caídas del sistema o del disco
Elección del tamaño del bloque
El desperdicio de espacio debido a que algunos bloques no están completamente llenos Bloque pequeño
El desperdicio de espacio debido a los punteros de enlace Bloque grande
La unidad de transferencia de datos entre Memoria Principal y el dispositivo de almacenamiento El bloque debería ser múltiplo de esta unidad
La cantidad de memoria necesaria para cada lectura/escritura en un fichero Bloque índice o el mapa deben estar presentes en memoria principal
Técnicas directas y simples o muy sofisticadas; minimizar el tiempo de búsqueda
Integridad
Es esencial tener un mecanismo adecuado de copias de seguridad (back up) y recuperación de ficheros.
Salvado periódico o masivo
El sistema de ficheros completo se vuelca en un medio auxiliar, generalmente cintas, cada cierto tiempo
Desventajas
El sistema debe ponerse fuera de uso durante el tiempo del volcado de memoria
Lleva mucho tiempo
Salvado incremental
Solo se salva la información que ha cambiado desde la última vez que se realizó
Solo nuevos ficheros creados o modificados y entradas de directorios modificadas
Menor cantidad de información .- Se puede realizar con mayor frecuencia
Se realiza junto con un salvado periódico
Flags que indiquen si el directorio/fichero deben copiarse
Salta todos los ficheros cuyo flag no está activo
Tampoco salva los que están siendo actualizados en el instante del salvado
Desventaja .- Gran cantidad de datos que genera y la dificultad de reconstrucción
Reconstruir a la inversa la secuencia de hechos salvada
Automatizar estos mecanismos
Abrir y cerrar ficheros
El S.O. debe buscar un dispositivo y la posición donde está el fichero almacenado cuando un stream de E/S es asociado a un fichero.
El procedimiento inverso es cerrar un fichero
Explícitamente desde el proceso
Implícitamente cuando termina el proceso
Los procedimientos son:
open(nombre_fich,modo)
close(nombre_fich)
donde:
nombre_fich Nombre simbólico del fichero
modo Tipo de acceso solicitado (Lectura, Escritura o Creación)
Operaciones para abrir un fichero
Buscar la entrada en el directorio del fichero
Verificar que la petición la realiza un proceso perteneciente a un usuario con los privilegios de acceso adecuados
Asegurarse que si el fichero está ya abierto para lectura no se abre para escritura y si lo está en escritura no se puede abrir
Determinar el dispositivo y la localización del fichero; si es nuevo, asignarle memoria
Crear un descriptor de fichero con toda la información sobre el fichero necesaria para las transferencias de datos
La información incluida en el descriptor de fichero será:
Nombre del fichero
Dirección del descriptor de dispositivo en el cual está almacenado el fichero
Posición del primer bloque del fichero
Posición del siguiente bloque que será leído o escrito
Modo de acceso
El descriptor es apuntado desde del descriptor de stream apropiado.
10. Clases de Almacenamiento
Clase de Almacenamiento
El nombre de una variable identifica alguna ubicación física en memoria (o en registro).
La cadena de bits representa el valor de la variable almacenada.
Una clase de almacenamiento determina:
-
Si la variable se almacena en memoria o en registro
-
Cuándo la variable está activa (su ámbito)
Variables automáticas
La mayoría de las variables.
{
auto int a;
auto int c;
char d='a';
}
Características
-
La palabra clave auto (no se utiliza normalmente)
-
Declaradas dentro de un bloque de código
-
El ámbito es limitado al bloque donde se definen
-
Se crean cuando se empieza a ejecutar el bloque
-
Se destruyen cuando se termina
-
No se ven desde otras funciones
Prácticas
Analizar el siguiente programa
main()
{
int x=1;
{
int y=2;
printf(“%d %d\n”,x,y);
}
{
printf(“%d %d\n”,x,y);
}
}
Analizar el siguiente programa
main()
{
int x=2;
{
int x=3;
printf(“%d\n”,x);
}
{
printf(“%d\n”,x);
}
printf(“%d\n”,x);
}
Modifique el programa anterior mediante funciones
¿Cuál será la salida del siguiente programa?
main()
{
int x=1;
{
func_interna();
}
}
func_interna()
{
printf(“%d\n”,x);
}
Variables externas
Definición
Se definen externamente a cualquier función
Características
-
Son variables globales
-
Existen durante todo el tiempo de ejecución del programa
-
Ocupan la misma zona de memoria todo el tiempo
-
Puede usarse el especificador extern para indicar que la variable se encuentra en otro fichero
Prácticas
-
Analizar el ámbito de las variables en los siguientes programas. ¿Cuál es su salida?
double x=2.3, y=4.6, z;
main()
{
double f();
z=f(); printf(“%.1f %.1f %.1f\n”, x, y, z);
}
double f()
{
double y,z;
x=y=z=3.0; return(x+y+z);
}
Fichero 1
double v;
main()
{
int i=2;
v=3.0;
printf(“%d %.2f %.2f\n”, i, v, f(v));
}
Fichero 2
double g(double z)
{
return (4.0*z*v);
}
double f(dobule x)
{
return (g(1.0)*x*v);
}
Variables estáticas
Definición
{
static int a=1;
static float d=1.2;
}
Características
-
Igual que las variables automáticas en cuanto a su ámbito
-
Igual que las variables externas en cuanto a su duración y asignación de memoria
-
Permite recordar el valor de una variable local de una función de una llamada a otra
-
Su utilización proporciona un mecanismo de privacidad de la variable y una existencia permanente
Prácticas
-
Probar el siguiente generador de números aleatorios
#define LIMITE 333
#define FACTOR 25173
#define MODULO 65536
#define INC 13849
#define SEMILLA 17
main()
{
int i;
for (i=0; i<LIMITE; ++i)
if (i%6 == 0)
printf(“\n%12d”,random());
else
printf(“%12d”,random()); printf(“\n\n”);
}
random()
{
static int semilla=SEMILLA; semilla=(FACTOR*semilla+INC)%MODULO; return(semilla);
}
Variables externas estáticas
Definición
static int p
f1()
{
}
f2()
{
}
Características
-
Existencia permanente
-
Ámbito global, pero sólo en el fichero donde se define
-
Permite que una variable sea conocida sólo a un conjunto de funciones
Variables registro
Definición
{
register int i;
...
}
Características
-
Las variables registro serán almacenadas en registros del microprocesador, siempre que sea posible
-
Candidatos son los contadores de bucles
Prácticas
Construya un programa que cuente el número de espacios, dígitos, letras, líneas, “otros” y total de caracteres de la entrada de texto que se le suministre
Construya un programa que calcule la suma de los N primeros números pares, y la suma de los N primeros números impares.
Construya un programa que genere los N primeros números primos.
5. Estructuras de Control
Sentencias if
Sentencia if
if (expresión)
sentencia
Donde sentencia puede ser una sentencia simple:
sentencia_simple;
o bien una sentencia compuesta:
{
lista_sentencias;
}
if - then - else
if (expresión)
sentencia1
else
sentencia2
if - then - else - if - then - else - ... else
if (expr1)
sent1
else if (expr2)
sent2
......
else if (expr3)
sent3
else
sent_defecto
Sentencias bucle
Bucle while
while (expresión)
sentencia
Bucle do - while
do
sentencia
while (expresión);
Bucle for
for (expr1; expr2; expr3)
sentencia
Sentencia switch
Sintaxis
sent_switch ::= switch (exp) s_list
s_list ::= {et_case:}0+ sent | { {s_list}0+ }
et_case ::= default | case exp_const
Ejemplo
switch(c)
{
case `a':
++a_cnt;
break;
case `b':
++b_cnt;
break;
case `c':
case `C':
++cC_cnt;
break;
default:
++resto_cnt;
}
Argumentos de main
Interfaz
main(int argc, char *argv[])
-
La orden echo de Unix
#include <stdio.h>
main(int argc, char *argv[])
{
int i;
printf(“\nargc = %d%9s”, argc, “”);
for (i=0; i<argc; ++i)
printf(“argv[%d] = %s\n%17s”, i, argv[i], “”)
printf(“\n”);
}
11. Estructura
Introducción
struct es el nombre que reciben en C los tipos de datos agregados por otros tipos heterogéneos (como RECORD de PASCAL, etc.).
Una estructura es un conjunto de una o más variables de cualquier tipo que se agrupan bajo un mismo nombre para manejarlas convenientemente.
Se declaran:
struct nombre_etiqueta
{
tipo1 miembro1;
tipo2 miembro2;
......
tipon miembron;
};
Las estructuras ayudan a organizar datos comunicados. En muchas situaciones permiten que un grupo de variables relacionadas se traten como una unidad y no como entidades separadas.
Ejemplo
struct fecha
{
int dia;
int mes;
int anyo;
int dia_anyo;
char nom_mes[4];
};
-
struct comienza una declaración de estructura, que es una lista de declaraciones entre llaves, {}.
-
El nombre fecha es una etiqueta de estructura (tag). Es opcional y aparece detrás de la palabra struct
-
Sirve para utilizar esta clase de estructura en otras declaraciones sin necesidad de escribirla de nuevo.
struct fecha aniver, fin_mes, fiesta_1;
-
También se pueden hacer las dos cosas a la vez
struct fecha
{
int dia;
int mes;
int anyo;
int dia_anyo;
char nom_mes[4];
} aniver, fin_mes, fiesta_1;
Las variables que aparecen dentro de las llaves se les denomina miembros de la estructura.
La llave cerrada } donde acaba la lista puede ir seguida de una lista de variables, como cualquier declaración.
En este caso no es necesario poner la etiqueta de estructura:
struct
{
...
} x, y, z;
sintácticamente es análogo a:
int x, y, z;
en el sentido de que ambas declaran x, y, z de un tipo y ambas le asignan un almacenamiento.
Si la declaración no va seguida de ninguna variable no se le asigna ningún espacio; simplemente se describe un patrón de estructura.
Hay que distinguir claramente entre lo que representa la etiqueta (un tipo) y lo que representan las variables que aparecen detrás de la llave (variables del tipo anterior).
Es la misma diferencia que ha entre int y x.
Las estructuras estáticas y externas se pueden inicializar mediante una lista de valores que siguen a la definición.
struct fecha fiesta_2 = {23,12,1990,357,”DIC”};
Las estructuras automáticas no se pueden inicializar. Es igual que la inicialización de los arrays.
Operador miembro de Estructura (.)
Para acceder a un miembro de una estructura son necesarios el nombre de la variable (fiesta_2), el nombre del miembro (anyo) y el operador (.).
La sintaxis es:
nombre_estructura.miembro
El operador (.) conecta el nombre de una estructura y el nombre del miembro.
fiesta_2.anyo%4==0
if(strcmp(aniver.nom_mes,”AGO”)==0)
Ejemplos de uso
struct fecha f;
int es_bisiesto;
es_bisiesto = (f.anyo%4==0)&&(f.anyo%100!=0)
ññ(f.anyo%400==0);
if(strcmp(f.nom_mes,fiesta_1.nom_mes)==0);
f.nom_mes[0] = `F';
f.nom_mes[1] = `E';
f.nom_mes[2] = `B';
f.nom_mes[4] = `\0';
f.nom_mes = `FEB';
Las estructuras se pueden anidar:
struct registro
{
char nombre[MAXNOM];
char direccion[MAXDIR];
long dni;
double salario;
strcut fecha nacimiento;
strcut fecha antigüedad;
};
struct registro empleado;
Para sabe en qué mes nació:
empleado.necimiento.mes
Restricciones de las estructuras
-
Los operadores miembro (.) y dirección (&) son los únicos que pueden aplicarse a las estructuras (K&R)
-
No pueden ser pasadas o devueltas en funciones (K&R)
-
No se pueden copiar enteras (K&R)
ANSI ofrece la posibilidad de comparación, asignación y uso como argumentos y/o retorno de funciones, con paso por valor.
La solución en el C de K&R es el uso de punteros a estructuras, aunque también en el C ANSI son de uso muy frecuente.
struct etiqueta var1, *ptr_st;
ptr_st = &var1;
Si queremos acceder a un miembro de la estructura usando el puntero.
(*ptr_st).miembro1
donde los paréntesis son absolutamente imprescindibles.
Es tan frecuenta esta operación que C ofrece un símbolo especial para la misma:
ptr_st -> miembro1
Estructuras y funciones
Ejemplo .- Función que calcula el día del año
struct fecha f;
main()
{
...
f.dia_anyo = dia_del_anyo(f.anyo,f.mes,f.dia);
...
}
dia_del_anyo(anyo,mes,dia)
int anyo,mes,dia;
{
int num_dia;
...
return(num_dia)
}
Ejemplo .- Usando punteros a estructura
struct fecha antigüedad;
main()
{
...
antigüedad.dia_anyo = dia_del_anyo(&antigüedad);
...
}
dia_del_anyo(ptr_f)
struct fecha *ptr_f;
{
int dia,i,bisiesto;
dia = ptr_f -> dia;
bisiesto = ptr_f -> anyo %4&&...;
for (i=1; i < ptr_f->mes; i++)
dia += dia_tab[bisiesto][i];
return(dia)
}
La asociatividad de `->' y `.' es de izquierda a derecha
p -> q -> miembro
equivale a:
(p -> q) -> miembro
emp.antigüedad.mes
equivale a:
(emp.antigüedad).mes
Estos operadores `.' y `->' tienen la máxima precedencia junto con () y [].
struct
{
int x;
int y;
} *p;
-
++p -> x Incrementa x
-
(++p) -> x Incrementa p
-
(p++) -> x Incrementa p
-
p++ -> x Incrementa p
-
*p -> y Obtiene el contenido apuntado por y equivalente a *(p -> y)
-
*p -> y++ Incrementa y después de acceder a lo que apuntaba (igual que *p++)
-
(*p -> y)++ Incrementa lo apuntado por y
-
*p++ -> y Incrementa p después de acceder a lo que apunta y
Ejemplos
struct lapiz /*Declaración de estructura */
{
int dureza;
char fab;
int codigo;
};
struct lapiz 10,11,12; /*Declaración de variables */
Otra forma:
struct
{
int dureza;
char fab;
int codigo;
}10,11,12;
Y otra forma (más incómoda):
struct
{
int dureza;
char fab;
int codigo;
}10;
struct
{
int dureza;
char fab;
int codigo;
}11;
struct
{
int dureza;
char fab;
int codigo;
}12;
main()
{
struct lapiz 10,11,12;
l0.dureza = 2;
l0.fab = `F';
l0.codigo = 482;
l1.dureza = 0;
l1.fab = `G';
l1.codigo = 33;
l2.dureza = 3;
l2.fab = `E';
l2.codigo = 107;
printf(“La dureza del lapiz 0 es %d.\n”, l0.dureza);
printf(“El fabricante del lapiz 1 es %c.\n”, l1.fab);
printf(“El codigo del lapiz 2 es %d.\n”, l2.codigo);
}
Array de estructuras
Declaración
Struct etiqueta_est nombre[dimension]
Ejemplos
main()
{
int i;
struct lapiz l[3];
l[0].dureza = 2;
l[0].fab = `F';
l[0].codigo = 482;
l[1].dureza = 0;
l[1].fab = `G';
l[1].codigo = 33;
l[2].dureza = 3;
l[2].fab = `E';
l[2].codigo = 107;
printf(“Dureza Fabricante Codigo \n\n”);
for (i=0; i<3; i++)
printf(“ %d %c %d\n”,
l[i].dureza, l[i].fab, l[i].codigo);
}
main()
{
struct lapiz l[3], *ptr_l;
l[0].dureza = 2;
l[0].fab = `F';
l[0].codigo = 482;
l[1].dureza = 0;
l[1].fab = `G';
l[1].codigo = 33;
l[2].dureza = 3;
l[2].fab = `E';
l[2].codigo = 107;
printf(“ Dureza Fabricante Codigo\n\n”);
for (ptr_l=1; ptr_l < 1+3; ptr_l++)
printf(“ %d %c %d\n”,
ptr_l->dureza,ptr_l->fab,ptr_l->codigo);
}
Inicialización
struct lapiz
{
int dureza;
char fab;
int codigo;
} l[] = {
{2,'F',482},
{0,'G',33},
{3,'E',107}
};
Las llaves de cada elemento del array se pueden quitar si son variables simples o cadenas.
Una función puede devolver un puntero a una estructura:
struct lapiz bd_lapiz[TAM_TAB];
struct lapiz*busca_tab(codigo)
int codigo;
{
struct lapiz*l_buscado;
int i;
l_buscado = NULL;
for(i=0; i<TAM_TAB&& l_buscado==NULL; i++)
if(bd_lapiz[i].codigo==codigo)
l_buscado = &bd_lapiz[i];
return(l_buscado);
}
Estructuras autorreferenciadas
struct tnode
{
char *word;
int count;
struct tnode*next;
}
Es ilegal una referencia a la propia estructura pero no a un puntero a la propia estructura.
Uniones
Una unión es una variables que puede contener (en momentos diferentes) objetos de tipos y tamaños distintos.
Proporciona una forma de manejar diferentes clases de datos en una misma zona de memoria.
Se utiliza el mismo almacenamiento (dirección de memoria) y el mismo nombre (de etiqueta y de variable) para almacenar variables que pueden ser de diferente tipo.
Se declaran como las estructuras:
union nom_etiq
{
tipo1 miembro_1;
tipo2 miembro_2;
...
tipon miembro_n;
};
Ejemplo .- Tabla de símbolos
union u_tag
{
int ival;
float fval;
char *pval;
}uval;
uval será suficientemente grande como para contener el mayor de los tres tipos de datos.
Cualquiera de estos tipos puede ser asignado a uval y usarse en expresiones. Para usarlo consistentemente el tipo usado debe ser el último almacenado.
Es responsabilidad del programador seguir la pista del tipo que hay realmente almacenado.
Depende de la máquina el resultado de almacenar algo como de un tipo y extraerlo como de otro tipo.
Sintácticamente:
nombre_union.miembro
puntero_union -> miembro
Ejemplo
if(utype == INT)
printf(“%d\n”,uval.ival);
else if(utype == FLOAT)
printf(“%f\n”,uval.fval);
else if(utype == STRING)
printf(“%s\n”,uval.pval);
else
printf(“TIPO ERRONEO %d EN utype.\n”, utype);
struct
{
int utype
union {
int ival;
float fval;
char *pval;
} uval;
} symtab[DIM]
if(symtab[i].utype == INT)
printf(“%d\n”, symtab[i].uval.ival);
Typedef
Es un recurso de C para crear tipos de datos y darles nombres propios. Su sintaxis es:
typedef tipo sinonimo;
permite posteriormente declaraciones de la forma:
sinonimo var;
que es totalmente equivalente a la declaración:
tipo var;
Ejemplo
typedef char * STRING;
STRING s1, s2;
El nombre del tipo definido aparece en la posición del nombre de variable, no a la derecha de la palabra typedef.
typedef struct tnode {
char *word;
int count;
struct tnode *next;
}TREENODE, *TREEPTR;
TREEPTR talloc()
{
char * alloc();
return ((TREEPTR) alloc (sizeof(TREENODE)));
}
8. Funciones de Ficheros
Ficheros
Puntero a FILE; estructura FILE en stdio.h.
Tres ficheros estándares:
Puntero C | Nombre | Conectado |
stdin | entrada estándar | al teclado |
stdout | salida estándar | a la pantalla |
stderr | error estándar | a la pantalla |
Una vez que el fichero se abre:
-
El sistema proporciona un puntero a FILE
-
Que debe usarse en todas las operaciones posteriores
Resumen de funciones
Abrir un fichero
FILE * fopen (char * n_fich, char * modo_fich)
-
n_fich nombre del fichero
-
modo_fich modo en el que se quiere abrir. Básicamente:
-
r lectura
-
w escritura
-
a añadir
-
Retorna un puntero a FILE
-
Retorna un ptr nulo si no es posible abrir el fichero
Cerrar un fichero
int fclose (FILE * ptr)
Leer un carácter
int getc (FILE * ptr)
int fgetc (FILE * ptr) #macro
Devolver un carácter a un fichero
int ungetc (char ch, FILE * ptr)
Escribir un carácter en un fichero
int putc (int ch, FILE * ptr)
int fputc (int ch, FILE * ptr) #macro
Leer una cadena desde un fichero
char *fgets(char *s, int n, FILE * ptr)
Escribir una cadena en un fichero
int fputs(char *s, FILE * ptr)
Prácticas
Construya un programa que gire cada una de las líneas de un fichero y después las invierta. El fichero debe recogerse como parámetro, y si no se indica ninguno, se leerá la entrada estándar. La salida se enviará a la salida estándar. El programa debe ser todo lo robusto que se pueda.
7. Funciones de Manejo de Tiras
Leer y escribir tiras
Leer una tira
char *gets (char * s)
-
Lee una cadena desde la entrada estándar y la copia en s
-
La función copia hasta que encuentra un carácter salto de línea (¡Cuidado con desbordar el tamaño de s!)
-
Este carácter se sustituye por un carácter NULO
-
Retorna la dirección de s
scribir una tira
int puts (char * s)
-
Se copia la tira s en la salida estándar
-
El carácter nulo es sustituido por un carácter salto de línea
-
Retorna el número de caracteres escritos, (error EOF)
Concatenar tiras
char *strcat(char *tira1, char *tira2)
Ejemplos
main()
{
static char tir1[30]=”Adios mundo”;
static char tir2[10]=”cruel\n”;
printf(“%s”, strcat(tira1,tira2));
}
main()
{
static char tir[30]=”Adios mundo”;
printf(“%s\n”, strcat(tir, “cruel”));
}
Comparar tiras
int strcomp(char *tira1, char *tira2)
-
Retorna 0 si las dos tiras coinciden
-
En caso contrario retorna la diferencia entre las dos tiras de caracteres.
main()
{
printf(“%d\n”, strcomp(“hola”,”hola”));
printf(“%d\n”, strcomp(“jola”,”hola”));
printf(“%d\n”, strcomp(“hola”,”hola mundo\n”));
}
Copiar tiras
char *strcopy(char *tir1, char *tir2)
main()
{
char tir1[30];
strcopy(tir1, “Hola mundo”);
printf(“%s\n”, tir1);
printf(“%s\n”, strcopy(tir1, “3 2 1”));
}
Calcular la longitud
#include “stdio.h”
main()
{
static char tir1[]=”Pedro Picapiedra”;
static char tir2[30]=”Pablo Marmol”;
printf(“%s %d %d\n”, tir1, strlen(tir1), sizeof tir1);
printf/”%s %d %d\n”, tir2, strlen(tir2), sizeof tir2);
}
Otras funciones
Index (s,c) | Un puntero a la primera ocurrencia de c en s, o Nulo si no se encuentra |
atoi(s) | Convertir a entero |
Sscanf(s, cntl, arg1, ..) | Leer (“scanf”) desde una tira |
Sprintf(s, cntl, arg2, ...) | Escribir (“printf”) a una tira |
Prácticas
Escribir una función que devuelva 1 si la cadena que se le pasa es un palíndromo, y 0 si no lo es.
1. Introducción al lenguaje C
El Lenguaje C
El lenguaje de programación C está íntimamente ligado al sistema Unix.
El primer lenguaje fue el BCPL (Richards, 1969), posteriormente apareció el lenguaje B (Johnson y Kernighan, 1973). En 1972 Dennis Rithchie diseñó e implementó el lenguaje C. Un año después Ritchie yThompson reescribieron el núcleo del sistema Unix en lenguaje C.
Más del 90% del código de Unix está escrito en C. Actualmente C se está extendiendo a diferentes entornos. También están apareciendo espectaculares compiladores en el mundo de los ordenadores personales.
Características de C
-
Estructuración
-
Modularidad
-
Flexibilidad
-
Portabilidad
-
Alto/bajo nivel
-
El sistema Unix ofrece varias herramientas para soportar el desarrollo de aplicaciones en C
No podíamos exponer el Entorno de Desarrollo de Unix sin presentar brevemente el lenguaje de programación C.
Apariencia sintáctica de un programa
/*********************************************************/
/* Convertir líneas a mayúsculas */
/*********************************************************/
#include “stdio.h”
#define MAXLINEA 81
/* Longitud máxima de la línea (uno más
para el carácter nulo */
#define CAR_NULO `\0'
/* Byte nulo */
#char linea[MAXLINEA]
/* Array donde se guardará una línea */
/*********************************************************/
/* Función main */
/*********************************************************/
main()
{
char resp;
/* Variable para obtener la respuesta del usr */
void amayu();
/* Función que convierte a mayúsculas */
do
{
printf (“Introduzca una linea (max. %d cars) \n”,
MAXLINEA-1);
gets(linea);
amayu();
printf (“Desea continuar (s/n)? “);
while ((resp=getchar()) == `\n');
}
while (resp != `N' && resp != `n');
}
/*********************************************************/
/* Función para convertir a mayúsculas */
/*********************************************************/
void amayu()
{
for (i=0; linea[i] != CAR_NULO; i++)
{
putchar(toupper(linea[i]));
}
putchar(`\n');
}
Esquema de un programa
Formato de una función:
tipo nombre_funcion(def_argumentos)
{
definicion_de_variable_locales;
sentencias;
}
Entre las funciones aparecen instrucciones para el preprocesador, definiciones de variables, declaración de nuevos tipos de datos, etc.
Comentarios:
/* ....... */
Preprocesador
Se ejecutará automáticamente cuando se compile un programa.
Incluir ficheros:
# include <stdio.h>
Los ficheros cabecera contienen declaraciones de variables, de nuevos tipos, de funciones, etc.
No se puede utilizar este procedimiento para incluir ficheros con código C.
Definir macros:
#define MAXLINEA 81
Ejecución del programa
El programa comienza a ejecutarse por la función main.
Salvo que el programa se interrumpa la ejecución empezará por la primera sentencia de main y terminará por la última.
El anidamiento de funciones se produce en el momento de ejecución del programa.
C de Kernighan-Ritchie y ANSI C
B.W. Kernighan y D.M. Ritchie escirbieron el libro The C Programming Language. Este estándar es conocido popularmente como el C de Kernighan-Ritchie.
El instituto americano ANSI (American National Standars Institute) ha desarrollado un estándar del lenguaje C.
Unix SVV4 y ANSI C
En la versión SVV4 de AT&T de Unix se ha incorporado el sistema de compilación C Edición 5.0. Este sistema incluy compilación ANSI C aunque no obligatoriamente.
En versiones de SVV4 la compilación se supondrá que implícitamente es en modo ANSI.
Algunas características ANSI C
Prototipos de funciones
tipo funcion(tipo1 arg1, ..., tipon argn)
Declaración:
tipo funcion(tipo1, ..., tipon);
Caracteres multibyte
ANSI C permite la utilización de secuencias de bytes en vez de bytes simples para representar caracteres especiales.
Prácticas
Escribir una función que devuelva 1 si la cadena que se le pasa es un palíndromo, y 0 si no lo es.
9. Funciones
Función C
Formato de una función:
tipo nombre_funcion(def_argumentos)
{
definición_de_variables_locales;
sentencias;
}
-
Argumentos reales ! Argumentos formales
-
C siempre pasa argumentos por valor
-
Siempre se hace una copia de los argumentos
-
El tipo de los argumentos reales debe concordar con el de los formales
Aunque el compilador hace algunas conversiones, el programador debe indicarlas explícitamente
Pasar Argumentos
Para usar una función necesitamos saber:
¿Cuáles son sus argumentos?
¿Qué resultados retorna?
¿Cuáles son sus efectos laterales?
Pasar valores:
main()
{
int n;
printf(“Valor para n\n”);
scanf(“%d”, &n);
sum_cuadrados(n);
}
void sum_cuadrados(int n)
{
int i, sum=0;
for (i=1; i<=n; ++i)
sum += i*i;
printf(“Resultado %d\n”, sum);
}
Devolver valores
main()
{
int n;
printf(“Valor para n\n”);
scanf(“%d”, &n);
printf(“Resultado %d\n”, sum_cuad(n));
}
int sum_cuad(int n)
{
int i, sum=0;
for (i=1; i<=n; ++i)
sum += i*i;
return(sum);
}
Usar punteros
main()
{
int n;
int resul;
printf(“Valor para n\n”);
scanf(“%d”, &n);
sum_cuad(n, &resul)
printf(”Resultado %d\n”, resul);
}
void sum_cuad(int n, in *ptr_res)
{
int i;
*ptr_res=0;
for (i=1; i<=n; ++i)
*ptr_res += i*i
Efectos laterales
int Result=0;
main()
{
int n;
printf(“Valor para n\n”);
scanf(“%d”, &n);
sum_cuad(n);
printf(“Resultado %d/n”, Result));
}
void sum_cuad(int n)
{
int i;
for (i=1; i<=n; ++i)
Result += i*i;
}
4. Operadores y Conversión deTipo
Precedencia y asociatividad
Resumen de operadores, su precedencia (de mayor a menor) y asociatividad
Prec | Operadores | Símbolos | Asociatividad |
1 | Llamada a función y selección de campos | () [] . -> | izda dcha |
2 | Unario | *&-!"++--sizeof | dcha izda |
3 | Multiplicativos | * / % | izda dcha |
4 | Aditivos | + - | izda dcha |
5 | Desplazamientos | << >> | izda dcha |
6 | Relacionales | < > <= >= | izda dcha |
7 | Igualdad | == != | izda dcha |
8 | “and” de bits | & | izda dcha |
9 | “or” exclusivo de bits | ^ | izda dcha |
10 | “or” de bits | | | izda dcha |
11 | “and” lógico | && | izda dcha |
12 | “or” lógico | || | izda dcha |
13 | Condicional | ?: | dcha izda |
14 | Asignación | = /= *= += -= | dcha izda |
15 | Coma | , | izda dcha |
Prácticas
Evalúe las siguientes expresiones
c=3+8
5/(c=b=4)
6+(c=3+8)
Escriba un programa que convierta un número de segundos en minutos y segundos usando el operador %
Construya un programa que cuente hasta un valor. Utilice el operador `++' en una pregunta while
Conteste cómo se evaluarán las siguientes expresiones
x*y++
*y += 1
*c++
No confunda precedencia con orden de evaluación. Discuta sobre los siguientes ejemplos
y=2; n=3; sig=(y+n++)*6;
printf (“%6d %d\n”, num, num*num++);
resp=num/2+10*(1+num++);
Conversión de tipo
El rango de categoría de mayor a menor:
-
double
-
float
-
long
-
int
-
short
-
char
Las variables y constantes float se convierten a double.
Promoción .- En cualquier operación en la que aparezcan dos tipos diferentes, se promociona la del menor a la del mayor.
Sentencia de asignación
-
El resultado final se reconvierte al tipo de la variable a la que se está asignando
-
Puede producirse por tanto una promoción, o una pérdida de rango
-
Conviene normalmente hacer una conversión explícita “(tipo)”
Prácticas
Analice el siguiente programa
main()
{
char ch;
int i;
float fl;
fl=i=ch='A';
printf(“ch = %c, i = %d, fl = %2.2f\n”, ch, i, fl);
ch = ch + 1;
i = fl + 2 * ch;
fl = 2.0*ch + i;
printf(“ch = %c, i = %d, fl = %2.2f\n”, ch, i, fl);
ch = 2.0e30;
printf(“Ahora ch = %c\n”, ch);
}
¿Cuál es el resultado de las siguientes expresiones?
var_int = 1.8 + 1.9
var_int = (int) 1.6 + (int) 1.9
12. Punteros
Introducción
Un puntero es una variable que contiene la dirección de otra variable.
Los punteros se han comparado con la sentencia goto como una forma maravillosa de crear programas imposibles de entender.
Sin embargo, con cierta disciplina los punteros pueden ser usados para conseguir claridad y simplicidad.
Un puntero tiene un contenido directo:
una dirección
y un contenido indirecto:
el objeto al que apunta
En C no hay punteros, hay punteros a tipos de datos.
Operador &
-
El operador unario & da la dirección de un objeto.
-
Como un puntero contiene la dirección de un objeto, es posible acceder a ese objeto indirectamente por medio del puntero.
-
Sea x una variable entera (int x;) y px un puntero, la sentencia:
px = &x;
asigna la dirección de x a la variable px; decimos ahora que px apunta a x.
-
Ejemplo .- Supongamos la variable alfa:
Dirección Valor Nombre
9634 1 alfa
main()
{
int alfa = 1;
printf(“La direccion %u contiene el valor
%d.\n”, alfa, &alfa);
}
Salida
La direccion 9634 contiene el valor 1.
-
& solo se puede aplicar a variables o elementos de un array.
-
No se puede aplicar a expresiones no constantes
-
Ejemplos .- Utilización ilegal de &
&(x+1)
&3
-
Tampoco se puede aplicar a variables register.
Operador *
-
El operador unario * trata su operando como la dirección de su objetivo, y accede a esa dirección para obtener su contenido
-
Ejemplo
int x, y;
int *px;
...
...
px = &x; /* Equivalente a y=x */
y = *px;
Asigna a y el contenido de la dirección apuntada por pc.
-
Ejemplo
Dirección Valor Nombre
9364 1 alfa
9370 9364 beta
main()
{
int alfa;
int *beta;
alfa = 1;
beta = &alfa;
printf(“La direccion %u contiene el valor %d.\n”,
&alfa,alfa);
printf(“La direccion %u contiene el valor %d.\n”,
&beta,beta);
printf(“La direccion %u contiene el valor %d.\n”,
*(&alfa),alfa);
}
Salida
La direccion 9364 contiene el valor 1
La direccion 9370 contiene el valor 9364
La direccion 9364 contiene el valor 1
-
Los punteros deben declararse, la forma es:
tipo *nombre_ptr;
Ejemplos
double atof(),valor, *ptr_d;
int x, y, *px;
Estas declaraciones nos dicen que si en una expresión aparece atof()o *ptr_d tienen valores double, y si aparecen x, y o *px tienen valores int.
-
Además, la declaración de un puntero obliga a que éste apunte a una clase determinada de objeto.
Los punteros pueden aparecer en expresiones:
y = *px + 1;
printf(“%d\n”,*px);
d = sqrt((double)*px);
Pueden ser el lado izquierdo (lvalue) de una asignación:
*px = 0;
*px += 1;
*px ++;
Como son variables, se pueden usar como tales:
int *px, *py;
py = px;
Punteros y Argumentos de Función
En C no existe una forma directa de alterar el valor de una variable de la función que hizo la llamada:
f()
{
int a,b;
...
...
swap_erronea(a,b);
}
swap_erronea(x,y)
int x,y;
{
int temp;
temp = x;
x = y;
y = temp;
}
swap no afecta ni a ni b
Hacerlo mediante punteros:
f()
{
int a,b;
...
...
swap(&a,&b);
...
}
swap(px,py)
int *px, *py;
{
int temp;
temp = *px;
*px = *py;
*py = temp;
}
Arrays
Se declaran con la siguiente sintaxis:
tipo nombre[dimension]
Donde dimension es una expresión constante.
Ejemplo
int ndigitos[10]
Declara ndigitos como un array de 10 enteros
ndigitos[0]ndigitos[1],...,ndigitos[9]
Se usa de elemento en elemento (no tiene meta-operadores):
nombre[indice];
donde:
0 <= indice <= dimension-1
El indicepuede ser cualquier expresión entera. Su resultado significa el elemento (a[i]); en la posición i desde el comienzo o el elemento i+1.
Punteros y arrays
-
Si
int *pa;
pa = &a[0];
pone a pa apuntando al elemento cero del array a.
Entonces:
x = *pa;
es equivalente a:
x = a[0];
Operador sizeof
-
Sintaxis:
sizeof expresion
sizeof (tipo)
-
Da el valor del tamaño, en bytes, de su operando
-
Aplicado a un array da el tamaño del array en bytes
-
El tamaño se determina a partir de la declaración de los objetos
-
Si es un tipo, da el tamaño de ese tipo
-
El símbolo para el operador tamaño es la palabra sizeof
-
Devuelve el tamaño en bytes de su operando
int i;
sizeof(i);
Producirá el valor 2 (o 4) si un entero ocupa 2 bytes en memoria.
-
Si declaramos un array:
char who[7];
la expresión:
sizeof(who)
devolverá el valor 7, suponiendo que un char ocupa un byte en memoria.
-
Se puede usar también para determinar el tamaño de cualquier tipo de dato
-
Ejemplo
main()
{
printf(“char%d\nint %d\nshort %d\nlong %d\nfloat
%d\ndouble %d\nchar* %d\n”,sizeof(char),
sizeof(int),sizeof(short),sizeof(long),
sizeof(float),sizeof(double),
sizeof(char*));
}
Salida
char 1
int 2
short 2
long 4
float 4
double 8
char* 2
-
Se usa frecuentemente para independizar el código del tamaño particular de un tipo de dato en una máquina determinada
-
Por definición, pa+1 apunta al siguiente elemento
-
pa+i apunta i elementos después
-
pa-i apunta i elementos antes
-
La definición de sumar 1 a un puntero y toda la aritmética de direcciones, es tal que el incremento tiene una escala: el tamaño de almacenamiento del objeto al que apunta
tipo *pa,i;
pa+i;
sed referencia la dirección:
pa + (sizeof(tipo)*i)
-
Si a es un array y p es un puntero, ambos de igual tipo:
tipo *p,a[];
si
p = &a[0];
entonces *(p+i); es equivalente a a[i];
-
El nombre de un array (a) es una expresión puntero
-
Por definición, a es el nombre de un puntero constante que siempre apunta al primer elemento del array a[0]
-
Entonces:
a[i] Elemento i del array
a Puntero constante al elemento 0
a+i Expresión tipo puntero, que apunta al elemento i
-
Equivalencias, con diferentes sintaxis:
p = &a[0] ! p = a
&a[i] ! a + i
a[i] ! *(a+i)
*(p+i) ! p[i]
-
Un array y su índice se pueden escribir como un puntero y un offset
-
Diferencia
-
Un puntero es una variable
-
Un nombre de array es una constante, no se puede modificar pero sí usar
Operaciones ilegales con arrays
a = p;
a++;
p = &a;
Operaciones válidas con punteros
-
Asignar una dirección p=&v; siendo v homogénea
-
Referenciar *p; se obtiene el objeto
-
Restar dos punteros homogéneos p-q; se obtienen los que caben
-
Comparar dos punteros p>q; compara que dirección es mayor, etc.
-
Incrementar p++, p+i
-
Asignar un entero p=14; en general NO se permite (¿Qué hay en 14?)
-
El resto de las operaciones, aunque factibles, son impredecibles
Punteros y arrays como argumentosç
-
Cuando se pasa el nombre de un array a una función, lo que se pasa realmente es la posición de comienzo del array
-
En la dirección 0000 no hay dato alguno, ningún puntero asignado puede ser 0
-
Por convenio, p=0 indica puntero no asignado (NULL)
-
Sea la declaración
tipo i,*ptr,a[DIM]
Si se usa como argumento real en una llamada:
i se pasa por valor (se copia) la variable
&i se pasa por valor la dirección de la variable, o sea se pasa por referencia la variable
a[i] se pasa por valor el elemento
a se pasa por valor la dirección de comienzo, o sea se pasa por referencia el array
*p se pasa por valor el objeto
p se pasa por valor el puntero, o sea se pasa por referencia el objeto
&p se pasa por valor la dirección del puntero, o sea se pasa por referencia el puntero
-
Dentro de la función llamada este argumento es una variable y por eso un nombre de array argumento es un verdadero puntero
-
Ejemplo
main()
{
static char cadena=”Hola”;
int strlen();
printf(“Longitud de cadena es %d\n”,
strlen(cadena));
}
int strlen(s)
char s[];
{
int i;
i=0;
while (s[i]!='\0')
i++;
return(i);
}
Otra forma de escribir la función strlen()
int strlen()
char *s;
{
int n;
for (n=0; *s != `\0'; s++)
n++;
return(n);
}
-
Para pasar parte de un array
f(&a[2]);
f(a+2);
Ejercicio
/* a se almacena en la posición 1018 */
/* un int ocupa 2 bytes */
/* ptr1 se almacena en la posición 5590 */
/* ptr2 se almacena en la posición 5596 */
main()
{
static int a[3]={100,200,300}
int *ptr1, *ptr2;
ptr1 = a;
printf(“&ptr1 = %u. ptr1 = %u. *ptr1 = %u\n”,
&ptr1,ptr1,*ptr1);
ptr2 = &a[2];
printf(“&ptr = %u. ptr2 = %u. *ptr2 = %u\n”,
&ptr2,ptr2,*ptr2);
ptr1++;
printf(“&ptr1 = %u. ptr1 = %u. *ptr1 = %u\n”,
&ptr1,ptr1,*ptr1);
ptr2++;
printf(“&ptr2 = %u. ptr2 = %u. *ptr2 = %u\n”,
&ptr2,ptr2,*ptr2);
printf(“ptr2-ptr1 = %u\n”, ptr2-ptr1);
}
¿Cuál es la salida del programa?
Cadenas de caracteres (strings)
Un tipo especialmente frecuente e importante son los arrays de caracteres.
Una constante cadena de caracteres como:
“soy una cadena”
es un array de caracteres. Internamente, el último elemento contiene `\0'.
Se pueden inicializar de una manera especial:
char s[] = “texto”
declara un array de 6 char ya que el último elemento contiene `\0'.
En C no se dispone de operadores propios para cadenas de caracteres que las procesen como una unidad.
Las operaciones comunes de comparación, concatenación, etc. se realizan mediante funciones de librería, que utilizan todas el convenio del NULL (`\0') final.
El acceso a estas cadenas se suele hacer mediante punteros a caracteres.
Ejemplo
char *texto;
entonces:
mensaje = “Hoy hay futbol”;
asigna a mensaje un puntero a los caracteres reales. No es una copia de la cadena; solo están involucrados los punteros.
Punteros a caracteres
strcopy_1(s,t)
char s[],t[];
{
int i=0;
while((s[i] = t[i]) != NULL)
i++;
}
strcopy_2(s,t)
char *s,*t;
{
while((*s = *t) != NULL)
s++;
t++;
}
strcopy_3(s,t)
char *s,*t;
{
while((*s++ = *t++) != NULL)
;
}
strcopy_4(s,t)
char *s,*t;
{
while(*s++ = *t++)
;
}
-
Otras combinaciones de * y ++ o - son posibles, aunque menos frecuentes:
*++p
*--p
-
Los punteros no son enteros, p++ no siempre incrementa en uno; incrementa el tamaño del objeto apuntado.
-
Una función puede devolver como su resultado un puntero.
-
Ejemplo .- Función que devuelve un puntero a un carácter
char *strsave(s)
char s[];
{
char *ptr;
...
...
return ptr;
}
Arrays multidimensionales y arrays de punteros
-
Declaración de array bidimensional:
tipo nombre[DIM_1][DIM_2];
-
Los elementos se almacenan por filas, el índice derecho varía más rápido
Punteros Compuestos
-
Inicialización
static int biarray[3][4] = {
{0,1,2,3}
{10,11,12,13}
{20,21,22,23}
};
main()
{
f(biarray);
...
}
f(matriz) /* erronea */
int matriz[];
{...}
f(matriz) /* erronea */
int matriz[][];
{...}
f(matriz) /* correcta */
int matriz[][4];
{...}
Arrays de punteros
Se declaran:
tipo *nombre[DIM];
Se interpreta como nombre es un array de DIM punteros a tipo
-
nombre[i] Es un elemento (un puntero)
-
*nombre[i] Es el objeto apuntado por el puntero
Ejemplo
main()
{
char *ptrlin[MAXLIN];
leer(ptrlin,...);
escribir(ptrlin,MAXLIN);
}
escribir(ptrlin,num)
char *ptrlin[];
int num;
{
int i;
for(i=0; i<num; i++)
printf(“%s\n”,ptrlin[i]);
}
ptrlin es un array de MAXLIN elementos, cada elemento es un puntero a un char
ptrlin[i] puntero a carácter
*ptrlin[i] accede a un carácter
Como ptrlin es un array, puede tratarse como un puntero. La función escribir puede ponerse:
escribir(ptrlin,num)
char *ptrlin[];
int num;
{
while(--num >= 0)
printf(“%s\n”,*ptrlin++);
}
Existen dos formas parecidas de declarar un array bidimensional:
int a[10][10];
int *b[10];
Es legal poner a[5][5] y b[5][5].
-
a tiene asignadas 100 posiciones
-
b tiene asignados 10 punteros
Acceso por indirección, no suma y multiplicación.
Filas de diferentes longitudes.
Inicialización de arrays de punteros
-
Un ejemplo, función nombre_mes(), devuelve un puntero a una cadena de caracteres conteniendo el nombre del mes n, que se le pasa como argumento
char *nombre_mes(n)
int n;
{
static char *nombre[] = {
“mes ilegal”,
“Enero”,
“Febrero”,
“Marzo”,
“Abril”,
“Mayo”,
“Junio”,
“Julio”,
“Agosto”,
“Septiembre”,
“Octubre”,
“Noviembre”,
“Diciembre”
};
return((n<1||n>12)? name[0];name[n]);
}
Punteros a Funciones
Aunque un función NO es una variabl, C permite definir punteros a funciones, para permitir manipularlos y, más especialmente, pasarlos como parámetros a otras funciones.
Se declaran:
tipo(*nombre)();
establece que nombre es un puntero a una función que devuelve tipo.
Es necesario los paréntesis, ya que:
tipo *nombre()
declara nombre como una función que devuelve un puntero a un entero.
C reconoce una función mediante el para de paréntesis () que siguen al nombre de la función.
Cuando ha reconocido un identificador como una función, si se encuentra de nuevo el nombre SIN paréntesis, evalúa ese nombre como la dirección de la función.
main()
{
int abc();
printf(“La direccion de la funcion abc es %u.\n”,
abc);
}
abc() /* Se almacena en la posición 1310 */
{
}
Salida
La dirección de la función abc es 1310.
Declarar abc en main, antes de usar.
Ejemplo .- Uso de un puntero a una función
main()
{
int abc();
int (*ptr_abc)();
printf(“La direccion de la funcion abc es %u.\n”,
abc);
abc();
ptr_abc = abc;
printf(“El valor de ptr_abc es %u.\n”, ptr_abc);
}
abc() /* Se almacena en la posición 1310 */
{
printf(“ABC!\n”);
}
Salida
La dirección de la funcion abc es 1310.
ABC!
El valor de ptr_abc es 1310.
ABC!
En main estamo declarando abc como una función que devuelve un entero, y ptr_abc como un puntero a una función que devuelve un entero.
La asignación:
ptr_abc = abc
asigna la dirección de abc() al puntero ptr_abc.
No es necesario emplear el operador dirección (&), ya que al quitar los paréntesis de función se supone implícitamente que nos referimos a la dirección de la función.
abc() y (*ptr_abc)() aunque son sentencias diferentes, ambas invocan la misma función.
Ejemplo
main()
{
char *abc();
char *(ptr_abc)();
ptr_abc = abc;
printf(“%s”,abc());
printf(“%s”,(*ptr_abc)());
}
char *abc() /* Se almacena en la posición 1310 */
{
return(“ABC!\n”);
}
Salida
ABC!
ABC!
Declaraciones Barrocas
Declaraciones normales
-
int x entero
-
int *x puntero a un entero
-
int *x[3] array de 3 punteros a enteros
-
int (*x)[3] puntero a un array de 3 enteros
-
int *x() función que devuelve un puntero a un entero
-
int (*x)() puntero a una función que devuelve un entero
Declaraciones curiosas
-
int **p puntero a un puntero a un entero
-
int (*p[3])[4] array de 3 punteros a arrays de 4 enteros
-
int (*p)[3][4] puntero a un array de 3×4 enteros
-
char *fp()[3] función que devuelve un puntero a un array de 3 char
-
char *fp[3]() array de 3 punteros a función que devuelve un char
-
Mayor prioridad si está más cerca del identificador
-
[] y () mayor prioridad que *
-
Si se usan paréntesis estos prevalecen
3. Tipos de Datos Simples
Constantes
-
Constantes enteras
143, 0, -1285
-
Constantes “long”
715L, 715l, 123456L, 123456l
-
Constantes en octal (precedidas por 0)
07, 0377, 020070
-
Constantes en hexa
0x7c, 0xa1b, 0Xa1B
-
Constantes “carácter”
`M', `a', `+', `-`
-
Tiras: Una secuencia de caracteres entre “ “
Secuencias
-
Secuencias de escape
\n salto de línea
\t tabulador
\b retroceso
\r retorno de carro
\f salto de página
\\ barra invertida
\' comilla simple
-
Secuencias de escape numéricas
\ddd
\007, \07, \7
Caracteres
La sintaxis para definir variable de un tipo es la siguiente:
tipo id1, id2, ...;
Caracteres
char car;
-
Un carácter se guarda como su código dentro del conjunto de caracteres que se esté usando
-
Por tanto es posible también tratar numéricamente una variable de tipo char
-
Muchas funciones que tratan con caracteres devuelven un entero en situaciones de error
Enteros
Existen tres tipos de variables enteras:
-
int
-
short
-
long
tam_short <= tam_int <= tam_long
Las situaciones habituales que se suelen encontrar son:
tam_short tam_int tam_long
16 16 32
16 32 32
Ejemplos
int var, suma, resto;
short cont;
long i, maximo;
Punto flotante y booleanos
Punto flotante
-
Tipos float y double para numeros reales
-
La aritmética en punto flotante es realizada en doble precisión
float tasa, integral;
double masa, energia;
Valores Booleanos
-
El lenguaje C no dispone de tipos Booleanos
-
Falso: el valor 0
-
Verdadero: Cualquier valor distinto de cero
#define TRUE 1
#define FALSE 0
6. Tipos de Datos Compuestos
Arrays
Es un tipo compuesto formado por una sucesión de objetos del mismo tipo.
tipo nombre_array [d1][d2]...[dn];
Algunas características importantes de los arrays son:
-
Los elementos del array se almacenan consecutivamente en memoria
-
El nombre del array es la dirección del mismo
-
El nombre del array es una dirección constante
Para acceder a los elementos de un array:
nombre_array [i][j]...[k]
Array de caracteres
-
Las tiras son arrays de una dimensión de tipo char
-
Una tira en C se termina siempre por un carácter nulo `\0'
-
El tamaño reservado para una tira debe tener en cuenta este carácter nulo
-
Por ejemplo “a” ocupa dos bytes, uno para `a' y otro para `\0'
Estructuras
En otros lenguajes son conocidas como registros.
Una forma de definir una estructura:
struct
{
tipo1 campo 1;
tipo2 campo 2;
......
}
nombre_estructura;
Para acceder a un campo de la estructura:
nombre_estructura.campoi
Uniones
Similar a una estructura excepto que únicamente uno de sus se usa en cada instante.
La definición de una unión es la siguiente:
union
{
tipo1 campo1;
tipo2 campo2;
......
}
nombre_union;
Para acceder a un campo de una unión se hace igual que en una estructura:
nombre_union.campoi
Punteros
Un puntero es una variable que apunta a un determinado tipo de datos. Su definición:
tipo *nombre_ptr;
Características
-
Una variable puntero puede asignarse
-
Es quizás uno de los errores más frecuentes de la programación en C, usar un puntero que todavía no ha sido asignado
-
Para acceder al contenido referenciado por un puntero:
*nombre_ptr;
-
Esta expresión es manejada exactamente igual que una variable de tipo tipo
Prácticas
El siguiente programa muestra algunas diferencias entre arrays y punteros. Analice lo que hace:
char s[]=”Primera ley de Murphy”;
char *p=”Si algo puede fallar, fallará”;
main()
{
char *q=”A que te suena esto?”;
printf(“\n%s:\n%s\n%s\n”, s, p, q);
for (p=q; *q!='\0'; q++)
*q+=1;
printf(“\n%s\n\n”,p);
}
Nota .- Falta una hoja en los apuntes
Prácticas
Si i es un entero y p es un puntero a entero ¿qué imprimirá el siguiente código?
i=3;
p=&i;
printf(“%d %d %d %d”, p, *p+7, **&p, p-(p-2));
Si i y j son enteros, y p y q son punteros a enteros, ¿qué sentencias son ilegales?
p = &i p = &*&i i = (int)p q = &p
*q = &j i = (*&)j i = *&*&j i = *p++ + *q
¿Cuál es la salida del siguiente programa?
#include <stdio.h>
main()
{
char *pc = NULL;
int *pi = NULL;
double *pd = NULL;
printf(“\n%d %d %d\n%d %d &d\n\n”,
(int)(pc+1),(int)(pi+1),(int)(pd+1),
(int)(pc+3),(int)(pi+5),(int)(pd+7)));
}
Escribir un programa que cuente caracteres, palabras y líneas (“word count”).
Aplicaciones
S.O.
HW
CRC
Código
CRC
Código
Código
CRC
Código
Nº Bytes
Dir. Carga
CRC
Nº Bytes
Dir. Carga
Nº Bytes
Dir. Carga
Nº Bytes
Dir. Carga
CRC
Código
Nº Bytes
NOMBRE
Nº Bloques
Tipo
Dir. Carga
Estados Básicos de un Proceso
PAGE NUMBER
p
BYTE NUMBER
b
Program
address
PAGE p
PAGE TABLE
P' + B
P'
Simple addres map for paging
Descargar
Enviado por: | Rarael Marañón Abreu |
Idioma: | castellano |
País: | España |