Commit | Line | Data |
---|---|---|
4ed24f86 JD |
1 | #!/usr/bin/env python3 |
2 | # | |
3 | # The MIT License (MIT) | |
4 | # | |
a3fa57c0 | 5 | # Copyright (C) 2015 - Julien Desfossez <jdesfossez@efficios.com> |
39fec005 | 6 | # 2015 - Antoine Busque <abusque@efficios.com> |
4ed24f86 JD |
7 | # |
8 | # Permission is hereby granted, free of charge, to any person obtaining a copy | |
9 | # of this software and associated documentation files (the "Software"), to deal | |
10 | # in the Software without restriction, including without limitation the rights | |
11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
12 | # copies of the Software, and to permit persons to whom the Software is | |
13 | # furnished to do so, subject to the following conditions: | |
14 | # | |
15 | # The above copyright notice and this permission notice shall be included in | |
16 | # all copies or substantial portions of the Software. | |
17 | # | |
18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
24 | # SOFTWARE. | |
25 | ||
bd3cd7c5 | 26 | import socket |
5ea5dc05 | 27 | from linuxautomaton import common |
bd3cd7c5 JD |
28 | |
29 | ||
30 | class StateVariable: | |
31 | pass | |
32 | ||
33 | ||
34 | class Process(): | |
3ce8423c AB |
35 | def __init__(self, tid=None, pid=None, comm=''): |
36 | self.tid = tid | |
37 | self.pid = pid | |
38 | self.comm = comm | |
bd3cd7c5 JD |
39 | # indexed by fd |
40 | self.fds = {} | |
b5f11254 | 41 | self.current_syscall = None |
bd3cd7c5 | 42 | # the process scheduled before this one |
2b4a3c12 | 43 | self.prev_tid = None |
bd3cd7c5 JD |
44 | |
45 | ||
46 | class CPU(): | |
3c9bf3fa AB |
47 | def __init__(self, cpu_id): |
48 | self.cpu_id = cpu_id | |
2b4a3c12 | 49 | self.current_tid = None |
f4522f09 | 50 | self.current_hard_irq = None |
26facb3a AB |
51 | # softirqs use a dict because multiple ones can be raised before |
52 | # handling. They are indexed by vec, and each entry is a list, | |
53 | # ordered chronologically | |
54 | self.current_softirqs = {} | |
bd3cd7c5 JD |
55 | |
56 | ||
994541c3 AB |
57 | class MemoryManagement(): |
58 | def __init__(self): | |
59 | self.page_count = 0 | |
60 | ||
5ea5dc05 | 61 | |
21167679 | 62 | class SyscallEvent(): |
3b1dda96 AB |
63 | def __init__(self, name, begin_ts): |
64 | self.name = name | |
65 | self.begin_ts = begin_ts | |
66 | self.end_ts = None | |
21167679 JD |
67 | self.ret = None |
68 | self.duration = None | |
a4a9fb7b AB |
69 | # Only applicable to I/O syscalls |
70 | self.io_rq = None | |
bd3cd7c5 | 71 | |
3b1dda96 AB |
72 | def process_exit(self, event): |
73 | self.end_ts = event.timestamp | |
74 | self.ret = event['ret'] | |
75 | self.duration = self.end_ts - self.begin_ts | |
76 | ||
77 | @classmethod | |
78 | def new_from_entry(cls, event): | |
5ea5dc05 AB |
79 | name = common.get_syscall_name(event) |
80 | return cls(name, event.timestamp) | |
3b1dda96 | 81 | |
bd3cd7c5 JD |
82 | |
83 | class Disk(): | |
84 | def __init__(self): | |
3b1dda96 | 85 | # pending block IO Requests, indexed by sector |
bd3cd7c5 | 86 | self.pending_requests = {} |
bd3cd7c5 JD |
87 | |
88 | ||
bd3cd7c5 JD |
89 | class FDType(): |
90 | unknown = 0 | |
91 | disk = 1 | |
92 | net = 2 | |
93 | # not 100% sure they are network FDs (assumed when net_dev_xmit is | |
94 | # called during a write syscall and the type in unknown). | |
95 | maybe_net = 3 | |
96 | ||
8f9a5260 AB |
97 | @staticmethod |
98 | def get_fd_type(name, family): | |
99 | if name in SyscallConsts.NET_OPEN_SYSCALLS: | |
100 | if family in SyscallConsts.INET_FAMILIES: | |
101 | return FDType.net | |
102 | if family in SyscallConsts.DISK_FAMILIES: | |
103 | return FDType.disk | |
104 | ||
105 | if name in SyscallConsts.DISK_OPEN_SYSCALLS: | |
106 | return FDType.disk | |
107 | ||
108 | return FDType.unknown | |
109 | ||
bd3cd7c5 JD |
110 | |
111 | class FD(): | |
ccfc666b AB |
112 | def __init__(self, fd, filename='unknown', fd_type=FDType.unknown, |
113 | cloexec=False, family=None): | |
114 | self.fd = fd | |
115 | self.filename = filename | |
ccfc666b AB |
116 | self.fd_type = fd_type |
117 | self.cloexec = cloexec | |
118 | self.family = family | |
bd3cd7c5 | 119 | |
6f2c9e92 AB |
120 | @classmethod |
121 | def new_from_fd(cls, fd): | |
ccfc666b AB |
122 | return cls(fd.fd, fd.filename, fd.fd_type, fd.cloexec, fd.family) |
123 | ||
124 | @classmethod | |
125 | def new_from_open_rq(cls, io_rq): | |
126 | return cls(io_rq.fd, io_rq.filename, io_rq.fd_type, io_rq.cloexec, | |
127 | io_rq.family) | |
bd3cd7c5 JD |
128 | |
129 | ||
130 | class IRQ(): | |
0297cb98 | 131 | def __init__(self, id, cpu_id, begin_ts=None): |
f4522f09 | 132 | self.id = id |
9b94fe47 | 133 | self.cpu_id = cpu_id |
0297cb98 AB |
134 | self.begin_ts = begin_ts |
135 | self.end_ts = None | |
bd3cd7c5 | 136 | |
f4522f09 AB |
137 | |
138 | class HardIRQ(IRQ): | |
0297cb98 AB |
139 | def __init__(self, id, cpu_id, begin_ts): |
140 | super().__init__(id, cpu_id, begin_ts) | |
f4522f09 AB |
141 | self.ret = None |
142 | ||
143 | @classmethod | |
144 | def new_from_irq_handler_entry(cls, event): | |
3f52a605 | 145 | id = event['irq'] |
9b94fe47 | 146 | cpu_id = event['cpu_id'] |
0297cb98 AB |
147 | begin_ts = event.timestamp |
148 | return cls(id, cpu_id, begin_ts) | |
f4522f09 AB |
149 | |
150 | ||
151 | class SoftIRQ(IRQ): | |
0297cb98 AB |
152 | def __init__(self, id, cpu_id, raise_ts=None, begin_ts=None): |
153 | super().__init__(id, cpu_id, begin_ts) | |
f4522f09 AB |
154 | self.raise_ts = raise_ts |
155 | ||
156 | @classmethod | |
157 | def new_from_softirq_raise(cls, event): | |
3f52a605 | 158 | id = event['vec'] |
9b94fe47 | 159 | cpu_id = event['cpu_id'] |
f4522f09 | 160 | raise_ts = event.timestamp |
9b94fe47 | 161 | return cls(id, cpu_id, raise_ts) |
f4522f09 AB |
162 | |
163 | @classmethod | |
164 | def new_from_softirq_entry(cls, event): | |
3f52a605 | 165 | id = event['vec'] |
9b94fe47 | 166 | cpu_id = event['cpu_id'] |
0297cb98 AB |
167 | begin_ts = event.timestamp |
168 | return cls(id, cpu_id, begin_ts=begin_ts) | |
ca95b1c3 | 169 | |
bd3cd7c5 JD |
170 | |
171 | class IORequest(): | |
bd3cd7c5 JD |
172 | # I/O operations |
173 | OP_OPEN = 1 | |
174 | OP_READ = 2 | |
175 | OP_WRITE = 3 | |
176 | OP_CLOSE = 4 | |
177 | OP_SYNC = 5 | |
54106800 AB |
178 | # Operation used for requests that both read and write, |
179 | # e.g. splice and sendfile | |
180 | OP_READ_WRITE = 6 | |
bd3cd7c5 | 181 | |
3b1dda96 AB |
182 | def __init__(self, begin_ts, size, tid, operation): |
183 | self.begin_ts = begin_ts | |
184 | self.end_ts = None | |
bd3cd7c5 | 185 | self.duration = None |
3b1dda96 AB |
186 | # request size in bytes |
187 | self.size = size | |
188 | self.operation = operation | |
189 | # tid of process that triggered the rq | |
190 | self.tid = tid | |
a4a9fb7b AB |
191 | # Error number if request failed |
192 | self.errno = None | |
3b1dda96 | 193 | |
5157d702 AB |
194 | @staticmethod |
195 | def is_equivalent_operation(left_op, right_op): | |
196 | """Predicate used to compare equivalence of IO_OPERATION. | |
197 | ||
198 | This method is employed because OP_READ_WRITE behaves like a | |
199 | set containing both OP_READ and OP_WRITE and is therefore | |
200 | equivalent to these operations as well as itself | |
201 | """ | |
202 | if left_op == IORequest.OP_READ_WRITE: | |
203 | return right_op in [IORequest.OP_READ, IORequest.OP_WRITE, | |
204 | IORequest.OP_READ_WRITE] | |
205 | if left_op == IORequest.OP_READ: | |
206 | return right_op in [IORequest.OP_READ, IORequest.OP_READ_WRITE] | |
207 | if left_op == IORequest.OP_WRITE: | |
208 | return right_op in [IORequest.OP_WRITE, IORequest.OP_READ_WRITE] | |
209 | ||
210 | return left_op == right_op | |
3b1dda96 AB |
211 | |
212 | class SyscallIORequest(IORequest): | |
a4a9fb7b AB |
213 | def __init__(self, begin_ts, size, tid, operation, syscall_name): |
214 | super().__init__(begin_ts, None, tid, operation) | |
bd3cd7c5 | 215 | self.fd = None |
a4a9fb7b | 216 | self.syscall_name = syscall_name |
3b1dda96 AB |
217 | # Number of pages alloc'd/freed/written to disk during the rq |
218 | self.pages_allocated = 0 | |
219 | self.pages_freed = 0 | |
220 | self.pages_written = 0 | |
221 | # Whether kswapd was forced to wakeup during the rq | |
bd3cd7c5 | 222 | self.woke_kswapd = False |
3b1dda96 | 223 | |
a4a9fb7b AB |
224 | def update_from_exit(self, event): |
225 | self.end_ts = event.timestamp | |
226 | self.duration = self.end_ts - self.begin_ts | |
227 | if event['ret'] < 0: | |
228 | self.errno = -event['ret'] | |
229 | ||
230 | ||
231 | class OpenIORequest(SyscallIORequest): | |
232 | def __init__(self, begin_ts, tid, syscall_name, filename, | |
233 | fd_type): | |
234 | super().__init__(begin_ts, None, tid, IORequest.OP_OPEN, syscall_name) | |
235 | # FD set on syscall exit | |
236 | self.fd = None | |
237 | self.filename = filename | |
238 | self.fd_type = fd_type | |
239 | self.family = None | |
240 | self.cloexec = False | |
241 | ||
242 | def update_from_exit(self, event): | |
243 | super().update_from_exit(event) | |
244 | if event['ret'] >= 0: | |
245 | self.fd = event['ret'] | |
246 | ||
247 | @classmethod | |
248 | def new_from_disk_open(cls, event, tid): | |
249 | begin_ts = event.timestamp | |
250 | name = common.get_syscall_name(event) | |
251 | filename = event['filename'] | |
252 | ||
253 | req = cls(begin_ts, tid, name, filename, FDType.disk) | |
254 | req.cloexec = event['flags'] & common.O_CLOEXEC == common.O_CLOEXEC | |
255 | ||
256 | return req | |
257 | ||
258 | @classmethod | |
259 | def new_from_accept(cls, event, tid): | |
260 | # Handle both accept and accept4 | |
261 | begin_ts = event.timestamp | |
262 | name = common.get_syscall_name(event) | |
263 | req = cls(begin_ts, tid, name, 'socket', FDType.net) | |
264 | ||
265 | if 'family' in event: | |
266 | req.family = event['family'] | |
267 | # Set filename to ip:port if INET socket | |
268 | if req.family == socket.AF_INET: | |
269 | req.filename = '%s:%d' % (common.get_v4_addr_str( | |
270 | event['v4addr']), event['sport']) | |
271 | ||
272 | return req | |
273 | ||
274 | @classmethod | |
275 | def new_from_socket(cls, event, tid): | |
276 | begin_ts = event.timestamp | |
277 | req = cls(begin_ts, tid, 'socket', 'socket', FDType.net) | |
278 | ||
279 | if 'family' in event: | |
280 | req.family = event['family'] | |
281 | ||
282 | return req | |
283 | ||
284 | @classmethod | |
285 | def new_from_old_fd(cls, event, tid, old_fd): | |
286 | begin_ts = event.timestamp | |
287 | name = common.get_syscall_name(event) | |
288 | if old_fd is None: | |
289 | filename = 'unknown' | |
290 | fd_type = FDType.unknown | |
291 | else: | |
292 | filename = old_fd.filename | |
293 | fd_type = old_fd.fd_type | |
294 | ||
295 | return cls(begin_ts, tid, name, filename, fd_type) | |
296 | ||
297 | ||
298 | class CloseIORequest(SyscallIORequest): | |
299 | def __init__(self, begin_ts, tid, fd): | |
300 | super().__init__(begin_ts, None, tid, IORequest.OP_CLOSE, 'close') | |
301 | self.fd = fd | |
302 | ||
303 | ||
304 | class ReadWriteIORequest(SyscallIORequest): | |
305 | def __init__(self, begin_ts, size, tid, operation, syscall_name): | |
306 | super().__init__(begin_ts, size, tid, operation, syscall_name) | |
1b645a61 AB |
307 | # The size returned on syscall exit, in bytes. May differ from |
308 | # the size initially requested | |
309 | self.returned_size = None | |
a4a9fb7b AB |
310 | # Unused if fd is set |
311 | self.fd_in = None | |
312 | self.fd_out = None | |
313 | ||
314 | def update_from_exit(self, event): | |
315 | super().update_from_exit(event) | |
316 | ret = event['ret'] | |
317 | if ret >= 0: | |
318 | self.returned_size = ret | |
319 | # Set the size to the returned one if none was set at | |
320 | # entry, as with recvmsg or sendmsg | |
321 | if self.size is None: | |
322 | self.size = ret | |
323 | ||
324 | @classmethod | |
325 | def new_from_splice(cls, event, tid): | |
326 | begin_ts = event.timestamp | |
327 | size = event['len'] | |
328 | ||
54106800 | 329 | req = cls(begin_ts, size, tid, IORequest.OP_READ_WRITE, 'splice') |
a4a9fb7b AB |
330 | req.fd_in = event['fd_in'] |
331 | req.fd_out = event['fd_out'] | |
332 | ||
333 | return req | |
334 | ||
335 | @classmethod | |
336 | def new_from_sendfile64(cls, event, tid): | |
337 | begin_ts = event.timestamp | |
338 | size = event['count'] | |
339 | ||
54106800 | 340 | req = cls(begin_ts, size, tid, IORequest.OP_READ_WRITE, 'sendfile64') |
a4a9fb7b AB |
341 | req.fd_in = event['in_fd'] |
342 | req.fd_out = event['out_fd'] | |
343 | ||
344 | return req | |
345 | ||
346 | @classmethod | |
347 | def new_from_fd_event(cls, event, tid, size_key): | |
348 | begin_ts = event.timestamp | |
349 | # Some events, like recvmsg or sendmsg, only have size info on return | |
350 | if size_key is not None: | |
351 | size = event[size_key] | |
352 | else: | |
353 | size = None | |
354 | ||
355 | syscall_name = common.get_syscall_name(event) | |
356 | if syscall_name in SyscallConsts.READ_SYSCALLS: | |
357 | operation = IORequest.OP_READ | |
358 | else: | |
359 | operation = IORequest.OP_WRITE | |
360 | ||
361 | req = cls(begin_ts, size, tid, operation, syscall_name) | |
362 | req.fd = event['fd'] | |
363 | ||
364 | return req | |
365 | ||
366 | ||
367 | class SyncIORequest(SyscallIORequest): | |
368 | def __init__(self, begin_ts, size, tid, syscall_name): | |
369 | super().__init__(begin_ts, size, tid, IORequest.OP_SYNC, syscall_name) | |
370 | ||
a4a9fb7b AB |
371 | @classmethod |
372 | def new_from_sync(cls, event, tid): | |
373 | begin_ts = event.timestamp | |
374 | size = None | |
375 | ||
376 | return cls(begin_ts, size, tid, 'sync') | |
377 | ||
378 | @classmethod | |
379 | def new_from_fsync(cls, event, tid): | |
380 | # Also handle fdatasync | |
381 | begin_ts = event.timestamp | |
382 | size = None | |
383 | syscall_name = common.get_syscall_name(event) | |
384 | ||
385 | req = cls(begin_ts, size, tid, syscall_name) | |
386 | req.fd = event['fd'] | |
387 | ||
388 | return req | |
389 | ||
390 | @classmethod | |
391 | def new_from_sync_file_range(cls, event, tid): | |
392 | begin_ts = event.timestamp | |
393 | size = event['nbytes'] | |
394 | ||
395 | req = cls(begin_ts, size, tid, 'sync_file_range') | |
396 | req.fd = event['fd'] | |
397 | ||
398 | return req | |
399 | ||
3b1dda96 AB |
400 | |
401 | class BlockIORequest(IORequest): | |
402 | # Logical sector size in bytes, according to the kernel | |
403 | SECTOR_SIZE = 512 | |
404 | ||
405 | def __init__(self, begin_ts, tid, operation, dev, sector, nr_sector): | |
406 | size = nr_sector * BlockIORequest.SECTOR_SIZE | |
407 | super().__init__(begin_ts, size, tid, operation) | |
408 | self.dev = dev | |
409 | self.sector = sector | |
410 | self.nr_sector = nr_sector | |
411 | ||
412 | def update_from_rq_complete(self, event): | |
413 | self.end_ts = event.timestamp | |
414 | self.duration = self.end_ts - self.begin_ts | |
415 | ||
416 | @classmethod | |
417 | def new_from_rq_issue(cls, event): | |
418 | begin_ts = event.timestamp | |
419 | dev = event['dev'] | |
420 | sector = event['sector'] | |
421 | nr_sector = event['nr_sector'] | |
422 | tid = event['tid'] | |
423 | # An even rwbs indicates read operation, odd indicates write | |
424 | if event['rwbs'] % 2 == 0: | |
425 | operation = IORequest.OP_READ | |
426 | else: | |
427 | operation = IORequest.OP_WRITE | |
428 | ||
429 | return cls(begin_ts, tid, operation, dev, sector, nr_sector) | |
430 | ||
431 | ||
432 | class BlockRemapRequest(): | |
433 | def __init__(self, dev, sector, old_dev, old_sector): | |
434 | self.dev = dev | |
435 | self.sector = sector | |
436 | self.old_dev = old_dev | |
437 | self.old_sector = old_sector | |
bd3cd7c5 JD |
438 | |
439 | ||
bd3cd7c5 JD |
440 | class SyscallConsts(): |
441 | # TODO: decouple socket/family logic from this class | |
442 | INET_FAMILIES = [socket.AF_INET, socket.AF_INET6] | |
443 | DISK_FAMILIES = [socket.AF_UNIX] | |
444 | # list nof syscalls that open a FD on disk (in the exit_syscall event) | |
baa4cfe9 | 445 | DISK_OPEN_SYSCALLS = ['open', 'openat'] |
bd3cd7c5 JD |
446 | # list of syscalls that open a FD on the network |
447 | # (in the exit_syscall event) | |
baa4cfe9 | 448 | NET_OPEN_SYSCALLS = ['accept', 'accept4', 'socket'] |
bd3cd7c5 | 449 | # list of syscalls that can duplicate a FD |
9110f542 | 450 | DUP_OPEN_SYSCALLS = ['fcntl', 'dup', 'dup2', 'dup3'] |
baa4cfe9 | 451 | SYNC_SYSCALLS = ['sync', 'sync_file_range', 'fsync', 'fdatasync'] |
bd3cd7c5 JD |
452 | # merge the 3 open lists |
453 | OPEN_SYSCALLS = DISK_OPEN_SYSCALLS + NET_OPEN_SYSCALLS + DUP_OPEN_SYSCALLS | |
3f52a605 | 454 | # list of syscalls that close a FD (in the 'fd =' field) |
baa4cfe9 | 455 | CLOSE_SYSCALLS = ['close'] |
bd3cd7c5 | 456 | # list of syscall that read on a FD, value in the exit_syscall following |
a692ae7b AB |
457 | READ_SYSCALLS = ['read', 'recvmsg', 'recvfrom', 'readv', 'pread', |
458 | 'pread64', 'preadv'] | |
bd3cd7c5 | 459 | # list of syscall that write on a FD, value in the exit_syscall following |
a692ae7b AB |
460 | WRITE_SYSCALLS = ['write', 'sendmsg', 'sendto', 'writev', 'pwrite', |
461 | 'pwrite64', 'pwritev'] | |
54106800 AB |
462 | # list of syscalls that both read and write on two FDs |
463 | READ_WRITE_SYSCALLS = ['splice', 'sendfile64'] | |
21167679 JD |
464 | # All I/O related syscalls |
465 | IO_SYSCALLS = OPEN_SYSCALLS + CLOSE_SYSCALLS + READ_SYSCALLS + \ | |
54106800 | 466 | WRITE_SYSCALLS + SYNC_SYSCALLS + READ_WRITE_SYSCALLS |