Assinatura de procedimentos em SQL Server

Para além da minha função de administrador de bases-de-dados, desempenho ainda a de analista de sistemas na qual desenvolvo e analiso soluções de integração entre sistemas heterogéneos. Neste âmbito, foi-me requerido o melhoramento do seguinte processo:

  1. Sempre que existe a necessidade de processar um determinado conjunto de dados, um email é enviado ao operador;
  2. O operador executa uma aplicação que extrai um conjunto de dados em forma tabular de uma base-de-dados Oracle;
  3. O operador executa uma macro de Excel sobre os dados contidos no ficheiro, incluindo informação adicional;
  4. O operador envia o resultado ao seu cliente.

Não é difícil concluir uma melhoria trivial sobre este processo, nomeadamente, a eliminação dos três primeiros passos, com o auxílio de uma aplicação web que poderia ser facilmente criada pela equipa de desenvolvimento. O processo anterior evoluiria para:

  1. O utilizador autentica-se e inicia a extracção do relatório;
  2. A aplicação conecta-se à base-de-dados Oracle e extrai e processa os dados necessários;
  3. O utilizador descarrega ou visualiza o relatório final.

Neste caso específico, poderia ser criado na base-de-dados um utilizador cujas permissões se cingiriam à leitura das tabelas requeridas para a extracção dos dados cujas credenciais poderiam ser do conhecimento da equipa de desenvolvimento. No entanto, os administradores da base-de-dados não estão predispostos a criar um utilizador com as permissões pretendidas. Por outro lado, disponibilizando à equipa de desenvolvimento as credenciais de um utilizador com permissões demasiado elevadas viola claramente o princípio do menor privilégio, segundo o qual só deverão ser atribuídas apenas as permissões necessárias para a execução de uma tarefa. Dada a quantidade de dados envolvida, a solução passa pela interposição de uma base-de-dados que exponha uma interface para a consulta pretendida. Tendo em conta a experiência da equipa de desenvolvimento, a escolha recaiu sobre o SQL Server.

A extracção de dados Oracle a partir do SQL Server efectua-se facilmente com o auxílio do conector de dados Attunity. Mais uma vez a escolha particular desta tecnologia em detrimento do ODAC (Oracle Database Access Components) deveu-se particularmente à sua aplicabilidade no SSIS (SQL Server Integration Services). Após a instalação do Attunity, é necessário configurar uma conexão ODBC sobre a qual é criado um Linked Server no SQL Server e mapeado um login, que será designado por elevated_login ao utilizador conhecido com permissões elevadas. Não serão aqui detalhados os passos para o efeito. É também criado o login e respectivo utilizador rectricted_login que não será mapeado no Linked Server.

Se criarmos o procedimento na base-de-dados MyDatabase


CREATE PROCEDURE [dbo].[MyProcedure]
WITH EXECUTE AS 'elevated_login'
AS
BEGIN
SELECT * FROM OPENQUERY(My_Oracle_Linked_Server, '
 SELECT * FROM ORASCHEMA.ORATABLE
');
END;

sobre o qual atribuímos permissões de execução aos utilizadores elevated_login e restricted_login. Se executarmos o procedimento no contexto do utilziador elevated_login, o procedimento retorna-nos os dados pretendidos. A execução no contexto do utilizador restricted_login resulta no erro:

Cannot execute as the server principal because the principal “elevated_login” does not exist, this type of principal cannot be impersonated, or you do not have permission.

De facto, o utilizador restricted_login não tem permissões para personificar o utilizador elevated_login e a chamada do procedimento falha. Por outro lado, o princípio do menor privilégio é violado caso sejam atribuídas ao utilizador mais restrito permissões de personificação de um utilizador elevado. Tais atribuições permitiriam, após personificação, a elevação do utilizador restrito ao nível em que este seria capaz de executar qualquer consulta sobre o Linked Server. Coloca-se, portanto, a questão: É possível dar permissões a um utilizador menos elevado para executar um procedimento num contexto de um utilizador mais elevado sem o personificar? A resposta à pergunta é positiva e o método baseia-se no conceito de certificado.

Um certificado consiste num objecto assinado electronicamente ao qual se encontra associada uma chave pública (ver criptografia de chave pública). Esse objecto permite assistir a criação de conexões seguras entre bases-de-dados espelho, encriptar dados ou assinar packages e procedimentos. Vejamos como é possível recorrer ao certificado para permitir ao utilizador restricted_login executar apenas o procedimento, personificando o elevated_login.

Em primeiro lugar, como o Linked Server é um objecto de servidor, o certificado terá de ser conhecido ao nível da base-de-dados master. Podemos, portanto, criar aí um certificado.


USE master;
GO

CREATE CERTIFICATE [SigningCertificate]
ENCRYPTION BY PASSWORD = '$tr0ngp@$$w0rd'
WITH SUBJECT = 'Certificate for signing a Stored Procedure';
GO

O comando permite criar um certificado, isto é, um par de chave pública e chave privada. A chave privada é subsequentemente encriptada com o auxílio da palavra-chave “$tr0ngp@$$w0rd”. De modo a ser possível importar o par na base-de-dados MyDatabase, exportamos o respectivo conteúdo.


BACKUP CERTIFICATE [SigningCertificate]
TO FILE = 'd:\tmp\SigningCertificate.cer'
WITH PRIVATE KEY (
FILE = 'c:\tmp\SigningCertificate.pvk',
ENCRYPTION BY PASSWORD = 'b4ckup$tr0ngp@$$w0rd'
DECRYPTION BY PASSWORD = '$tr0ngp@$$w0rd'
)
;

Note-se que, para importar o certificado, é necessário conhecer a chave privada que lhe está associada. O processo de exportação encripta a chave privada com o auxílio da palavra-chave “b4ckup$tr0ngp@$$w0rd”. Como é óbvio, é necessário proporcionar a palavra-chave que desencripta a chave privada associada ao certificado que se encontra armazenada na base-de-dados. Criamos um login associado ao certificado e atribuímos-lhe permissões de personificação do elevated_login.


CREATE LOGIN [SigningLogin]
FROM CERTIFICATE [SigningCertificate];

GRANT IMPERSONATE ON LOGIN::elevated_login to [SigningLogin];

Importamos, de seguida, o certificado na base-de-dados MyDatabase onde foi criado o procedimento.


USE MyDatabase;
GO

CREATE CERTIFICATE [SigningCertificate]
FROM FILE = 'd:\tmp\SigningCertificate.cer'
WITH PRIVATE KEY (
FILE = 'd:\tmp\SigningCertificate.pvk',
ENCRYPTION BY PASSWORD = 'M1D4b453$tr0ngp@$$w0rd',
DECRYPTION BY PASSWORD = 'b4ckup$tr0ngp@$$w0rd');
GO

O comando anterior permite importar o certificado e a respectiva chave privada que foi encriptada com o auxílio da chave “b4ckup$tr0ngp@$$w0rd”. Será armazenada na base-de-dados MyDatabase encriptada pela palavra-chave “M1D4b453$tr0ngp@$$w0rd”. Criamos o utilizador associado ao certificado.


CREATE USER [SigningLogin] FROM LOGIN [SigningLogin]
GO

Finalmente, assinamos o procedimento.


ADD SIGNATURE TO OBJECT::[dbo].[MyProcedure]
BY CERTIFICATE [SigningCertificate]
WITH PASSWORD = 'M1D4b453$tr0ngp@$$w0rd';
GO

Se o utilizador restricted_login executar o procedimento assinado, a personificação do elevated_login será conseguida por intermédio do login SigningLogin. Verificamos que as permissões são atribuídas ao certificado ao invés de o serem ao utilizador menos elevado.

Será tentador assumir que é possível mapear o SigningLogin no Linked Server. No entanto, a sua tentativa será fútil uma vez que um utilizador associado a um certificado serve apenas o propósito de assinar objectos e não pode ser usado para conexões às base-de-dados.

Publicado em Computadores e Internet | Etiquetas , , , | Publicar um comentário

Programação CUDA em C#

A tecnologia CUDA, acrónimo para Compute Unified Device Architecture, foi desenvolvida pelo fabricante de placas gráficas NVIDIA e consiste numa plataforma e modelo de programação em C/C++ vocacionada para o aumento do desempenho das aplicações com o auxílio dos GPU (Graphics Processing Unit). Dada a especificidade dos dispositivos gráficos, a sua arquitectura viabiliza a realização paralela de operações matemáticas com um desempenho superior aos CPUs (Central Processing Unit) com custos reduzidos. Esta característica tem disseminado o recurso a este tipo de dispositivos em HPC (High Performance Computing) o que, por si só, é suficiente para considerar o domínio da tecnologia como uma mais-valia. A alternativa ao CUDA, definida pelo consórcio Khronos Group, é proporcionada pelo padrão livre de computação paralela em sistemas heterogéneos OpenCL (Open Computing Language). Este padrão é transversal a um número significativo e crescente de fabricantes de dispositivos tais como a Intel, IBM, AMD ou ARM, incluindo-se mesmo nesta lista a NVIDIA.

Não pretendo aqui justificar a utilização de uma tecnologia em detrimento de outra. Pretendo, por outro lado, descrever como é possível utilizar a tecnologia CUDA com linguagens alternativas como é o caso do C# ou do JAVA. Escolhi descrever CUDA devido ao facto de já ter efectuado algumas digressões neste sentido.

Princípios básicos

É possível encontrar no Cuda Toolkit Documentation toda a informação necessária para desenvolver aplicações CUDA nas linguagens C e C++. Aí encontram-se também definidos os passos para a instalação e configuração. Será suposto, nos exemplos que se seguem, que tantos os drivers da NVIDIA como o respectivo CUDA Toolkit se encontram propriamente instalados e configurados. Supôr-se-á ainda que o sistem onde os programas são executados contêm dispositivos com suporte CUDA.

Começamos por considerar o código de exemplo

// Device code
extern "C" __global__ void VecAdd(float* A, float* B, float* C, int N)
{
int i = blockDim.x * blockIdx.x + threadIdx.x;
if (i < N)
C[i] = A[i] + B[i];
}
// Host code
int main()
{
int i;
int N = 3;
size_t size = N * sizeof(float);

// Allocate input vectors h_A and h_B in host memory
float* h_A = (float*)malloc(size);
float* h_B = (float*)malloc(size);
float* h_C = (float*)malloc(size);

// Initialize input vectors
for(i = 0; i &amp;amp;lt; N; ++i)
{
h_A[i] = (float)i;
h_B[i] = (float)(N + i);
}

// Allocate vectors in device memory
float* d_A;
cudaMalloc(d_A, size);
float* d_B;
cudaMalloc(d_B, size);
float* d_C;
cudaMalloc(d_C, size);

// Copy vectors from host memory to device memory
cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);

// Invoke kernel
int threadsPerBlock = 256;
int blocksPerGrid =
(N + threadsPerBlock - 1) / threadsPerBlock;
VecAdd<<<blocksPerGrid, threadsPerBloc>>>(d_A, d_B, d_C, N);

// Copy result from device memory to host memory
// h_C contains the result in host memory
cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);

// Free device memory
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);

// Print result
for(i = 0; i < N; ++i)
{
printf("%f", h_A[i]);
}

printf("\n");

for(i = 0; i < N; ++i)
{
printf("%f", h_B[i]);
}

printf("\n");
for(i = 0; i < N; ++i)
{
printf("%f", h_C[i]);
}

// Free host memory
if(h_A != NULL) free(h_A);
if(h_B != NULL) free(h_B);
if(h_C != NULL) free(h_C);
}

Tata-se da codificação de um programa que recorre a uma placa gráfica para efectuar a soma de dois vectores. Se supusermos que este código se encontra definido no ficheiro AddVector.cu (com extensão CUDA), a sua compilação num programa executável pode ser conseguida com o auxílio do comando:

nvcc -arch=sm_20 -rdc=true AddVector.cu -o AddVector -lcudadevrt -ccbin “C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\bin\x86_amd64”

Assumimos aqui que o Visual Studio 2012 se encontra instalado no computador, uma vez que versões do compilador não são suportadas pelo programa nvcc. De facto, a directoria especificada na opção -ccbin deverá conter qualquer um dos compiladores suportados. A opção -arch permite indiciar ao compilador que o código a ser gerado deverá ser executado por dispositivos que suportem a arquitectura 2.0.

A execução do comando anterior permite-nos criar o programa AddVector.exe que imprime o seguinte resultado:

0.000000 1.000000 2.000000
3.000000 4.000000 5.000000
3.000000 5.000000 7.000000

De um modo resumido, as funções marcadas com o qualificador __global__ são executadas na gráfica mas chamadas no anfitrião. As funções marcadas como __device__ são chamadas e executadas na gráfica. As restantes funções são de anfitrião, cujo qualificador é __host__. No código acima, percebemos a definição da função

VecAdd(float*, float*, float*,int)

que é chamada no código de anfitrião na função main,

VecAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C)

As funções cudaMalloccudaMemcpycudaFree são definidas no CUDA Runtime API que constitui uma interface CUDA para C/C++.

O compilador nvcc

A abordagem anterior não se presta ao desenvolvimento de aplicações CUDA baseadas em outras linguagens de programação por razões óbvias, entre as quais se destaca a utilização da Runtime API. Assim, torna-se necessário perceber como o compilador gera o executável final.

Na secção The CUDA Compilation Trajectory do manual são descritas as fases da compilação de uma aplicação CUDA. O programa de entrada é pré-processado para compilação em binários CUDA (cubin) ou código intermédio (PTX) de dispositivo, os quais são colocados num ficheiro binário fatbinary. O programa de entrada é processado para compilação de anfitrião com o auxílio do compilador C/C++ especificado na linha de comandos. Neste processo, o binário fatbinay é imbuído no código de anfitrião onde as extensões CUDA são tranformadas em construções C++. O resultado final é compilado num objecto de anfitrião. Quando o código de dispositivo é lançado, o fatbinary é inpeccionado de modo a que o programa de anfitrião obtenha a imagem correcta para o GPU actual. O utilitário de compilação CUDA, como descrito na secção Using Separate Compilation in CUDA, pode ser configurado de modo a separar todo o processo de compilação de vários ficheiros CUDA. Esta funcionalidade encontra-se disponível desde a versão 5.0.

Cada etapa da compilação CUDA gera, numa directoria temporária, cada um dos ficheiros resultantes, os quais são eliminados imediatamente antes da sua conclusão. No entanto, como descrito na secção Keeping intermediate phase files o nvcc pode ser configurado por intermédio de uma opção de modo a que todos os ficheiros gerados sejam mantidos na directoria de compilação. Este tipo de compilação pode ser conseguido com o auxílio da execução da seguinte linha de comandos:

nvcc -arch=sm_20 -rdc=true AddVector.cu -o AddVector -lcudadevrt -ccbin “C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\bin\x86_amd64” -keep

É possível identificar três ficheiros, nomeadamente AddVector.ptxAddVector.sm_20.cubinAddVector.fatbin. O primeiro ficheiro, do tipo ptx consiste num conjunto textual de instruções muito semelhante ao assembly para processadores de CPU. O código contido neste ficheiro é orientado para um determinado conjunto de arquitecturas virtuais. Numa outra fase da compilação do nvcc, são gerados os binários cubin que são suportados por uma arquitectura real (e que poderão não funcionar com outras arquitecturas). No entanto, a arquitectura real deverá constituir uma implementação da arquitectura virtual de modo a que a compilação seja bem sucedida. Este processo permite mitigar o efeito da compatibilidade das aplicações face à evolução das GPU. De facto, o driver CUDA permite a compilação de ficheiros ptx em tempo de execução durante o qual já é conhecida a arquitectura real da GPU. Os binários fatbinary contêm os códigos de várias arquitecturas reais que implementam uma determinada arquitectura virtual.

O CUDA driver API

É natural assumirmos que o cubin imbuído no distribuível é carregado no GPU aquando das chamadas do kernel. Tal carregamento é conseguido com o auxílio de funções de mais baixo nível descritas no CUDA driver API. No sistema operativo Windows, essas funções são disponibilizadas pela dll nvcuda.dll instalada na pasta System32.

É possível mapear a interface disponibilizada pela driver API em funções C# com o auxílio do atributo DllImport. De facto, tal mapeamento pode ser encontrado no projecto matutils. A título de exemplo, seja considerado seguinte código:


static void Main(string[] args)
 {
 var cudaResult = CudaApi.CudaInit(0);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("CUDA function failed.");
 }

// Obtém o número de dispositivos disponíveis
 var deviceCount = default(int);
 cudaResult = CudaApi.CudaDeviceGetCount(ref deviceCount);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("CUDA function failed.");
 }

 for (int i = 0; < deviceCount; ++i)
 {
 var currentDevice = default(int);
 cudaResult = CudaApi.CudaDeviceGet(ref currentDevice, i);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("CUDA function failed.");
 }

 var deviceName = new StringBuilder();
 cudaResult = CudaApi.CudaDeviceGetName(deviceName, 64, i);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("CUDA function failed.");
 }

 var major = default(int);
 var minor = default(int);
 cudaResult = CudaApi.CudaDeviceGetAttribute(
 ref major,
 ECudaDeviceAttr.ComputeCapabilityMajor,
 i);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("CUDA function failed.");
 }

cudaResult = CudaApi.CudaDeviceGetAttribute(
 ref minor,
 ECudaDeviceAttr.ComputeCapabilityMinor,
 i);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("CUDA function failed.");
 }

Console.WriteLine(
 "GPU: {0}; Compute Capability: {1}.{2}",
 deviceName,
 major,
 minor);
 }

}

Constata-se facilmente que a primeira etapa consiste na inicialização do driver. A execução da função CuInit, mapeada no matutils como CudaInit, é obrigatória sempre que se pretenda recorrer às GPU em CUDA. O seguimento do código permite obter o número de dispositivos CUDA conectados ao anfitrião e obter, relativamente a cada um deles, o respectivo nome e poder de computação. A compilação do AddVector.cu com a linha de comandos

nvcc -arch=compute_30 -code=sm_30 -rdc=true AddVector.cu -lcudadevrt -ccbin “C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\bin\x86_amd64” -cubin -o AddVector.sm_30.cubin

permite gerar um ficheiro cubin que pode ser carregado e executado numa GPU com poder computacional 3.0. O código que se segue permite carregar o módulo que contém a função VecAdd compilado anteriormente e executá-lo com o auxílio do lançamento de um kernel.


static void Main(string[] args)
 {
 // Inicializa CUDA e avalia os dispositivos existentes
 var cudaResult = CudaApi.CudaInit(0);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Obtém o primeiro dispositivo
 var device = default(int);
 cudaResult = CudaApi.CudaDeviceGet(ref device, 0);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// O contexto é automaticamente colocado
// como corrente para a linha de fluxo actual
 var context = default(SCudaContext);
 cudaResult = CudaApi.CudaCtxCreate(
ref context,
ECudaContextFlags.SchedAuto, device);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Carrega o módulo no contexto actual
 var module = default(SCudaModule);
 cudaResult = CudaApi.CudaModuleLoad(
ref module,
"Data\\AddVector.sm_30.cubin");
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Obtém a função a ser chamada
 var cudaFunc = default(SCudaFunction);
 cudaResult = CudaApi.CudaModuleGetFunction(
 ref cudaFunc,
 module,
 "VecAdd");
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

var elemensNum = 10;

//var start = 0;
 var firstVector = new int[elemensNum];
 var secondVector = new int[elemensNum];
 var result = new int[elemensNum];
 for (int i = 0; i < elemensNum; ++i)
 {
 firstVector[i] = i + 1;
 secondVector[i] = elemensNum - i;
 }

// Reserva o primeiro vector
 var firstCudaVector = default(SCudaDevicePtr);
 cudaResult = CudaApi.CudaMemAlloc(
 ref firstCudaVector,
 Marshal.SizeOf(typeof(int)) * elemensNum);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Reserva o segundo vector
 var secondCudaVector = default(SCudaDevicePtr);
 cudaResult = CudaApi.CudaMemAlloc(
 ref secondCudaVector,
 Marshal.SizeOf(typeof(int)) * elemensNum);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Reserva o terceiro vector
 var resultCudaVector = default(SCudaDevicePtr);
 cudaResult = CudaApi.CudaMemAlloc(
 ref resultCudaVector,
 Marshal.SizeOf(typeof(int)) * elemensNum);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

var cudaSize = default(SCudaDevicePtr);
 cudaResult = CudaApi.CudaMemAlloc(
 ref cudaSize,
 Marshal.SizeOf(typeof(int)));
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Efectua a cópia do primeiro vector para o dispositivo
 var handle = GCHandle.Alloc(
firstVector,
GCHandleType.Pinned);
 var size = Marshal.SizeOf(typeof(int));
 var hostPtr = handle.AddrOfPinnedObject();

cudaResult = CudaApi.CudaMemcpyHtoD(
 firstCudaVector,
 hostPtr,
 elemensNum * size);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

handle.Free();

// Efectua a cópia do segundo vector para o dispositivo
 handle = GCHandle.Alloc(
secondVector,
GCHandleType.Pinned);
 hostPtr = handle.AddrOfPinnedObject();

cudaResult = CudaApi.CudaMemcpyHtoD(
 secondCudaVector,
 hostPtr,
 elemensNum * size);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

handle.Free();

var vectorSizePtr = Marshal.AllocHGlobal(
Marshal.SizeOf(typeof(int)));
 cudaResult = CudaApi.CudaMemcpyHtoD(
 cudaSize,
 hostPtr,
 size);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

Marshal.FreeHGlobal(vectorSizePtr);

// Reserva espaço para o vector de argumentos do kernel
 var managedPtrArray = new IntPtr[4];
 var ptrSize = Marshal.SizeOf(typeof(IntPtr));
 var unmanagedArrayPtr = Marshal.AllocHGlobal(ptrSize * 3);

// Procede à criação dos objectos em código não gerido
 var managedElementPtr = Marshal.AllocHGlobal(Marshal.SizeOf(
typeof(SCudaDevicePtr)));
 managedPtrArray[0] = managedElementPtr;
 Marshal.StructureToPtr(
firstCudaVector,
 managedElementPtr,
false);
 Marshal.WriteIntPtr(unmanagedArrayPtr, 0, managedElementPtr);

managedElementPtr = Marshal.AllocHGlobal(
Marshal.SizeOf(typeof(SCudaDevicePtr)));
 managedPtrArray[1] = managedElementPtr;
 Marshal.StructureToPtr(
secondCudaVector,
managedElementPtr,
false);
 Marshal.WriteIntPtr(
unmanagedArrayPtr,
ptrSize,
managedElementPtr);

managedElementPtr = Marshal.AllocHGlobal(
Marshal.SizeOf(typeof(SCudaDevicePtr)));
 managedPtrArray[2] = managedElementPtr;
 Marshal.StructureToPtr(
resultCudaVector,
managedElementPtr,
false);
 Marshal.WriteIntPtr(
unmanagedArrayPtr,
2 * ptrSize,
managedElementPtr);

managedElementPtr = Marshal.AllocHGlobal(
Marshal.SizeOf(typeof(SCudaDevicePtr)));
 managedPtrArray[3] = managedElementPtr;
 Marshal.StructureToPtr(
cudaSize,
 managedElementPtr,
false);
 Marshal.WriteIntPtr(
unmanagedArrayPtr,
3 * ptrSize,
managedElementPtr);

// Realiza a chamada
 cudaResult = CudaApi.CudaLaunchKernel(
 cudaFunc,
 (uint)elemensNum,
 1,
 1,
 1,
 1,
 1,
 0,
 new SCudaStream(),
 unmanagedArrayPtr,
 IntPtr.Zero);

cudaResult = CudaApi.CudaCtxSynchronize();

// Liberta o conjunto de argumentos reservado
 Marshal.FreeHGlobal(unmanagedArrayPtr);
 for (int i = 0; i &amp;amp;lt; 4; ++i)
 {
 var current = managedPtrArray[i];
 Marshal.FreeHGlobal(current);
 }

// Copia de volta o terceiro vector para o anfitrião
 handle = GCHandle.Alloc(result, GCHandleType.Pinned);
 hostPtr = handle.AddrOfPinnedObject();
 cudaResult = CudaApi.CudaMemcpyDtoH(
 hostPtr,
 resultCudaVector,
 size * elemensNum);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

handle.Free();

// Imprime o conteúdo dos vectores
 for (int i = 0; i < elemensNum; ++i)
 {
 Console.Write("{0} ", firstVector[i]);
 }

Console.WriteLine();

for (int i = 0; i < elemensNum; ++i)
 {
 Console.Write("{0} ", secondVector[i]);
 }

Console.WriteLine();

for (int i = 0; i < elemensNum; ++i)
 {
 Console.Write("{0} ", result[i]);
 }

// Liberta o primeiro vector do GPU
 cudaResult = CudaApi.CudaMemFree(firstCudaVector);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Liberta o segundo vector do GPU
 cudaResult = CudaApi.CudaMemFree(secondCudaVector);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Liberta o vector do resultado do GPU
 cudaResult = CudaApi.CudaMemFree(resultCudaVector);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Liberta o espaço reservado para conter o número
// de elementos de cada vector
 cudaResult = CudaApi.CudaMemFree(cudaSize);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Remove o módulo do contexto actual
 cudaResult = CudaApi.CudaModuleUnload(module);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Descarta o contexto
 cudaResult = CudaApi.CudaCtxDestroy(context);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }
 }

Dada a pretensão de utilizar CUDA, é necessário, em primeiro lugar, inicializar o driver CUDA. De seguida, é obtido o primeiro dispositivo registado cujo valor ordinal é 0. Um contexto é muito semelhante a um processo de anfitrião onde, por exemplo, cada conjunto de variáveis tem um espaço de endereçamento próprio. As funções são, portanto, carregadas e executadas no âmbito de um processo. Todos os recursos reservados num determinado contexto são libertados aquando da sua destruição. Os passos que se seguem à criação do contexto são o carregamento do módulo que contém a função que pretendemos executar que, neste caso, é a VecAdd, a obtenção do seu apontador para chamada de execução a partir do anfitrião e o lançamento do kernel respectivo. Como as funções do driver executam no exterior da máquina virtual do .NET, é necessária uma interligação entre processos .NET e processos de código não gerido. Não serão aqui discutidas as  formas de transferir dados entre código gerido e não gerido.

Após a execução do kernel é sincronizado o contexto do dispositivo com o processo de anfitrião. Apesar da destruição do contexto libertar os recursos reservados, talvez constitua uma boa prática implementar a libertação dos recursos do contexto de modo a ser possível reutilizá-lo em computações futuras.

 

Publicado em Computadores e Internet | Etiquetas , , , | Publicar um comentário

A análise do ponto de vista histórico

Nestes últimos tempos tenho investigado alguns dos principais temas de análise com o intuito de expor as ideias básicas de um ponto de vista histórico. O texto, que se encontra em fase de desenvolvimento e será submetido a futuras actualizações, pode ser encontrado aqui, organizado por capítulos. Apesar de aí encontrarmos uma exposição mais detalhada, talvez seja pertinente o resumo de alguns dos tópicos mais interessantes. Exponho-os aqui numa ordem arbitrária.

Dirijo, em primeiro lugar, a atenção para a função gama que pode ser encarada como a função que define x!=x(x-1)(x-2)\cdots 1 para valores não inteiros de x. Numa carta de 1729 dirigida a Goldbach, Bernoulli sugeriu a seguinte função de interpolação para n!,

n!\approx \left(A+\frac{n}{2}\right)^{n-1}\left(\frac{2}{1+n}\cdot\frac{3}{2+n}\cdot\frac{4}{3+n}\cdots\frac{A}{A-1+n}\right)

O autor notou que, quanto maior for o valor de A, maior é a aproximação da expressão ao valor de n!. De facto, com n=3 e A=16, temos

3!\approx \left(16+\frac{2}{2}\right)^2\left(\frac{2}{17}\frac{3}{18}\right)=6\frac{1}{204}

isto é, proporciona-nos o valor de 3! a menos de um valor tão pequeno quanto \frac{1}{204}. Se n for um valor inteiro, verificamos facilmente que, de acordo com a expressão, temos

\lim_{k\to+\infty}\left\lbrack \frac{2\times 3\times\cdots\times n}{(k+1)(k+2)\cdots (k+n-1)}\left(k+\frac{n}{2}\right)^{n-1}\right\rbrack

Se notarmos que

\frac{1}{(k+1)(k+2)\cdots (k+n-1)}=\frac{k!}{(k+n-1)!}=\frac{1}{n!}\frac{k!}{(n+1)(n+2)\cdots (n+k-1)}

Podemos, portanto, definir

x!=\lim_{k\to +\infty}\left\lbrack \frac{k!}{(x+1)(x+2)\cdots(x+k-1)}\left(k+\frac{x}{2}\right)^{x-1}\right\rbrack

para qualquer valor de x>0. Convém notar que o autor quedou-se na primeira expansão e, portanto, podemos considerar que, apesar de estar próximo da definição de uma função de interpolação para o factorial de um número, não a escreveu explicitamente.
Uns dias mais tarde, Euler escreveu uma carta ao mesmo destinatário onde sugeriu a fórmula

x!=\lim_{k\to +\infty}\left\lbrack \frac{k!}{(n+1)(n+2)\cdots (n+k)}(k+1)^x\right\rbrack

e a demonstração de que realmente x! coincide com o valor do factorial de x quando x é um valor inteiro. Se observarmos os resultados de ambos os autores, concluímos que

x!=\lim_{k\to +\infty}\left\lbrack \frac{k!}{(x+1)(x+2)\cdots(x+k)}\left(k+\alpha(x)\right)^x\right\rbrack

onde \alpha(x) é uma função sempre finita de x. A consideração de \alpha(x)=0 conduz-nos à definição de Gauss, nomeadamente,

x!=\lim_{k\to +\infty}\left\lbrack \frac{k!k^x}{(x+1)(x+2)\cdots(x+k)}\right\rbrack

No seu trabalho sobre o cálculo integral, Legendre introduziu a notação \Gamma(x) para a função que satisfaz a relação de recorrência \Gamma(x+1)=x\Gamma(x), seguindo-se que \Gamma(x+1)=x! e, portanto,

\Gamma(x)=\lim_{k\to +\infty}\left\lbrack \frac{k!k^x}{x(x+1)(x+2)\cdots(x+k)}\right\rbrack

A representação integral \Gamma(x)=\int_0^{+\infty}t^xe^{-t} foi obtida por Euler, tendo-se inspirado no trabalho de Wallis sobre a quadratura da circunferência.

Como segundo ponto, considero aqui a expansão do binómio e algumas das suas consequências. No seu tratado sobre o triângulo aritmético, Pascal estudou a estrutura triangular de números que recebeu o seu nome. Nessa mesma obra publicou algumas das aplicações de tal estrutura, entre elas, encontrando-se a expansão do binómio com expoente inteiro positivo. De modo a determinar uma fórmula fechada para cada entrada do triângulo, Pascal observou que, sendo \binom{n}{k} o coeficiente binomial então vale a identidade

\frac{\binom{n}{k+1}}{\binom{n}{k}}=\frac{n-k}{k+1}

Como \binom{n}{0}=1, concluímos facilmente que

\binom{n}{k}=\frac{n!}{k!(n-k)!}

Uma vez conhecida a fórmula para os coeficientes binomiais, não é difícil mostrar a sua validade com base nas suas propriedades. Estas propriedades podem ser usadas para obter a expansão do binómio para valores inteiros do expoente. Estas mesmas propriedades permitem mostrar a identidade de Leibniz, nomeadamente,

\frac{d^n(fg)}{dx^n}=\sum_{i=0}^n\binom{n}{i}\frac{d^{n-i}f}{dx^{n-i}}\frac{d^ig}{dx^i}

A generalização da expansão do binómio a quaisquer valores do expoente foi obtida por Newton com o auxílio do seu método das fluxões aliado à teoria das equações infinitas. Apresentamos agora um esboço da sua demonstração, recorrendo aos trâmites do cálculo diferencial. Seja então f(x)=(x+1)^\alpha o binómio que pretendemos expandir. Sabemos que a sua derivada, f'(x) é dada por f'(x)=\alpha(x+1)^{\alpha-1}, isto é, a função f(x) satisfaz a seguinte equação diferencial

(1+x)f'(x)-\alpha f(x)=0

sujeita à condição fronteira f(0)=1. Procuramos, portanto, escrever f(x) na forma

f(x)=a_0+a_1 x+a_2 x^2+\cdots+a_nx^n+\cdots

Se substituirmos esta fórmula na equação diferencial anterior obtemos

0=\left(a_1-\alpha a_0\right)+\left(2a_2+a_1-\alpha a_1\right)x+\left(3a_3+2a_2-\alpha a_2\right)x^2+\cdots+\left\lbrack (n+1)a_{n+1}+(n-\alpha)a_n\right\rbrack x^n+\cdots

A equação vale para qualquer valor de x se se anularem todos os coeficientes, isto é,

\left\lbrace \begin{matrix} a_1-\alpha a_0=0\\ 2a_2+(1-\alpha)a_1=0\\ 3a_3+(2-\alpha)a_2=0\\ \vdots\\ (n+1)a_{n+1}+(n-\alpha)a_n=0\\ \cdots \end{matrix} \right.

A solução do sistema proporciona-nos

a_k=\binom{\alpha}{k}=\frac{\alpha(\alpha-1)\cdots(\alpha-k+1)}{k!}

e, portanto, de um modo geral,

(x+1)^\alpha=\sum_{k=0}^{+\infty}\binom{\alpha}{k}x^k

Um método semelhante permitiu ao mesmo autor obter a expansão das funções trigonométricas. Para o efeito, começou por considerar uma partícula que se move com um movimento circular uniforme ao longo de uma circunferência de raio uniátio de equação x^2+y^2=1. Sabemos que tal movimento é o resultado da sobreposição dos movimentos coordenados

\left\lbrace \begin{matrix} x=\cos t\\ y=\sin t \end{matrix} \right.

Como a partícula se move com velocidade unitária, então

\frac{ds}{dt}=1=\sqrt{\left(\frac{dx}{dt}\right)^2+\left(\frac{dy}{dt}\right)^2}

representado t, neste caso, o ângulo associado ao deslocamento da partícula medido em radianos. A derivação da equação da circunferência, x^2+y^2=1 conduz-nos a

x\frac{dx}{dt}+y\frac{dy}{dt}=0

que, substituído na equação \frac{ds}{dt}=1 resulta em

\frac{dy}{dt}=\sqrt{1-y^2}

A expansão do binómio \sqrt{1-y^2}=\left(1-y^2\right)^{\frac{1}{2}} permite-nos obter uma série para a função t=\arcsin y. Podemos, porém, obter uma expansão para y=\sin t do seguinte modo. Começamos por derivar a equação anterior, advindo

\frac{d^2y}{dt^2}=-\frac{y}{\sqrt{1-y^2}}\frac{dy}{dy}=-y

ou \frac{dy}{dt}+y=0. Observamos ainda que y(0)=0 e \frac{dy}{dt}(t=0)=1. Se considerarmos que

\sin t=a_0+a_1t+a_2t^2+a_3t^3+\cdots+a_nt^n

a sua substituição na equação diferencial aliada à consideração das condições iniciais conduz-nos à expansão

\sin t=\sum_{k=0}^{+\infty}(-1)^k\frac{x^{2k+1}}{(2k+1)!}

O mesmo raciocínio poderá ser utilizado para obter a expansão

\cos t=\sum_{k=0}^{+\infty}(-1)^k\frac{x^{2k}}{(2k)!}

Como terceiro ponto, considero a fórmula de De Moivre, a qual foi originalmente obtida com o auxílio do método das fluxões com um raciocínio em muito semelhante ao que foi utilizado nas expansões consideradas no ponto anterior. De acordo com esta fórmula, temos

\left(\cos x+i\sin x\right)^n=\cos(nx)+i\sin(nx)

Esta fórmula mostra-se facilmente com o auxílio das fórmulas da soma para as funções trigonométricas

\cos(x+y)=\cos x\cos y-\sin x\sin y

e

\sin(x+y)=\cos x\sin y+\cos y\sin x

De facto, a utilização de tais fórmulas permite-nos escrever

\left(\cos x + i\sin x\right)\left(\cos y + i\sin y\right)=\cos(x+y)+i\sin(x+y)

A aplicação recursiva da mesma, com y=x proporciona-nos o resultado enunciado. A demonstração original, apesar de ser mais complicada, permite, mais uma vez, proporcionar-nos uma aplicação do cálculo diferencial. Se definirmos y=\sin(n\theta) e x=\sin\theta, segue-se que

\frac{dy}{d\theta}=n\sqrt{1-y^2}

e

\frac{dx}{d\theta}=\sqrt{1-x^2}

de onde concluímos a expressão

\frac{dy}{dx}=\frac{n\sqrt{1-y^2}}{\sqrt{1-x^2}}

Se fizermos agora as substituições y=i\eta e x=i\xi obtemos

\frac{d\eta}{d\xi}=\frac{\sqrt{1+\eta^2}}{1+\xi^2}

Vejamos que equação diferencial satisfará a função

\varphi=\sqrt{1-\eta^2}+\eta.

Se derivarmos, obtemos a equação separável

\frac{d\varphi}{d\xi}=\frac{n\varphi}{\sqrt{1+\xi^2}}

cuja solução a escolher é aquela que satisfaz a condição \varphi(0)=1. A solução da equação diferencial proporciona-nos a solução

\varphi=\left(\sqrt{1+\xi^2}+\xi^2\right)^n

que, uma vez que \xi=i\sin\theta e \eta=i\sin(n\theta) constitui a fórmula apresentada.

No quarto ponto discutimos a fórmula de Euler de um ponto de vista histórico. No seu artigo de 1702, Bernoulli submeteu um artigo sobre a integração de fracções racionais com base na expansão em fracções parciais. Como exemplo de aplicação, observou que, da identidade,

\frac{1}{1+x^2}=\frac{1}{2}\frac{1}{1+ix}+\frac{1}{2}\frac{1}{1-ix}

se segue o integral

\int \frac{dx}{1+x^2}=\frac{1}{2i}\log\frac{1+ix}{1-ix}

Por outro lado, se aplicarmos a transformação x=\sqrt{1/t^2-1} ao mesmo integral obtemos

\int\frac{dt}{\sqrt{1-t^2}}=\int\frac{dx}{\sqrt{1+x^2}}

O autor pode observar, deste modo, uma relação entre a função logaritmo e a quadratura do círculo. Porém, coube a Euler, numa carte endereçada a Maupertuis, a aplicação do mesmo método para obter

\int_0^1\frac{dx}{1+x^2}=\int_1^\frac{\sqrt{2}}{2}\frac{dt}{\sqrt{1-t^2}}=\frac{\pi}{4}

isto é,

\log\frac{1+i}{1-i}=\log i=i\frac{\pi}{2}

A exponenciação da identidade anterior conduz-nos à interessante fórmula

i=e^{i\frac{\pi}{2}}

A fórmula mais geral pode ser obtida a partir da conhecida expansão para a função exponencial

e^x=\sum_{n=0}^{+\infty}\frac{x^n}{n!}

substituindo x por ix.

Relativamente ao quinto ponto, será pertinente enveredar por um tema que é frequentemente associado à análise complexa mas que talvez tenha sido iniciado no âmbito da análise real. Estamos aqui a referir-nos à teoria dos resíduos. Nas suas lições sobre o cálculo das funções, Lagrange expos uma teoria das funções analíticas com base na expansão em série de Taylor com resto. Por exemplo, por um processo de interpolação, podemos obter

\frac{1}{i}=\frac{1}{x}-\frac{i-x}{x^2}+\frac{(i-x)^2}{x^3}-\frac{(i-x)^3}{x^4}+\cdots+(-1)^{n-1}\frac{(i-x)^{n-1}}{x^n}+(-1)^n\frac{(i-x)^{n+1}}{ix^n}

onde i aqui não representa a unidade imaginária. Trata-se da expansão da função f(x)=\frac{1}{x} em torno do ponto x. Facilmente constatamos que a expressão não é válida quando fazemos x=0 e dizemos que a função nesse ponto não é analítica. O autor observou que as funções habituais são analíticas em toda a parte com excepção de alguns casos pontuais. Em particular, uma função racional \frac{p(x)}{q(x)} é analítica em todos os pontos com excepção dos zeros de q(x). Suponhamos agora, como o fez Cauchy, que q(x)=(x-\alpha)^mq_1(x) onde q_1(\alpha)\ne0. Segue-se que

f(x)=\frac{p(x)}{q(x)}=\frac{g(x)}{(x-\alpha)^m}

onde g(x) é uma função analítica no ponto \alpha, podendo não o ser, com efeito, em outros pontos. Vale, portanto, a expansão

g(x)=g(\alpha)+g'(\alpha)(x-\alpha)+\frac{g''(\alpha)}{2!}(x-\alpha)^2+\cdots+\frac{g^{(m-1)}}{(m-1)!}(x-\alpha)^{m-1}+\psi(x)(x-\alpha)^m

sendo \psi(\alpha) assume um valor finito. Vemos, deste modo, que

f(x)=\frac{g(x)}{(x-\alpha)^m}=\psi(x)+\sum_{k=1}^m\frac{a_k}{(x-\alpha)^k}

onde

\lim_{x\to\alpha}\frac{d^{m-k}}{dx^{m-k}}\frac{(x-\alpha)^mf(x)}{(m-k)!}

É útil notar que se \beta\ne\alpha for um zero de ordem n de q(x) então

f(x)=\psi_1(x)+\sum_{k=1}^n\frac{b_k}{(x-\beta)^k}+\sum_{k=1}^m\frac{a_k}{(x-\alpha)^k}

onde os coeficientes b_k se calculam a partir de f(x) por intermédio da mesma expressão. Definimos, assim, um processo para determinar a expansão em fracções parciais de uma função racional. Porém, tal método requer a determinação de \alpha+\beta+\cdots coeficientes onde \alpha, \beta, … são as ordens dos zeros do denominador. Para simplificarmos o processo observamos que, por exemplo, o coeficiente de ordem -2 de f(x) é igual ao coeficiente de ordem -1 de (x-\alpha)f(x). Esta observação permite construir uma função h(z,x) de tal forma que, sendo a_{-1}(x) o coeficiente de ordem -1 de h(z,x) em torno do ponto z=\alpha, temos

f(x)=\psi(x)+a_{-1}(x)

Ao valor a_{-1} de f(x) damos a designação de resíduo de f(x) no ponto \alpha e denotamos por Res(f(x),x=\alpha). Mostra-se que, sendo x_i com i=1,2,\cdots,n os zeros de q(x),

f(x)=Res\left(\frac{f\left(\frac{1}{z}\right)}{z(1-xz)},z=0\right)+\sum_{i=1}^nRes\left(\frac{f(z)}{x-z},z=x_i\right)

Uma das demonstrações do teorema fundamental da álgebra consiste em mostrar que, sendo p(x) um polinómio não constante então, fazendo f(x)=\frac{1}{p(x)}, temos sempre, se considerarmos o conjunto dos números complexos,

Res\left(\frac{f\left(\frac{1}{z}\right)}{z(1-xz)},z=0\right)=0

e, portanto,

f(x)=\sum_{i=1}^nRes\left(\frac{f(z)}{x-z},z=x_i\right)

Como f(x) é não constante, deverá existir pelo menos um x_i tal que p\left(x_i\right)=0. De facto, de acordo com o teorema de Liouville, se f(x) for uma função analítica então terá de ser constante. Este teorema implica imediatamente que se p(x) não for constante então f(x) não é analítica de onde se segue que deverá existir um x_i tal que p\left(x_i\right)=0. É útil notar ainda que o teorema foi originalmente enunciado pelo autor no decurso do seu estudo das funções duplamente periódicas sobre o qual construíu a sua teoria das funções elípticas.

Como sexto ponto, não incluirei qualquer resultado mas farei alguams observações históricas que considero curisoas. Começando com Ampère, este é sobejamente conhecido pelo trabalho que desenvolveu no âmbito do electromagnetismo e, em particular, pelo seu estudo sobre o campo magnético produzido por correntes eléctricas. Dois dos seus interesses são menos conhecidos. O primeiro (vide), prende-se com a teoria que aventou, independentemente de Avogadro, sobre a constituição molecular da matéria. O segundo resume-se aos seus trabalhos em matemática. Num conjunto de artigos, trabalhou o estendeu o método das características de Monge para a resolução de equações diferenciais em derivadas parciais. Num artigo de 1806, o mesmo autor apresentou a sua teoria das funções analíticas no qual é frequentemente considerar-se ter demonstrado o resultado erróneo de que uma função contínua é diferenciável em toda a parte com a excepção de um número finito de pontos. No entanto, o objectivo do artigo prende-se com a simplificação da teoria de Lagrange das funções analíticas. O seu ponto de partida para o estabelecimento da existência da derivada de uma função é o seguinte:
Uma função da variável x advém nula ou infinita quando x tende para zero se a função diminuir ou aumentar quando x se aproxima de zero de modo que seja inferior a qualquer quantidade dada no primeiro caso e superior a qualquer quantidade dada no segundo.
Além disso, considerou que uma função não é derivável se a sua derivada for infinita. Neste caso, as funções que oscilam indefinidamente numa vizinhança do ponto, tal como \frac{1}{x}\sin\left(\frac{1}{x}\right) não poderão ser consideradas uma vez que apesar do seu valor não estar definido na origem, para qualquer quantidade dada existem sempre valores de x tais que o valor da função é inferior a essa quantidade. Além disso, o conceito de continuidade surgiria cerca de onze anos mais tarde.
Como segunda curiosidade destaco a opinião de Rolle, introdutor de um dos mais importantes teoremas em análise, que era um fervoroso crítico dessa disciplina. Este autor expos o teorema que recebe o seu nome no âmbito da teoria a que designou por método das cascatas. Este método, apesar de ser muito semelhante ao do cálculo diferencial em termos algorítmicos, distanciava-se deste em termos metafísicos. A sua refutação por parte Varignon ajudou a clarificar alguns aspectos mais obscuros do cálculo diferencial. Por outro lado, incutiu a necessidade de elevar o rigor da disciplina.
Por fim, é interessante lembrar o papel do matemático José Anastácio da Cunha que antecipou, nos seus Principios Mathematicos, alguns dos resultados que viriam, muitos anos mais tarde, a ser atribuídos a outros autores. Entre estes, refiro a noção de convergência de uma série que lhe permitiu construir um estudo rigoroso das funções exponencial e logaritmica.

Publicado em Matemática, Sem categoria | Etiquetas , , , , , , , , , , | Publicar um comentário

Migração de dados com o SSIS no modo diferencial

Parte do trabalho que agora desenvolvo prende-se com o tratamento de dados oriundos de um conjunto heterogéneo de fontes com o auxílio do SSIS. Um método para a actualização de uma determinada base-de-dados central consiste em eliminar todos os registos e actualizá-la com os dados mais recentes, o qual requer o movimento de uma enorme quantidade de dados sempre que se pretenda proceder à actualização. Um segundo método consiste apenas na migração dos dados associados às diferenças. Este método, apesar de ser mais elaborado do ponto de vista técnico, tem a vantagem de ser mais eficaz.

Partilhei o ficheiro Migração de dados no modo diferencial.zip no qual exponho o método diferencial sobre um conjunto de ficheiros de XML e onde tentei expor algumas técnicas que permitem a fácil generalização a outros cenários.

Publicado em Computadores e Internet, Sem categoria | Etiquetas , , , , , , | Publicar um comentário

Um texto e uma tradução

O facto de aqui ter deixado de publicar textos em matemática com a mesma regularidade que antes  não significa que tenha deixado de me interessar ou sequer de ter tempo para a disciplina. Com efeito, vou coleccionando várias ideias em rascunhos, à medida que o tempo mo permite, que pretendo partilhar quando considerar que têm um certo grau de maturidade.

Ultimamente tenho implementado alguns dos algoritmos mais conhecidos, nomeadamente no âmbito da álgebra linear e teoria das matrizes. À medida que o vou fazendo, vou anotando as ideias teóricas de um ponto de vista que me parece não ser excessivamente abstracto. Daqui resultou o Alguns resultados em teoria das matrizes onde expus algumas dessas ideias, deixando a nota de que o texto poderá ser continuamente enriquecido.

Para além disso, terminei a tradução que havia começado há algum tempo atrás do artigo seminal de Poincaré, Analysis Situs, que se diz ter revolucionado a área de topologia. Como se trata de um artigo que incluiu cinco suplementos subsequentes, trata-se de uma tradução que poderá ser aumentada.

Publicado em Matemática | Etiquetas , , , , | Publicar um comentário

Administração de bases-de-dados Oracle – cópias de segurança

Quando nos referimos à protecção dos dados, não estamos, normalmente, apenas interessados em vedá-los ao acesso de utilizadores não autorizados mas também na garantia da sua integridade bem como na disponibilidade em caso de falhas graves. Iremos aqui discutir apenas como mitigar os efeitos da ocorrência de problemas no sistema que possam conduzir a perdas ou corrupção de dados com o recurso a cópias de segurança.
Suponhamos que todos ou parte dos ficheiros associados à nossa instância de base-de-dados se encontram armazenados, fisicamente, no mesmo disco ou RAID que, por algum motivo, falha. Por exemplo, poderá ocorrer uma falha no disco que contém os ficheiros de controlo, mantendo-se intactos os ficheiros de dados por se encontrarem num disco diferente. De modo a ser possível recuperar a base-de-dados neste tipo de cenários, é importante agendar cópias de segurança para discos reservados apenas para esse efeito e até para fitas (tapes). No que se segue, serão discutidas a criação de cópias de segurança e a criação de uma nova instância a partir das cópias de segurança associadas a uma outra instância.
O Recovery Manager (RMAN) é a ferramenta que nos facilita as tarefas de gestão de cópias de segurança. Supomos que o SID, o serviço e a palavra-chave da nossa instância são orcl_sid, orcl_sid.local e orcl_pass, respectivamente. Para nos ligarmos à base-de-dados com o RMAN, fazemos, na linha de comandos num computador remoto (utilizando o EZConnect)

>rman target sys/orcl_pass@_._._._:1521/orcl_sid.local

No anfitrião, podemos fazer

>set oracle_sid=orcl_sid
>rman target sys

Neste caso, será pedida a introdução da palavra-chave para continuar. Se fizermos, no RMAN,

RMAN>show all;

obtemos a lista de todos os parâmetros de configuração para a gestão das cópias de segurança. Entre as várias opções de configuração interessam-nos apenas algumas. Começamos por configurar convenientemente a cópia de segurança dos ficheiros de controlo. Para o efeito, indicamos ao RMAN que deverá ser realizada uma cópia automática do ficheiro de controlo em cada cópia de segurança feita:

RMAN>configure controlfile autobackup on;

Este comando indica ao RMAN para efectuar uma cópia de segurança do ficheiro de controlo sempre que este seja alterado, quando é realizada uma cópia de segurança genérica. Este parâmetro encontra-se, por defeito, no estado desligado, off. Isto deve-se ao facto de ser possível realizar cópias de segurança que incluam os ficheiros de controlo sempre que tenham sofrido alterações. Como o administrador pode decidir fazer cópias de segurança que incluam apenas os ficheiros de dados, ignorando os de controlo, torna-se aconselhável ligar o autobackup.
Se a flash recovery area estiver configurada na instância, as cópias de segurança far-se-ão nessa localização por defeito. Caso contrário, no Windows Server, estas cópias serão armazenadas por defeito na directoria Oracle_home\database. A consulta do SQLPlus

SQL>show parameter db_recovery_file_dest;

permite-nos obter a directoria da flash recovery area caso esta esteja definida. Para escolher uma localização para as cópias de segurança dos ficheiros de controlo, fazemos, no RMAN,

RMAN>configure controlfile autobackup format for device type disk to '...\Desktop\%F';

Assim, todas as cópias de segurança automáticas serão realizadas na directoria …\Desktop, sendo os respectivos nomes gerados de acordo com o formato %F. Neste formato, o nome do ficheiro será dado por c-IIIIIIIIII-YYYYMMDD-QQ onde IIIIIIIIII contém o DBID da instância, YYYMMDD contém a data de criação e QQ contém um número hexadecimal que assume valores entre 00 e FF. É conveniente especificar uma directoria diferente para cada instância como veremos. A título de curiosidade, o parâmetro DBID pode ser obtido no SQLPlus com o auxílio da consulta

SQL>select dbid from v$database;

Porém, não é possível realizar esta consulta quando os ficheiros de controlo da base-de-dados se encontram corrompidos. Neste caso, a recuperação requer o conhecimento deste parâmetro que só é possível obter a partir desses ficheiros. O registo deste valor encontra-se no nome dos ficheiros que constituem as cópias e, se estas se encontrarem numa directoria específica à instância, não corremos o risco de confundir os DBID de várias instâncias. Se estes fossem gravados na mesma directoria, Oracle_home/database, seria necessário associar os ficheiros correctos às respectivas instâncias.
O segundo passo a tomar no que concerne à configuração do RMAN consiste em definir a política de retenção das cópias de segurança. Suponhamos que definimos o parâmetro associado no RMAN com

RMAN>configure retention policy to recovery window of 3 days;

As cópias de segurança mais antigas do que 3 dias serão marcadas como obsoletas, sendo eliminadas sempre que o espaço em disco se torne crítico ou se o administrador configurar o RMAN para, ao fazer as cópias de segurança, eliminar as que se encontram nesse estado. O comando

RMAN>delete obsolete;

permite eliminar todas as cópias de segurança que se encontram marcadas como obsoletas.
Para consultar o tempo de retenção das cópias de segurança dos ficheiros de controlo, podemos recorrer ao SQLPlus e fazer a consulta

SQL>show parameter control_file_record_keep_time;

É conveniente escolher um valor de retenção para este tipo de ficheiros superior ao tempo de retenção das cópias de segurança da base-de-dados. Para o efeito, fazemos, por exemplo,

SQL>alter system set control_file_record_keep_time=6;

no SQLPlus. O sistema fica assim configurado para manter cópias de segurança dos ficheiros de controlo durante 6 dias.

Realizar cópias de segurança com o RMAN

O RMAN permtie fazer cópias de segurança de toda a base-de-dados, dos tablespaces, dos ficheiros de dados, dos archive logs, como descrito nos Backup Principles. As cópias de segurança podem ser incrementais, isto é, é realizada uma cópia de segurança de toda a base-de-dados e as cópias de segurançaa seguintes registam apenas as alterações. Por exemplo, para efectuar uma cópia de segurança que sirva de base a um processo incremental, realiza-se no RMAN com o auxílio do bloco de comandos

RMAN>SET COMPRESSION ALGORITHM 'BASIC' AS OF RELEASE 'DEFAULT' OPTIMIZE FOR LOAD TRUE;
run {
allocate channel oem_backup_disk1 type disk format '...\Desktop\%U';
backup incremental level 0 cumulative as COMPRESSED BACKUPSET tag '%TAG' database;
backup as COMPRESSED BACKUPSET tag '%TAG' archivelog all not backed up delete all input;
release channel oem_backup_disk1;
}
RMAN>allocate channel for maintenance type disk;
RMAN>delete noprompt obsolete device type disk;
RMAN>release channel;

A primeira linha define o canal que será utilizado para efectuar a cópia de segurança (neste caso, para o disco). A segunda linha realiza a cópia que servirá de base (nível 0) a um processo incremental acumulativo, isto é, todas as cópias de segurança de nível 1 mantêm as diferenças relativamente à cópia de nível 0, em contraste com o diferencial no qual a última cópia do nível 1 mantém as diferenças relativas à cópia anterior nesse nível (ou à cópia do nível zero, caso seja a primeira do nível 1). A terceira linha, permite indicar que os archive logs serão também copiados e os originais são eliminados após a cópia. O canal requerido inicialmente terá de ser libertado novamente. Os últimos três comandos permitem reservar um canal para manutenção e que é utilizado para eliminar as cópias de segurança marcadas como obsoletas.
O script apresentado acima é gerado pelo Enterprise Manager que se trata de uma ferramenta útil no agendamento de cópias des segurança. No caso da Oracle 11g, a ligação para o agendamento das cópias de segurança encontra-se no separador Availability, grupo Manage, item Schedule Backup. As várias opções definidas na página permitem configurar vários tipo de cópias de segurança.
Se atentarmos na segunda linha do scrip verificamos que esta configura a aplicação para realizar a cópia de segurança naquilo que recebe o nome de backup set. Este tipo de ficheiros podem agrupar os vários tipos de ficheiros que constituem as cópias de segurança da base-de-dados e a sua leitura só pode ser realizada por intermédio do RMAN.

A recuperação de uma instância a partir das cópias de segurança

Vários cenários podem ocorrer aquando da corrupção ou destruição de ficheiros da base-de-dados. Aqui iremos analisar o cenário no qual temos apenas as cópias de segurança, tendo sido eliminada a instância da base-de-dados que pretendemos recuperar. O procedimento para reconstruir uma instância a partir das cópias de segurança descreve-se no seguinte conjunto de passos.

  1. Instalamos a mesma versão do motor de bases-de-dados no anfitrião onde pretendemos efectuar a recuperação. Se a instalação já existir, convém verificar se se encontra na mesma versão (quando nos conectamos a uma instância com o SQLPlus, a versão surge à cabeça).
  2. Tornamos acessíveis as cópias de segurança, incluindo as dos ficheiros de controlo, a partir do novo anfitrião, por intermédio de uma pasta partilhada ou de uma cópia de ficheiros. Vamos supor que as cópias de segurança são disponibilizadas na directoria BCK do anfitrião ou por este acessível. Aí dever-se-ão encontrar também as cópias de segurança spfiles e dos ficheiros de controlo que se encontram na directoria configurada para o efeito (ver acima).
  3. Criamos uma base-de-dados cuja parte inicial do nome (as primeira oito letras) deverá coincidir com a parte inicial do nome da instância original. Podemos criar a instância por intermédio do comando create database ou recorrer ao ambiente gráfico proporcionado pelo Database Configuration Assistant. Após a criação, deveremos colocar a base-de-dados no estado nomount:
    SQL>shutdown immediate;
    SQL>startup nomount;
    

    quer seja no SQLPlus, quer seja no RMAN.

  4. Alteramos valor do DBID para o da instância original. O DBID obtém-se a partir do nome dos ficheiros nas cópias de segurança dos ficheiros de controlo, caso tenhamos configurado o ambiente. Para consultar o DBID numa instância que se encontra em funcionamento, é suficiente fazer
    SQL>select dbid from v$instance;
    

    Para alterar o valor do DBID da nova instância, de modo a compatibilizar com a da instância original, fazer, no RMAN (conectado à nova instância),

    RMAN>set dbid 1561521314;
    
  5. Recuperamos o spfile que contenha as configurações da instância original. Para o efeito, fazemos, no RMAN,
    RMAN>RUN
    {
    ALLOCATE CHANNEL disk1 DEVICE TYPE DISK FORMAT ‘BCK‘;
    SET CONTROLFILE AUTOBACKUP FORMAT FOR DEVICE TYPE DISK TO '%F';
    RESTORE SPFILE
    TO PFILE '...\temp\inittarget.ora'
    FROM AUTOBACKUP;
    SHUTDOWN ABORT;
    }
    

    onde BCK corresponde à directoria onde colocámos as cópias de segurança. Note-se que este comando permite recuperar um ficheiro pfile para a directoria …\temp.

  6. Recuperamos o ficheiro de controlo. Em primeiro lugar, editamos o ficheiro pfile de modo a alterarmos o caminhos dos ficheiros de controlo e dos archive logs caso existam. De seguida, reiniciamos a base-de-dados no estado nomount, a partir do ficheiro pfile
    SQL>startup force nomount pfile='...\temp\inittarget.ora';
    

    O comando anterior pode ser emitido tanto no SQLPlus como no RMAN. Para recuperar o ficheiro de controlo, identificamos a data para a qual pretendemos recuperar a instância, supondo que existem as cópias de sergurança respectivas. Com o auxílio do RMAN, fazemos

    RMAN>RUN
    {
    ALLOCATE CHANNEL disk1 DEVICE TYPE DISK FORMAT ‘BCK‘;
    RESTORE CONTROLFILE FROM AUTOBACKUP;
    ALTER DATABASE MOUNT;
    }
    

    O ficheiro de controlo é, portanto, recuperado para as directorias especificadas no pfile e a base-de-dados já pode ser elevada ao estado de mount.

    SQL>alter database mount;
    
  7. Restauramos os ficheiros de dados e depois recuperamo-los. Note-se que se as cópias de segurança contiverem cada um dos ficheiros de dados isoladamente, é suficiente copiá-los para a nova directoria. Aqui assumimos que as cópias de segurança são realizadas em backup set e, portanto, são apenas restauráveis com o auxílio do RMAN. Uma vez que o ficheiro de controlo já foi restaurado, fazemos, no SQLPlus
    SQL>spool '...\temp\datafiles_result.txt';
    SQL>select file#, name from v$datafile;
    SQL>select file#, name from v$tempfile;
    SQL>spool off;
    

    O ficheiro datafiles_result.txt com a lista dos caminhos para os ficheiros de dados e para os ficheiros dos tablespaces temporários será criado na directoria …\temp. Ao editarmos o ficheiro, verificamos que se tratam dos caminhos associados à instância antiga que terão, eventualmente, de ser alterados na nova instância. Fazemos, no RMAN,

    RMAN>catalog start with BCK;
    RMAN>Run{
    # Changes the backup restore data file paths
    SET NEWNAME FOR DATAFILE 1 TO ‘New Datafile 1 path’;
    SET NEWNAME FOR DATAFILE 2 TO ‘New Datafile 2 path’;
    …
    # Changes the name of data files in control file
    SQL "ALTER DATABASE RENAME FILE ''Old Datafile 1 path'' TO ‘'New Datafile 1 path’’";
    SQL "ALTER DATABASE RENAME FILE ''Old Datafile 2 path'' TO ‘'New Datafile 2 path’’";
    …
    SQL "ALTER DATABASE RENAME FILE ''Old Temp path'' TO ‘’New Temp path’’";
    SET UNTIL TIME 'SYSDATE-15';
    RESTORE DATABASE;
    SWHICH DATAFILE ALL;
    RECOVER DATABASE;
    }
    

    O primeiro comando permite catalogar as cópias de segurança que existem na directoria BCK. O bloco seguinte permite recuperar todos os ficheiros da cópia de segurança com quinze dias para os caminhos especificados e, de seguida, corrigir o ficheiro de controlo de modo a contemplar os novos caminhos para os ficheiros. A base-de-dados é, finalmente, recuperada.

  8. Abrir a base-de-dados, emitindo o comando
    SQL>alter database open resetlogs;
    

Note-se que, caso a base-de-dados original estivesse configurada no modo de ARCHIVELOGS, é ainda necessário proceder à alteração dos respectivos caminhos. Para o efeito, primeiro consultamos os caminhos actuais,

SQL>show parameter log_archive_dest;

Verificamos se alguns destes parâmetros requerem actualização do caminho. Por exemplo, supondo que o parâmetro log_archive_dest_1 requer uma alteração, fazemos

SQL>alter system set log_archive_dest_1='...\archive_log_dest_1';

para colocar a pasta …\achive_log_dest_1 como primeiro destino dos diários de arquivo. O caso em que seja necessário aplicar os archive logs para recuperar a base-de-dados para um instante posterior não foi aqui descrito.

Publicado em Computadores e Internet | Etiquetas , , , , | Publicar um comentário

Administração de bases-de-dados Oracle 11g (Básico)

Depois de adquirir alguma experiência na área de administração de bases-de-dados Oracle e, em particular, na recuperação de bases-de-dados em cenário de catástrofe, considero pertinente abordar o tema, em primeiro lugar, expondo os conceitos na vertente prática e, em segundo, complementá-los de forma teórica. É este o objectivo deste texto. No que se segue, em todas as referências a processos de sistema operativo no qual este não esteja identificado, deverá sempre assumir-se o Windows Server.

Instalação

A instalação do motor da base-de-dados é o primeiro passo a tomar caso este ainda não tenha sido dado. Antes de proceder à sua instalação é conveniente ler o manual de utilização de modo a garantir que o sistema que se pretende ser o anfitrião é suportado e possui o mínimo de recuros necessários. É possível encontrar este manual na ligação Database Quick Installation Guide. Supondo que o sistema se encontra de acordo com os requisitos e os recursos de memória e processamento são suficientes, estamos aptos a proceder à instalação do programa, o qual pode ser obtido em Oracle Database Software Downloads. É importante lembrar que os programas disponibilizados pela Oracle são regulados por uma licença que restringe legalmente a sua utilização e deverá ser lida com a devida atenção.
A instalação da base-de-dados Oracle 11g não oferece grandes dificuldades se for realizada em sistemas operativos profissionais, empresariais ou de servidor (como é o caso do Windows Server/Enterprise ou Linux). A sua distribuição é realizada por intermédio de dois ficheiros compactados em formato zip, os quais terão de ser descompactados para uma directoria à escolha. Entre os ficheiros descompactados encontra-se o executável Setup.exe que permite iniciar o instalador. O instalador requer a introdução de alguns parâmetros de configuração, nomedamente, a Oracle Base Location (localização onde serão guardados os binários de base-de-dados) e a Oracle Home Location (localização dos ficheiros que constituem o motor em si). Além disso, permite criar uma instância inicial. Como cada instância consiste numa base-de-dados e o motor suporta várias instâncias, a sua criação poderá ser adiada para uma fase posterior desde que seja criado um Listener. A criação de uma instância aquando da instalação resulta numa base-de-dados completamente funcional com o menor esforço possível e é aconselhável numa fase inicial (de facto, suporei aqui que esta opção foi seleccionada). Para efeitos de testes, a escolha de uma instalação por defeito é suficiente.
Suponhamos que instalámos o motor da base-de-dados, criando uma instância denominada orcl com palavra-chave do sistema orcl num anfitrião qualquer. Pretendemos, portanto, conectar-nos ao sistema. Duas formas são imediatamente disponibilizadas, nomeadamente, o Oracle Enterprise Manager e o SQLPlus.
O Oracle Enterprise Manager consiste numa aplicação de servidor que pode ser acedida por intermédio de um navegador como o Chrome ou o Firefox. Em ambiente Windows, é criada uma ligação no menu Oracle – OraDb11g_home1 sita no submenu Todos os Programas (All Programs) do menu Iniciar (Start) designada por Database Control – orcl que permite lançar o navegador definido por defeito no endereço e porta correctos. Para acedermos às páginas de controlo da base-de-dados, caso esta opção tenha sido escolhida durante a instalação da instância de base-de-dados, introduzimos o utilizador SYS, a palavra-chave definida durante a instalação e conectamo-nos como SYSDBA. Neste ambiente é possível realizar praticamente todas as tarefas de administração da base-de-dados.
O SQLPlus consiste num utilitário de linha de comandos que permite ao administrador executar qualquer comando ou script na base-de-dados, incluindo consultas de SQL. Se executarmos, na linha de comandos do anfitrião,

>sqlplus sys/orcl as sysdba

o SQLPlus tenta uma ligação à instância que se encontra definida por defeito (no caso do Windows Server, vem definido na tabela dos registos – Configuration Parameters and the Registry) com o utilizador SYS e palavra-chave orcl. É útil observar que, quando é executado o SQLPlus, surge a sua versão no cabeçalho. Esta informação torna-se importante quanto pretendemos recuperar bases-de-dados em instalações diferentes, sendo os erros, devidos a diferenças nas versões, difíceis de detectar.
Para aceder à instância específica, teremos de alterar a variável de ambiente oracle_sid na sessão actual. Para garantir que o SQLPlus se conecta à instância orcl, introduzimos, na linha de comandos, supondo que pretendemos conectar-nos à base-de-dados com SID=orcl (o SID é definido durante a criação da base-de-dados, caso tenha sido seleccionada a opção de criação de uma instância durante a instalação),

>set oracle_sid=orcl
>sqlplus sys/orcl as sysdba

Uma vez iniciado o SQLPlus, podemos emitir comandos de administração bem como realizar consultas sobre as bases-de-dados. Por exemplo, a consulta

SQL>select instance_name, status from v$instance;

permite-nos confirmar o SID da base-dados ao qual nos conectámos e o seu estado (STARTED, MOUNTED, OPEN).
Deixo aqui uma nota final sobre a conexão à base-de-dados com o auxílio do SQLPlus é efectuada a partir do anfitrião (máquina onde se encontra instalada a base-de-dados). Como podemos ver no capítulo Configuring Users, Groups and Environments for Oracle Database do Database Installation Guide, vários grupos são criados no servidor aquando da instalação e, em particular, o grupo ORA_DBA. Se executarmos o SQLPlus numa conta de utilizador que se encontre registada no grupo ORA_DBA (ver como acrescentar um utilizador a um grupo no Windwos Server aqui), é-nos suficiente emitir os comandos

>set oracle_sid=orcl
>sqlplus / as sysdba

para nos conectarmos à base-de-dados como adiministradores.
Se não proporcionarmos a palavra-chave, isto é, se fizermos

>sqlplus sys as sysdba

o programa força-nos a introduzi-la posteriormente. Esta opção é mais segura na medida em que o seu conteúndo não é imprimido no ecrã.

Encerramento e Inicialização

Apesar de ser obrigatório, à primeira vista, manter uma base-de-dados ligada indefinidamente, por vezes é necessário encerrá-la por motivos de manutenção. Assim, é útil compreender todos os estados em que uma determinada base-de-dados se pode encontrar. Veja-se, por exemplo, Starting Up and Shutting Down.
Existem quatro modos de encerramento de uma base-de-dados Oracle. Estes são

  1. Normal: Para encerrar a base-de-dados no modo normal é sufciente emitir o comando
    SQL>shutdown normal;

    no SQLPlus. Neste modo, a base-de-dados é encerrada apenas quando todos os utilizadores ligados fecharem as suas conexões, apesar de novas conexões serem recusadas. Convém notar que se algum utilizador não terminar a sua sessão, o encerramento não é finalizado.

  2. Imediato: Para encerrar a base-de-dados no modo imediato, emitimos o comando
    SQL>shutdown immediate;

    no SQLPlus. Neste modo, a base-de-dados é encerrada independentemente dos utilizadores que se encontram ligados, sendo todas as suas transacções anuladas (dados úteis possivelmente descartados).

  3. Transaccional: O comando
    >SQL>shutdown transactional;

    no SQLPlus permite encerrar a base-de-dados no modo transaccional. Este tipo de encerramento aguarda pela conclusão de todas as transacções (conjuntos de alterações que aguardam a conclusão da submissão para a base-de-dados) que se encontrem activas, apesar de nenhuma nova transacção ser permitida.

  4. Abortar: Por vezes é necessário abortar uma base-de-dados com o comando
    SQL>shutdown abort;

    Neste modo, a base-de-dados é imediamtamente encerrada podendo ser necessária uma recuperação dos ficheiros uma vez que estes poderão não se encontrar devidamente actualizados.

A inicialização de uma base-de-dados Oracle efectua-se em três fases, nomeadamente, STARTED, MOUNTED e OPEN. O comando

SQL>startup;

permite executar consecutivamente todas as fases, elevando a base-de-dados ao estado OPEN e tornando-a completamente funcional. Note-se que o SQLPlus continua a ligar-se à instância configurada mesmo quando esta se encontra encerrada. Neste estado, a base-de-dados aceita um conjunto limitado de comandos.
O comando

SQL>startup nomount;

permite elevar a base-de-dados ao estado de STARTED. Caso pretendamos passar ao estado MOUNTED teremos de utilizar o comando

SQL>alter database mount;

De seguida, se pretendermos disponibilizá-la, recorremos ao comando

SQL>alter database open;

Vemos que o startup é executado apenas uma vez, sendo as restantes elevações conseguidas por intermédio da instrução alter database. Se a base-de-dados se encontrar encerrada, é possível iniciá-la em qualquer um dos estados. Por exemplo,

SQL>startup mount;

permite iniciar a base-de-dados no modo MOUNTED, passando automaticamente pelo modo STARTED.

Os ficheiros spfile e controlfile

Para termos uma ideia do que acontece em cada um dos estados de inicialização, é conveniente percebermos o propósito de dois ficheiros fundamentais, nomedamente o spfile (Server Parameter File) e o control file. O comando no SQLPlus

SQL>create pfile='...\Desktop\pfile_orcl.ora' from spfile

cria um ficheiro denominado pfile_orcl.ora na directoria ...\Desktop. Se editarmos o ficheiro com um editor detexto como o notepad++ verificamos que este contém uma lista de parâmetros e respectivos valores que parametrizam a instância durante a sua inicialização. Este formato era utilizado para parametrizar bases-de-dados em versões antigas. O spfile constitui uma evolução do mesmo e não pode ser alterado com um auxílio de um editor de texto. Entre estes encontra-se definido o parâmetro control_files, definindo a localização dos ficheiros de controlo (uma ou mais cópias). O ficheiro spfile consiste, portanto, num ficheiro de servidor onde estão persistidas as configurações associadas a uma determinada instância. Para consultar a directoria onde o ficheiro de inicialização se encontra, podemos emitir o comando no SQLPlus

SQL>show parameter spfile;

A instrução

SQL>show parameter;

permite obter o valor de todos os parâmetros configuráveis na base-de-dados. O comando

SQL>startup pfile='...\Desktop\pfile_orcl.ora'

permite iniciar a base-de-dados configurada pelos parâmetros definidos no ficheiro pfile_orcl.ora. Encontrando-se a base-de-dados inicializada com um pfile, é possível criar um ficheiro spfile com o comando

SQL>create spfile from pfile;

Assim, um ficheiro spfile é criado na directoria que está definida para conter este tipo de ficheiros. De um modo geral, é possível criar um ficheiro spfile a partir de um pfile com a ajuda da instrução

SQL>create spfile='...\Desktop\spfile_orcl.ora' from pfile='...\Desktop\pfile_orcl.ora';

e também um ficheiro pfile a partir de um spfile com

SQL>create pfile='...\Desktop\pfile_orcl_created.ora' from pfile='...\Desktop\spfile_orcl.ora';

Se pretendermos inicializar a base-de-dados, importando os parâmetros de um ficheiro do tipo spfile que se encontre numa localização diferente da definida, por exemplo, …\Desktop\spfile.ora,
criamos um ficheiro …\Desktop\pfile_orcl.ora com a linha
SPFILE=’…\Desktop\spfile.ora’
e iniciamos a base-de-dados com

SQL>startup pfile='pfile.ora';

Os parâmetros do spfile podem ser alterados no SQLPlus com o auxílio da instrução “alter system”.
Verificámos atrás que um dos parâmetros explicitados no spfile contém o caminho para os ficheiros de controlo e como este parâmetro pode ser consultado, isto é,

SQL>show parameter control_files;

proporciona-nos essa lista. Os ficheiros de controlo contêm a informação sobre a estrutura física da base-de-dados, mantendo, por exemplo, o caminho para todos os ficheiros de dados que a constituem. Além desta informação, também lá se poderão encontrar os últimos registos de cópias de segurança efectuadas com o auxílio do RMAN.
É possível obter um script que nos permita criar um ficheiro de controlo da nossa base-de-dados. Para o efeito temos de determinar, em primeiro lugar, onde se encontra a directoria de rastreio da base-de-dados com a consulta

SQL>select value from v$diag_info where name='Default Trace File';

no SQLPlus. O comando

SQL>alter database backup controlfile to trace;

permite salvaguardar o ficheiro de controlo no ficheiro de rastreio actual. No final do ficheiro (à altura da execução do comando) deverá surgir o script que possibilita a construção de um ficheiro de controlo semelhante ao que se encontra na nossa base-de-dados. Verificamos que um ficheiro de controlo é criado com a base-de-dados em modo STARTED através do comando create controlfile. Como se pode observar, a criação manual de um ficheiro de controlo pode ser extensa, sendo aconselhável manter uma cópia de segurança regular destes ficheiros.
De um modo resumido, a inicialização da base-de-dados começa com a leitura do spfile para a obtenção da parametrização. A entrada na fase STARTED não requer a existência do ficheiro de controlo nem dos ficheiros de dados. Se este ficheiro não existir é nesta fase que deverá ser criado.
Na fase MOUNTED, tanto o ficheiro de controlo como os ficheiros de dados são carregados. A base-de-dados só passa ao estado OPEN se toda a estrutura física se encontrar livre de problemas.

Os ficheiros de dados

Os dados numa base-de-dados relacional, como é o caso da Oracle, são estruturados em forma tabular, sendo definidas restrições sobre as suas relações. Fisicamente, são armazenados em disco, fita ou outro tipo de dispositivo sob a forma de ficheiros. Neste motor, as tabelas que contêm os dados são logicamente agrupadas naquilo que recebe a designação de tablepsaces. Esta separação permite-nos apartar os metadados que descrevem as tabelas e a estrutura geral da base-de-dados dos dados físicos, bem como os dados físicos de acordo com a aplicação que os consome. Deste modo, é possível desligar um tablespace, mantendo ligados os outros. Além disso, é possível restringir os processos de savaguardas a este tipo de estruturas.
Dois tablespaces são criados aquando da criação da base-de-dados, nomeadamente, o SYSTEM e o SYSAUX, os quais não podem ser renomeados ou removidos. O primeiro contém os metadados que definem a estrutura da base-de-dados, isto é, contém os dicionários de dados. O segundo contém informação sobre os utilitários Oracle. Em adição aos tablespaces fundamentais, existem ainda os tablespaces temporários e os de undo. Os tablespaces temporários são utilizados pelo motor da base-de-dados para armazenar informação necessária para a execução de consultas. Apesar de uma base-de-dados funcionar sem conter nenhum tabelspace temporário, determinadas consultas deixarão de funcionar. Os tablespaces de undo contêm informação que permite reverter alterações à base-de-dados por motivos de consistência ou quando for emitido o comando ROLLBACK.
Para criar um tablepsace, utilizamos a instrução no SQLPlus

SQL>create tablespace lmtbsb datafile '...\Desktop\lmtbsb.dbf' size 50m extent management autoallocate;

Este comando permite criar um tablespace com um ficheiro associado iniciado com 50 MB de tamanho. A instrução EXTENT MANAGEMENT AUTOALLOCATE indica ao sistema que a reserva e libertação de espaço em disco é gerida pelo sistema.
Suponhamos, por exemplo, que pretendemos mover o ficheiro de dados associado ao tablespace para numa nova pasta. Tal é possível sem encerrar a base dados, desligando temporariamente o respectivo tablepsace. A lista de comandos é a seguinte:

SQL>alter tablespace lmtbsb offline normal;
SQL>alter tablespace lmtbsb rename datafile '...\Desktop\lmtbsb.dbf' to '...\Desktop\Data\lmtbsb.dbf':
SQL>alter tablespace lmtbsb online;

A primeira instrução permite-nos desligar o tablespace para podermos efectuar a respectiva alteração do nome do ficheiro, a qual é realizada com a emissão do segundo comando. A última linha permite disponibilizar novamente o tablespace. A gestão de tablespaces temporários e de undo é muito semelhante. De facto, as instruções

SQL>create temporary tablespace tmptbsp datafile '...\Desktop\tmptbsp.dbf' size 50m extent management autoallocate;
SQL>create undo tablespace undotbsp datafile '...\Desktop\undotbsp.dbf' size 50m extent management autoallocate;

permitem criar um tablespace temporário e um outro de undo, respectivamente. A lista de ficheiros por tablespace pode ser obtida com

SQL>select ts.name, df.file#, df.name from v$datafile df, v$tablespace ts where df.ts# = ts.ts#;

A lista proporcionada pela consulta não inclui os tablespaces temporários. Para os obter, é necessário emitir a consulta

SQL>select ts.name, tf.file#, tf.name from v$tempfile tf, v$tablespace ts where tf.ts# = ts.ts#;

A lista de ficheiros é útil no caso em que pretendemos estabelecer uma estratégia para cópias de segurança mas não temos o conhecimento de toda a estrutura física da base-de-dados.

Conexão remota e o Listener

Para podermos conectar-nos remotamente às bases-de-dados Oracle necessitamos dos programas de cliente. A sua instalação não oferece quaisquer desafios. Convém-nos apenas anotar a directoria escolhemos para Oracle_home. No Windows, esta directoria está definida na tabela dos registos. Para obtê-la, iniciamos o regedit e navegamos até à chave HKEY_LOCAL_MACHINE/SOFTWARE/ORALCE/OraDb11gHome1.
Vimos atrás como nos conectar a uma instância de base-de-dados a partir do anfitrião, isto é, a partir da máquina onde a instância se encontra alojada. Para o efeito, é suficiente definir a variável de ambiente oracle_sid com o valor do SID  associado à base-de-dados. O SID é um valor único dentro do mesmo sistema. Deste modo, todas as instâncias contidas no mesmo anfitrião possuem valores diferentes do SID. O comando

SQL> select instance from v$thread;

emitido no SQLPlus retorna-nos o valor do SID.
A consulta

SQL> show parameter service_names;

no SQLPlus permite-nos obter todos os serviços que estão registados na instância. Os serviços, bem como os SID, desempenham um papel importante nas conexões remotas.
Vimos que, para nos conectarmos à base-de-dados no anfitrião, fazemos, numa consola do sistema operativo do anfitrião,

>set oracle_sid=orcl
>sqlplus sys as sysdba

sendo depois introduzida a palavra-chave. Suponhamos agora que nos encontramos numa máquina diferente daquela onde se encontra alojada a base-de-dados à qual nos pretendemos conectar com o auxílio do SQLPLus. É evidente que teremos de especificar o endereço e a porta do anfitrião bem como o SID da base-de-dados à qual nos pretendemos conectar. Para o efeito, fazemos, na consola do sistema operativo onde nos encontramos,

>sqlplus sys@(description=(address=(protocol=tcp)(host=_._._._)(port=1521))(connect_data=(sid=orcl))) as sysdba;

O parâmetro host (anfitrião) contém o endereço de IP da máquina que contém a base-de-dados ou o nome do domínio que seja resolvido nesse mesmo endereço (note-se que o utilitário ping permite-nos obter o endereço de IP quando o nome no domínio é conhecido bem como o inverso se utilizarmos a opção -a). Se a base-de-dados estiver associada ao serviço orcl.local, podemos fazer

>sqlplus sys@(description=(address=(protocol=tcp)(host=_._._._)(port=1521))(connect_data=(service_name=orcl.local))) as sysdba;

para nos conectarmos remotamente.
As conexões assim definidas são muito extensas. Interessa-nos, pois, tornar o processo de conexão remota mais simples. Dois métodos poderão vir em nosso auxílio. O primeiro, recebe a designação de EZconnect (que soa como easy connect – conexão fácil). Esta tecnologia permite-nos conectar à nossa base-de-dados, registada com o serviço orcl.local com o auxílio do comando

>sqlplus sys/orcl@host:1521/orcl.local

Note-se que a palavra-chave terá de ser especificada ou então a conexão irá falhar.
O outro método requer a configuração das conexões num ficheiro engendrado para esse propósito que se encontra na directoria Oracle_home\network\admin. Criemos nesta directoria, caso não exista, um ficheiro de texto designado por tnsnames.ora e acrescentemos as linhas seguintes :

orcl_service =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = _._._._)(PORT = 1521))
)
(CONNECT_DATA =
(SID = orcl)
)
)

De seguida, na linha de comandos da consola, fazemos

>sqlplus sys@orcl_service as sysdba

Depois de introduzirmos a palavra-chave, encontramo-nos conectados. O ficheiro tnsnames.ora é utilizado pelos programas cliente para resolver o nome do serviço que passamos durante a conexão.

Convém neste ponto dar uma ideia do fluxo associado a um pedido de cliente. Todos os pedidos de conexão à base-de-dados são realizados por intermédio de um processo que se encontra em execução no anfitrião designado por Listener. Quando uma nova instância de base-de-dados é configurada no anfitrião, esta deverá ser registada num Listener de modo a possibilitar conexões de cliente. Nas versões superiores à 9i, o registo das instâncias é garantido por um processo que se encontra em execução, não sendo necessária qualquer configuração (as configurações estáticas dos serviços associados a um Listener são configuradas num ficheiro designado por listener.ora que se encontra na mesma directoria Oracle_home\network\admin). Para consultar quais são os serviços que estão registados nos Listeners, emitir o comando na consola do sistema operativo do anfitrião

>lsnrctl status

Como já foi referido atrás, a criação de uma base-de-dados durante a instalação do motor Oracle efectua a criação de um Listener. Caso essa opção não tenha sido tomada, é necessário proceder à sua criação com o auxílio do Oracle Net Configuration Assistant antes de proceder à criação da primeira instância, utilizando o Database Configuration Assistant.

O ficheiro de palavras-chave

Suponhamos que nos conectamos remotamente ao anfitrião com endereço IP _._._._ na porta por defeito 1521, assumindo que a palavra-chave para o utilizador sys é orcl com o serviço orcl.local registado,

>sqlplus sys/orcl@._._._.:1521/orcl.local

Aí, fazemos

SQL>shutdown immediate;
SQL>exit

Ora, a base-de-dados é, deste modo, desligada. Tentemos ligar-nos novamente à mesma instância que agora se encontra em baixo.

>sqlplus sys/orcl@._._._.:1521/orcl.local

Provavelmente iremos obter um erro a indicar que o listener não reconhece o serviço especificado. Isto deve-se ao facto do resgisto da base-de-dados efectuado no listener ser realizado automaticamente (ver serviço de registos). Quando a base-de-dados é desligada, o seu registo automático no listener é eliminado do mesmo modo. Para podermos continuar a conectar-nos à base-de-dados quando esta foi desligada, o serviço associado deverá encontrar-se configurado estaticamente no ficheiro listener.ora. Para registarmos estaticamente o serviço, começamos por parar o listener

>lsnrctl stop

Editamos o ficheiro listener.ora que se encontra na directoria Oracle_home\network\admin, acrescentando-lhe as linhas

SID_LIST_LISTENER=
(SID_LIST=
(SID_DESC=
(GLOBAL_DBNAME=orcl.local)
(ORACLE_HOME=[Oracle_home])
(SID_NAME=orcl)))

Observe-se que se trata de uma lista de listas de registos de SID. Se alguns outros SID se encontrarem registados, adicionar

(SID_DESC=
(GLOBAL_DBNAME=orcl.local)
(ORACLE_HOME=[Oracle_home])
(SID_NAME=orcl))

à lista SID_LIST. A variável GLOBAL_DBNAME deverá conter o nome global da base-de-dados e pode ser obtido por intermédio da consulta

SQL>select global_name from global_name

no SQLPlus quando a base-de-dados se encontra ligada. Agora, podemos voltar a tentar a ligação

>sqlplus sys/orcl@._._._.:1521/orcl.local

quando a nossa base-de-dados orcl se encontra desligada. Caso a nossa ligação não seja bem-sucedida devido a privilégios insuficientes, o problema encontrar-se-á certamente na ausência do ficheiro de palavras-chave.
De facto, quando nos conectamos remotamente a uma base-de-dados que foi desligada o nome de utilizador e a palavra-chave não podem ser comparados com os registos das tabelas internas uma vez que estas não se encontram disponíveis quando a base-de-dados se encontra nesses estado. Deste modo, é necessário criar um ficheiro de palavras-chave, com o auxílio da ferramenta orapwd,

>orapwd pwdfile.pwd entries=4 force=n ignorecase=n

Este comando permite-nos criar um ficheiro de palavras-chave com capacidade para quatro administradores na directoria corrente da consola. De modo a ser reconhecido pelo motor de bases-de-dados, este terá de se encontrar nas directorias especificadas na seguinte tabela.

Plataforma Nome obrigatório Localização requerida
UNIX and Linux orapwORACLE_SID ORACLE_HOME/dbs
Windows PWDORACLE_SID.ora ORACLE_HOME\database

Depois de criar o ficheiro na directoria correcta, é necessário sincronizar a palavra-chave do SYS com este ficheiro. Para o efeito, é suficiente emitir o comando no SQLPlus

SQL>alter user sys identified by nova_palavra_chave;

Esta alteração permite criar uma nova palavra-chave tanto no dicionário de dados como no ficheiro de palavras-chave. Se criarmos um utilizador

SQL>create nome_do_utilizador identified by palavra_chave_do_novo_utilizador;

podemos acrescentá-lo ao ficheiro de palavras-chave e ao dicionário como administrador com o comando

SQL>grant sysdba to nome_do_utilizador;

Se o utilizador já existia com privilégios de sysdba é necessário remover-lhe e voltar a adicionar-lhe o privilégio de modo a proceder à respectiva sincronização com a base-de-dados. Os comandos são

SQL>revoke sysdba from nome_do_utilizador;
SQL>grant sysdba to nome_do_utilizador;

Para obter uma lista de utilizadores listados no ficheiro, fazemos

SQL>select username from v$pwfile_users;

Resta, neste ponto, enfatizar o facto de que um ficheiro de palavras-chave pode ser partilhado por várias bases-de-dados. Para verificar se a base-de-dados se encontra a utilizar um ficheiro partilhado, fazer

SQL>show parameter remote_login_passwordfile;

Este parâmetro, que se encontra configurado no spfile admite três valores possíveis, nomeadamente, none, exclusive e shared. O primeiro, indica que a base-de-dados não possui ficheiro, o segundo indica que o ficheiro é exclusivo e o terceiro que se trata de um ficheiro partilhado. Apenas podemos acrescentar ou eliminar utilizadores do ficheiro caso este esteja no modo exclusivo. Para alterar para none o valor deste parâmetro, fazemos

SQL>alter system set remote_login_password=none scope=spfile;

Trata-se de uma parâmetro de configuração que surtirá efeito apenas quando a base-de-dados for reiniciada e, por isso, trata-se de um daqueles parâmetros que terá de ser configurado apenas ao nível do spfile.
Como nota final, o algoritmo de encriptação utilizado pela Oracle encontra-se descrito no livro Special Ops Host And Network Security For Microsoft, Unix, And Oracle. Apesar de alguns ataques poderem ser realizados, resultando em possíveis falhas de segurança associadas à publicação deste tipo de ficheiros, não iremos aqui debater o assunto. Convém utilizar, no caso de utilizadores com privilégios de administração, boas palavras-chave.

Os diários de arquivo – archive logs

A estrutura mais crucial no que concerne à recuperação da base-de-dados em caso de falha na instância são os designados por ficheiros redo. Estes ficheiros contêm as sucessivas alterações à medida que estas ocorrem. Para consultar que ficheiros do género estão associados à nossa base-de-dados, podemos fazer

SQL>select l.group#, l.sequence#, l.status, l.bytes/(1024 * 1024) || ' MB' as "Size", lf.member from v$log l, v$logfile lf where lf.group# = l.group#;

no SQLPlus. A consulta permite-nos obter os grupos e a sequência, estado e ficheiros associados a cada grupo. Para acrescentar um redo log, basta fazer

SQL>alter database add logfile('caminho_do_ficheiro') size 50M;

É possível acrescentar um membro ao grupo 1, por exemplo, com o comando

SQL alter database add logfile member 'caminho_do_novo_ficheiro' to group 1;

Para remover um membro, basta fazer

SQL>alter database drop logfile membmer 'caminho_do_ficheiro_a_remover';

À medida que a base-de-dados recebe actualizações, estas são registadas no grupo redo log cujo estado se encontra marcado com current – actual. Quando os ficheiros se encontram completos, o estado current move-se para o grupo seguinte e deixa o anterior no estado active – activo. Este movimento recebe a designação de log switch. Os grupos de redo log que estão no estado activos são necessários para a recuperação da base-de-dados em caso de falha de instância. A utilização de grupos é importante, na medida em que é possível gravar as alterações em várias localizações distintas, diminuindo a probabilidade de falha. Se pretendermos mudar o grupo actual, fazemos

SQL>alter system switch logfile;

Este comando permite mudar o estado de current para o grupo seguinte. A consulta acima permite verificar que agora o grupo anterior se encontra activo, isto é, continua a ser necessário pela base-de-dados. Se pretendermos colocá-lo como inactivo, fazemos

SQL>alter system checkpoint;

Esta instrução permite forçar a escrita da informação que se encontra em memória e nos ficheiros redo para os ficheiros de dados. Se uma instância colapsar um instante após a realização de um checkpoint, a sua recuperação, em caso de falha, irá requerer apenas os ficheiros de dados. Ver, por exemplo, How to change the Redo Log File size in Oracle Database, onde é apresentado um processo para alteração do tamanho dos ficheiros sem desligar a instância.

Relativamente a este tipo de ficheiros de diário, ou log, a instância pode-se encontrar configurada para executar em um dos dois modos. Estes são o modo NOARCHIVELOG e o modo ARCHIVELOG. No primeiro modo, a escrita da informação de redo é realizada alternadamente sobre os grupos. Se um grupo se encontrar cheio, o processo passa a escrever no próximo ficheiro, mesmo que este ainda se encontre no estado activo. No segundo modo, os vários ficheiros activos de redo são arquivados em ficheiros designados por archive logs. Para verificarmos em que modo se encontra a instância, podemos fazer, no SQLPlus,

SQL>archive log list;

ou

SQL>select log_mode from v$database;

A alteração da base-de-dados de um destes estados para o outro requer que seja efectuada uma cópia de segurança (backup). Suponhamos que a instância se encontra configurada no modo NOARCHIVELOG e pretendemos alterar o seu estado para o modo ARCHIVELOG. Em primeiro lugar, efectuamos uma cópia de segurança. Depois fazemos

SQL>alter system set log_archive_dest_1='caminho_para_o_primeiro_arquivo' scope = spfile;
SQL>alter system set log_archive_dest_2='caminho_para_o_segundo_arquivo' scope = spfile;

que configura ambos os parâmetros log_archive_dest_1 e log_archive_dest_2 no spfile. De seguida, desligamos a instância e inciamo-la no estado mount:

SQL>shutdwon immediate;
SQL>startup mount;

Alteramos o estado da base-de-dados com

SQL>alter database archivelog;

e abrimo-la

SQL>alter database open;

Neste ponto é importante realizar uma nova cópia de segurança. Para colocar a base-de-dados em NOARCHIVELOG, é suficiente colocar a base-de-dados no estado mount, alterar o estado e depois abri-la:

SQL>shutdwon immediate;
SQL>startup mount;
SQL>alter database noarchivelog;
SQL>alter database open;

Note-se que, em ambos os casos, é importante ter em mente que deverão ser realizadas uma cópia de segurança antes e depois de alterar o modo de arquivo dos diários.

Criação e eliminação de instâncias de base-de-dados

A criação de novas instâncias de bases-de-dados podem ser realizadas de duas maneiras. A mais complicada consiste em criar manualmente todos os elementos necessários ao funcionamento de uma base-de-dados com o auxílio de comandos. Este método requer, contudo, um conhecimento demasiado profundo do funcionamento do sistema. Deste forma, a Oracle disponibiliza a ferramenta Oracle Database Configuration Assistant. Se nenhum listener se encontrar configurado, é necessário proceder à criação de um com o Oracle Net Configuration Assistant. Ambas as ferramentas encontram-se disponibilizadas sob o submenu todos os programas do menu iniciar. O Oracle Database Configuration Assistant permite também remover instâncias de bases-de-dados.
Convém notar que a desinstalação de uma uma instância poderá não remover todos os ficheiros. É necessário, portanto, eliminá-los manualmente. Entre estes, encontram-se os ficheiros de dados, diários, ficheiros de palavras-chave, entradas nas configurações do listener, entre outros. Por vezes é conveniente desinstalar o Enterprise Manager, caso este tenha sido instalado, com o auxílio do emca. É difícil remover instâncias sem deixar para trás grandes quantidades de lixo. Assim, é conveniente manter instâncias definitivas em ambientes de produção e criar instâncias de teste em ambientes que poderão ser completamente reconstruídos mais tarde.

Publicado em Computadores e Internet | Etiquetas , , , , | Publicar um comentário