Relationships between major views Jianing Hu April, 2000 Component-Connector View and Context View A context view is often drawn as a component-connector-like diagram, with the components as entities and the relations as interactions. In this case, the “machine” in a context view is an abstraction of the system described in the C-C view. Therefore the relationship between C-C view and context view is that between an abstract and refined system representation. In a context view constraints could be attached to the “machine”, (e.g., the protocol used by the machine to communicate with its environment), and we can check if these constraints are satisfied in the C-C view. For our compiler example we can check the following for consistency. The context view says the source file has to be a character stream. The C-C view is consistent with it by stating the same. Here the format of source is described in plain English. We could also use other techniques to describe the format and check the consistency, e.g., Wright protocol description and checking. The context view says the compiler writes to storage via some interface. The C-C view doesn’t specify this interface. We could extend the description of “Generator” to include the protocol it uses to write compiled data to storage and can therefore check the consistency. The context view says the compiler displays status and error messages on some display device using a display interface. The C-C view doesn’t show the display device at all. Suppose the representation of each component in the C-C view describes how it interacts with the display device, we can check the consistency of the interfaces of the display device in both views. The C-C view does not specify from where and how the compiler receives commands. The C-C view we have is a highly abstracted one and does not contain much detailed information about the compiler. Detailed information like how the compiler receives commands and the formats of the commands could be declared in a refined C-C view. Thus we can check the consistency of commands in both views. The context view could also specify performance and resource consumption constraints. These are specified as system design requirements. The C-C view has to satisfy these constraints. E.g., if the context view says the compiler needs no more than 32MB of memory to run, the C-C view should show (probably not directly but through some kinds of analysis) that it requires less than 32 MB of memory to run. Component-Connector View and Hardware View The C-C view shows aspects of the run-time structure of a software system. A hardware view shows the underlying hardware structure of that system. Generally speaking, relationships between the two views are resource allocation and performance constraints. Resource allocation shows which resource in the hardware view is allocated to which element in the C-C view. That could include allocating memory and processor time to a process, network bandwidth to a pipe, etc. Constraints can be attached to resource allocation. A basic constraint that all resource allocation has to meet is that the sum of resources allocated must not be more than the total amount of resources available in the hardware view. Specific constraints could be specified, too. Performance constraints show the relationships between the performance properties of the C-C view and those of the hardware view. Performance constraints, resource allocation and the performance of the hardware together determine the performance of a software element. For example, the execution speed of a process depends on how much memory and CPU time it is allocated (resource allocation) and how fast the CPU runs (hardware performance property) and the relation between the performance of the process and how fast the CPU runs (performance constraint). In the compiler example we have the following relationships. Resource allocation: Memory, storage, and processor time consumption by processes, bandwidth consumption by pipes. Besides the basic constraint, we could also constrain that the network utilization to be under a certain limit, or a certain amount of CPU time be reserved for the operating system, etc. Performance constraints: The throughput and latency of a pipe are constrained by the throughput and latency of the network underneath, the performance of a process is constrained by the speed of hardware (CPU, memory, backplane, etc.) on which it runs. Another analysis we can perform on these two views is finding the best resource allocation scheme. Performance of a software component/connector could be affected by the amount of resources allocated. One useful analysis is to find the resource allocation scheme that achieves the best overall system performance. Component-Connector View and Layered View Layered views show the implementation structure of a system. It is not unusual that a system has multiple layered views, each describing the implementation of a critical element or element type of the system. The top layer of a layered view hides details of the implementation to the rest of the system. Therefore when we consider relationships between a layered view and other views, we can focus on the top layer and ignore the others. The top layer of a layered view describes software elements that are used in a C-C view. It specifies their interfaces and rules that have to be obeyed when using them, such as communication protocols. In the C-C view these elements are used as building blocks and the C-C view might also specify the rules under which these elements are used. It is very important to make sure the rules specified in both views agree with each other. When an element in the C-C view is composed of elements in the layered view, the performance of the composed element depends on the performances of elements it uses, and the way it interacts with those elements. The relations between performance properties can be captured as performance constraints. Context View and Hardware View Both views describe the environment in which the system runs, but from different points of view. Generally there is no direct relationship between the two. In the compiler example, the context view has components called “storage” and “display”, which also appear in the hardware view. The difference is that in the context view these devices are considered abstract devices while in hardware view their hardware implementation details are revealed. We can check the consistency between the devices. For example, if the context view says the display is a GUI, then the display component in the hardware view must be able to display graphics. Another example of consistency constraint is that the storage device in the hardware view must be writable, because the context view says compiled code is written to it. Context View and Layered View No relationship is found between the two views for the compiler example. However, in general a component in the context view might correspond to the top layer of a layered view. Suppose we have a layered view of the display device, then its top layer would be the interface that the compiler calls and we can then check the consistency of the interface specified in the layered view and that specified in the context view. Hardware View and Layered View In some cases counterparts of layers can be found in the hardware view. In the compiler example, the network stack’s CSMA/CD layer corresponds to LAN in the hardware view. Consistency can therefore be checked. Consistency constraints between the two views include performance consistency, e.g., throughput of a CSMA/CD layer link has to be less than throughput of the LAN, and protocol consistency, e.g., the LAN has to be an Ethernet LAN that implements CSMA/CS protocol. Summary Generally speaking, the constraints between views are all consistency constraints. Consistency constraints ensure that different views don’t conflict each other. Consistency constraints can be further divided into sub-categories. In the compiler example we see the following kinds of consistency constraints: - Interface consistency: ensures two views agree on the interface of the same element, e.g., the agreement between the C-C view and the context view on the display interface. - Typing consistency: constrains format/type of data, e.g., both the C-C view and the context view must agree on the format of source files. - Protocol consistency: ensures communication protocols are held consistently across views. Formal specification languages like Wright can be used in protocol specification and checking. - Existence consistency: different views have to agree on the existence of an element. E.g., the context view and the hardware view must agree on the existence of the display device. This constraint does not have to be a 1-1 relation. - Functionality consistency: different views have to agree on the functionality of an element. E.g., if the context view says the display device is capable of displaying graphs, the hardware view should specify so, too. - Performance consistency: declares associations between performance properties. It often shows dependency of performance properties in different views. For example the performance of a pipe depends on the performance of the LAN over which the pipe is created. Question: How could constraints be violated and what are the consequences? Answer: Constraints could be violated in various ways. Two views could be inconsistent because they are developed by different people who have different knowledge of the system, or because one is updated and the other is not, or even because of a typo. Inconsistency between views could lead to serious system deficiency. A performance inconsistency could lead to a system that doesn't satisfy its performance requirements; a protocol inconsistency might result in a system that can not communicate with its environment correctly; a functionality inconsistency could make the system lack critical functionality. Because major views are developed at the beginning of system development and used as guidance through the development process, it is critical to find the inconsistencies early and keep checking them when views are updated. Pipe Layer Protocol In this section we describe the calling protocol to functions pipe(), read(), write() and close(), in the pipe layer. To facilitate understanding our descriptions, let us first introduce our notations of sets, tuples, and lists, as the following: {} : an empty set; ∪ : set union -- {A}∪{B} = {A,B} [A,B] : a tuple that contains A and B < > : an empty list; ^ : list concatenation -- <A>^<B> = <A,B> States of the pipe layer are represented by processes with subscripts. Two processes represent different states if they have different subscripts. Each subscript is a set of tuples. A tuple in the subscript set is in the form of [[fd1,L1],[fd1,L2]]. It represents the state of a pipe, or, a pair of file descriptors, each represented by a tuple in the form of [fd,L], respectively. The tuple [fd,L] represents the value of the file descriptor as an integer fd1, and the data that has been written to the file descriptor but has not been read out at the other end of the pipe yet, as a list L. The whole subscript set represents the states of all opened pipes in that pipe layer state. SPEC = Open1{} where Open1S = PipeS ▯ ReadS▯ WriteS▯ CloseS PipeS = pipe() ((return [fd1,fd2]Open1s∪{[[fd1,<>],[fd2,<>]]}) ⊓ (return [-1,-1]Open1S)) (read(fd1)!x2Open1S'∪{[[fd1,L1'^<x1>],[fd2,L2']]}) ▯ (read(fd2)!x1Open1S'∪{[[fd1,L1'],[fd2,L2'^<x2>]]})) when S = S'∪{[[fd1,L1'^<x1>],[fd2,L2'^<x2>]]} and fd1 ≠ -1 and fd2 ≠ -1 ReadS = read(fd1)!xOpen1S'∪{[[fd1,L1],[-1,L2']]} when S = S'∪{[[fd1,L1],[-1,L2'^<x>]]} and fd1 ≠ -1 read(fd2)!xOpen1S'∪{[[-1,L1'],[fd2,L2]]} when S = S'∪{[[-1,L1'^<x>],[fd2,L2]]} and fd2 ≠ -1 (write(fd1,x1)Open1S'∪{[[fd1,<x1>^L1],[fd2,L2]]}) ▯ (write(fd2,x2)Open1S'∪{[[fd1,L1],[fd2,<x2>^L2]]})) when S = S'∪{[[fd1,L1],[fd2,L2]]} and fd1 ≠ -1 and fd2 ≠ -1 WriteS = write(fd1,x)Open1S'∪{[[fd1,<x>^L1],[-1,L2]]} when S = S'∪{[[fd1,L1],[-1,L2]]} and fd1 ≠ -1 write(fd2,x)Open1S'∪{[[-1,L1],[fd2,<x>^L2]]} when S = S'∪{[[-1,L1],[fd2,L2]]} and fd2 ≠ -1 (close(fd1)Open1S'∪{[[-1,L1],[fd2,L2]]}) ▯ (close(fd2)Open1S'∪{[[fd1,L1],[-1,L2]]})) when S = S'∪{[[fd1,L1],[fd2,L2]]} and fd1 ≠ -1 and fd2 ≠ -1 CloseS = close(fd1)Open1S' when S = S'∪{[[fd1,L1],[-1,L2]]} and fd1 ≠ -1 close(fd2)Open1S' when S = S'∪{[[-1,L1],[fd2,L2]]} and fd2 ≠ -1 This specification described a basic function calling protocol: read() or write() to a file descriptor can not be called if that file descriptor has not bee allocated by calling pipe() read() or write() to a file descriptor can not be called if that file descriptor is closed by calling close and has not bee reallocated by another pipe() call. read() from a file descriptor can not be called if there is no data for that file descriptor to read in the pipe. The protocol defined above describes some basic constraints on function call sequences. However, it is still not the "real" protocol to call these functions, particularly, in this protocol one can call Write() at one end of a pipe arbitrary number of times without calling read() at the other end. In reality a pipe has certain capacity. If it reaches its full capacity, calling write() does not change its state any more. To address the pipe capacity issue in our protocol, we need to add the capacity and load of a pipe to its state representation. This is done by adding capacity and load elements to the tuple that represents a file descriptor. Now the tuple is in the form of [fd,L,n,c], where fd and L have the same meanings as in the protocol described above, n is the length of L, i.e., the load of fd, and c is the capacity of fd. The values of n and c have to satisfy that n<=c at any time. Below we describe a protocol in which all file descriptor capacities are set to 5. SPEC = Open2{} where Open2S = PipeS ▯ ReadS▯ WriteS▯ CloseS PipeS = pipe() ( (return [fd1,fd2]Open2s∪{[[fd1,<>,0,5],[fd2,<>,0,5]]}) ⊓ (return [-1,-1]Open2S) ) (read(fd1)!x2Open2S'∪{[[fd1,L1'^<x1>,n1,c1],[fd2,L2',n2-1,c2]]}) ▯ (read(fd2)!x1Open2S'∪{[[fd1,L1',n1-1,c1],[fd2,L2'^<x2>]]})) when S = S'∪{[[fd1,L1'^<x1>,n1,c1],[fd2,L2'^<x2>,n2,c2]]} and fd1 ≠ -1 and fd2 ≠ -1 ReadS = read(fd1)!xOpen2S'∪{[[fd1,L1,n1,c1],[-1,L2',n2-1,c2]]} when S = S'∪{[[fd1,L1,n1,c1],[-1,L2'^<x>,n2,c2]]} and fd1 ≠ -1 read(fd2)!xOpen2S'∪{[[-1,L1',n1-1,c1],[fd2,L2,n2,c2]]} when S = S'∪{[[-1,L1'^<x>,n1,c1],[fd2,L2,n2,c2]]} and fd2 ≠ -1 (write(fd1,x1)Open2S'∪{[[fd1,<x1>^L1,n1+1,c1],[fd2,L2,n2,c2]]}) ▯ (write(fd2,x2)Open2S'∪{[[fd1,L1,n1,c1],[fd2,<x2>^L2,n2+1,c2]]})) when S = S'∪{[[fd1,L1,n1,c1],[fd2,L2,n2,c2]]} and fd1 ≠ -1 and fd2 ≠ -1 and n1 < c1 and n2 < c2 write(fd1,x)Open2 S'∪{[[fd1,<x>^L1,n1+1,c1],[fd2,L2,c2,c2]]} when S = S'∪{[[fd1,L1,n1,c1],[fd2,L2,c2,c2]]} and fd1 ≠ -1 and n1 < c1 write(fd2,x)Open2 S'∪{[[fd1,<x>,c1,c1],[fd2,<x>^L2,n2+1,c2]]} when S = S'∪{[[fd1,L1,c1,c1],[fd2,L2,n2,c2]]} and fd2 ≠ -1 and n2 < c2 WriteS = write(fd1,x)Open2S'∪{[[fd1,<x>^L1,n1+1,c1],[-1,L2,n2,c2]]} when S = S'∪{[[fd1,L1,n1,c1],[-1,L2,n2,c2]]} and fd1 ≠ -1 and n1 < c1 write(fd2,x)Open2S'∪{[[-1,L1,n1,c1],[fd2,<x>^L2,n2+1,c2]]} when S = S'∪{[[-1,L1,n1,c1],[fd2,L2,n2,c2]]} and fd2 ≠ -1 and n2 < c2 (close(fd1)Open2S'∪{[[-1,L1,n1,c1],[fd2,L2,n2,c2]]}) ▯ (close(fd2)Open2S'∪{[[fd1,L1,n1,c1],[-1,L2,n2,c2]]})) when S = S'∪{[[fd1,L1,n1,c1],[fd2,L2,n2,c2]]} and fd1 ≠ -1 and fd2 ≠ -1 CloseS = close(fd1)Open2S' when S = S'∪{[[fd1,L1,n1,c1],[-1,L2,n2,c2]]} and fd1 ≠ -1 close(fd2)Open2S' when S = S'∪{[[-1,L1,n1,c1],[fd2,L2,n2,c2]]} and fd2 ≠ -1 Compatibility Checking With the layered view specifies how the functions it provides should be used, we could check if they are used in the C-C view as they are supposed to be. Let us check if the pipe layer functions are correctly called in the C-C view. Of course, to do that, we need to specify the pipe protocol in the C-C view, too. Followed is a pipe protocol from [AG98]. connector Pipe = role Writer = writeWriter Π close§ role Reader = let ExitOnly = close§ in let DoRead = (readReader▯read-eofExitOnly) in DoRead Π ExitOnly glue = let ReadOnly = Reader.readReadOnly ▯Reader.read-eofReader.close§ ▯Reader.close§ in let WriteOnly = Writer.writeWriteOnly▯Writer.close§ in Writer.write->glue▯Reader.readglue▯Writer.closeReadOnly▯Reader.closeWriteOnly Showing that the pipe protocol is a correct realization of the pipe layer protocol is a similar problem as showing the compatibility of a role and a port. We basically require the pipe protocol to be a refinement of the pipe layer protocol. Why is it the case? In short, for a process P to be a refinement of a process Q, P has to satisfy all the properties of Q. That means, if the pipe protocol is a refinement of the pipe layer protocol, then all traces of the pipe protocol can be "replayed" in the pipe layer protocol, i.e., each event trace of the pipe protocol can find its corresponding function calling sequences in the pipe layer protocol. On the other hand, if the pipe protocol is not a refinement of the pipe layer protocol, then there must be a trace of the pipe protocol that can not be "replayed" in the pipe layer protocol. That means the pipe protocol tries to use these functions defined in the pipe layer in a way that is not allowed by the pipe layer protocol. Even without using a checker like FDR we can see that the pipe protocol we have here is not a refinement of the pipe layer protocol described in the last section. Our pipe protocol here does not have initialization (calling pipe()) and allows read and write at any time regardless of the load and capacity of the pipe. That is obviously inconsistent with our pipe layer protocol which says: before reading or writing, one has to call pipe() to allocate file descriptors; one can not read from an empty pipe, and, the number of writes allowed is constrained by the capacity of the pipe. However, such an inconsistency between the C-C view and layered view does not necessarily mean fault designs. In fact, people all tend to (as a right practice) ignore implementation details like initialization, protocol details, when they do the high level design. Unnecessary details in the high level design could make it hairy and hard to understand and communicate. At some point people will turn the high level designs into lower level designs that are more implementation oriented and that is when such inconsistencies should be checked and resolved. question: our pipe layer protocol has infinite states. Can it be checked? If it can't be, we can actually make a finite state protocol. That could lead to even more analysis.