Page 1 of 1

wait_for_disk_interrupt

Posted: Sat Feb 27, 2016 1:35 am
by blackoil
USER: readfile( readsector ( push lba; interrupt_to_disk_service) )

------------------------------------------------------------------------------------------

KERNEL: disk_service( pop lba; port_in (lba); wait_for_disk_interrupt; )

If don't want into polling whether disk_interrupt occurred, what can I do when step in wait_for_disk_interrupt?

Re: wait_for_disk_interrupt

Posted: Sat Feb 27, 2016 1:50 am
by Octocontrabass
Block the user thread until the data is ready.

(Or, use an asynchronous API and make the user thread worry about what to do until the data is ready.)

Re: wait_for_disk_interrupt

Posted: Sat Feb 27, 2016 2:50 am
by iansjack
It's the same as when any thread is waiting for a resource. You block the thread and switch to the next ready one. It's the major cause of task switches. There's nothing special about the disk interrupt, and there are far longer waits involved for the keyboard interrupt.

Re: wait_for_disk_interrupt

Posted: Sat Feb 27, 2016 3:25 am
by blackoil
disk_service:
pop ( device, lba, buffer );
set ( device, lba , buffer , ret_addr ) in disk_interrupt;
mark the thread as suspended one
clear stack
jmp to task_queue


disk_interrupt:
determine( read | write, device , buffer);
port data from/to buffer
unmark the thread to be ready
interrupt_ret( thread )

Re: wait_for_disk_interrupt

Posted: Sat Feb 27, 2016 7:50 am
by blackoil
It works fine in Bochs. Keystroke triggers readsector(), then int48 issues parameter to ata_reg_set, then back to main loop.
When harddisk interrupt 46 occurs, port_in data to buffer.

Code: Select all

				mov [KB_Scancode],byte 0
SystemLoop:			hlt
				cmp [KB_Scancode],byte 0
				je wait
				mov [KB_Scancode],byte 0
				push dword [VideoAddr]
				push dword 0
				call ATA_ReadSector
				add esp,8
wait:				jmp SystemLoop

InterruptHandler_46:
				pop ecx
				pop ecx
				pop ecx
				push dword [_eflags]
				push dword [_cs]
				push dword [_eip]

				pushad
				in al,ATA_Port_Primary_Status
				movzx eax,al
				mov [ATA_STATUS],eax
				push dword [ATA_BUFFER]
				push dword [ATA_STATUS]
				call ATA_Interrupt
				add esp,4

				mov al,EOI
				out PIC8259B_Port_Command,al
				out PIC8259A_Port_Command,al
				popad
				iret

InterruptHandler_48:		pop dword [_eip]
				pop dword [_cs]
				pop dword [_eflags]
				jmp SystemLoop

void	ATA_Interrupt(UINT Status,UWORD[256]* MEM)
{
	#asm
		{
		cld
		mov ecx,256
		mov edi,[MEM]
		mov dx,[ATA_PORT_DATA]
		rep insw
		}
}

void	ATA_ReadSector(UINT LBA,UWORD[256]* MEM)
{
	ATA_PORT_DATA		=ATA_Port_Primary_Data;
	ATA_PORT_ERROR		=ATA_Port_Primary_Error;
	ATA_PORT_FEATURE	=ATA_Port_Primary_Feature;
	ATA_PORT_SECTOR 	=ATA_Port_Primary_Sectors;
	ATA_PORT_LBA0		=ATA_Port_Primary_LBA0;
	ATA_PORT_LBA8		=ATA_Port_Primary_LBA8;
	ATA_PORT_LBA16		=ATA_Port_Primary_LBA16;
	ATA_PORT_LBA24		=ATA_Port_Primary_LBA24;
	ATA_PORT_STATUS 	=ATA_Port_Primary_Status;
	ATA_PORT_CMD		=ATA_Port_Primary_Command;
	ATA_PORT_ALTSTATUS	=ATA_Port_Primary_AlterStatus;
	ATA_PORT_DEVCTRL	=ATA_Port_Primary_DeviceCtrl;

	ATA_LBA0=LBA<<24>>24;
	ATA_LBA8=LBA<<16>>24;
	ATA_LBA16=LBA<<8>>24;
	ATA_LBA24=(LBA>>24) | ATA_DEV | ATA_Bit_Command_LBA;
	ATA_SECTOR=1;

	OutPortB(ATA_PORT_LBA0,ATA_LBA0);
	OutPortB(ATA_PORT_LBA8,ATA_LBA8);
	OutPortB(ATA_PORT_LBA16,ATA_LBA16);
	OutPortB(ATA_PORT_LBA24,ATA_LBA24);
	OutPortB(ATA_PORT_SECTOR,ATA_SECTOR);
	OutPortB(ATA_PORT_CMD,ATA_Cmd_Read);

	#asm int 48
}

Re: wait_for_disk_interrupt

Posted: Sat Feb 27, 2016 9:13 am
by Nable
Ough.
to blackoil
This code, erm, it may work but there are a lot of things in it that should never ever be used^W^W^W^W be improved. Would you mind if someone lists them, so you can improve your style?

Re: wait_for_disk_interrupt

Posted: Sat Feb 27, 2016 9:27 am
by blackoil
yes

Re: wait_for_disk_interrupt

Posted: Sat Feb 27, 2016 10:00 am
by Nable
Ok, here are some points that shouldn't bring any harm:

1. Global variables are evil in most cases. It's hard to control where and when they are assigned, they make functions non-reentrant (i.e. you can't call a function from it's callees or when it's already called in another thread), they easily lead to problems when you define variables with the same name in different modules. It looks like you use them to pass parameters and return values between ASM and C code but there are better ways to do it (it's time to read about calling conventions).
2. Where did you get those magic numbers (46, 48, etc)? Such non-trivial numbers are also kind of evil things - one should either find that these are universal constants (and give them sensible names) or suddenly understand that these numbers are not constants at all (such situation is very common) and these numbers should be somehow determined or configured on each machine (by reading/writing some registers or calling firmware and parsing structures with system information) and saved to variables.
3. It's better to stick to some known naming convention. It helps in bringing order in your code and avoiding some mistakes. Btw, in most conventions names in all-capitals (also called "CAPS" after CapsLock) style are for constants, so it's quite surprising to read when someone assings values to such variables.
4. It's better to avoid manual register allocation and anything that is longer than 1-2 lines in inline assembly. Here are good examples: http://wiki.osdev.org/Inline_Assembly/Examples#INx
5. It's better to use standard data types with well-defined parameters and behaviour (such as uint{8,16,32,64}_t from <stdint.h>) instead of unknown beasts such as UINT, UWORD, etc. Relying on integer overflow (such as in ATA_LBA0=LBA<<24>>24;) instead of using masks (look at this byte extraction: (LBA >> 24) & 0xFF) is also a harmful thing.
6. It may be OK for the beginning but it's better to separate working with ATA from working with PIC, so that you wouldn't have to change ATA code when you want to use PIC or APIC.
7. What compiler are you using? Is it MS's one? It may be not-the-best-choice for bare-metal development.

Re: wait_for_disk_interrupt

Posted: Sat Feb 27, 2016 2:17 pm
by ~
Nable wrote:Ok, here are some points that shouldn't bring any harm:

1. Global variables are evil in most cases. It's hard to control where and when they are assigned, they make functions non-reentrant (i.e. you can't call a function from it's callees or when it's already called in another thread), they easily lead to problems when you define variables with the same name in different modules. It looks like you use them to pass parameters and return values between ASM and C code but there are better ways to do it (it's time to read about calling conventions).
If you already have code, even if it's procedural, what you can do is create a simple subsystem to load data.

Then you could encapsulate the values of the global variables as if it was a data object.

Then you could select between data sets and it would matter less to have many global variables if you can control them and fill them with those data objects or data packets that are encapsulated and actually separated from the actual application.

Re: wait_for_disk_interrupt

Posted: Sat Feb 27, 2016 8:51 pm
by blackoil
Nable wrote:Ok, here are some points that shouldn't bring any harm:

1. Global variables are evil in most cases. It's hard to control where and when they are assigned, they make functions non-reentrant (i.e. you can't call a function from it's callees or when it's already called in another thread), they easily lead to problems when you define variables with the same name in different modules. It looks like you use them to pass parameters and return values between ASM and C code but there are better ways to do it (it's time to read about calling conventions).
2. Where did you get those magic numbers (46, 48, etc)? Such non-trivial numbers are also kind of evil things - one should either find that these are universal constants (and give them sensible names) or suddenly understand that these numbers are not constants at all (such situation is very common) and these numbers should be somehow determined or configured on each machine (by reading/writing some registers or calling firmware and parsing structures with system information) and saved to variables.
3. It's better to stick to some known naming convention. It helps in bringing order in your code and avoiding some mistakes. Btw, in most conventions names in all-capitals (also called "CAPS" after CapsLock) style are for constants, so it's quite surprising to read when someone assings values to such variables.
4. It's better to avoid manual register allocation and anything that is longer than 1-2 lines in inline assembly. Here are good examples: http://wiki.osdev.org/Inline_Assembly/Examples#INx
5. It's better to use standard data types with well-defined parameters and behaviour (such as uint{8,16,32,64}_t from <stdint.h>) instead of unknown beasts such as UINT, UWORD, etc. Relying on integer overflow (such as in ATA_LBA0=LBA<<24>>24;) instead of using masks (look at this byte extraction: (LBA >> 24) & 0xFF) is also a harmful thing.
6. It may be OK for the beginning but it's better to separate working with ATA from working with PIC, so that you wouldn't have to change ATA code when you want to use PIC or APIC.
7. What compiler are you using? Is it MS's one? It may be not-the-best-choice for bare-metal development.
Thanks for your programming advice. I use my own language & bootstrapped compiler to build/test my logic.

My current ATA access logic is: One thread will be marked/unmarked to suspended in task queue, when the resource it requires is unavailble/available respectively. To support more concurrent threads, a queue mechanism or something etc. must be established.

Re: wait_for_disk_interrupt

Posted: Sun Feb 28, 2016 10:38 am
by ~
blackoil wrote:Thanks for your programming advice. I use my own language & bootstrapped compiler to build/test my logic.

My current ATA access logic is: One thread will be marked/unmarked to suspended in task queue, when the resource it requires is unavailble/available respectively. To support more concurrent threads, a queue mechanism or something etc. must be established.
If you use any language of a higher level than assembly, starting with C, Java, JavaScript, BASIC, Visual Basic... you can do things much easier if you reimplement stack operations for push, pop, read, etc., be it for the low level CPU stack or for a higher level stack.

Have you ever thought about using stack operations to save GDT descriptor fields or to push/pop paging structures so that you can interpret and rebuild them later?

By reimplementing the stack functions and using a custom stack, you can easily treat all of the local and global variables (and even structures or structure fields) as regular CPU registers that you can preserve and restore. That stack can then contain any sort of mixed data. All you need is make sure that you have at least one of those stacks per thread and that you pop the stored values in order just like for the stack kept in SS:ESP so that the process state doesn't get destroyed.

If you think about it, using a stack again (which is in general very rare to do normally once we get to use a language with the level of C and above) allows you to simplify any component of the program. Thinking about it, there are few things that cannot be saved with a stack (for example, reserving more memory, the I/O ports of a hardware device), and even those things could be pushed and popped in the higher level stack to simplify things, reduce the number of variables required, implement a fundamental memory management sublayer managed more by the user than just by the kernel.




You could probably use 2 high level stacks (at the very least). One would be to keep the pending processes/threads and other would be to keep the processes you haven't run.

Then in the middle you could have the single running task for the current CPU. You would start with a stack with a single process. When requested by the scheduler, you would send it the pointer of the process stack. If there is more than 1 thread or task apart from the main process, you would need to pop the stack of threads from the process you are already running field by field and put it in the stack of suspended threads (you can save the fields or packed subfields one by one and rebuild a task descriptor or structure later or have the task/thread scheduler do so). Once you are left with the main thread (which probably would do little more than handle/start/stop/create/delete the rest of the threads, and do basic processing on system/user events to send them such events), you can pop the fields of the suspended tasks back to the tasks to run and you could pass a structure to a pointer of the built task structure of the most urgent task/thread ID while doing so (for instance inspecting the relevance or wake time of a task while popping back to the running processes task), or run every task while moving structures from the stack of the ones that are supposed to run and those that are suspended.


You could have an event stack per thread, then you would always have the most recent event on top of the stack.

You could mark an event in the stack as INVALID_EVENT followed by the number of bytes it contains. For example you could have the following header for all of the events of the stack:

Code: Select all

dword EVENT_NAME
dword SIZE_IN_BYTES
Then the stack would be increased or decreased according to the size of the data including the size of the event name DWORD and header.


In this way you can mark an event you wish to discard and you can leave discarding it pending for later. It will distribute both time and reduce fragmentation because you just need to free the offset of the latest invalid event and then find the next valid one. Then the program will process pending free operations only when there is the immediate demand to do so (on demand).

As you can see, you get a simple and automatic sublayer that can allocate and free memory.

For a higher level stack, it's probably to have a forward stack instead of an inverted stack such as that of the x86 CPU itself. It's better for a regular application level because it allows you to allocate more memory and increase it and it also allows to do so without having to contain mostly inverted structure fields.

You can later implement functions to realign fields in the stack in a coherent way (for instance keeping the size of the most recent padding in the latest byte itself or in an external field), and in general to navigate in any order even when you have just one header per contained stack element/event/variable, etc...

The stack can have mixed data, raw bytes, words, dwords and structures such as events as long as only your thread itself handles it and knows what it left there since last time.

Re: wait_for_disk_interrupt

Posted: Sun Feb 28, 2016 9:43 pm
by blackoil
Stacking context structure is good idea for FILO logic. Is it necessary to switch descriptors in GDT often?