1. Users and Groups
真实用户ID和真实组ID
真实用户ID和组ID表示运行进程的真实用户 ID 和 组ID。
有效用户ID和有效组IDp
有效 ID 是进程进行相关操作(比如系统调用)的凭证。
为什么需要有效用户ID和组ID?
通常有效ID和真实ID一致,但是当执行set-user-id 和 set-group-id程序时,有效用户ID被改为程序的拥有者的ID。
set-user id 和 set-group-id
执行set-user id 和 set-group-id程序时将设置进程的有效ID为程序的拥有者的ID,使得其他用户可以通过该程序执行相关操作。
File System User ID and File System Group ID
在Linux操作系统中,是文件系统用户ID和组ID决定文件系统的操作权限而不是有效用户ID和组ID。通常情况下文件系统ID和有效用户ID一致。
为甚要设置文件系统用户ID和组ID呢,也就是说什么情况下有效用户ID和文件系统用户ID不一样呢?
文件系统ID在Linux1.2中开始使用,在Linux1.2的内核中进程如果要发送信号给目标进程,必须与目标进程有相同的有效用户ID。像NFS那样的程序将收到影响,NFS Server 为了能够代表客户端访问文件系统必须将自己的有效用户ID设置为客户端有效用户ID,这样NFS Server将能够接受其他进程的信号。设置文件系统ID可以防止出现这样的情况。
2. File System
Linux文件系统采用索引文件系统,索引文件系统的好处是文件大小可以动态的变化而且支持随机读写,连续文件分配文件块或者串联文件系统都不支持。
Linux文件系统中i结点的作用是用来表示树形文件系统中文件结点(包括目录和数据文件),i 结点存储了文件的属性信息(文件大小,用户者和组,文件权限等,链接计数等)以及数据块索引。12个直接索引,1个间接索引,一个二级索引,一个三级索引。
Linux文件数据文件并不存储文件名,而是通过路径名找到文件对应的i结点,目录文件中存储的是子文件的i node号。
IO Buffer
Kernel Buffer: 当读写磁盘文件时,read
和write
系统调用都不直接访问磁盘,而是通过读或者写缓存(kernel buffer),内核负责从磁盘读数据到缓存或者将数据flush到磁盘。Kernel buffer减少了系统调用的次数和访问磁盘的次数。
Buffer in stdio:
The standard I/O library provides a simple and efficient buffered stream I/O interface.
stdio stream buffer 设置
#include <stdio.h>
int setvbuf(FILE * stream , char * buf , int mode , size_t size );
Returns 0 on success, or nonzero on error
- _IONBF 无缓冲
- _NOLBF 行缓冲
- _IOFBF 完全缓冲,即缓冲满才调用系统调用read或者write,磁盘文件的默认缓冲方式
#include <stdio.h>
void setbuf(FILE * stream , char * buf);
等价于:setvbuf(fp, buf, (buf != NULL) ? _IOFBF: _IONBF, BUFSIZ);
,BUFSIZE 定义在stdio.h头文件中
flush stdio buffer
#include <stdio.h>
int fflush(FILE* stream);
Returns 0 on success, EOF on error
结合使用stdio和fd
#include <stdio.h>
int fileno(FILE* stream);
Returns file descriptor on success, or –1 on error
FILE* fdopen(int fd, const char* mode);
Returns (new) file pointer on success, or NULL on error
fdopen在读写非磁盘文件时非常有用,因为可以直接使用标准IO库函数读写非磁盘文件,比如socket。
3. Signal
信号是Linux进程间通信中的异步IO通信方式,每个进程维护一个信号集,如果收到信号则将对应的信号位置为1,当重新调度时会进程会执行信号处理函数。每个进程同样维护一个信号掩码集(mask),如果所有在Mask中的信号都将被阻塞,直到信号从mask集中删除。由于信号集并不记录信号的发生的次数,所以信号并不会排队,如果在执行信号处理程序之前收到多个信号也只调用一次信号处理函数。
信号默认处理方式:
- ignore
- terminate process
- stop running process
- restart process
定义信号处理函数API:
#include <signal.h>
void signal(int sig, void (*handler)(int))
int sigaction(int sig, const struct sigaction *act, struct sigaction* oldact)
return 0 on Success, -1 on Error
struct sigaction{
void (* sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (* sa_restore)(void);
}
4. Timer
Software Clock,软时钟单位定义在内核源代码中,是内核分配给进程的时间片单位,2.4版本的代码中为100Hz,2.6版本中为1000Hz。软时钟单位决定了timer的精确度。
- struct itimerval
- setitimer(int which, const struct itimerval* newtimer, struct itimerval* oldtimer)
- alarm(int sec)
5. Process
Process Creation
调用fork后子进程父进程的拷贝,子进程的拷贝了父进程的代码段,数据段,堆,堆栈断,父进程和子进程在fork返回后分别继续运行,根据fork返回值区分父进程和子进程。
调用fork后子进程可以调用exec加载新的程序,加载新的程序意味着代码端和数据段替换,堆和栈被重置。
如果每次fork都需要通过拷贝生成父进程的内存镜像,fork的效率会很低,fork使用了copy on write
技术,fork只是拷贝了虚拟内存的页表,父进程和子进程共享相同的物理内存也,后面每次写内存都会拷贝到子进程的物理页面。
Process Termination
非正常结束:收到Signal,该Signal的默认处理方式结束进程。
正常结束:调用_exit或者exit
#include <unistd.h>
void _exit(int status);
void exit(int status):
进程一般不使用_exit而使用exit结束进程,exit执行过程:
- 执行handler(function registered with atexit() and on_exit())
- flush stdio buffers
- 执行_exit
从main函数返回等价于exit(main())
进程结束都干了啥?
- 关闭所有进程已经打开的文件描述符,包括普通文件描述符,目录文件描述符,套接字描述符,消息描述符等。
- 关闭文件后,加在该文件上的锁被释放
- 共享内存detached
- 所有打开的POSIX信号量都被关闭?
- POSIX消息队列被关闭
- 内存锁被释放(mlock or mlockall)
- 内存映射(mmap)解除
exit handler
标准c库提供了两种注册exit handler的方方式:
方式一:
#include <stdlib.h>
int atexit(void (*func)(void))
Returns 0 on success, or nonzero on error
可以注册多个handler,执行顺序与注册顺序相反。
执行fork后子进程继承父进程所有的exit handler,如果子进程执行exec,将删除所有继承的exit handler。
atexit
的限制,首先,exit handler不知道exit status,其次,不能传参数给exit handler。
方式二:
#define _BSD_SOURCE /* Or: #define _SVID_SOURCE */
#include <stdlin.h>
int on_exit(void (*func)(int, void*), void* arg);
Returns 0 on success, or nonzero on error
6. pthread
下图是线程虚拟内存地址
每个线程都有自己的堆栈,线程共享虚拟地址空间,因此线程间共享数据非常方便。
6.1 线程创建:
数据类型 | 作用 |
---|---|
pthread_t | Thread identifier |
pthread_mutex_t | Mutex |
pthread_mutexattr_t | Mutex attributes object |
pthread_cond_t | Condition variable |
pthread_condattr_t | Condition variable attributes object |
pthread_key_t | Key for thread-specific data |
pthread_once_t | One-time initialization control context |
pthread_attr_t | Thread attributes object |
#include <pthread.h>
nt pthread_create(pthread_t * thread , const pthread_attr_t * attr ,void *(* start )(void *), void * arg );
Returns 0 on success, or a positive error number on error
6.2 线程退出
#include <pthread.h>
void pthread_exit(void * retval );
传给pthread_exit的参数是返回值,该返回值不应该存储在线程栈上,使用pthread_join可以获取线程的返回值。
6.3线程ID
#include <pthread.h>
pthread_t pthread_self(void);
Returns the thread ID of the calling thread
返回线程自己的ID。
#include <pthread.h>
int pthread_equal(pthread_t t1 , pthread_t t2 );
Returns nonzero value if t1 and t2 are equal, otherwise 0
判断两个线程id是否相等,之所以需要这个函数是因为pthread_t是opaque data(不透明数据类型)
6.4 join and detach
有的线程需要等待其他线程结束才能继续运行,可以使用pthread_join等待其他线程结束,并获取它们的返回值。如果线程既没有被detach也没有被join,那么该线程将变为僵尸(zombie)线程,僵尸线程积累过多将无法创建新的线程。
#include <pthread.h>
int pthread_join(pthread_t thread , void ** retval );
Returns 0 on success, or a positive error number on error
使用注意:重复join可能导致不可预料的结果
与waitpid()的区别:
- 线程是对等的,任意两个线程可以join,而进程是等级关系( hierarchical relationship ),只有父进程才能等待子进程。
- 进程可以通过waitpid(–1, &status, options))等待任意的子进程,而join必须指定线程id
有的时候我们并不用等待线程结束,也不需要线程的返回状态,可以使用pthread_detach标记线程为detached。
#include <pthread.h>
int pthread_detach(pthread_t thread );
Returns 0 on success, or a positive error number on error
线程一旦被detach,就不能变回joinable状态,而且exit()任然能使detached线程终止。
6.5 threads and errno
在传统的Unix API中errno是一个全局变量,但是在多线程程序中如果errno还是一个全局变量将会出现条件竞争,因此多线程程序中每个线程都有自己的errno(具体如何实现的有待研究)。
thread specific data
thread specific data 可以避免函数使用静态数据或者全局数据,可以用来实现thread safe函数
#include <iostream>
#include <pthread.h>
#include <stdio.h>
using namespace std;
const int MAX_LEN = 100;
pthread_key_t key;//a key index the specific data
pthread_once_t once = PTHREAD_ONCE_INIT;
void destructor(void* arg){
delete (char*)arg;
}
void create_key(void){
pthread_key_create(&key, destructor);
}
void* thread_func(void* arg){
//allocate specific data key
pthread_once(&once,create_key);//pthread_key_create called only once
char* buf;
buf = (char*)pthread_getspecific(key);
if(buf == NULL){
buf = new char[MAX_LEN];
pthread_setspecific(key, buf);
}
printf("thread:%ld specific data pointer:%p
", pthread_self(), buf);
return NULL;
}
int main(){
pthread_t t1, t2;
pthread_create(&t1,NULL,thread_func,NULL);
pthread_create(&t2,NULL,thread_func,NULL);
pthread_join(t1,NULL);
pthread_join(t2,NULL);
return 0;
}