6.1 Linux进程概述
- 交互进程:
- 批处理进程:
- 守护进程:
内核把进程存放在任务队列(task list)的双向循环链表中,其中链表的每一项都是类型为task_struct,称为进程描述符的结构,定义在<include/linux/sched.h>中。
task_struct结构中最重要的两个域:state 和 pid
1)进程状态
- 运行(TASK_RUNNING):包括就绪和运行,指进程随时可以被调度运行或正在运行
- 可中断(TASK_INTERRUPTIBLE):进程停止运行,直到满足继续运行条件
- 不可中断(TASK_UNINTERRUPTIBLE):停止运行,满足条件,也不会马上被激活。
- 僵死(TASK_ZOMBIE):进程运行结束,等待父进程销毁
- 停止(TASK_STOPPED):进程停止运行,当进程收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号,就会停止
2)任务标识
内核通过唯一的进程标识值PID来识别每个进程。PID是一个非负数,实际是一个短整型数据,最大32768。可以查看/proc/sys/kernel/pid_max来确定。
进程的创建、执行和终止
fork() 和 exec()
fork()通过复制当前进程的内容创建一个子进程,子进程与父进程的区别仅仅在于不同的PID、PPID和其他一些资源。
写时复制技术。
exec()函数负责读取可执行文件并将其载入地址空间开始运行。
6.1.2、进程的调度
优先级在0 ~ MAX_PRIO-1之间,数值越低优先级越高。
实时程序的优先级范围在0 ~ MAX_RT_PRIO-1,一般进程的优先级在MAX_RT_PRIO ~ MAX_PRIO-1之间。
内核中默认配置:优先级0 ~ 139,实时进程占用0 ~ 99,一般进程100 ~ 139
6.2、Linux进程控制相关API
1、fork函数
执行一次,返回两个值。创建一个新进程,称为子进程。
父进程返回子进程的进程号,子进程返回0.
#include// 提供类型pid_t的定义#include pid_t for(void);// 0:子进程// 子进程ID(大于0的整数):父进程// -1:出错
int result = fork();if (result == -1){ exit(-1);} else if (result ==0) { /*子进程相关语句*/} else { /*父进程相关语句*/}
2、exec函数族
取代原调用进程的数据段、代码段和堆栈段。
#includeint execl(const char *path, const char *arg, ...);int execv(const char *path, char * const argv[]);int execle(const char *path, const char *arg, ..., char *const envp[]);int execve(const char *path, char *const argv[], char *const envp[]);int execlp(const char *file, const char *arg, ...);int execvp(const char *file, char * const argv[]);// 出错:-1
前4个完整路径,后2个文件名,从PATH查找。
l(list):逐个列举,char * arg
v(vector):指针数组,*const argv[]
必须以NULL结尾。
3、exit 和 _exit
exit检查进程中文件的打开情况,把文件缓冲区中的内容写回文件。
_exit直接将进程关闭,缓冲区中的数据会丢失。
exit:#include_exit:#include void exit/_exit(int status);// 0 标识正常
4、wait 和 waitpid
wait函数可以使父进程阻塞。直到任意一个子进程结束或者父进程接到了一个指定的信号为止。如果该父进程没有子进程或者他的子进程已经结束,则wait函数会立即返回。
wait是waitpid的一个特例。
#include#include pid_t wait(int *status);pid_t waitpid(pid_t pid, int *status, int options);// 成功:子进程的进程号 或 0(调用成功但子进程还未退出)// 失败:-1
status若为空,则不保存子进程的退出状态。
5、避免僵死进程实例
当一个进程已经终止,但是其父进程尚未对其进行回收的进程被称为僵死进程。
为了避免,一种办法是父进程调用wait/waitpid等待子进程结束,但是在子进程结束前父进程会一直阻塞,不能做任何事情。
另一种更好的方法就是调用两次fork函数。
6.3、进程间通信
System V IPC单机,Socket
- Unix进程间通信(IPC)方式包括管道、FIFO、信号
- System V进程间通信(IPC)包括System V消息队列、System V信号灯、System V共享内存区
- Posix 进程间通信(IPC)包括Posix消息队列、Posix信号灯、Posix共享内存区
Linux中使用较多的进程间通信方式:
- 管道(Pipe)及有名管道(named pipe)。管道可用于有亲缘关系进程间的通信;有名管道除具有管道的功能外,还允许无亲缘关系进程之间进行通信。
- 信号(Signal)是在软件层次上对中断机制的一种模拟。
- 消息队列是消息的链接表,包括Posix消息队列和System V消息队列。它克服了前两种通信方式种信息量有限的缺点,具有写权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息。
- 共享内存,是一种最高效的进程间通信方式。多个进程访问同一块内存空间。需要依靠某种同步机制,入互斥锁和信号量等。
- 信号量,主要作为进程间和同一进程中不同线程之间的同步手段。
- 套接字(Socket)一种更为通用的进程间通信机制,可用于不同主机之间的进程间通信。
6.3.1、管道通信
1、无名管道(PIPE)
半双工、具有固定的读端和写端。
一种特殊的文件,存在于内存中。
2、有名管道(FIFO)
可使互不相关的两个进程实现彼此通信。
可以通过路径名指出,并且在文件系统中是可见的。
FIFO严格地遵循先进先出规则,对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。
有名管道的创建
mkfifo函数,类似于open操作,可以指定管道的路径和读/写权限。
“mknod 管道名 p”命令来创建有名管道。
管道创建成功后,就可以使用open、read、write函数了。与普通文件不同的是阻塞问题。
普通文件不阻塞,管道中有阻塞的可能。O_NONBLOCK
#include#include int mkfifo(const char *filename, mode_t mode);// 成功:0// 出错:-1mkfifo("myfifo", 0666);mkfifo("myfifo", O_RDWR | O_NONBLOCK);// mkfifo仅创建了管道,并没有打开管道。
6.3.2、信号通信
信号是在软件层次上对中断机制的一种模拟。
信号是异步的。唯一的异步通信方式。
两个来源:硬件来源和软件来源。
响应:
- 忽略信息:但是SIGKILL和SIGSTOP不能忽略
- 捕捉信息:定义信号处理函数
- 执行默认操作:
信号的处理包括信号的发送、捕获以及信号的处理,
- 发送信号的函数:kill、raise
- 设置信号处理的函数:signal
- 其他相关的函数:alarm、pause
kill 和 raise
raise函数只允许进程向自身发送信号。
#include#include int kill(pid_t pid, int sig);int raise(int sig);// 成功:0// 出错:-1raise(SIGSTOP);kill(pis, SIGKILL);
alarm 和 pause
设置一个定时器,到时间时发送SIGALARM信号。一个进程只能有一个闹钟时间,新值代替之前的值。
pause函数是用于将调用进程睡眠直到捕捉到信号为止。通常可以用来让进程等待信号到达。
#includeunsigned int alarm(unsigned int seconds)int pause(void);// 成功:上一个时钟的剩余时间,否则返回0// 出错:-1,并且把error值设为EINTRret = alarm(5);pause();
signal
使用signal函数时,只需要把处理的信号和处理函数列出即可。主要用于前32种非实时信号的处理,不支持信号传递信息。
#includevoid (*signal(int signum, /*指定信号*/ void (*handler))(int)))(int) /*对信号的处理*/// 函数原型typedef void func(int);func *signal(int, func *);// 成功:以前的信号处理配置// 出错:-1
void my_func(int signo){ ...}signal(SIGINT, my_func);signal(SIGQUIT, my_func);
6.3.3、共享内存
共享内存允许多个进程共享一个给定的内存区域。数据不需要在多个进程之间复制,所以是最高效的一种通信方式。
使用共享内存的关键在于如何在多个进程之间对一给定的存储区进行同步访问。
通常信号量被用来实现对共享内存的访问。
实现步骤:
- 创建共享内存,shmget函数。从内存中获得一段共享内存区域。
- 映射共享内存,把这段创建的内存映射到具体的进程空间去。shmat函数。
- 撤销映射的操作。函数为shmdt
- 删除共享内存对象,函数为shmctl
#include#include #include int shmget(key_t key, /*IPC_PRIVATE*/ int size, /*共享内存区大小*/ int shmflg); /*通open函数的权限位,也可以用8进制表示法*/// 成功:共享内存段标识符// 出错:-1void *shmat(int shmid, /*要映射的共享内存区标识符*/ const void *shmaddr, /*将共享内存映射到指定地址(0:由操作系统决定映射地址)*/ int shmflg); /*SHM_RDONLY:共享内存只读。默认0,可读写*/// 成功:被映射后的地址// 出错:-1int shmdt(const void *shmaddr); /*被映射的共享内存的地址*/// 成功:0// 出错:-1int shmctl(int shmid, /*共享内存的标识符*/ int cmd, /*要执行的操作的命令字*/ struct shmid_ds *buf); /*获取/设置共享内存属性的结构体的地址*/// 成功:0// 出错:-1
/*创建共享内存*/int shmid = shmget(IPC_PRIVATE, BUFSZ, 0666);/*映射共享内存*/char *shmaddr = (char *)shmat(shmid, 0, 0);/*查看共享内存对象*/system("ipcs -m");/*取消共享内存映射*/shmdt(shmaddr);/*删除共享内存*/shmctl(shmid, IPC_RMID, NULL);
6.3.4、消息队列
消息队列由内核维护。
- 创建或打开消息队列。使用函数msgget,这里创建的消息队列的数量会受到系统消息队列数量的限制。
- 添加消息队列。使用函数msgsnd,它把消息添加到已打开的消息队列末尾。
- 读取消息队列。使用函数msgrcv,它把消息从消息队列中取走,与FIFO不同的是,这里可以指定取走某一类型的消息。
- 控制消息队列。使用函数msgctl,可以完成多项功能。
#include#include #include int msgget(key_t key, /*返回新的或已有队列的队列ID、IPC_PRIVATE*/ int flag);// 成功:消息队列ID// 出错:-1int msgsnd(int msqid, /*消息队列的队列ID*/ const void *prt, /*指向消息结构的指针*/ size_t size, /*消息的大小,不包括消息类型*/ int flag); /*IPC_NOWAIT:若消息并没有立即发送而调用进程会立即执行返回。0:阻塞直到消息发送完成为止*/// prt消息的结构用户自己定义,第一个成员的类型必须为longstruct msgbuf{ long mtype; /*消息类型*/ char mtext[LEN]; /*消息正文*/ ...}// 成功:0// 出错:-1int msgrcv(int msqid, /*消息队列的队列ID*/ const void *msgp, /*接收消息缓冲区*/ int size, /*消息的字节数*/ long msgtype, /*接收的消息类型*/ int flag); /*标志位*/// 成功:接收到的消息的字节数// 失败:-1int msgctl(int msqid, int cmd, struct msqid_ds *buf); /*消息队列缓冲区*/// 成功:0// 失败:-1
typedef struct{ long mtype; ...} MSG;MSG msg;/*生成消息队列需要的key*/key_t key = ftok(".", 'q');/*创建消息队列*/int msqid = msgget(key, IPC_CREATE|0666);/*添加消息到消息队列*/msgsnd(msqid, &msg, sizeof(msg)-sizeof(long), 0);/*读取消息队列中第一个消息*/msgrcv(msqid, &msg, sizeof(msg)-sizeof(long), 0, 0);/*从系统内核中移走消息队列*/msgctl(msqid, IPC_RMID, NULL);
6.4 线程相关API
pthread线程库,遵循POSIX规范,具有很好的可移植性。
1、线程创建和退出
- 创建线程。指定线程所要执行的函数,使用的函数是pthread_create。
- 调用相关线程函数。在线程创建之后,就开始运行相关的线程函数。
- 线程退出。在线程调用函数运行完之后,该线程也就自动结束了。另一种退出线程的方法是使用函数pthread_exit,这是线程的主动行为。
- 线程资源回收。由于一个进程中的多个线程是共享数据段的,因此通常在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放。pthread_join函数,pthread_join可以拥有将当前主线程挂起,等待子线程的结束。调用它的线程将一直阻塞直到指定的线程结束为止。
#includeint pthread_create(pthread_t *thread, /*线程对象*/ pthread_attr_t *attr, /*线程属性设置,默认为NULL,可以使用其他函数来设置*/ void *(*start_routine)(void *), /*线程执行的函数*/ void *arg); /*传递给start_routine的参数*/// 成功:0// 出错:-1void pthread_exit(void *retval); /*线程的返回值,可由其他函数如pthread_join来检测获取*/int pthread_join(pthread_t thread, void ** thread_return); /*用户定义的指针,用来存放被子线程的返回值(不为NULL时)*/// 成功:0// 出错:-1
2、mutex线程访问控制
线程共享进程的资源和地址空间。POSIX中线程同步的方法主要有互斥锁和信号量。
- 互斥锁初始化:pthread_mutex_init
- 互斥锁上锁:pthread_mutex_lock
- 互斥锁判断上锁:pthread_mutex_trylocks
- 互斥锁解锁:pthread_mutex_unlock
- 删除互斥锁:pthread_mutex_destroy
#includeint pthread_mutex_init(pthread_mutex_t *mutex, /*指向互斥锁对象的指针*/ const pthread_mutexattr_t *mutexattr); /*互斥锁的属性*/int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_trylock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);int pthread_mutex_destroy(pthread_mutex_t *mutex);// 成功:0// 出错:-1
mutexattr 如果为NULL表示按默认属性创建互斥锁。
3、信号量线程控制
信号量也就是操作系统中的PV原语。信号量本质上是一个非负的整数计数器,被用来控制对公共资源的访问。
PV原语是对整数计数器信号量sem的操作。一次P操作使sem减一,一次V操作使sem加一。进程(或线程)根据信号量的值来判断是否对公共资源具有访问权限。
当信号量sem的值大于0时,该进程(或线程)具有公共资源的访问权限;相反,当信号量sem的值等于0时,该进程(或线程)就将阻塞直到信号量sem的值大于0为止。
PV原语主要用于进程或线程间的同步和互斥这两种典型情况。若用于互斥,几个进程(或线程)往往只设置一个信号量sem。
当信号量用于同步操作时,往往会设置多个信号量,并安排不同的初始值来实现他们之间的顺序执行。
Linux实现了POSIX的无名信号量,主要用于线程间的互斥同步。
- sem_init用于创建一个信号量,并能初始化它的值。
- sem_wait 和 sem_trywait 相当于P操作,他们都能将信号量的值减一,两种区别在于若信号量等于0时,sem_wait将会阻塞线程,而sem_trywait则会立即返回。
- sem_post 相当于V操作,它将信号量的值加一同时发出信号唤醒等待的线程。
- sem_getvalue用于得到信号量的值
- sem_destroy用于删除信号量。
#includeint sem_init(sem_t *sem, /*信号量*/ int pshated, /*决定信号量能否在几个进程间共享。由于目前Linux还没有实现进程间共享信号量,所以这个值只能够取0*/ unsigend int value); /*信号量初始化值*/
#includeint sem_wait(sem_t *sem);int sem_trywait(sem_t *sem);int sem_post(sem_t *sem);int sem_getvalue(sem_t *sem);int sem_destroy(sem_t *sem);// 成功:0// 出错:-1