一,进程控制
进程标识
每个进程都有一个非负整型表示的唯一进程ID。进程ID是可复用的,当一个进程终止后,其进程ID也会被其他进程使用。
除了进程ID,每个进程还有一些其他标识符可以获取。
#include <unistd.h>
pid_t getpid(void); --返回调用进程的进程ID
pid_t getppid(void); --返回调用进程的父进程ID
uid_t getuid(void); --返回调用进程的实际用户ID
uid_t geteuid(void); --返回调用进程的有效用户ID
gid_t getgid(void); --返回调用进程的实际组ID
gid_t getegid(void); --返回调用进程的有效组ID
*上面这些get类函数,也有与之对应的set函数,比如 setuid()、setgid()等。
控制进程的常见函数
1.创建新进程(子进程)--fork/vfork
#include <unistd.h>
pid_t fork(void);
返回:若成功,子进程返回0,父进程返回子进程ID。若失败,返回-1
fork函数被调用一次,但是有两次返回:子进程返回0,父进程返回子进程的进程ID。
子进程可以调用getppid()函数获得父进程的进程ID。
由于子进程是父进程的副本,所以子进程可以获得父进程的数据空间,堆和栈的副本。
#include <unistd.h>
pid_t vfork(void);
返回:若成功,子进程返回0,父进程返回子进程ID。若失败,返回-1。
vfork与fork的区别是,vfork并不将父进程的地址空间完全复制到子进程中。
vfork创建子进程后,子进程先运行,子进程调用exec或exit后,父进程再开始被调度运行。
2.挂起进程--wait/waitpid/waitid
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
--pid参数取值:
pid==-1,等待任一子进程,此时的waitpid()与wait()等效
pid>0,等待“进程ID==pid”的子进程
pid==0,等待“组ID==调用进程组ID”的子进程
pid<-1,等待“组ID==pid的绝对值”的子进程
返回:若成功,返回进程ID。若失败,返回0或-1。
父进程调用wait/waitpid后会被挂起,直到子进程终止运行并将退出状态返回给父进程。
父进程也可以通过wait/waitpid来检验子进程是否已经运行结束。
waitpid与wait的区别:
1.wait返回任一终止子进程的状态,waitpid可以指定一个特定的进程进行等待。
2.waitpid提供了wait的非阻塞版本,可以不阻塞地获取一个子进程的状态。
3.waitpid可以通过WUNTRACED和WCONTINUED选项支持作业控制。
3.终止进程--exit
#include <stdlib.h>
void exit(int status)
--status: 进程退出码,主要返回给父进程
无返回
4.在进程中启动另一个进程并退出--exec函数族
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
返回:若成功,不做返回。若失败,返回-1。
第一个参数,file和path的区别: 当参数为path时,传入的数据为路径名;当参数为file时,传入的数据为可执行文件名。
l:表示通过逐个列举的方式传入参数(execl、execle、execlp)
v:表示通过构造指针数组的方式传入参数(execv、execve、execvp)
e:可传递新进程环境变量(execle、execve)
p:可执行文件查找方式为文件名(execlp、execvp)
exec可以根据指定的文件名或目录名找到可执行文件,并取代原进程的数据段、代码段和堆栈。exec执行完以后,原进程的内容除了进程ID外,其他全部被新的进程替换了
5.进程中运行shell指令--system
#include <stdlib.h>
int system(const char *cmdstring)
system调用fork产生子进程, 由子进程来执行shell脚本cmdstring, 此命令执行完后返回到原调用的父进程。
system函数在系统中的实现:system函数执行时,会调用fork、execve、waitpid等函数。
int system(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL)
{
return (1);
}
if((pid = fork())<0)
{
status = -1;
}
else if(pid == 0)
{
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
_exit(127); //exec只在执行失败时才返回原有的进程
}
else
{
while(waitpid(pid, &status, 0) < 0)
{
if(errno != EINTR)
{
status = -1;
break;
}
}
}
return status; //如果waitpid成功,则返回子进程的退出状态
}
如果fork()、exec()和waitpid()都执行成功,system()返回shell的终止状态。
代码样例:
#include <iostream>
#include <cstdlib>
#include <pthread.h>
#include <unistd.h>
using namespace std;
#define NUM_THREADS 5
void *wait(void *t) {
int i;
long tid;
tid = (long)t;
sleep(1);
cout << "Sleeping in thread " << endl;
cout << "Thread with id : " << tid << " ...exiting " << endl;
pthread_exit(NULL);
}
int main () {
int rc;
int i;
pthread_t threads[NUM_THREADS];
pthread_attr_t attr;
void *status;
// Initialize and set thread joinable
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
for( i = 0; i < NUM_THREADS; i++ ) {
cout << "main() : creating thread, " << i << endl;
rc = pthread_create(&threads[i], &attr, wait, (void *)i );
if (rc) {
cout << "Error:unable to create thread," << rc << endl;
exit(-1);
}
}
// free attribute and wait for the other threads
pthread_attr_destroy(&attr);
for( i = 0; i < NUM_THREADS; i++ ) {
rc = pthread_join(threads[i], &status);
if (rc) {
cout << "Error:unable to join," << rc << endl;
exit(-1);
}
cout << "Main: completed thread id :" << i ;
cout << " exiting with status :" << status << endl;
}
cout << "Main: program exiting." << endl;
pthread_exit(NULL);
}
二,线程控制:
同一个进程的所有线程都可以访问该进程的组成部件,如文件描述符,内存和进程中的代码。
正由于同一进程内的所有线程共享相同的内存,所以线程之间同步信息很方便,不需要使用IPC机制。
线程控制的编码实现:
POSIX版的线程模块--pthread
存放线程属性的结构体--pthread_attr_t
typedef struct
{
__detachstate; --两种状态:detachable和joinable
__schedpolicy; --线程的调度策略
__schedparam; --线程的调度优先级
__inheritsched; --线程的继承策略
__scope; --线程的作用域
......
} pthread_attr_t;
1.线程ID的获取和比较--pthread_self/pthread_equal
正如每个进程都有一个进程ID,每个线程也有对应的线程ID--线程标识。进程ID的数据类型是pid_t,线程ID的数据类型是pthread_t。
比较两个线程ID是否相等的函数--pthread_equal:
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2)
返回:若相等,返回非0数值。否则,返回0。
获得线程自身的线程ID的函数--pthread_self:
#include <pthread.h>
pthread_t pthread_self(void)
返回:调用线程的线程ID
2.创建线程--pthread_create
#include <pthread.h>
int pthread_create(pthread_t *tid,
const pthread_attr_t *attr,
void * (*start_routine)(void *),
void *arg);
void * (*start_routine)--指向要线程化的函数的指针
返回:若成功,返回0。否则,返回错误编号
进程中的主线程由exec执行后创建,其余的线程由pthread_create创建。
如果新线程创建成功,其线程ID可以通过tid指针返回。
pthread_attr_t类型的attr指针,指向了线程的属性,例如线程优先级,栈的大小等等。如果需要使用默认属性,可以把attr参数置为空指针。
当线程内封装了一个函数func时,arg参数用来给该函数传参。
3.线程终止--pthread_exit/pthread_join/pthread_cancel
如果进程中的任意线程调用了exit、_Exit或者_exit,那么整个进程就会终止。
有三种方式可以让单个线程退出的时候不终止整个进程:
(1)线程被启动线程的函数返回时,返回值是线程的退出码(pthread_create的第三个参数)
(2)线程被同一进程中的其他线程取消(pthread_cancel)
(3)线程调用pthread_exit
#include <pthread.h>
void pthread_exit(void *status);
不向调用者返回
status是void*类型的指针,这种指针可以指向任何类型的值, status所指向的数据将作为线程退出时的返回值。
pthread_exit相当于执行了“线程退出+return返回"。
#include <pthread.h>
int pthread_join(pthread_t tid, void **status);
返回:若成功,返回0。否则,返回错误编号
类似于wait/waitpid将进程挂起,以等待它的子进程结束。线程场景中可以使用pthread_join将一个线程挂起以等待另一个线程结束并获得它的退出状态。
被等待的线程的退出状态将存放到status指针指向的位置。
只有线程未分离时(pthread_detach),才可以使用pthread_join从另一个线程获得线程id和返回值。
#include <pthread.h>
int pthread_cancel(pthread_t tid);
返回:若成功,返回0。否则,返回错误编号
线程成功接收到其他线程发来的Cancel信号,不代表它一定会终止运行。
如果线程接收Cancel信号后终止运行,相当于调用了“pthread_exit(PTHREAD_CANCELED);”
4.分离线程--pthread_detach
#include <pthread.h>
int pthread_detach(pthread_t tid);
返回:若成功,返回0。否则,返回错误编号
该函数主要用来清理线程终止后残留的资源占用。
被分离后的线程,其终止时,线程的存储资源会被立即收回。
该函数会将线程从主线程的分支中分离出去,当线程结束后,它的退出状态不由其它线程通过pthread_join获取,而是由该线程主动释放。
代码样例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#ifndef NUM_THREADS
#define NUM_THREADS 8
#endif
void *printHello(void *threadid) {
long tid;
tid = (long)threadid;
printf("Hello from thread %ld, pthread ID - %lu\n", tid, pthread_self());
return NULL;
}
int main(int argc, char const *argv[]) {
pthread_t threads[NUM_THREADS];
int rc;
long t;
for (t = 0; t < NUM_THREADS; t++) {
rc = pthread_create(&threads[t], NULL, printHello, (void *)t);
if (rc) {
printf("ERORR; return code from pthread_create() is %d\n", rc);
exit(EXIT_FAILURE);
}
}
int ret;
for (t = 0; t < NUM_THREADS; t++) {
void *retval;
ret = pthread_join(threads[t], &retval);
if (retval == PTHREAD_CANCELED)
printf("The thread was canceled - ");
else
printf("Returned value %d - ", (int)retval);
}
pthread_exit(NULL);
}
三,锁机制
互斥锁可以确保同一时间只有一个线程能访问指定数据。
互斥锁的用法:在访问共享资源前对资源加锁,访问完成后对资源解锁。
一个线程对共享资源加互斥锁以后,其他试图对共享资源加互斥锁的线程就会阻塞,直到当前资源的锁被释放。
有了互斥锁以后,每次只有一个线程可以访问共享资源。
数据类型:
pthread_mutex_t(使用之前,必须初始化)
常用函数:
初始化互斥锁--pthread_mutex_init
销毁互斥锁--pthread_mutex_destroy
加上互斥锁--pthread_mutex_lock
解除互斥锁--pthread_mutex_unlock
有时间限制的互斥锁
--pthread_mutex_timedlock
加上互斥锁(非阻塞模式)
--pthread_mutex_trylock
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex)
*带有"try"关键词的函数,表示一次尝试不成功就报错返回然后再试一次,代表着非阻塞模式。
2.读写锁
读写锁有三种状态:
1.读模式下的加锁状态
2.写模式下的加锁状态
3.不加锁状态
写模式一次只能允许一个线程加锁,但是读模式可以允许多个线程同时加锁。
写模式被加锁后,其他试图给写模式或读模式加锁的线程都会被阻塞。
读模式被加锁后,其他试图给写模式加锁的线程会被阻塞,给读模式加锁的线程会得到访问权。
读模式被加锁后,这时有一个线程试图对写模式加锁,其后到来的读模式加锁线程会被阻塞,这样可以避免读模式锁被长期占用。
当对一个共享资源的读次数远大于写次数时,可以考虑使用读写锁。
写模式和互斥锁很像,一次只有一个线程可以访问。
数据类型:
pthread_rwlock_t
常用函数:
初始化读写锁--pthread_rwlock_init
销毁读写锁--pthread_rwlock_destroy
读模式加锁--pthread_rwlock_rdlock
写模式加锁--pthread_rwlock_wrlock
解锁--pthread_rwlock_unlock
有时间限制的读锁--pthread_rwlock_timedrdlock
有时间限制的写锁--pthread_rwlock_timedwrlock
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock,
const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER;
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
3.自旋锁
自旋锁和互斥锁的区别是,自旋锁机制在获得锁之前会一直忙等,而不会让线程因为阻塞而休眠。
如果一个共享资源被加锁的时间很短,更推荐使用自旋锁。
数据类型:
pthread_spinlock_t
常用函数:
初始化自旋锁--pthread_spin_init
销毁自旋锁--pthread_spin_destroy
自旋锁加锁--pthread_spin_lock
pthread_spin_trylock
自旋锁解锁--pthread_spin_unlock
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
4.条件变量
条件变量搭配互斥锁使用,互斥锁提供互斥机制,条件变量提供信号机制。
条件变量被用来阻塞一个线程,当条件不满足时,线程解除互斥锁并等待条件成立。当条件成立的时候,线程重新给互斥锁加锁。
条件变量相当于给互斥锁加了if-else,if条件满足时才允许加锁。
数据类型:
pthread_cond_t
常用函数:
初始化条件变量--pthread_cond_init
销毁条件变量--pthread_cond_destroy
用条件变量阻塞当前线程,直到条件成立
--pthread_cond_wait
pthread_cond_timedwait
解除线程的阻塞状态
--pthread_cond_signal
pthread_cond_broadcast
(此处的signal,不是操作系统中的“SIGXXX"信号)
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond,
const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond,
pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond,
pthread_mutex_t *mutex,
const struct timespec *abstime);
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
代码样例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREADS 5
typedef struct _thread_data_t {
int tid;
double stuff;
} thread_data_t;
/* shared data between threads */
double shared_x;
pthread_mutex_t lock_x;
void *thr_func(void *arg) {
thread_data_t *data = (thread_data_t *)arg;
printf("hello from thr_func, thread id: %d\n", data->tid);
/* get mutex before modifying and printing shared_x */
pthread_mutex_lock(&lock_x);
shared_x += data->stuff;
printf("x = %f\n", shared_x);
pthread_mutex_unlock(&lock_x);
pthread_exit(NULL);
}
int main(int argc, char **argv) {
pthread_t thr[NUM_THREADS];
int i, rc;
/* create a thread_data_t argument array */
thread_data_t thr_data[NUM_THREADS];
/* initialize shared data */
shared_x = 0;
/* initialize pthread mutex protecting "shared_x" */
pthread_mutex_init(&lock_x, NULL);
/* create threads */
for (i = 0; i < NUM_THREADS; ++i) {
thr_data[i].tid = i;
thr_data[i].stuff = (i + 1) * NUM_THREADS;
if ((rc = pthread_create(&thr[i], NULL, thr_func, &thr_data[i]))) {
fprintf(stderr, "error: pthread_create, rc: %d\n", rc);
return EXIT_FAILURE;
}
}
/* block until all threads complete */
for (i = 0; i < NUM_THREADS; ++i) {
pthread_join(thr[i], NULL);
}
return EXIT_SUCCESS;
}
*本次代码分享不提供运行结果,建议使用GDB工具进行调试分析,收获会更大
进程原语和线程原语的比较:
参考教程:
《UNIX环境高级编程 第3版》
《UNIX网络编程 卷1:套接字联网API 第3版》
《UNIX网络编程 卷2:进程间通信 第2版》
http://www.csc.villanova.edu/
https://randu.org/tutorials/threads/
https://www.tutorialspoint.com/cplusplus/cpp_multithreading.htm
https://www.delftstack.com/howto/c/pthread_join-return-value-in-c/